diff --git a/admin/Gemfile b/admin/Gemfile index a2544488b..756225a1a 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -14,7 +14,7 @@ else ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" end -gem 'rails', '~> 3.2.11' +gem 'rails', '~> 3.2.22' gem 'bootstrap-sass', '2.0.4' gem 'bcrypt-ruby', '3.0.1' @@ -44,7 +44,7 @@ gem 'rails3-jquery-autocomplete' gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable' gem 'mime-types', '1.25' gem 'meta_search' -gem 'fog', "~> 1.18.0" +gem 'fog', "~> 1.32.0" gem 'unf', '0.1.3' #optional fog dependency gem 'country-select' gem 'aasm', '3.0.16' @@ -58,7 +58,7 @@ gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' gem 'resque-lonely_job', '~> 1.0.0' -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' gem 'pg_migrate' @@ -77,7 +77,7 @@ gem 'influxdb-rails', '0.1.10' gem 'recurly' group :libv8 do - gem 'libv8', "~> 3.11.8" + gem 'libv8', "~> 4.5.95" end @@ -101,15 +101,16 @@ end group :development, :test do gem 'capybara' gem 'rspec-rails', '2.14.2' - gem 'guard-rspec', '0.5.5' + gem 'guard-rspec' gem 'jasmine', '1.3.1' gem 'execjs', '1.4.0' - gem 'therubyracer' #, '0.11.0beta8' + #gem 'therubyracer' #, '0.11.0beta8' gem 'factory_girl_rails', '4.1.0' gem 'database_cleaner', '0.7.0' gem 'launchy' gem 'faker', '1.3.0' gem 'puma' + gem 'test-unit' end group :test do diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index d51e92f31..534b65903 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -5,9 +5,14 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do config.sort_order = 'name_asc' config.batch_actions = false - filter :genre + filter :genres filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS + scope("Default", default: true) { |scope| scope } + scope("Onboarding TODO") { |scope| scope.where('onboarding_exceptions is not null') } + scope("Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'") } + scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') } + form :partial => 'form' index do @@ -24,11 +29,21 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do column :original_artist column :name - column :onboarding_flags do |jam_track| jam_track.onboard_warnings end + column :onboarding_exceptions do |jam_track| + if jam_track.onboarding_exceptions + exceptions = JSON.parse(jam_track.onboarding_exceptions) + exceptions.keys.join(',') + else + '' + end + + end column :status column :master_track do |jam_track| jam_track.master_track.nil? ? 'None' : (link_to "Download", jam_track.master_track.url_by_sample_rate(44)) end column :licensor - column :genre + column :genres do |jam_track| + jam_track.genres.map(&:description).join(',') + end column :price column :reproduction_royalty column :public_performance_royalty diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index 51341d812..334cdc27e 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -12,7 +12,7 @@ = f.input :songwriter, :input_html => { :rows=>1, :maxlength=>1000 } = f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 } = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true - = f.input :genre, collection: JamRuby::Genre.all, include_blank: false + = f.input :genres = f.input :duration, hint: 'this should rarely need editing because it comes from the import process' = f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false = f.input :price, :required => true, :input_html => {type: 'numeric'} diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb index 778d5dc89..efa44b0f8 100644 --- a/admin/app/views/email/dump_emailables.csv.erb +++ b/admin/app/views/email/dump_emailables.csv.erb @@ -1,2 +1,2 @@ <%- headers = ['email', 'name', 'unsubscribe_token'] -%> -<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file +<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file diff --git a/admin/config/initializers/email.rb b/admin/config/initializers/email.rb index 41e1651d0..dc39145d1 100644 --- a/admin/config/initializers/email.rb +++ b/admin/config/initializers/email.rb @@ -1,5 +1,10 @@ ActionMailer::Base.raise_delivery_errors = true -ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp +begin + ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp +rescue + # this can happen on the build server when it's compiling assets and doesn't have the 'jam' database + ActionMailer::Base.delivery_method = :test +end ActionMailer::Base.smtp_settings = { :address => Rails.application.config.email_smtp_address, :port => Rails.application.config.email_smtp_port, diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb index cd02eccee..33efd995c 100644 --- a/admin/config/initializers/jam_tracks.rb +++ b/admin/config/initializers/jam_tracks.rb @@ -16,7 +16,6 @@ class JamRuby::JamTrack end def jmep_json_generate - self.genre_id = nil if self.genre_id == '' self.licensor_id = nil if self.licensor_id == '' self.jmep_json = nil if self.jmep_json == '' self.time_signature = nil if self.time_signature == '' diff --git a/admin/config/initializers/secret_token.rb b/admin/config/initializers/secret_token.rb index f71a3cafa..f1967d9d5 100644 --- a/admin/config/initializers/secret_token.rb +++ b/admin/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -JamAdmin::Application.config.secret_token = 'e27a8deff5bc124d1c74cb86ebf38ac4a246091b859bcccf4f8076454e0ff3e04ffc87c9a0f4ddc801c4753e20b2a3cf06e5efc815cfe8e6377f912b737c5f77' +JamAdmin::Application.config.secret_token = 'ced345e01611593c1b783bae98e4e56dbaee787747e92a141565f7c61d0ab2c6f98f7396fb4b178258301e2713816e158461af58c14b695901692f91e72b6200' diff --git a/build b/build index db8ab33a4..1c5677dc3 100755 --- a/build +++ b/build @@ -61,9 +61,10 @@ popd > /dev/null if [ ! -z "$PACKAGE" ]; then - -DEB_SERVER=http://localhost:9010/apt-`uname -p` -GEM_SERVER=http://localhost:9000/gems + + source /etc/lsb-release + DEB_SERVER=https://int.jamkazam.com:9010/apt-`uname -p`/$DISTRIB_CODENAME + GEM_SERVER=http://localhost:9000/gems # if still going, then push all debs up if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* || "$GIT_BRANCH" == *feature* || "$GIT_BRANCH" == *hotfix* ]]; then diff --git a/db/Gemfile b/db/Gemfile index 903a1c2dc..79ec5bca4 100644 --- a/db/Gemfile +++ b/db/Gemfile @@ -3,4 +3,4 @@ source 'http://rubygems.org' # Assumes you have already cloned pg_migrate_ruby in your workspace # $ cd [workspace] # $ git clone https://github.com/sethcall/pg_migrate_ruby -gem 'pg_migrate', '0.1.13' +gem 'pg_migrate', '0.1.13', :source => 'http://rubygems.org/' diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 8d6d039c2..88080fb81 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -15,7 +15,7 @@ PLATFORMS ruby DEPENDENCIES - pg_migrate (= 0.1.13) + pg_migrate (= 0.1.13)! BUNDLED WITH - 1.10.3 + 1.10.5 diff --git a/db/build b/db/build index a0084254e..5c97d25a5 100755 --- a/db/build +++ b/db/build @@ -31,6 +31,7 @@ if [ ! -z "$PACKAGE" ]; then bundle install --path target/vendor/bundle pushd target fpm -s gem -t deb ruby_package/jam_db-$VERSION.gem + find vendor/bundle/ruby/2.2.0/cache -name '*.gem' | xargs -rn1 fpm -s gem -t deb find vendor/bundle/ruby/2.0.0/cache -name '*.gem' | xargs -rn1 fpm -s gem -t deb popd fi diff --git a/db/jenkins b/db/jenkins index 275cb28a4..c617fd59f 100755 --- a/db/jenkins +++ b/db/jenkins @@ -1,7 +1,7 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems -DEB_SERVER=http://localhost:9010/apt-`uname -p` +GEM_SERVER=https://int.jamkazam.com:9000/gems +DEB_SERVER=https://int.jamkazam.com:9010/apt-`uname -p` echo "starting build..." ./build diff --git a/db/manifest b/db/manifest index a3bddb189..f7212acc1 100755 --- a/db/manifest +++ b/db/manifest @@ -276,12 +276,6 @@ jam_track_duration.sql sales.sql show_whats_next_count.sql recurly_adjustments.sql -alter_type_columns.sql -user_presences_rename.sql -add_genre_type.sql -add_description_to_perf_samples.sql -alter_genre_player_unique_constraint.sql -musician_search.sql signup_hints.sql packaging_notices.sql first_played_jamtrack_at.sql @@ -292,6 +286,17 @@ signing.sql optimized_redeemption.sql optimized_redemption_warn_mode.sql affiliate_partners2.sql -enhance_band_profile.sql broadcast_notifications.sql broadcast_notifications_fk.sql +calendar.sql +alter_type_columns.sql +user_presences_rename.sql +add_genre_type.sql +add_description_to_perf_samples.sql +alter_genre_player_unique_constraint.sql +musician_search.sql +enhance_band_profile.sql +alter_band_profile_rate_defaults.sql +repair_band_profile.sql +jam_track_onboarding_enhancements.sql +jam_track_name_drop_unique.sql \ No newline at end of file diff --git a/db/up/alter_band_profile_rate_defaults.sql b/db/up/alter_band_profile_rate_defaults.sql new file mode 100644 index 000000000..e716f1cda --- /dev/null +++ b/db/up/alter_band_profile_rate_defaults.sql @@ -0,0 +1,2 @@ +ALTER TABLE bands ALTER COLUMN hourly_rate SET DEFAULT NULL; +ALTER TABLE bands ALTER COLUMN gig_minimum SET DEFAULT NULL; diff --git a/db/up/calendar.sql b/db/up/calendar.sql new file mode 100644 index 000000000..5e27c7f14 --- /dev/null +++ b/db/up/calendar.sql @@ -0,0 +1,13 @@ +CREATE TABLE calendars ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_uid VARCHAR(64) NOT NULL, + name VARCHAR(128), + description VARCHAR(8000), + trigger_delete BOOLEAN DEFAULT FALSE, + start_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + recurring_mode VARCHAR(50) NOT NULL DEFAULT 'once', + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); \ No newline at end of file diff --git a/db/up/jam_track_name_drop_unique.sql b/db/up/jam_track_name_drop_unique.sql new file mode 100644 index 000000000..d283b34cd --- /dev/null +++ b/db/up/jam_track_name_drop_unique.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks DROP CONSTRAINT jam_tracks_name_key; \ No newline at end of file diff --git a/db/up/jam_track_onboarding_enhancements.sql b/db/up/jam_track_onboarding_enhancements.sql new file mode 100644 index 000000000..2a373eb5c --- /dev/null +++ b/db/up/jam_track_onboarding_enhancements.sql @@ -0,0 +1,78 @@ +-- "rsvp_slots_instrument_id_fkey" FOREIGN KEY (instrument_id) REFERENCES instruments(id) +-- "musicians_instruments_instrument_id_fkey" FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE +-- "saved_tracks_instrument_id_fkey" FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE +ALTER TABLE rsvp_slots DROP CONSTRAINT rsvp_slots_instrument_id_fkey; +ALTER TABLE musicians_instruments DROP CONSTRAINT musicians_instruments_instrument_id_fkey; +ALTER TABLE recorded_tracks DROP CONSTRAINT saved_tracks_instrument_id_fkey; +UPDATE instruments SET id = 'double bass', description = 'Double Bass' WHERE id = 'upright bass'; +UPDATE rsvp_slots SET instrument_id = 'double bass' where instrument_id = 'upright bass'; +UPDATE musicians_instruments SET instrument_id = 'double bass' where instrument_id = 'upright bass'; +UPDATE recorded_tracks SET instrument_id = 'double bass' where instrument_id = 'upright bass'; +ALTER TABLE rsvp_slots ADD CONSTRAINT rsvp_slots_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE SET NULL; +ALTER TABLE musicians_instruments ADD CONSTRAINT musicians_instruments_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE; +ALTER TABLE recorded_tracks ADD CONSTRAINT saved_tracks_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE; + +INSERT INTO instruments (id, description, popularity) VALUES ('steel guitar', 'Steel Guitar', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('orchestra', 'Orchestra', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('glockenspiel', 'Glockenspiel', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('dobro', 'Dobro', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('harp', 'Harp', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('vocoder', 'Vocoder', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('flugelhorn', 'Flugelhorn', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('timpani', 'Timpani', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('bassoon', 'Bassoon', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('charango', 'Charango', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('theremin', 'Theremin', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('sitar', 'Sitar', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('piccolo', 'Piccolo', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('bagpipes', 'Bagpipes', 1); +ALTER TABLE jam_tracks ADD COLUMN onboarding_exceptions JSON; +ALTER TABLE jam_track_tracks ADD COLUMN original_filename VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN additional_info VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN language VARCHAR NOT NULL DEFAULT 'eng'; +ALTER TABLE jam_tracks ADD COLUMN year INTEGER; +ALTER TABLE jam_tracks ADD COLUMN vendor_id VARCHAR; + +INSERT INTO jam_track_licensors (name, description) VALUES ('Tency Music', 'Tency Music is a music production company specialized in re-recordings.'); + +CREATE TABLE genres_jam_tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + genre_id VARCHAR(64) NOT NULL REFERENCES genres(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO genres_jam_tracks (jam_track_id, genre_id) ((SELECT jam_tracks.id, jam_tracks.genre_id FROM jam_tracks)); +ALTER TABLE jam_tracks DROP COLUMN genre_id; + +-- holds precount, click.wav, click.txt +CREATE TABLE jam_track_files ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) REFERENCES jam_tracks(id) ON DELETE CASCADE, + file_type VARCHAR NOT NULL, + original_filename VARCHAR NOT NULL, + precount_num INTEGER, + url VARCHAR, + md5 VARCHAR, + length bigint, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO genres (id, description) VALUES ('soft rock', 'Soft Rock'); +INSERT INTO genres (id, description) VALUES ('rap', 'Rap'); +INSERT INTO genres (id, description) VALUES ('tv & movie soundtrack', 'TV & Movie Soundtrack'); +INSERT INTO genres (id, description) VALUES ('holiday', 'Holiday'); +INSERT INTO genres (id, description) VALUES ('kids', 'Kids'); +INSERT INTO genres (id, description) VALUES ('disco', 'Disco'); +INSERT INTO genres (id, description) VALUES ('soul', 'Soul'); +INSERT INTO genres (id, description) VALUES ('hard rock', 'Hard Rock'); +INSERT INTO genres (id, description) VALUES ('funk', 'Funk'); +INSERT INTO genres (id, description) VALUES ('dance', 'Dance'); +INSERT INTO genres (id, description) VALUES ('creole', 'Creole'); +INSERT INTO genres (id, description) VALUES ('traditional', 'Traditional'); +INSERT INTO genres (id, description) VALUES ('oldies', 'Oldies'); +INSERT INTO genres (id, description) VALUES ('world', 'World'); +INSERT INTO genres (id, description) VALUES ('musical', 'Musical'); +INSERT INTO genres (id, description) VALUES ('celtic', 'Celtic'); diff --git a/db/up/repair_band_profile.sql b/db/up/repair_band_profile.sql new file mode 100644 index 000000000..1a87f51a4 --- /dev/null +++ b/db/up/repair_band_profile.sql @@ -0,0 +1,3 @@ +ALTER TABLE musicians_instruments ALTER COLUMN player_id SET NOT NULL; +ALTER TABLE performance_samples ALTER COLUMN player_id SET NOT NULL; +ALTER TABLE online_presences ALTER COLUMN player_id SET NOT NULL; \ No newline at end of file diff --git a/db/up/user_profile_corrections.sql b/db/up/user_profile_corrections.sql new file mode 100644 index 000000000..de6161845 --- /dev/null +++ b/db/up/user_profile_corrections.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ALTER paid_sessions_hourly_rate TYPE integer; +ALTER TABLE users ALTER paid_sessions_daily_rate TYPE integer; \ No newline at end of file diff --git a/pb/jenkins b/pb/jenkins index 74e4ccbb0..fb8778501 100755 --- a/pb/jenkins +++ b/pb/jenkins @@ -1,6 +1,6 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems +GEM_SERVER=https://int.jamkazam.com:9000/gems echo "starting build..." ./build diff --git a/ruby/.gitignore b/ruby/.gitignore index be1654c09..76db742a2 100644 --- a/ruby/.gitignore +++ b/ruby/.gitignore @@ -9,6 +9,7 @@ _yardoc coverage doc/ lib/bundler/man +jt_metadata.json pkg rdoc spec/reports diff --git a/ruby/Gemfile b/ruby/Gemfile index d46e44e82..dc66cc735 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -18,15 +18,15 @@ end gem 'pg', '0.17.1', :platform => [:mri, :mswin, :mingw] gem 'jdbc_postgres', :platform => [:jruby] -gem 'activerecord', '3.2.13' +gem 'activerecord', '3.2.22' gem "activerecord-import", "~> 0.4.1" gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'amqp', '1.0.2' gem 'will_paginate' -gem 'actionmailer', '3.2.13' +gem 'actionmailer', '3.2.22' gem 'sendgrid', '1.2.0' gem 'aws-sdk', '~> 1' gem 'carrierwave', '0.9.0' @@ -64,6 +64,7 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' + gem 'icalendar' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/jenkins b/ruby/jenkins index 8d4742fc6..97d39c693 100755 --- a/ruby/jenkins +++ b/ruby/jenkins @@ -1,6 +1,6 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems +GEM_SERVER=https://int.jamkazam.com:9000/gems echo "starting build..." ./build diff --git a/ruby/jt_metadata.json b/ruby/jt_metadata.json deleted file mode 100644 index cc85875b4..000000000 --- a/ruby/jt_metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/jam-track-35.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/7452fa4a-0c55-4cb2-948e-221475d7299c.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpGdncJS"} \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 2878fe935..7d7ffd4e6 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -51,8 +51,10 @@ require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" +require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" +require "jam_ruby/resque/scheduled/music_session_reminder" require "jam_ruby/resque/scheduled/music_session_scheduler" require "jam_ruby/resque/scheduled/active_music_session_cleaner" require "jam_ruby/resque/scheduled/score_history_sweeper" @@ -94,6 +96,7 @@ 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/calendar" require "jam_ruby/models/feedback" require "jam_ruby/models/feedback_observer" #require "jam_ruby/models/max_mind_geo" @@ -203,6 +206,8 @@ require "jam_ruby/models/jam_track" require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" require "jam_ruby/models/jam_track_tap_in" +require "jam_ruby/models/jam_track_file" +require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" require "jam_ruby/app/mailers/progress_mailer" @@ -227,6 +232,7 @@ require "jam_ruby/models/sale_line_item" require "jam_ruby/models/recurly_transaction_web_hook" require "jam_ruby/models/broadcast_notification" require "jam_ruby/models/broadcast_notification_view" +require "jam_ruby/calendar_manager" require "jam_ruby/jam_tracks_manager" require "jam_ruby/jam_track_importer" require "jam_ruby/jmep_manager" @@ -236,6 +242,7 @@ require "jam_ruby/models/json_store" require "jam_ruby/models/base_search" require "jam_ruby/models/musician_search" require "jam_ruby/models/band_search" +require "jam_ruby/import/tency_stem_mapping" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 46275db4a..40d5480f0 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -182,7 +182,7 @@ email = user.email subject = "Your band has a new follower on JamKazam" unique_args = {:type => "new_band_follower"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -390,13 +390,23 @@ end end - def scheduled_session_reminder(user, msg, session) + def scheduled_session_reminder_upcoming(user, session) + subject = "Your JamKazam session starts in 1 hour!" + unique_args = {:type => "scheduled_session_reminder_upcoming"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def scheduled_session_reminder_day(user, session) + subject = "JamKazam Session Reminder" + unique_args = {:type => "scheduled_session_reminder_day"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def send_scheduled_session_reminder(user, session, subject, unique_args) return if !user.subscribe_email email = user.email - subject = "Session Rescheduled" - unique_args = {:type => "scheduled_session_reminder"} - @body = msg + @user = user @session_name = session.name @session_date = session.pretty_scheduled_start(true) @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" @@ -448,7 +458,7 @@ @sessions_and_latency = sessions_and_latency @title = 'New Scheduled Sessions Matched to You' - mail(:to => receiver.email, + mail(:to => receiver.email, :subject => EmailBatchScheduledSessions.subject) do |format| format.text format.html @@ -461,7 +471,7 @@ email = user.email subject = "A band that you follow has joined a session" unique_args = {:type => "band_session_join"} - + @body = msg @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" sendgrid_category "Notification" @@ -482,7 +492,7 @@ email = user.email subject = "A musician has saved a new recording on JamKazam" unique_args = {:type => "musician_recording_saved"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -502,7 +512,7 @@ email = user.email subject = "A band has saved a new recording on JamKazam" unique_args = {:type => "band_recording_saved"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -522,7 +532,7 @@ email = user.email subject = "You have been invited to join a band on JamKazam" unique_args = {:type => "band_invitation"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb deleted file mode 100644 index 8582bfdbe..000000000 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% provide(:title, 'Scheduled Session Reminder') %> - -

<%= @body %>

- -

- <%= @session_name %>
- <%= @session_date %> -

- -

View Session Details

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb deleted file mode 100644 index 40ec73f65..000000000 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= @body %> - -<%= @session_name %> -<%= @session_date %> - -See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb new file mode 100644 index 000000000..b72d3c133 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, 'JamKazam Session Reminder') %> + + +
+Hi <%= @user.first_name %>, +
+
+
+ This is a reminder that your JamKazam session + <%= @session_name %> + is scheduled for tomorrow. We hope you have fun! +
+
+
+Best Regards, +
+Team JamKazam +
\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb new file mode 100644 index 000000000..c3f0576bf --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb @@ -0,0 +1,8 @@ +Hi <%= @user.first_name %>, + +This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun! + +Best Regards, +Team JamKazam + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb new file mode 100644 index 000000000..4fbc59ace --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, 'Your JamKazam session starts in 1 hour!') %> + +
+Hi <%= @user.first_name %>, +
+
+
+ This is a reminder that your JamKazam session + <%= @session_name %> + starts in 1 hour. We hope you have fun! +
+
+
+Best Regards, +
+Team JamKazam +
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb new file mode 100644 index 000000000..70726a9e6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb @@ -0,0 +1,10 @@ +Hi <%= @user.first_name %>, + +This is a reminder that your JamKazam session +<%=@session_name%> +starts in 1 hour. We hope you have fun! + +Best Regards, +Team JamKazam + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/calendar_manager.rb b/ruby/lib/jam_ruby/calendar_manager.rb new file mode 100644 index 000000000..523e683d9 --- /dev/null +++ b/ruby/lib/jam_ruby/calendar_manager.rb @@ -0,0 +1,106 @@ +module JamRuby + class CalendarManager < BaseManager + DATE_FORMAT="%Y%m%dT%H%M%SZ" + def initialize(options={}) + super(options) + @log = Logging.logger[self] + end + + def cancel_ics_event(music_session, user) + Calendar.where( + user_id: user.id, + target_uid: music_session.id, + name: music_session.description) + .first_or_create( + description: music_session.description, + start_at: music_session.scheduled_start, + end_at: music_session.scheduled_start+music_session.safe_scheduled_duration, + trigger_delete: true) + + end + + # Remove all "delete" event calendar records older than 4 weeks: + def cleanup() + Calendar.where("trigger_delete=TRUE AND created_at < ?", 4.weeks.ago) + .destroy_all() + end + + # @return event (as ICS string) for a given music session + def ics_event_from_music_session(music_session, delete=false) + # Determine properties of calendar event and create: + uid = "#{music_session.id}@JamKazam" + text = "JamKazam Session #{music_session.description}" + rrule = nil + start_at = music_session.scheduled_start + stop_at = music_session.scheduled_start+music_session.safe_scheduled_duration + if !delete && music_session.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + create_ics_event(uid, text, text, start_at, stop_at, delete, rrule) + end + + # @return event (as ICS string) for a given music session + def ics_event_from_calendar(calendar) + # Determine properties of calendar event and create: + rrule = nil + if !calendar.trigger_delete && calendar.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + + create_ics_event( + calendar.target_uid, + "JamKazam Session #{calendar.name}", + calendar.description, + calendar.start_at, + calendar.end_at, + calendar.trigger_delete, + rrule + ) + end + + # @return calendar (as ICS string) for specified user + # Includes all RSVPed sessions, as well as any calendar + # entries for the given user: + def create_ics_feed(user) + ics_events = "" + MusicSession.scheduled_rsvp(user, true).each do |music_session| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_music_session(music_session) + end + + user.calendars.each do |user| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_calendar(user) + end + + create_ics_cal(ics_events) + end + + # @return event (as ICS string) for given arguments + def create_ics_event(uuid, name, description, start_at, end_at, delete=false, rrule=nil, sequence=nil) + uuid ||= UUID.timestamp_create + event = "BEGIN:VEVENT\r\n" + event << "UID:#{uuid}\r\n" + event << "DTSTAMP:#{Time.now.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTSTART:#{start_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTEND:#{end_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "SUMMARY:#{name}\r\n" + event << "DESCRIPTION:#{description}\r\n" + if delete + event << "METHOD:CANCEL\r\n" + event << "STATUS:CANCELLED\r\n" + end + if rrule + event << "RRULE:#{rrule}\r\n" + end + event << "SEQUENCE:#{sequence}\r\n" if sequence + event << "END:VEVENT" + end + + # @return calendar (as ICS string) for specified events + def create_ics_cal(ics_events) + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:JamKazam\r\n#{ics_events}\r\nEND:VCALENDAR" + end + + end # class +end # module \ No newline at end of file diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index 7460a2fbb..e05c8e00e 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -26,7 +26,9 @@ module NotificationTypes SCHEDULED_SESSION_RSVP_CANCELLED_ORG = "SCHEDULED_SESSION_RSVP_CANCELLED_ORG" SCHEDULED_SESSION_CANCELLED = "SCHEDULED_SESSION_CANCELLED" SCHEDULED_SESSION_RESCHEDULED = "SCHEDULED_SESSION_RESCHEDULED" - SCHEDULED_SESSION_REMINDER = "SCHEDULED_SESSION_REMINDER" + SCHEDULED_SESSION_REMINDER_DAY = "SCHEDULED_SESSION_REMINDER_DAY" + SCHEDULED_SESSION_REMINDER_UPCOMING = "SCHEDULED_SESSION_REMINDER_UPCOMING" + SCHEDULED_SESSION_REMINDER_IMMINENT = "SCHEDULED_SESSION_REMINDER_IMMINENT" SCHEDULED_SESSION_COMMENT = "SCHEDULED_SESSION_COMMENT" # recording notifications diff --git a/ruby/lib/jam_ruby/import/tency_stem_mapping.rb b/ruby/lib/jam_ruby/import/tency_stem_mapping.rb new file mode 100644 index 000000000..769496562 --- /dev/null +++ b/ruby/lib/jam_ruby/import/tency_stem_mapping.rb @@ -0,0 +1,360 @@ +module JamRuby + + # this is probably a one-off class used to map Tency-named stems into JamKazam-named stems + class TencyStemMapping + + @@log = Logging.logger[TencyStemMapping] + + def s3_manager + @s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def initialize + @originals_folder = "/Volumes/sethcall/Dropbox/seth@jamkazam.com/JamTracks - Tency Music - Original Folder for Normalization Map" + @mapping_folder = "/Volumes/sethcall/Dropbox/seth@jamkazam.com/JamTracks - Tency Music" + @original_songs = {} + @mapping_songs = {} + @mappings = {} + end + + def create_map + tency_originals + tency_maps + + dump + end + + def create_mapping_map + tency_maps + + dump_map + end + + def hydrate + @original_songs = YAML.load_file('original_songs.yml') + @mapping_songs = YAML.load_file('mapping_songs.yml') + end + + def parse_sanitized_filename(filename) + instrument = nil + part = nil + + basename = File.basename(filename) + stem = basename.index('Stem') + + if stem + stripped = basename[(stem + 'Stem'.length)..-5] # takes of 'stem' and '.wav' + stripped.strip! + dash = stripped.index('-') + + if dash == 0 + stripped = stripped[1..-1].strip! + # now we should have something like "Vocal - Lead" (instrument - part) + instrument, part = stripped.split('-') + instrument.strip! if instrument + part.strip! if part + else + "no or misplaced dash for #{filename}" + end + + else + raise "no stem for #{filename}" + end + + [instrument, part] + end + + # For all the tracks that I have labeled manually as + # Instrument = Upright Bass and Part = Upright Bass, + # can you please change both the Instrument and Part to Double Bass instead? + # + def check_mappings + missing_instrument = 0 + missing_part = 0 + part_names = [] + + hydrate + @mapping_songs.each do |cache_id, data| + mapped_filename = data[:filename] + @@log.debug("parsing #{mapped_filename}") + instrument, part = parse_sanitized_filename(mapped_filename) + @@log.debug("parsed #{instrument} (#{part})") + missing_instrument = missing_instrument + 1 unless instrument + missing_part = missing_part + 1 unless part + part_names << mapped_filename unless part + end + + @@log.info("SUMMARY") + @@log.info("-------") + @@log.info("missing instruments:#{missing_instrument} missing parts: #{missing_part}") + @@log.info("files with no parts: #{part_names}") + + # files with no parts: + # ["Huey Lewis And The News - Heart And Soul - 31957/Heart And Soul Stem - Synth 2.wav", + # "ZZ Top - Tush - 20852/Tush Stem - Clicktrack.wav", + # "Crosby Stills And Nash - Teach Your Children - 15440/Teach Your Children Stem - Bass Guitar.wav", + # /Brad Paisley - She's Everything - 19886/She's Everything Stem - Clicktrack.wav", + # "Toby Keith - Beer For My Horses - 7221/Beer For My Horses Stem - Lap Steel.wav", + # Toby Keith - Beer For My Horses - 7221/Beer For My Horses Stem - Acoustic Guitar.wav" + + end + + def track_mapping(basename, instr_part) + instrument = instr_part[:instrument] + part = instr_part[:part] + + basename.downcase! + + info = @mappings[basename] + + unless info + info = {matches:[]} + @mappings[basename] = info + end + + info[:matches] << instr_part + end + + def correlate + mapped = 0 + unmapped = 0 + unmapped_details = [] + no_instrument = [] + common_unknown_instruments = {} + + hydrate + @mapping_songs.each do |cache_id, data| + # go through each track hand-mapped, and find it's matching song if any. + + mapped_filename = data[:filename] + found_original = @original_songs[cache_id] + if found_original + # mapping made + + original_filename = found_original[:filename] + original_basename = File.basename(original_filename).downcase + + mapped = mapped + 1 + + instrument, part = parse_sanitized_filename(mapped_filename) + instr_part = JamTrackImporter.determine_instrument(instrument, part) + + instr_part[:instrument] + + if instr_part[:instrument] + + # track the mapping of this one + track_mapping(original_basename, instr_part) + + else + @@log.error("unable to determine instrument for #{File.basename(mapped_filename)}") + no_instrument << ({filename: File.basename(mapped_filename), instrument: instrument, part: part}) + common_unknown_instruments["#{instrument}-(#{part})"] = 1 + end + + else + unmapped = unmapped + 1 + unmapped_details << {filename: mapped_filename} + end + end + + puts("SUMMARY") + puts("-------") + puts("MAPPED:#{mapped} UNMAPPED:#{unmapped}") + unmapped_details.each do |unmapped_detail| + puts "UNMAPPED FILE: #{File.basename(unmapped_detail[:filename])}" + end + puts("UNKNOWN INSTRUMENT: #{no_instrument.length}") + no_instrument.each do |item| + puts("UNKNOWN INSTRUMENT: #{item[:filename]}") + end + common_unknown_instruments.each do |key, value| + puts("#{key}") + end + @mappings.each do |basename, mapping| + matches = mapping[:matches] + counts = matches.each_with_object(Hash.new(0)) { |word,counts| counts[word] += 1 } + ordered_matches = counts.sort_by {|k, v| -v} + output = "" + ordered_matches.each do |match| + detail = match[0] + count = match[1] + output << "#{detail[:instrument]}(#{detail[:part]})/#{count}, " + end + + puts "map detail: #{basename}: #{output}" + + mapping[:ordered] = ordered_matches + mapping[:detail] = output + end + CSV.open("mapping.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + item = mapping[:ordered] + + trust_worthy = item.length == 1 + unless trust_worthy + # if the 1st item is at least 4 'counts' more than the next item, we can consider it trust_worthy + if item[0][1] - 4 > item[1][1] + trust_worthy = true + end + end + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1], trust_worthy ] + end + end + CSV.open("determinate-single-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length == 1 && mapping[:ordered][0][1] == 1 + item = mapping[:ordered] + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1] ] + end + end + end + CSV.open("determinate-multi-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length == 1 && mapping[:ordered][0][1] > 1 + item = mapping[:ordered] + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1] ] + end + end + end + CSV.open("ambiguous-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length > 1 + csv << [ basename, mapping[:detail] ] + end + end + end + end + + def dump + File.open('original_songs.yml', 'w') {|f| f.write(YAML.dump(@original_songs)) } + File.open('mapping_songs.yml', 'w') {|f| f.write(YAML.dump(@mapping_songs)) } + end + def dump_map + File.open('mapping_songs.yml', 'w') {|f| f.write(YAML.dump(@mapping_songs)) } + end + + def md5(filepath) + Digest::MD5.file(filepath).hexdigest + end + + def tency_original_check + songs = Pathname.new(@originals_folder).children.select { |c| c.directory? } + songs.each do |song| + dirs = Pathname.new(song).children.select {|c| c.directory? } + + @@log.debug "SONG #{song}" + dirs.each do |dir| + @@log.debug "#{dir.basename.to_s}" + end + @@log.debug "" + end + end + + def tency_originals + songs = Pathname.new(@originals_folder).children.select { |c| c.directory? } + songs.each do |filename| + id = parse_id(filename.basename.to_s ) + files = Pathname.new(filename).children.select {|c| c.file? } + + # also look into any 1st level folders we might find + + dirs = Pathname.new(filename).children.select {|c| c.directory? } + dirs.each do |dir| + more_tracks = Pathname.new(dir).children.select {|c| c.file? } + files = files + more_tracks + end + + files.each do |file| + @@log.debug("processing original track #{file.to_s}") + md5 = md5(file.to_s) + song = {md5:md5, filename:file.to_s, id:id} + @original_songs[cache_id(id, md5)] = song + end + end + + end + + def tency_maps + songs = Pathname.new(@mapping_folder).children.select { |c| c.directory? } + songs.each do |song_filename| + id = parse_id_mapped(song_filename.basename.to_s ) + @@log.debug "processing song #{song_filename.to_s}" + + tracks = Pathname.new(song_filename).children.select {|c| c.file? } + tracks.each do |track| + if track.to_s.include? "Stem" + @@log.debug("processing mapped track #{track.to_s}") + md5 = md5(track.to_s) + + song = {md5:md5, filename:track.to_s} + @mapping_songs[cache_id(id, md5)] = song + end + end + end + end + + def cache_id(id, md5) + "#{id}-#{md5}" + end + + def parse_id(filename) + #amy-winehouse_you-know-i-m-no-good-feat-ghostface-killah_11767 + + index = filename.rindex('_') + if index + id = filename[(index + 1)..-1] + + if id.end_with?('/') + id = id[0...-1] + end + + id = id.to_i + + if id == 0 + raise "no valid ID in filename: #{filename}" + end + else + raise "no _ in filename: #{filename}" + end + id + end + + def parse_id_mapped(filename) + #Flyleaf - I'm So Sick - 15771 + + index = filename.rindex('-') + if index + id = filename[(index + 1)..-1] + + if id.end_with?('/') + id = id[0...-1] + end + + id.strip! + + id = id.to_i + + if id == 0 + raise "no valid ID in filename: #{filename}" + end + else + raise "no - in filename: #{filename}" + end + id + end + + + + def tency_originals2 + s3_manager.list_directories('mapper').each do |song_folder| + @@log.debug("searching through tency directory. song folder:'#{song_folder}'") + + id = parse_id(song_folder) + @@log.debug("ID #{id}") + + top_folder = s3_manager.list_directories(song_folder) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 70ef72e6a..f929a8545 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -14,6 +14,7 @@ module JamRuby attr_accessor :name attr_accessor :reason attr_accessor :detail + attr_accessor :storage_format def jamkazam_s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) @@ -23,11 +24,140 @@ module JamRuby @public_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_public, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def initialize(storage_format = 'default') + @storage_format = storage_format + end + def finish(reason, detail) self.reason = reason self.detail = detail end + + # this method was created due to Tency-sourced data having no master track + # it goes through all audio tracks, and creates a master mix from it. (mix + normalize) + def create_master(metadata, metalocation) + + parsed_metalocation = parse_metalocation(metalocation) + + if parsed_metalocation.nil? + finish("invalid_metalocation", metalocation) + return + end + + original_artist = parsed_metalocation[1] + meta_name = parsed_metalocation[2] + + self.name = metadata[:name] || meta_name + + + audio_path = metalocation[0...-"/meta.yml".length] + + all_files = fetch_important_files(audio_path) + + audio_files = [] + master_found = false + all_files.each do |file| + + parsed_wav = parse_file(file) + if parsed_wav[:master] + master_found = true + elsif parsed_wav[:type] == :track + + audio_files << file + end + end + + if master_found + @@log.debug("master exists... skipping #{self.name} ") + finish('success', nil) + return + else + + tracks = [] + + #tmp_dir = Dir.mktmpdir + #tmp_dir = "/var/folders/05/1jpzfcln1hq9p666whnd7chr0000gn/T/d20150809-9945-1ykr85u" + Dir.mktmpdir do |tmp_dir| + @@log.debug("downloading all audio files in #{tmp_dir}") + audio_files.each do |s3_track| + track = File.join(tmp_dir, File.basename(s3_track)) + tracks << track + JamTrackImporter.song_storage_manager.download(s3_track, track) + end + + # first have to check if all are the same sample rate. If not, we have to make it so + + first_sample_rate = nil + normalize_needed = false + tracks.each do |track| + sample_rate = `soxi -r "#{track}"`.strip + + if first_sample_rate.nil? + first_sample_rate = sample_rate + else + if first_sample_rate != sample_rate + # we need to normalize all of them + normalize_needed = true + break + end + end + end + + normalized_tracks = [] + if normalize_needed + tracks.each do |track| + normalized_track = File.join(tmp_dir, 'normalized-' + File.basename(track)) + output = `sox "#{track}" "#{normalized_track}" rate #{first_sample_rate}` + @@log.debug("resampling #{normalized_track}; output: #{output}") + normalized_tracks << normalized_track + end + tracks = normalized_tracks + end + + + + temp_file = File.join(tmp_dir, "temp.wav") + output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav") + output_file = File.join(tmp_dir, output_filename) + command = "sox -m " + tracks.each do |track| + command << " \"#{track}\"" + end + command << " \"#{temp_file}\"" + + @@log.debug("mixing with cmd: " + command) + sox_output = `#{command}` + result_code = $?.to_i + + if result_code != 0 + @@log.error("unable to generate master mix") + finish("sox_master_mix_failure", sox_output) + else + + # now normalize the audio + + command = "sox --norm \"#{temp_file}\" \"#{output_file}\"" + @@log.debug("normalizing with cmd: " + command) + sox_output = `#{command}` + result_code = $?.to_i + if result_code != 0 + @@log.error("unable to normalize master mix") + finish("sox_master_mix_failure", sox_output) + else + + # now we need to upload the output back up + s3_target = audio_path + '/' + output_filename + @@log.debug("uploading #{output_file} to #{s3_target}") + JamTrackImporter.song_storage_manager.upload(s3_target, output_file ) + finish('success', nil) + end + + end + end + end + end + def dry_run(metadata, metalocation) metadata ||= {} @@ -38,35 +168,92 @@ module JamRuby original_artist = parsed_metalocation[1] name = parsed_metalocation[2] + JamTrackImporter.summaries[:unique_artists] << original_artist + success = dry_run_metadata(metadata, original_artist, name) return unless success - dry_run_audio(metadata, "audio/#{original_artist}/#{name}") + audio_path = metalocation[0...-"/meta.yml".length] + + + dry_run_audio(metadata, audio_path) finish("success", nil) end + def is_tency_storage? + assert_storage_set + @storage_format == 'Tency' + end + + def assert_storage_set + raise "no storage_format set" if @storage_format.nil? + end + + def parse_metalocation(metalocation) + # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml + if is_tency_storage? - bits = metalocation.split('/') + suffix = '/meta.yml' - if bits.length != 4 - finish("invalid_metalocation", "metalocation not valid #{metalocation}") - return nil + unless metalocation.end_with? suffix + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + metalocation = metalocation[0...-suffix.length] + + first_path = metalocation.index('/') + if first_path.nil? + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + metalocation = metalocation[(first_path + 1)..-1] + + bits = ['audio'] + # example: Sister Hazel - All For You - 10385 + first_dash = metalocation.index(' - ') + if first_dash + artist = metalocation[0...(first_dash)].strip + bits << artist + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + last_dash = metalocation.rindex('-') + if last_dash + song = metalocation[(first_dash+3)...last_dash].strip + bits << song + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + bits << 'meta.yml' + bits + else + bits = metalocation.split('/') + + if bits.length != 4 + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + if bits[0] != "audio" + finish("invalid_metalocation", "first bit is not 'audio' #{metalocation}") + return nil + end + + if bits[3] != 'meta.yml' + finish('invalid_metalocation', "last bit is not 'meta.yml' #{metalocation}") + return nil + end + + bits end - - if bits[0] != "audio" - finish("invalid_metalocation", "first bit is not 'audio' #{metalocation}") - return nil - end - - if bits[3] != 'meta.yml' - finish('invalid_metalocation', "last bit is not 'meta.yml' #{metalocation}") - return nil - end - - bits end # if you change this, it will (at least without some work )break development usage of jamtracks @@ -91,6 +278,83 @@ module JamRuby true end + def determine_genres(metadata) + + genres = [] + if metadata[:genres] + metadata[:genres].each do |genre| + if genre == 'hard/metal' + genres << Genre.find('hard rock') + genres << Genre.find('metal') + elsif genre == 'christmas' + genres << Genre.find('holiday') + elsif genre == 'alternative' + genres << Genre.find('alternative rock') + elsif genre == '80s' + # swallow + elsif genre == 'love' + # swallow + elsif genre == 'christian' || genre == 'gospel' + genres << Genre.find('religious') + elsif genre == 'punk/grunge' + genres << Genre.find('punk') + elsif genre == 'electro' + genres << Genre.find('electronic') + elsif genre == 'teen pop' + genres << Genre.find('pop') + elsif genre == "rock 'n roll" + genres << Genre.find('rock') + elsif genre == 'zouk/creole' + genres << Genre.find('creole') + elsif genre == 'world/folk' + genres << Genre.find('world') + genres << Genre.find('folk') + elsif genre == 'french pop' + # swallow + elsif genre == 'schlager' + #swallow + elsif genre == 'humour' + # swallow + elsif genre == 'oriental' + genres << genre.find('asian') + else + found = Genre.find_by_id(genre) + genres << found if found + end + + end + end + + genres + end + + def determine_language(metadata) + + found = ISO_639.find_by_code('eng') + + language = metadata[:language] + + if language + language.downcase! + + if language == 'instrumental' + return 'instrumental' + end + + if language.include? 'spanish' + found = ISO_639.find_by_code('spa') + elsif language.include? 'german' + found = ISO_639.find_by_code('ger') + elsif language.include? 'portuguese' + found = ISO_639.find_by_code('por') + elsif language.include? 'english' + found = ISO_639.find_by_code('eng') + end + end + + found[0] # 3 letter code + end + def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) metadata ||= {} @@ -104,14 +368,24 @@ module JamRuby jam_track.metalocation = metalocation jam_track.original_artist = metadata["original_artist"] || original_artist jam_track.name = self.name - jam_track.genre_id = 'rock' + jam_track.additional_info = metadata[:additional_info] + jam_track.year = metadata[:year] + jam_track.genres = determine_genres(metadata) + jam_track.language = determine_language(metadata) jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name) jam_track.price = 1.99 - jam_track.reproduction_royalty_amount = 0 - jam_track.licensor_royalty_amount = 0 + jam_track.reproduction_royalty_amount = nil + jam_track.reproduction_royalty = true + jam_track.public_performance_royalty = true + jam_track.licensor_royalty_amount = 0.4 jam_track.sales_region = 'Worldwide' jam_track.recording_type = 'Cover' jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"." + + if is_tency_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music') + end else if !options[:resync_audio] #@@log.debug("#{self.name} skipped because it already exists in database") @@ -167,12 +441,18 @@ module JamRuby else instrument = 'electric guitar' end - elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' + elsif potential_instrument == 'acoustic' + instrument = 'acoustic guitar' + elsif potential_instrument == 'acoutic guitar' + instrument = 'electric guitar' + elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' instrument = 'electric guitar' elsif potential_instrument == 'keys' instrument = 'keyboard' elsif potential_instrument == 'vocal' || potential_instrument == 'vocals' instrument = 'voice' + elsif potential_instrument == 'upright bass' + instrument = 'double bass' elsif potential_instrument == 'bass' instrument = 'bass guitar' elsif potential_instrument == 'drum' @@ -185,8 +465,9 @@ module JamRuby else part = 'Sound FX' end - - + elsif potential_instrument == 'computer scratches' + instrument = 'computer' + part = 'Scratches' elsif potential_instrument == "sax" instrument = 'saxophone' elsif potential_instrument == "vocal back up" @@ -213,18 +494,43 @@ module JamRuby elsif potential_instrument == 'fretless bass' instrument = 'bass guitar' part = 'Fretless' + elsif potential_instrument == 'lap steel' || potential_instrument == 'pedal steel' + instrument = 'steel guitar' elsif potential_instrument == 'clock percussion' instrument = 'computer' part = 'Clock' - elsif potential_instrument == 'horns' + elsif potential_instrument == 'horns' || potential_instrument == 'horn' instrument = 'other' - part = 'Horns' + part = 'Horns' if potential_part.nil? + elsif potential_instrument == 'english horn' + instrument = 'other' + part = 'English Horn' + elsif potential_instrument == 'bass clarinet' + instrument = 'other' + part = 'Bass Clarinet' + elsif potential_instrument == 'recorder' + instrument = 'other' + part = 'Recorder' + elsif potential_instrument == 'marimba' + instrument = 'keyboard' + part = 'Marimba' elsif potential_instrument == 'strings' - instrument = 'other' + instrument = 'orchestra' part = 'Strings' - elsif potential_instrument == 'orchestration' - instrument = 'computer' - part = 'Orchestration' + elsif potential_instrument == 'celesta' + instrument = 'keyboard' + elsif potential_instrument == 'balalaika' + instrument = 'other' + part = 'Balalaika' + elsif potential_instrument == 'tanpura' + instrument = 'other' + part = 'Tanpura' + elsif potential_instrument == 'quena' + instrument = 'other' + part = 'Quena' + elsif potential_instrument == 'bouzouki' + instrument = 'other' + part = 'Bouzouki' elsif potential_instrument == 'claps' || potential_instrument == 'hand claps' instrument = 'computer' part = 'Claps' @@ -246,20 +552,44 @@ module JamRuby end - def parse_wav(file) + def parse_file(file) bits = file.split('/') filename = bits[bits.length - 1] # remove all but just the filename filename_no_ext = filename[0..-5] comparable_filename = filename_no_ext.downcase # remove .wav + type = nil master = false instrument = nil part = nil + precount_num = nil + no_precount_detail = nil + if comparable_filename == "click" || comparable_filename.include?("clicktrack") + if filename.end_with?('.txt') + type = :clicktxt + else + type = :clickwav + end + elsif comparable_filename.include? "precount" + type = :precount + index = comparable_filename.index('precount') + precount = comparable_filename[(index + 'precount'.length)..-1].strip + if precount.start_with?('_') + precount = precount[1..-1] + end + if precount.to_i == 0 + no_precount_detail = comparable_filename + else + precount_num = precount.to_i + end - if comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") + + elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") master = true + type = :master else + type = :track stem_location = comparable_filename.index('stem -') unless stem_location stem_location = comparable_filename.index('stems -') @@ -294,72 +624,209 @@ module JamRuby result = determine_instrument(possible_instrument, possible_part) instrument = result[:instrument] part = result[:part] + else + if is_tency_storage? + # we can check to see if we can find mapping info for this filename + mapping = JamTrackImporter.tency_mapping[filename.downcase] + + if mapping && mapping[:trust] + instrument = mapping[:instrument] + part = mapping[:part] + end + + # tency mapping didn't work; let's retry with our own home-grown mapping + if instrument.nil? && !possible_instrument.nil? + result = determine_instrument(possible_instrument, possible_part) + instrument = result[:instrument] + part = result[:part] + end + end end + end - {filename: filename, master: master, instrument: instrument, part: part} + {filename: filename, master: master, instrument: instrument, part: part, type: type, precount_num: precount_num, no_precount_detail: no_precount_detail} end def dry_run_audio(metadata, s3_path) - all_files = fetch_wav_files(s3_path) + all_files = fetch_important_files(s3_path) all_files.each do |file| - if file.end_with?('.wav') - parsed_wav = parse_wav(file) - if parsed_wav[:master] - @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - else - if !parsed_wav[:instrument] || !parsed_wav[:part] - @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - else - @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + + # ignore click/precount + parsed_wav = parse_file(file) + if parsed_wav[:master] + @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + elsif parsed_wav[:type] == :track + + JamTrackImporter.summaries[:total_tracks] += 1 + + if parsed_wav[:instrument].nil? + detail = JamTrackImporter.summaries[:no_instrument_detail] + file_detail = detail[parsed_wav[:filename].downcase] + if file_detail.nil? + detail[parsed_wav[:filename].downcase] = 0 end + detail[parsed_wav[:filename].downcase] += 1 + + JamTrackImporter.summaries[:no_instrument] += 1 end + + JamTrackImporter.summaries[:no_part] += 1 if parsed_wav[:part].nil? + + if !parsed_wav[:instrument] || !parsed_wav[:part] + @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + else + @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + end + elsif parsed_wav[:type] == :clickwav + + elsif parsed_wav[:type] == :clicktxt + + elsif parsed_wav[:type] == :precount + if parsed_wav[:precount_num].nil? + JamTrackImporter.summaries[:no_precount_num] += 1 + JamTrackImporter.summaries[:no_precount_detail] << parsed_wav[:no_precount_detail] + end + else - @@log.debug("#{self.name} ignoring non-wav file #{file}") + JamTrackImporter.summaries[:unknown_filetype] += 1 end end - end - def sort_tracks(tracks) - def set_custom_weight(track) - weight = 5 - # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end + def set_custom_weight(track) - if track.persisted? - weight = track.position + slop = 800 + + instrument_weight = nil + # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end + + if track.persisted? + instrument_weight = track.position + else + if track.instrument_id == 'voice' + + if track.part && track.part.start_with?('Lead') + instrument_weight = 100 + elsif track.part && track.part.start_with?('Backing') + instrument_weight = 110 + else + instrument_weight = 120 + end + + elsif track.instrument_id == 'drums' + + if track.part && track.part == 'Drums' + instrument_weight = 150 + elsif track.part && track.part == 'Percussion' + instrument_weight = 160 + else + instrument_weight = 170 + end + + elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass' + instrument_weight = 180 + + elsif track.instrument_id == 'piano' && track.part && track.part == 'Piano' + instrument_weight = 250 + + elsif track.instrument_id == 'keyboard' + + if track.part && track.part.start_with?('Synth') + instrument_weight = 260 + elsif track.part && track.part.start_with?('Pads') + instrument_weight = 270 + else + instrument_weight = 280 + end + + elsif track.instrument_id == 'acoustic guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 300 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 310 + else + instrument_weight = 320 + end + elsif track.instrument_id == 'electric guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 400 + elsif track.part && track.part.start_with?('Solo') + instrument_weight = 410 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 420 + else + instrument_weight = 440 + end else - case track.instrument_id - when 'electric guitar' - weight = 100 - when 'acoustic guitar' - weight = 200 - when 'drums' - weight = 300 - when 'keys' - weight = 400 - when 'computer' - weight = 600 - else - weight = 500 - end - if track.track_type == 'Master' - weight = 1000 - end + instrument_weight = slop end - - weight + if track.track_type == 'Master' + instrument_weight = 1000 + end end + + instrument_weight + end + + def deduplicate_parts(tracks) + unique_instruments = {} + + tracks.each do |track| + + key = "#{track.instrument_id} | #{track.part}" + found = unique_instruments[key] + if !found + found = [] + unique_instruments[key] = found + end + + found << track + end + + unique_instruments.each do |key, value| + if value.length > 1 + count = 0 + + value.each do |track| + if track.part.nil? + track.part = (count + 1).to_s + else + track.part = "#{track.part} #{count + 1}" + end + count += 1 + end + + end + end + + # debug output + tracks.each do |track| + puts "TRACK #{track.instrument_id} #{track.part}" + end + end + + + def sort_tracks(tracks) + sorted_tracks = tracks.sort do |a, b| a_weight = set_custom_weight(a) b_weight = set_custom_weight(b) - a_weight <=> b_weight + if a_weight != b_weight + a_weight <=> b_weight + elsif a.instrument_id != b.instrument_id + a.instrument_id <=> b.instrument_id + else + a_part = a.part + b_part = b.part + a_part <=> b_part + end end # default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track @@ -387,9 +854,10 @@ module JamRuby attempt_to_match_existing_tracks = true # find all wav files in the JamTracks s3 bucket - wav_files = fetch_wav_files(s3_path) + wav_files = fetch_important_files(s3_path) tracks = [] + addt_files = [] wav_files.each do |wav_file| @@ -419,27 +887,51 @@ module JamRuby @@log.debug("no existing track found; creating a new one") track = JamTrackTrack.new + track.original_filename = wav_file track.original_audio_s3_path = wav_file - parsed_wav = parse_wav(wav_file) + file = JamTrackFile.new + file.original_filename = wav_file + file.original_audio_s3_path = wav_file + parsed_wav = parse_file(wav_file) + + unknowns = 0 if parsed_wav[:master] track.track_type = 'Master' - track.part = 'Master' + track.part = 'Master Mix' + track.instrument_id = 'computer' + tracks << track @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - else + elsif parsed_wav[:type] == :track + if !parsed_wav[:instrument] || !parsed_wav[:part] @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + unknowns += 1 else @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") end + track.instrument_id = parsed_wav[:instrument] || 'other' track.track_type = 'Track' - track.part = parsed_wav[:part] || 'Other' + track.part = parsed_wav[:part] || "Other #{unknowns}" + tracks << track + elsif parsed_wav[:type] == :clicktxt + file.file_type = 'ClickTxt' + addt_files << file + elsif parsed_wav[:type] == :clickwav + file.file_type = 'ClickWav' + addt_files << file + elsif parsed_wav[:type] == :precount + file.file_type = 'Precount' + file.precount_num = parsed_wav[:precount_num] + addt_files << file + else + finish("unknown_file_type", "unknown file type #{wave_file}") + return false end - tracks << track end jam_track.jam_track_tracks.each do |jam_track_track| @@ -450,10 +942,20 @@ module JamRuby end end + jam_track.jam_track_files.each do |jam_track_file| + unless addt_files.include?(jam_track_file) + @@log.info("destroying removed JamTrackFile #{jam_track_file.inspect}") + jam_track_file.destroy # should also delete s3 files associated with this jamtrack + end + end + @@log.info("sorting tracks") tracks = sort_tracks(tracks) + deduplicate_parts(tracks) + jam_track.jam_track_tracks = tracks + jam_track.jam_track_files = addt_files saved = jam_track.save @@ -505,7 +1007,7 @@ module JamRuby wav_file = File.join(tmp_dir, basename) # bring the original wav file down from S3 to local file system - JamTrackImporter::s3_manager.download(track.original_audio_s3_path, wav_file) + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) sample_rate = `soxi -r "#{wav_file}"`.strip @@ -553,7 +1055,10 @@ module JamRuby if !preview_succeeded return false end + elsif track.track_type == 'Track' + synchronize_track_preview(track, tmp_dir, ogg_44100) end + end track.save! @@ -582,6 +1087,68 @@ module JamRuby true end + def synchronize_track_preview(track, tmp_dir, ogg_44100) + + out_wav = File.join(tmp_dir, 'stripped.wav') + + burp_gaps = ['0.3', '0.2', '0.1', '0.05'] + + total_time_command = "soxi -D \"#{ogg_44100}\"" + total_time = `#{total_time_command}`.to_f + + result_code = -20 + stripped_time = total_time # default to the case where we just start the preview at the beginning + + burp_gaps.each do |gap| + command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%" + + @@log.debug("stripping silence: " + command_strip_lead_silence) + + output = `#{command_strip_lead_silence}` + + result_code = $?.to_i + + if result_code == 0 + stripped_time_command = "soxi -D \"#{out_wav}\"" + stripped_time_test = `#{stripped_time_command}`.to_f + + if stripped_time_test < 1 # meaning a very short duration + @@log.warn("could not determine the start of non-silencea. assuming beginning") + stripped_time = total_time # default to the case where we just start the preview at the beginning + else + stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break + break + end + else + @@log.warn("unable to determine silence for jam_track #{track.original_filename}, #{output}") + stripped_time = total_time # default to the case where we just start the preview at the beginning + end + + end + + preview_start_time = total_time - stripped_time + + # this is in seconds; convert to integer milliseconds + preview_start_time = (preview_start_time * 1000).to_i + + preview_start_time = nil if preview_start_time < 0 + + track.preview_start_time = preview_start_time + + if track.preview_start_time + @@log.debug("determined track start time to be #{track.preview_start_time}") + else + @@log.debug("determined track start time to be #{track.preview_start_time}") + end + + track.process_preview(ogg_44100, tmp_dir) if track.preview_start_time + + if track.preview_generate_error + @@log.warn(track.preview_generate_error) + end + + end + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) begin @@ -639,12 +1206,12 @@ module JamRuby end def fetch_all_files(s3_path) - JamTrackImporter::s3_manager.list_files(s3_path) + JamTrackImporter::song_storage_manager.list_files(s3_path) end - def fetch_wav_files(s3_path) + def fetch_important_files(s3_path) files = fetch_all_files(s3_path) - files.select { |file| file.end_with?('.wav') } + files.select { |file| file.end_with?('.wav') || file.end_with?('.txt') } end def synchronize(jam_track, metadata, metalocation, options) @@ -664,7 +1231,9 @@ module JamRuby return unless success - synchronized_audio = synchronize_audio(jam_track, metadata, "audio/#{original_artist}/#{name}", options[:skip_audio_upload]) + audio_path = metalocation[0...-"/meta.yml".length] + + synchronized_audio = synchronize_audio(jam_track, metadata, audio_path, options[:skip_audio_upload]) return unless synchronized_audio @@ -673,6 +1242,9 @@ module JamRuby finish("success", nil) end + # do a last check on any problems with the jamtrack + jam_track.sync_onboarding_exceptions + end def synchronize_recurly(jam_track) @@ -690,16 +1262,205 @@ module JamRuby class << self + attr_accessor :storage_format + attr_accessor :tency_mapping + attr_accessor :tency_metadata + attr_accessor :summaries + + def report_summaries + @@log.debug("SUMMARIES DUMP") + @@log.debug("--------------") + @summaries.each do |k, v| + + if k == :no_instrument_detail + @@log.debug("#{k}: #{v}") + elsif k == :no_precount_detail + v.each do |precount_detail| + @@log.debug("precount: #{precount_detail}") + end + elsif k == :unique_artists + v.each do |artist| + @@log.debug("artist: #{artist}") + end + else + @@log.debug("#{k}: #{v}") + end + end + end + + def song_storage_manager + if is_tency_storage? + tency_s3_manager + else + s3_manager + end + end + + def summaries + @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new} + end + + def tency_s3_manager + @tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end def private_s3_manager - @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + @private_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def extract_tency_song_id(metalocation) + # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml + + first_path = metalocation.index('/') + return nil unless first_path + metalocation = metalocation[(first_path + 1)..-1] + + suffix = '/meta.yml' + metalocation = metalocation[0...-suffix.length] + + last_dash = metalocation.rindex('-') + return nil if last_dash.nil? + + id = metalocation[(last_dash+1)..-1].strip + + return nil if id.to_i == 0 + + id + end + + def is_tency_storage? + assert_storage_set + @storage_format == 'Tency' + end + + def assert_storage_set + raise "no storage_format set" if @storage_format.nil? + end + + def iterate_tency_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + count += 1 + #break if count > 100 + + end + end + + def iterate_default_song_storage(&blk) + song_storage_manager.list_directories('audio').each do |original_artist| + @@log.debug("searching through artist directory '#{original_artist}'") + + songs = song_storage_manager.list_directories(original_artist) + songs.each do |song| + @@log.debug("searching through song directory' #{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + end + end + end + + def iterate_song_storage(&blk) + if is_tency_storage? + iterate_tency_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + else + iterate_default_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + end + end def dry_run + iterate_song_storage do |metadata, metalocation| + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.dry_run(metadata, metalocation) + end + + report_summaries + end + + # figure out which songs are in S3 that do not exist in the 2k spreadsheet (mapping.csv), and which songs are in the 2k spreadsheet that are not in S3 + def tency_delta + in_s3 = {} + in_mapping = {} + + load_tency_mappings + + JamTrackImporter.tency_metadata.each do |song_id, metadata| + in_mapping[song_id] = {artist: metadata[:original_artist], song: metadata[:name]} + end + + iterate_song_storage do |metadata, metalocation| + + importer = JamTrackImporter.new(@storage_format) + song_id = JamTrackImporter.extract_tency_song_id(metalocation) + parsed_metalocation = importer.parse_metalocation(metalocation) + + next if song_id.nil? + next if parsed_metalocation.nil? + + original_artist = parsed_metalocation[1] + meta_name = parsed_metalocation[2] + + in_s3[song_id] = {artist: original_artist, song: meta_name} + end + + in_s3_keys = Set.new(in_s3.keys) + in_mapping_keys = Set.new(in_mapping.keys) + only_in_mapping = in_mapping_keys - in_s3_keys + only_in_s3 = in_s3_keys - in_mapping_keys + + CSV.open("only_in_s3.csv", "wb") do |csv| + only_in_s3.each do |song_id| + csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ] + end + end + + CSV.open("only_in_2k_selection.csv", "wb") do |csv| + only_in_mapping.each do |song_id| + csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ] + end + end + + end + def create_masters + iterate_song_storage do |metadata, metalocation| + next if metadata.nil? + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.create_master(metadata, metalocation) + end + end + + def create_master(path) + metalocation = "#{path}/meta.yml" + + metadata = load_metalocation(metalocation) + + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.create_master(metadata, metalocation) + end + + def dry_run_original s3_manager.list_directories('audio').each do |original_artist| @@log.debug("searching through artist directory '#{original_artist}'") @@ -906,23 +1667,35 @@ module JamRuby end end + def remove_s3_special_chars(filename) + filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '') + end + def onboarding_exceptions + JamTrack.all.each do |jam_track| + jam_track.onboarding_exceptions + end + end def synchronize_all(options) importers = [] - s3_manager.list_directories('audio').each do |original_artist| - @@log.debug("searching through artist directory '#{original_artist}'") + count = 0 + iterate_song_storage do |metadata, metalocation| - songs = s3_manager.list_directories(original_artist) - songs.each do |song| - @@log.debug("searching through song directory' #{song}'") + next if metadata.nil? && is_tency_storage? - metalocation = "#{song}meta.yml" + importer = synchronize_from_meta(metalocation, options) + importers << importer - importer = synchronize_from_meta(metalocation, options) - importers << importer + if importer.reason != 'jam_track_exists' + count+=1 + end + + if count > 500 + break end end + @@log.info("SUMMARY") @@log.info("-------") importers.each do |importer| @@ -956,12 +1729,113 @@ module JamRuby end end + def genre_dump + load_tency_mappings + + genres = {} + @tency_metadata.each do |id, value| + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + genre4 = value[:genre4] + genre5 = value[:genre5] + + genres[genre1.downcase.strip] = genre1.downcase.strip if genre1 + genres[genre2.downcase.strip] = genre2.downcase.strip if genre2 + genres[genre3.downcase.strip] = genre3.downcase.strip if genre3 + genres[genre4.downcase.strip] = genre4.downcase.strip if genre4 + genres[genre5.downcase.strip] = genre5.downcase.strip if genre5 + end + + all_genres = Genre.select(:id).all.map(&:id) + + all_genres = Set.new(all_genres) + genres.each do |genre, value| + found = all_genres.include? genre + + puts "#{genre}" unless found + end + end + + def load_tency_mappings + Dir.mktmpdir do |tmp_dir| + mapping_file = File.join(tmp_dir, 'mapping.csv') + metadata_file = File.join(tmp_dir, 'metadata.csv') + + # this is a developer option to skip the download and look in the CWD to grab mapping.csv and metadata.csv + if ENV['TENCY_ALREADY_DOWNLOADED'] == '1' + mapping_file = 'mapping.csv' + metadata_file = 'metadata.csv' + else + tency_s3_manager.download('mapping/mapping.csv', mapping_file) + tency_s3_manager.download('mapping/metadata.csv', metadata_file) + end + + mapping_csv = CSV.read(mapping_file) + metadata_csv = CSV.read(metadata_file, headers: true, return_headers: false) + + @tency_mapping = {} + @tency_metadata = {} + # convert both to hashes + mapping_csv.each do |line| + @tency_mapping[line[0].strip] = {instrument: line[1], part: line[2], count: line[3], trust: line[4]} + end + + metadata_csv.each do |line| + @tency_metadata[line[0].strip] = {id: line[0].strip, original_artist: line[1], name: line[2], additional_info: line[3], year: line[4], language: line[5], isrc: line[10], genre1: line[11], genre2: line[12], genre3: line[13], genre4: line[14], genre5: line[15]} + end + + + @tency_metadata.each do |id, value| + + genres = [] + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + genre4 = value[:genre4] + genre5 = value[:genre5] + + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + genres << genre4.downcase.strip if genre4 + genres << genre5.downcase.strip if genre5 + + value[:genres] = genres + end + + + end + end + def load_metalocation(metalocation) - begin - data = s3_manager.read_all(metalocation) - return YAML.load(data) - rescue AWS::S3::Errors::NoSuchKey - return nil + + if is_tency_storage? + load_tency_mappings if @tency_mapping.nil? + song_id = extract_tency_song_id(metalocation) + + if song_id.nil? + puts "missing_song_id #{metalocation}" + return nil + end + + + tency_data = @tency_metadata[song_id] + + if tency_data.nil? + @@log.warn("missing tency metadata '#{song_id}'") + end + + return tency_data + else + begin + data = s3_manager.read_all(metalocation) + return YAML.load(data) + rescue AWS::S3::Errors::NoSuchKey + return nil + end end end @@ -975,7 +1849,7 @@ module JamRuby end def sync_from_metadata(jam_track, meta, metalocation, options) - jam_track_importer = JamTrackImporter.new + jam_track_importer = JamTrackImporter.new(@storage_format) JamTrack.transaction do #begin @@ -998,6 +1872,9 @@ module JamRuby meta = load_metalocation(metalocation) + if meta.nil? && is_tency_storage? + raise "no tency song matching this metalocation #{metalocation}" + end jam_track_importer = nil if jam_track @@log.debug("jamtrack #{jam_track.name} located by metalocation") diff --git a/ruby/lib/jam_ruby/lib/stats.rb b/ruby/lib/jam_ruby/lib/stats.rb index c51a14ae1..697965526 100644 --- a/ruby/lib/jam_ruby/lib/stats.rb +++ b/ruby/lib/jam_ruby/lib/stats.rb @@ -40,7 +40,7 @@ module JamRuby class Stats class << self - attr_accessor :client, :host + attr_accessor :client, :host, :ignore @@log = Logging.logger[JamRuby::Stats] end @@ -52,6 +52,7 @@ module JamRuby end def self.init(options) + influxdb_database = options[:influxdb_database] influxdb_username = options[:influxdb_username] influxdb_password = options[:influxdb_password] @@ -71,12 +72,15 @@ module JamRuby retry: -1 @host = `hostname`.strip else + self.ignore = true @@log.debug("stats client not initiated") end end def self.write(name, data) + return if self.ignore # doing any writes in a test environment cause annoying puts to occur + if @client && data && data.length > 0 data['host'] = @host data['time'] = Time.now.to_i diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index 47b5cfe97..2ba858c6e 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -3,10 +3,10 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:biography, :website, :name] - attr_accessible :name, :website, :biography, :city, :state, + attr_accessible :name, :website, :biography, :city, :state, :country, :original_fpfile_photo, :cropped_fpfile_photo, :cropped_large_fpfile_photo, :cropped_s3_path_photo, :cropped_large_s3_path_photo, :crop_selection_photo, :photo_url, :large_photo_url, - :band_type, :band_status, :concert_count, :add_new_members, :play_commitment, :touring_option, :paid_gigs, + :band_type, :band_status, :concert_count, :add_new_members, :play_commitment, :touring_option, :paid_gigs, :free_gigs, :hourly_rate, :gig_minimum attr_accessor :updating_photo, :skip_location_validation, :skip_genre_validation @@ -23,6 +23,8 @@ module JamRuby validate :validate_photo_info validate :require_at_least_one_genre, :unless => :skip_genre_validation validate :limit_max_genres + validates_numericality_of :hourly_rate, greater_than:0, less_than:100000, :if => :paid_gigs + validates_numericality_of :gig_minimum, greater_than:0, less_than:200000, :if => :paid_gigs before_save :check_lat_lng before_save :check_website_url @@ -47,10 +49,10 @@ module JamRuby has_many :recordings, :class_name => "JamRuby::Recording", :foreign_key => "band_id", dependent: :destroy # self.id = likable_id in likes table - has_many :likers, :as => :likable, :class_name => "JamRuby::Like", :dependent => :destroy, dependent: :destroy + has_many :likers, :as => :likable, :class_name => "JamRuby::Like", :dependent => :destroy # self.id = followable_id in follows table - has_many :followers, :as => :followable, :class_name => "JamRuby::Follow", :dependent => :destroy, dependent: :destroy + has_many :followers, :as => :followable, :class_name => "JamRuby::Follow", :dependent => :destroy # invitations has_many :invitations, :inverse_of => :band, :class_name => "JamRuby::BandInvitation", :foreign_key => "band_id", dependent: :destroy @@ -180,7 +182,7 @@ module JamRuby band = id.blank? ? Band.new : Band.find(id) # ensure user updating Band details is a Band member - unless band.new_record? || band.users.exists?(user) + unless band.new_record? || band.users.exists?(user) raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end @@ -193,15 +195,28 @@ module JamRuby band.photo_url = params[:photo_url] if params.has_key?(:photo_url) band.logo_url = params[:logo_url] if params.has_key?(:logo_url) - if params.has_key?(:genres) && params[:genres] + band.paid_gigs = params[:paid_gigs] if params.has_key?(:paid_gigs) + band.free_gigs = params[:free_gigs] if params.has_key?(:free_gigs) + band.hourly_rate = (params.has_key?(:hourly_rate) && params[:hourly_rate].to_i > 0) ? params[:hourly_rate] : nil + band.gig_minimum = (params.has_key?(:gig_minimum) && params[:hourly_rate].to_i > 0) ? params[:gig_minimum] : nil + band.add_new_members = params[:add_new_members] if params.has_key?(:add_new_members) + band.touring_option = params[:touring_option] if params.has_key?(:touring_option) + band.band_type = params[:band_type] if params.has_key?(:band_type) + band.band_status = params[:band_status] if params.has_key?(:band_status) + band.concert_count = params[:concert_count] if params.has_key?(:concert_count) + band.play_commitment = params[:play_commitment] if params.has_key?(:play_commitment) + + if params[:validate_genres] || params[:genres].present? # loop through each genre in the array and save to the db genres = [] - params[:genres].each { |genre_id| genres << Genre.find(genre_id) } + params[:genres].each { |genre_id| genres << Genre.find(genre_id) } if params[:genres].present? band.genres = genres + band.skip_genre_validation = false + else + params[:validate_genres] + band.skip_genre_validation = true end - band.skip_genre_validation = true unless params[:validate_genres] - unless band.new_record? OnlinePresence.delete_all(["player_id = ?", band.id]) PerformanceSample.delete_all(["player_id = ?", band.id]) @@ -210,7 +225,7 @@ module JamRuby online_presences = params[:online_presences] if online_presences.present? online_presences.each do |op| - new_presence = OnlinePresence.create(band, op, false) + new_presence = OnlinePresence.create(band, op, false) band.online_presences << new_presence end end @@ -218,15 +233,15 @@ module JamRuby performance_samples = params[:performance_samples] if performance_samples.present? performance_samples.each do |ps| - band.performance_samples << PerformanceSample.create(band, ps, false) + band.performance_samples << PerformanceSample.create(band, ps, false) end end - + band end # helper method for creating / updating a Band - def self.save(user, params) + def self.save(user, params) band = build_band(user, params) if band.save @@ -284,7 +299,7 @@ module JamRuby def check_lat_lng if (city_changed? || state_changed? || country_changed?) - update_lat_lng + update_lat_lng end true end diff --git a/ruby/lib/jam_ruby/models/calendar.rb b/ruby/lib/jam_ruby/models/calendar.rb new file mode 100644 index 000000000..4f244b587 --- /dev/null +++ b/ruby/lib/jam_ruby/models/calendar.rb @@ -0,0 +1,14 @@ +module JamRuby + class Calendar < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :description] + attr_accessible :name, :description, :target_uid, :trigger_delete, :start_at, :end_at + + @@log = Logging.logger[Calendar] + + self.table_name = "calendars" + self.primary_key = 'id' + + belongs_to :user, :class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :calendars + end +end diff --git a/ruby/lib/jam_ruby/models/email_batch_scheduled_sessions.rb b/ruby/lib/jam_ruby/models/email_batch_scheduled_sessions.rb index d388bce9e..68dfb9fb0 100644 --- a/ruby/lib/jam_ruby/models/email_batch_scheduled_sessions.rb +++ b/ruby/lib/jam_ruby/models/email_batch_scheduled_sessions.rb @@ -200,7 +200,7 @@ SELECT tmp_candidate_sessions.creator_score_idx AS creator_score_idx INTO TEMP TABLE tmp_candidate_recipients FROM users -INNER JOIN musicians_instruments AS mi ON mi.user_id = users.id +INNER JOIN musicians_instruments AS mi ON mi.player_id = users.id INNER JOIN tmp_candidate_sessions ON tmp_candidate_sessions.is_unstructured_rsvp = TRUE OR (tmp_candidate_sessions.open_rsvps = TRUE AND tmp_candidate_sessions.instrument_id = mi.instrument_id) OR tmp_candidate_sessions.invited_user_id = users.id diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 1b0cd9ada..91d80f755 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -16,7 +16,8 @@ module JamRuby has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres" # jam tracks - has_many :jam_tracks, :class_name => "JamRuby::JamTrack" + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "genre_id" + has_many :jam_tracks, :through => :genres_jam_tracks, :class_name => "JamRuby::JamTrack", :source => :genre def to_s description diff --git a/ruby/lib/jam_ruby/models/genre_jam_track.rb b/ruby/lib/jam_ruby/models/genre_jam_track.rb new file mode 100644 index 000000000..aa05e4fd8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/genre_jam_track.rb @@ -0,0 +1,8 @@ +module JamRuby + class GenreJamTrack < ActiveRecord::Base + + self.table_name = 'genres_jam_tracks' + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + belongs_to :genre, class_name: 'JamRuby::Genre' + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 8ab120c62..244cbc6f1 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -14,12 +14,12 @@ module JamRuby attr_accessor :uploading_preview attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type, - :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, + :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genres_jam_tracks_attributes, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin + :jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin - validates :name, presence: true, uniqueness: true, length: {maximum: 200} + validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } validates :description, length: {maximum: 1000} validates :time_signature, inclusion: {in: [nil] + [''] + TIME_SIGNATURES} # the empty string is needed because of activeadmin @@ -39,14 +39,17 @@ module JamRuby validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :duration, numericality: {only_integer: true}, :allow_nil => true - validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/ - validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,3}$/ + validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true + validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true - belongs_to :genre, class_name: "JamRuby::Genre" - belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' + belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks + + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id" + has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC' has_many :jam_track_tap_ins, :class_name => "JamRuby::JamTrackTapIn", order: 'offset_time ASC' + has_many :jam_track_files, :class_name => "JamRuby::JamTrackFile" has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" # ' @@ -67,6 +70,82 @@ module JamRuby accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true + + # we can make sure a few things stay in sync here. + # 1) the reproduction_royalty_amount has to stay in sync based on duration + # 2) the onboarding_exceptions JSON column + after_save :sync_reproduction_royalty + after_save :sync_onboarding_exceptions + + + def sync_reproduction_royalty + + # reproduction royalty table based on duration + + # The statutory mechanical royalty rate for permanent digital downloads is: + # 9.10¢ per copy for songs 5 minutes or less, or + # 1.75¢ per minute or fraction thereof, per copy for songs over 5 minutes. + # So the base rate is 9.1 cents for anything up to 5 minutes. + # 5.01 to 6 minutes should be 10.5 cents. + # 6.01 to 7 minutes should be 12.25 cents. + # Etc. + + royalty = nil + if self.duration + minutes = (self.duration - 1) / 60 + extra_minutes = minutes - 4 + extra_minutes = 0 if extra_minutes < 0 + royalty = (0.091 + (0.0175 * extra_minutes)).round(5) + end + self.update_column(:reproduction_royalty_amount, royalty) + + true + end + + def sync_onboarding_exceptions + + exceptions = {} + if self.duration.nil? + exceptions[:no_duration] = true + end + + if self.genres.count == 0 + exceptions[:no_genres] = true + end + + if self.year.nil? + exceptions[:no_year] = true + end + + if self.licensor.nil? + exceptions[:no_licensor] = true + end + + if self.missing_instrument_info? + exceptions[:unknown_instrument] = true + end + + if self.master_track.nil? + exceptions[:no_master] = true + end + + if missing_previews? + exceptions[:missing_previews] = true + end + + if duplicate_positions? + exceptions[:duplicate_positions] = true + end + + if exceptions.keys.length == 0 + self.update_column(:onboarding_exceptions, nil) + else + self.update_column(:onboarding_exceptions, exceptions.to_json) + end + + true + end + def duplicate_positions? counter = {} jam_track_tracks.each do |track| @@ -87,6 +166,17 @@ module JamRuby duplicate end + def missing_instrument_info? + missing_instrument_info = false + self.jam_track_tracks.each do |track| + if track.instrument_id == 'other' && (track.part == nil || track.part.start_with?('Other')) + missing_instrument_info = true + break + end + end + missing_instrument_info + end + def missing_previews? missing_preview = false self.jam_track_tracks.each do |track| @@ -171,7 +261,7 @@ module JamRuby end if options[:group_artist] - query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id") + query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') else @@ -180,7 +270,12 @@ module JamRuby end query = query.where("jam_tracks.status = ?", 'Production') unless user.admin - query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + + unless options[:genre].blank? + query = query.joins(:genres) + query = query.where('genre_id = ? ', options[:genre]) + end + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? @@ -231,7 +326,12 @@ module JamRuby query = query.order('jam_tracks.original_artist') query = query.where("jam_tracks.status = ?", 'Production') unless user.admin - query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + + unless options[:genre].blank? + query = query.joins(:genres) + query = query.where('genre_id = ? ', options[:genre]) + end + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? diff --git a/ruby/lib/jam_ruby/models/jam_track_file.rb b/ruby/lib/jam_ruby/models/jam_track_file.rb new file mode 100644 index 000000000..e7c880165 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_file.rb @@ -0,0 +1,78 @@ +module JamRuby + + # holds a click track or precount file + class JamTrackFile < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + # there should only be one Master per JamTrack, but there can be N Track per JamTrack + FILE_TYPE = %w{ClickWav ClickTxt Precount} + + @@log = Logging.logger[JamTrackFile] + + before_destroy :delete_s3_files + + attr_accessible :jam_track_id, :file_type, :filename, as: :admin + attr_accessible :url, :md5, :length, as: :admin + + attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error + + before_destroy :delete_s3_files + + validates :file_type, inclusion: {in: FILE_TYPE } + + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + + # create storage directory that will house this jam_track, as well as + def store_dir + "jam_track_files" + end + + # create name of the file + def filename(original_name) + "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + end + + def manually_uploaded_filename + if click_wav? + filename('click.wav') + elsif click_txt? + filename('click.txt') + elsif precount? + filename('precount.wav') + else + raise 'unknown file type: ' + file_type + end + + end + + def click_wav? + track_type == 'ClickWav' + end + + def click_txt? + track_type == 'ClickTxt' + end + + def precount? + track_type == 'Precount' + end + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[url], {:expires => expiration_time, :response_content_type => 'audio/wav', :secure => true}) + end + + def can_download?(user) + # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? + jam_track.owners.include?(user) + end + + + def delete_s3_files + s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url]) + end + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_licensor.rb b/ruby/lib/jam_ruby/models/jam_track_licensor.rb index d5ee3df75..e06e292bc 100644 --- a/ruby/lib/jam_ruby/models/jam_track_licensor.rb +++ b/ruby/lib/jam_ruby/models/jam_track_licensor.rb @@ -1,6 +1,8 @@ module JamRuby class JamTrackLicensor < ActiveRecord::Base + table_name = 'jam_track_licensors' + attr_accessible :name, :description, :attention, :address_line_1, :address_line_2, :city, :state, :zip_code, :contact, :email, :phone, as: :admin @@ -16,6 +18,6 @@ module JamRuby validates :email, length: {maximum: 200} validates :phone, length: {maximum: 200} - has_many :jam_tracks, :class_name => "JamRuby::JamTrack", foreign_key: 'licensor_id' + has_many :jam_tracks, :class_name => "JamRuby::JamTrack", foreign_key: 'licensor_id', :inverse_of => :licensor end end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index e20469076..c99246876 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -131,81 +131,19 @@ module JamRuby end + def generate_preview begin Dir.mktmpdir do |tmp_dir| input = File.join(tmp_dir, 'in.ogg') - output = File.join(tmp_dir, 'out.ogg') - output_mp3 = File.join(tmp_dir, 'out.mp3') - - start = self.preview_start_time.to_f / 1000 - stop = start + 20 raise 'no track' unless self["url_44"] s3_manager.download(self.url_by_sample_rate(44), input) - command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" - - @@log.debug("trimming using: " + command) - - sox_output = `#{command}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute cut command #{sox_output}" - else - # now create mp3 off of ogg preview - - convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - - @@log.debug("converting to mp3 using: " + convert_mp3_cmd) - - convert_output = `#{convert_mp3_cmd}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" - else - ogg_digest = ::Digest::MD5.file(output) - mp3_digest = ::Digest::MD5.file(output_mp3) - self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest - self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest - - @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") - s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) - @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") - s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) - - self.skip_uploader = true - - original_ogg_preview_url = self["preview_url"] - original_mp3_preview_url = self["preview_mp3_url"] - - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') - self["preview_length"] = File.new(output).size - # and finally update the JamTrackTrack with the new info - self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') - self["preview_mp3_length"] = File.new(output_mp3).size - self.save! - - # if all that worked, now delete old previews, if present - begin - s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] - s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] - rescue - puts "UNABLE TO CLEANUP OLD PREVIEW URL" - end - - end - end + process_preview(input, tmp_dir) end rescue Exception => e @@log.error("error in sox command #{e.to_s}") @@ -214,6 +152,76 @@ module JamRuby end + # input is the original ogg file for the track. tmp_dir is where this code can safely generate output stuff and have it cleaned up later + def process_preview(input, tmp_dir) + uuid = SecureRandom.uuid + output = File.join(tmp_dir, "#{uuid}.ogg") + output_mp3 = File.join(tmp_dir, "#{uuid}.mp3") + + start = self.preview_start_time.to_f / 1000 + stop = start + 20 + + command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" + + @@log.debug("trimming using: " + command) + + sox_output = `#{command}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute cut command #{sox_output}" + else + # now create mp3 off of ogg preview + + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" + + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" + else + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + # and finally update the JamTrackTrack with the new info + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + + end + end + end + private def normalize_position diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 96b9b7831..1f56efc08 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -44,10 +44,10 @@ module JamRuby has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id' has_one :feed, :class_name => "JamRuby::Feed", :inverse_of => :music_session, :foreign_key => 'music_session_id', :dependent => :destroy belongs_to :genre, :class_name => "JamRuby::Genre", :inverse_of => :music_sessions, :foreign_key => 'genre_id' - has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest", :foreign_key => "music_session_id" - has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation", :foreign_key => "music_session_id" + has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest" + has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation" has_many :invited_musicians, :through => :invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver - has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation", :foreign_key => "music_session_id" + has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation" has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver has_many :rsvp_slots, :class_name => "JamRuby::RsvpSlot", :foreign_key => "music_session_id", :dependent => :destroy has_many :music_notations, :class_name => "JamRuby::MusicNotation", :foreign_key => "music_session_id" @@ -880,6 +880,21 @@ SQL end result end + + def safe_scheduled_duration + duration = scheduled_duration + # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string + if scheduled_duration.class == String + begin + bits = scheduled_duration.split(':') + duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds + rescue Exception => e + duration = 1.hours + @@log.error("unable to parse duration #{scheduled_duration}") + end + end + duration + end # should create a timestamp like: # # with_timezone = TRUE @@ -910,17 +925,7 @@ SQL end end - duration = scheduled_duration - # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string - if scheduled_duration.class == String - begin - bits = scheduled_duration.split(':') - duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds - rescue Exception => e - duration = 1.hours - @@log.error("unable to parse duration #{scheduled_duration}") - end - end + duration = safe_scheduled_duration end_time = start_time + duration if with_timezone "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index bca0ecccc..86d132025 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -10,14 +10,8 @@ module JamRuby attr_accessible :max_concurrent_connections, :session_removed_at, :rating validates_inclusion_of :rating, :in => -1..1, :allow_nil => true - belongs_to(:user, - :class_name => "JamRuby::User", - :foreign_key => "user_id", - :inverse_of => :music_session_user_histories) - - belongs_to(:music_session, - :class_name => "MusicSession", - :foreign_key => "music_session_id") + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :music_session_user_histories + belongs_to :music_session, :class_name => "MusicSession", :foreign_key => "music_session_id" def self.latest_history(client_id) self.where(:client_id => client_id) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index a5a2cd270..f11e36e20 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -246,47 +246,54 @@ module JamRuby return 'Click search button to look for musicians with similar interests, skill levels, etc.' end jj = self.json - str = 'Current Search: ' - str += "Sort = #{SORT_ORDERS[json_value(MusicianSearch::KEY_SORT_ORDER)]}" + str = '' + if 0 < (val = jj[KEY_INSTRUMENTS]).length + str += ", Instruments = " + instr_ids = val.collect { |stored_instrument| stored_instrument['id'] } + instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description) + instrs.each_with_index do |ii, idx| + proficiency = val.detect { |stored_instrument| stored_instrument['id'] == ii.id }['level'] + str += "#{ii.description} / #{INSTRUMENT_PROFICIENCY[proficiency.to_i]}" + str += ', ' unless idx==(instrs.length-1) + end + end + if (val = jj[KEY_INTERESTS]) != INTEREST_VALS[0] - str += "; Interest = #{INTERESTS[val]}" + str += ", Interest = #{INTERESTS[val]}" end if (val = jj[KEY_SKILL].to_i) != SKILL_VALS[0] - str += "; Skill = #{SKILL_LEVELS[val]}" + str += ", Skill = #{SKILL_LEVELS[val]}" end if (val = jj[KEY_STUDIOS].to_i) != STUDIO_COUNTS[0] - str += "; Studio Sessions = #{STUDIOS_LABELS[val]}" + str += ", Studio Sessions = #{STUDIOS_LABELS[val]}" end if (val = jj[KEY_GIGS].to_i) != GIG_COUNTS[0] - str += "; Concert Gigs = #{GIG_LABELS[val]}" + str += ", Concert Gigs = #{GIG_LABELS[val]}" end val = jj[KEY_AGES].map(&:to_i) val.sort! if !val.blank? - str += "; Ages = " + str += ", Ages = " val.each_with_index do |vv, idx| str += "#{AGES[vv]}" str += ', ' unless idx==(val.length-1) end end if 0 < (val = jj[KEY_GENRES]).length - str += "; Genres = " + str += ", Genres = " genres = Genre.where(["id IN (?)", val]).order('description').pluck(:description) genres.each_with_index do |gg, idx| str += "#{gg}" str += ', ' unless idx==(genres.length-1) end end - if 0 < (val = jj[KEY_INSTRUMENTS]).length - str += "; Instruments = " - instr_ids = val.collect { |vv| vv['instrument_id'] } - instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description) - instrs.each_with_index do |ii, idx| - proficiency = val.detect { |vv| vv['instrument_id'] == ii.id }['proficiency_level'] - str += "#{ii.description} (#{INSTRUMENT_PROFICIENCY[proficiency.to_i]})" - str += ', ' unless idx==(instrs.length-1) - end + str += ", Sort = #{SORT_ORDERS[json_value(MusicianSearch::KEY_SORT_ORDER)]}" + + if str.start_with?(', ') + # trim off any leading , + str = str[2..-1] end + str = 'Current Search: ' + str str end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index db4018ab8..5b008a55f 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -73,6 +73,10 @@ module JamRuby @@message_factory = MessageFactory.new ################### HELPERS ################### + def notified?(music_session, notification_type) + Notification.where("session_id=? AND description=?", music_session, notification_type).count != 0 + end + def retrieve_friends(connection, user_id) friend_ids = [] connection.exec("SELECT f.friend_id as friend_id FROM friendships f WHERE f.user_id = $1", [user_id]) do |friend_results| @@ -203,9 +207,15 @@ module JamRuby when NotificationTypes::SCHEDULED_SESSION_RESCHEDULED return "The following session has been rescheduled." - when NotificationTypes::SCHEDULED_SESSION_REMINDER + when NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY return "A session to which you have RSVPd will begin in one hour, so get ready to play!" + when NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING + return "A session to which you have RSVPd will begin in one hour, so get ready to play!" + + when NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT + return "A session to which you have RSVPd is scheduled to start in 5 minutes!" + when NotificationTypes::SCHEDULED_SESSION_COMMENT return "New message about session." @@ -515,7 +525,7 @@ module JamRuby end def send_session_join(active_music_session, connection, user) - + notification_msg = format_msg(NotificationTypes::SESSION_JOIN, {:user => user}) msg = @@message_factory.session_join( @@ -553,8 +563,8 @@ module JamRuby end def send_musician_session_join(music_session, user) - - if music_session.musician_access || music_session.fan_access + + if music_session.musician_access || music_session.fan_access friends = Friendship.where(:friend_id => user.id) user_followers = user.followers @@ -804,7 +814,7 @@ module JamRuby def send_scheduled_session_cancelled(music_session) return if music_session.nil? - + rsvp_requests = RsvpRequest.index(music_session) target_users = rsvp_requests.where(:canceled => false).map { |r| r.user } @@ -890,33 +900,52 @@ module JamRuby end end - def send_scheduled_session_reminder(music_session) + # Send session reminders to sessions that + # start in less than 24 hours, and haven't been + # notified for a particular interval yet: + def send_session_reminders + MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS')").each do |candidate_session| + tm = candidate_session.scheduled_start + if (tm>(12.hours.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY)) + # Send 24 hour reminders: + send_session_reminder_day(candidate_session) + elsif (tm<=(65.minutes.from_now) && tm>(15.minutes.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING)) + # Send 1 hour reminders: + send_session_reminder_upcoming(candidate_session) + elsif (tm<=(10.minutes.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT)) + # Send 5 minute reminders: + send_session_reminder_imminent(candidate_session) + end + end + end - return if music_session.nil? + def send_session_reminder_day(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY) do |music_session, target_user, notification| + begin + UserMailer.scheduled_session_reminder_day(target_user, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_REMINDER_DAY email to user #{target_user.email} #{e}") + end + end + end - rsvp_requests = RsvpRequest.index(music_session) - target_users = rsvp_requests.where(:canceled => false).map { |r| r.user } - - # remove the creator from the array - target_users = target_users.uniq - [music_session.creator] - - target_users.each do |target_user| - source_user = music_session.creator - - notification = Notification.new - notification.description = NotificationTypes::SCHEDULED_SESSION_REMINDER - notification.source_user_id = source_user.id - notification.target_user_id = target_user.id - notification.session_id = music_session.id - notification.save - - notification_msg = format_msg(notification.description, {:session => music_session}) + def send_session_reminder_upcoming(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING) do |music_session, target_user, notification| + begin + UserMailer.scheduled_session_reminder_upcoming(target_user, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_REMINDER_UPCOMING email to user #{target_user.email} #{e}") + end + end + end + def send_session_reminder_imminent(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT) do |music_session, target_user, notification| if target_user.online msg = @@message_factory.scheduled_session_reminder( target_user.id, music_session.id, - notification_msg, + format_msg(notification.description, {:session => music_session}), music_session.name, music_session.pretty_scheduled_start(false), notification.id, @@ -925,12 +954,27 @@ module JamRuby @@mq_router.publish_to_user(target_user.id, msg) end + end + end - begin - UserMailer.scheduled_session_reminder(target_user, notification_msg, music_session).deliver - rescue => e - @@log.error("Unable to send SCHEDULED_SESSION_REMINDER email to user #{target_user.email} #{e}") - end + # @param music_session - the session for which to send reminder + # @param reminder_type - the type of reminder; one of: + # => SCHEDULED_SESSION_REMINDER_DAY 24 hours + # => SCHEDULED_SESSION_REMINDER_UPCOMING 15 minutes + # => SCHEDULED_SESSION_REMINDER_IMMINENT 5 minutes (in-app) + def send_session_reminder(music_session, reminder_type) + raise ArgumentError, "Block required" unless block_given? + source_user = music_session.creator + rsvp_requests = RsvpRequest.index(music_session) + rsvp_requests.where(:canceled => false).each do |rsvp| + target_user = rsvp.user + notification = Notification.new + notification.description = reminder_type + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.save + yield(music_session, target_user, notification) end end @@ -984,12 +1028,12 @@ module JamRuby def send_band_session_join(music_session, band) # if the session is private, don't send any notifications - if music_session.musician_access || music_session.fan_access + if music_session.musician_access || music_session.fan_access notification_msg = format_msg(NotificationTypes::BAND_SESSION_JOIN, {:band => band}) followers = band.followers.map { |bf| bf.user } - + # do not send band session notifications to band members followers = followers - band.users @@ -1328,7 +1372,7 @@ module JamRuby end def send_band_invitation_accepted(band, band_invitation, sender, receiver) - + notification = Notification.new notification.band_id = band.id notification.description = NotificationTypes::BAND_INVITATION_ACCEPTED @@ -1362,7 +1406,7 @@ module JamRuby msg = @@message_factory.musician_session_fresh( music_session.id, - user.id, + user.id, user.name, user.photo_url ) diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index f416640bf..20c3b9e9a 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -8,6 +8,7 @@ module JamRuby validates :user, presence: true validates :canceled, :inclusion => {:in => [nil, true, false]} validate :creator_rsvp_cancel + before_save :cancel_calendar # pulls all instruments from the associated rsvp_slots def instrument_list @@ -305,6 +306,15 @@ module JamRuby errors.add(:canceled, "can't be canceled by the session organizer") end end + + def cancel_calendar + calendar_manager = CalendarManager.new + if self.canceled + self.rsvp_slots.each do |slot| + calendar_manager.cancel_ics_event(slot.music_session, user) + end + end + end end end diff --git a/ruby/lib/jam_ruby/models/score.rb b/ruby/lib/jam_ruby/models/score.rb index f45248376..d19832c65 100644 --- a/ruby/lib/jam_ruby/models/score.rb +++ b/ruby/lib/jam_ruby/models/score.rb @@ -18,7 +18,7 @@ module JamRuby raise "blocidispid must be positive" if blocidispid <= 0 raise "score must be positive" if score <= 0 ascore = Score.create(alocidispid: alocidispid, anodeid: anodeid, aaddr: aaddr, auserid: user_info[:auserid], alatencytestid: user_info[:alatencytestid], blocidispid: blocidispid, bnodeid: bnodeid, baddr: baddr, buserid: user_info[:buserid], blatencytestid: user_info[:blatencytestid], score: score, scorer: 0, score_dt: score_dt, scoring_data: score_data) - bscore = Score.create(alocidispid: blocidispid, anodeid: bnodeid, aaddr: baddr, auserid: user_info[:buserid], blatencytestid: user_info[:blatencytestid], blocidispid: alocidispid, bnodeid: anodeid, baddr: aaddr, buserid: user_info[:auserid], blatencytestid: user_info[:alatencytestid], score: score, scorer: 1, score_dt: score_dt) if alocidispid != blocidispid + bscore = Score.create(alocidispid: blocidispid, anodeid: bnodeid, aaddr: baddr, auserid: user_info[:buserid], alatencytestid: user_info[:blatencytestid], blocidispid: alocidispid, bnodeid: anodeid, baddr: aaddr, buserid: user_info[:auserid], blatencytestid: user_info[:alatencytestid], score: score, scorer: 1, score_dt: score_dt) if alocidispid != blocidispid Score.connection.execute("select update_current_network_scores(#{alocidispid}, #{blocidispid})") return [ascore, bscore] end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 14dd0bf26..691f03a30 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -45,6 +45,9 @@ module JamRuby # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" + # calendars (for scheduling NOT in music_session) + has_many :calendars, :class_name => "JamRuby::Calendar" + # connections (websocket-gateway) has_many :connections, :class_name => "JamRuby::Connection" @@ -194,6 +197,11 @@ module JamRuby validates_numericality_of :last_jam_audio_latency, greater_than:MINIMUM_AUDIO_LATENCY, less_than:MAXIMUM_AUDIO_LATENCY, :allow_nil => true validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN] } + # stored in cents + validates_numericality_of :paid_sessions_hourly_rate, greater_than:0, less_than:200000, :if => :paid_sessions + # stored in cents + validates_numericality_of :paid_sessions_daily_rate, greater_than:0, less_than:5000000, :if => :paid_sessions + # custom validators validate :validate_musician_instruments validate :validate_current_password @@ -699,6 +707,20 @@ module JamRuby end end + # Build calendars using given parameter. + # @param calendars (array of hash) + def update_calendars(calendars) + unless self.new_record? + Calendar.where("user_id = ?", self.id).delete_all + end + + unless calendars.nil? + calendars.each do |cal| + self.calendars << self.calendars.create(cal) + end + end + end + # given an array of instruments, update a user's instruments def update_instruments(instruments) # delete all instruments for this user first diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb new file mode 100644 index 000000000..388516441 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb @@ -0,0 +1,17 @@ +module JamRuby + class DailyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_daily_job + @@log = Logging.logger[DailyJob] + + class << self + def perform + @@log.debug("waking up") + calendar_manager = CalendarManager.new + calendar_manager.cleanup() + @@log.debug("done") + end + end + end +end diff --git a/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb b/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb new file mode 100644 index 000000000..503e31e9b --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb @@ -0,0 +1,31 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + class MusicSessionReminder + extend Resque::Plugins::JamLonelyJob + + @queue = :music_session_reminder + + @@log = Logging.logger[MusicSessionReminder] + + def self.lock_timeout + 120 + end + + def self.perform + @@log.debug("MusicSessionReminder waking up") + + MusicSessionReminder.new.run + + @@log.debug("MusicSessionReminder done") + end + + def run + Notification.send_session_reminders() + end + end +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 90cd48c44..531d3cc46 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -740,7 +740,7 @@ FactoryGirl.define do licensor_royalty_amount 0.999 sequence(:plan_code) { |n| "jamtrack-#{n}" } - genre JamRuby::Genre.first + genres [JamRuby::Genre.first] association :licensor, factory: :jam_track_licensor factory :jam_track_with_tracks do diff --git a/ruby/spec/jam_ruby/calendar_manager_spec.rb b/ruby/spec/jam_ruby/calendar_manager_spec.rb new file mode 100644 index 000000000..1532fca64 --- /dev/null +++ b/ruby/spec/jam_ruby/calendar_manager_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'icalendar' + +describe CalendarManager do + CALENDAR_NAME="Test Cal" + + before :all do + @genre1 = FactoryGirl.create(:genre) + @calendar_manager = JamRuby::CalendarManager.new + + # Time resolution is seconds: + @start = Time.at(Time.now.utc.to_i) + @stop =(@start+1.hours) + end + + before(:each) do + + end + + describe "with music sessions" do + before :all do + @creator = FactoryGirl.create(:user) + @music_session = FactoryGirl.create(:music_session, :creator => @creator, :description => CALENDAR_NAME, :genre => @genre1, :scheduled_start=>@start, :scheduled_duration=>3600) + @music_session.reload + end + + it "validator detects bad calendar" do + lambda{verify_ical("Bad medicine calendar")} + .should raise_error(RuntimeError) + end + + it "can create calendar feed" do + ics = @calendar_manager.create_ics_feed(@creator) + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + describe "with manual calendars" do + before :all do + @creator = FactoryGirl.create(:user) + @creator.calendars<CALENDAR_NAME, :description=>"This is a test", :start_at=>(@start), :end_at=>@stop, :trigger_delete=>false, :target_uid=>"2112"}) + end + + it "can create calendar feed" do + #pending "foobar" + ics = @calendar_manager.create_ics_feed(@creator) + + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + def verify_ical(ics) + strict_parser = Icalendar::Parser.new(ics, true) + cals = strict_parser.parse + cals.should_not be_nil + cals.should have(1).items + + cal = cals.first + cal.should_not be_nil + cal.events.should have(1).items + event = cal.events.first + event.should_not be_nil + + event.summary.should eq("JamKazam Session #{CALENDAR_NAME}") + event.dtstart.to_i.should_not be_nil + event.dtend.to_i.should_not be_nil + (event.dtstart).to_time.utc.to_i.should eq(@start.to_i) + (event.dtend).to_time.utc.to_i.should eq(@stop.to_i) + end +end + diff --git a/ruby/spec/jam_ruby/jam_track_importer_spec.rb b/ruby/spec/jam_ruby/jam_track_importer_spec.rb index a2cf96620..fbdc3d9cc 100644 --- a/ruby/spec/jam_ruby/jam_track_importer_spec.rb +++ b/ruby/spec/jam_ruby/jam_track_importer_spec.rb @@ -22,11 +22,13 @@ describe JamTrackImporter do in_directory_with_file(metafile) before(:each) do + JamTrackImporter.storage_format = 'default' content_for_file(YAML.dump(sample_yml)) end it "no meta" do s3_metalocation = 'audio/Artist 1/Bogus Place/meta.yml' + JamTrackImporter.storage_format = 'default' JamTrackImporter.load_metalocation(s3_metalocation).should be_nil end @@ -38,9 +40,105 @@ describe JamTrackImporter do end end + describe "sort_tracks" do + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:importer) { JamTrackImporter.new() } + let(:vocal) {Instrument.find('voice')} + let(:drums) {Instrument.find('drums')} + let(:bass_guitar) {Instrument.find('bass guitar')} + let(:piano) {Instrument.find('piano')} + let(:keyboard) {Instrument.find('keyboard')} + let(:acoustic_guitar) {Instrument.find('acoustic guitar')} + let(:electric_guitar) {Instrument.find('electric guitar')} + let(:other) {Instrument.find('other')} + + it "the big sort" do + # specified in https://jamkazam.atlassian.net/browse/VRFS-3296 + vocal_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead') + vocal_lead_female = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead Female') + vocal_lead_male = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead Male') + vocal_backing = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Backing') + vocal_random = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Random') + drums_drums = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'Drums') + drums_percussion = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'Percussion') + drums_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'A') + drums_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'C') + bass_guitar_bass = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'Bass') + bass_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'some bass') + bass_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'zome bass') + piano_piano = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: piano, part: 'Piano') + keyboard_synth_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Synth 1') + keyboard_synth_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Synth 2') + keyboard_pads = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Pads') + keyboard_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'A') + keyboard_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Z') + acoust_guitar_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Lead') + acoust_guitar_lead_x = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Lead X') + acoust_guitar_solo_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Solo 1') + acoust_guitar_solo_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Solo 2') + acoust_guitar_rhythm = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Rhythm') + acoust_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'A') + acoust_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Z') + elect_guitar_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Lead') + elect_guitar_lead_x = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Lead X') + elect_guitar_solo_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Solo 1') + elect_guitar_solo_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Solo 2') + elect_guitar_rhythm = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Rhythm') + elect_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'A') + elect_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Z') + other_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: other, part: 'Other 1') + other_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: other, part: 'Other 2') + + expected = [ + vocal_lead, + vocal_lead_female, + vocal_lead_male, + vocal_backing, + vocal_random, + drums_drums, + drums_percussion, + drums_random_1, + drums_random_2, + bass_guitar_bass, + piano_piano, + keyboard_synth_1, + keyboard_synth_2, + keyboard_pads, + keyboard_random_1, + keyboard_random_2, + acoust_guitar_lead, + acoust_guitar_lead_x, + acoust_guitar_rhythm, + acoust_guitar_random_1, + acoust_guitar_solo_1, + acoust_guitar_solo_2, + acoust_guitar_random_2, + elect_guitar_lead, + elect_guitar_lead_x, + elect_guitar_solo_1, + elect_guitar_solo_2, + elect_guitar_rhythm, + elect_guitar_random_1, + elect_guitar_random_2, + bass_guitar_random_1, + bass_guitar_random_2, + other_1, + other_2 + ] + shuffled = expected.shuffle + sorted_tracks = importer.sort_tracks(shuffled) + + importer.set_custom_weight(vocal_lead).should eq(100) + + expected.each_with_index do |expected_track, i| + sorted_tracks[i].should eq(expected_track) + end + end + end + describe "synchronize" do let(:jam_track) { JamTrack.new } - let(:importer) { JamTrackImporter.new } + let(:importer) { JamTrackImporter.new() } let(:minimum_meta) { nil } let(:metalocation) { 'audio/Artist 1/Song 1/meta.yml' } let(:options) {{ skip_audio_upload:true }} @@ -64,7 +162,7 @@ describe JamTrackImporter do describe "parse_wav" do it "Guitar" do - result = JamTrackImporter.new.parse_wav('blah/Ready for Love Stem - Guitar - Main.wav') + result = JamTrackImporter.new.parse_file('blah/Ready for Love Stem - Guitar - Main.wav') result[:instrument].should eq('electric guitar') result[:part].should eq('Main') end diff --git a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb index ebcb92d2d..bc2c0c206 100644 --- a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb @@ -15,8 +15,12 @@ describe 'Band Search Model' do let!(:to_join) { search.search_filter_for_subtype(BandSearch::TO_JOIN) } let!(:to_hire) { search.search_filter_for_subtype(BandSearch::TO_HIRE) } - before(:each) do + before(:all) do + Recording.delete_all Band.delete_all + end + + before(:each) do @bands = [] @bands << @band1 = FactoryGirl.create(:band) @bands << @band2 = FactoryGirl.create(:band) diff --git a/ruby/spec/jam_ruby/models/band_search_spec.rb b/ruby/spec/jam_ruby/models/band_search_spec.rb index 934f63dda..79b3759fb 100644 --- a/ruby/spec/jam_ruby/models/band_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_search_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe User do +describe "Band Search" do let(:user) { FactoryGirl.create(:user) } let(:band) { FactoryGirl.create(:band, name: "Example Band") } @@ -15,6 +15,11 @@ describe User do } } + before(:all) do + Recording.delete_all + Band.delete_all + end + before(:each) do @user = FactoryGirl.create(:user) band.touch diff --git a/ruby/spec/jam_ruby/models/band_spec.rb b/ruby/spec/jam_ruby/models/band_spec.rb index 706f16691..6fed8ac1e 100644 --- a/ruby/spec/jam_ruby/models/band_spec.rb +++ b/ruby/spec/jam_ruby/models/band_spec.rb @@ -19,6 +19,17 @@ describe Band do } } + let(:band_params_no_genre) { + { + name: "The Band", + biography: "Biography", + city: 'Austin', + state: 'TX', + country: 'US', + validate_genres:true + } + } + describe 'with instruments' do it 'builds with instruments' do band.musician_instruments << FactoryGirl.build(:musician_instrument, player: band) @@ -46,6 +57,9 @@ describe Band do it "minimum genres" do new_band.save.should be_false new_band.errors[:genres].should == [ValidationMessages::BAND_GENRE_MINIMUM_NOT_MET] + + new_band.genres = [Genre.first] + new_band.save.should be_true end it "maximum genres" do @@ -56,6 +70,22 @@ describe Band do end describe "save" do + it "genres validate" do + band=Band.save(user, band_params_no_genre) + band.errors.any?.should be_true + band.errors[:genres].should == [ValidationMessages::BAND_GENRE_MINIMUM_NOT_MET] + + band = Band.save(user, band_params) + band.errors.any?.should be_false + + # Save again without a genre and make sure we get an error: + p = band_params_no_genre.clone + p[:id] = band.id + band = Band.save(user, p) + band.errors.any?.should be_true + band.errors[:genres].should == [ValidationMessages::BAND_GENRE_MINIMUM_NOT_MET] + end + it "can succeed" do band = Band.save(user, band_params) band.errors.any?.should be_false @@ -67,6 +97,33 @@ describe Band do band.country.should == band_params[:country] end + it "saves current interests" do + parms = band_params + parms[:paid_gigs]=true + parms[:free_gigs]=false + parms[:hourly_rate]=5000 + parms[:gig_minimum]=30000 + parms[:add_new_members]=true + parms[:touring_option]=false + parms[:band_type]="virtual" + parms[:band_status]="amateur" + parms[:concert_count]=3 + + band = Band.save(user, parms) + band.errors.any?.should be_false + + band.paid_gigs.should == true + band.free_gigs.should == false + band.hourly_rate.should == 5000 + parms[:gig_minimum]=30000 + band.add_new_members.should == true + band.touring_option.should == false + band.band_type.should == "virtual" + band.band_status.should == "amateur" + band.concert_count.should == 3 + end + + it "ensures user is a musician" do expect{ Band.save(fan, band_params) }.to raise_error("must be a musician") end @@ -173,5 +230,5 @@ describe Band do history = band.recent_history(nil, claimed_recording.id) history.size.should == 0 end - end + end end diff --git a/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb b/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb index 4ec86db70..dc2fc47e3 100644 --- a/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb +++ b/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb @@ -42,8 +42,10 @@ describe BroadcastNotification do bns = BroadcastNotification.viewable_notifications(user1) expect(bns.count).to be(3) - expect(bns[0].id).to eq(broadcast3.id) + expect(bns[0].id).to eq(broadcast4.id) expect(bns.detect {|bb| bb.id==broadcast2.id }).to be_nil + # now view broadcast 4, since it hasn't been seen, which should bring broadcast 3 up as next since it was seen longest ago + broadcast4.did_view(user1) expect(BroadcastNotification.next_broadcast(user1).id).to eq(broadcast3.id) end diff --git a/ruby/spec/jam_ruby/models/feed_spec.rb b/ruby/spec/jam_ruby/models/feed_spec.rb index fa4372a58..f24445637 100644 --- a/ruby/spec/jam_ruby/models/feed_spec.rb +++ b/ruby/spec/jam_ruby/models/feed_spec.rb @@ -2,6 +2,13 @@ require 'spec_helper' describe Feed do + before(:all) do + MusicSession.delete_all + Recording.delete_all + IcecastMount.delete_all + end + + let (:user1) { FactoryGirl.create(:user) } let (:user2) { FactoryGirl.create(:user) } let (:user3) { FactoryGirl.create(:user) } diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index d17019df7..c1aecf71d 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -12,6 +12,90 @@ describe JamTrack do jam_track = FactoryGirl.create(:jam_track) jam_track.licensor.should_not be_nil jam_track.licensor.jam_tracks.should == [jam_track] + jam_track.genres.length.should eq(1) + end + + describe 'sync_reproduction_royalty' do + it "all possible conditions" do + jam_track = FactoryGirl.create(:jam_track) + jam_track.reproduction_royalty_amount.should be_nil + + jam_track.duration = 0 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 1 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 5 * 60 - 1 # just under 5 minutes + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 5 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 6 * 60 - 1 # just under 6 minutes + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175) + + jam_track.duration = 6 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175) + + jam_track.duration = 7 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 2) + + jam_track.duration = 7 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 2) + + jam_track.duration = 8 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 3) + + jam_track.duration = 8 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 3) + + jam_track.duration = 9 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 4) + + jam_track.duration = 9 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 4) + + jam_track.duration = 10 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 5) + + jam_track.duration = 10 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 5) + + jam_track.duration = 11 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 6) + + jam_track.duration = 11 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 6) + + jam_track.duration = 12 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 7) + + jam_track.duration = 12 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 7) + + jam_track.duration = 13 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 8) + end end describe 'plays' do @@ -98,6 +182,26 @@ describe JamTrack do query[1].should eq(jam_track1) end + it "queries on genre" do + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'a') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'b') + jam_track1.genres = [Genre.find('rock')] + jam_track2.genres = [Genre.find('asian')] + jam_track1.save! + jam_track2.save! + + query, pager = JamTrack.index({genre: 'rock'}, user) + query.size.should == 1 + query[0].should eq(jam_track1) + + query, pager = JamTrack.index({genre: 'asian'}, user) + query.size.should == 1 + query[0].should eq(jam_track2) + + query, pager = JamTrack.index({genre: 'african'}, user) + query.size.should == 0 + end + it "supports showing purchased only" do jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'a') @@ -170,7 +274,7 @@ describe JamTrack do end it "100.1234" do - jam_track = FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.1234) + jam_track = FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.12345) jam_track.valid?.should be_false jam_track.errors[:reproduction_royalty_amount].should == ['is invalid'] end diff --git a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb index 6fb4343f6..67a50ec22 100644 --- a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb @@ -7,6 +7,7 @@ describe JamTrackTrack do it "created" do jam_track_track = FactoryGirl.create(:jam_track_track) jam_track_track.jam_track.should_not be_nil + jam_track_track.jam_track.reload jam_track_track.jam_track.jam_track_tracks.should == [jam_track_track] end diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index da97f5eaa..012dbf482 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -17,6 +17,8 @@ describe 'Musician Search Model' do describe "creates search obj" do before(:all) do User.delete_all + Score.connection.execute('delete from current_network_scores').check + Score.connection.execute('delete from scores').check end it "associates to user" do @@ -132,12 +134,13 @@ describe 'Musician Search Model' do describe "filters interests" do before(:all) do - MusicianSearch::INTEREST_VALS[1..-1].each do |val| - user_types.each { |utype| FactoryGirl.create(utype, val => true) } - end + end it "get expected number per interest" do + MusicianSearch::INTEREST_VALS[1..-1].each do |val| + user_types.each { |utype| FactoryGirl.create(utype, val => true, :paid_sessions_hourly_rate=>2300, :paid_sessions_daily_rate=>15000) } + end search.update_json_value(MusicianSearch::KEY_INTERESTS, MusicianSearch::INTEREST_VALS[1]) expect(search.do_search.count).to eq(user_types.count) end @@ -169,8 +172,8 @@ describe 'Musician Search Model' do end it "gets expected number of users" do - instjson = [{ instrument_id: Instrument.first.id, proficiency_level: 2 }, - { instrument_id: Instrument.first(2)[1].id, proficiency_level: 2 } + instjson = [{ id: Instrument.first.id, level: 2 }, + { id: Instrument.first(2)[1].id, level: 2 } ] search.update_json_value(MusicianSearch::KEY_INSTRUMENTS, instjson) expect(search.do_search.count).to eq(3) @@ -201,6 +204,7 @@ describe 'Musician Search Model' do describe "sort order by latency" do before(:each) do User.delete_all + Score.delete_all t = Time.now - 10.minute @user1 = FactoryGirl.create(:user, created_at: t+1.minute, last_jam_locidispid: 1) @@ -232,12 +236,12 @@ describe 'Musician Search Model' do Score.createx(2, 'b', 2, 4, 'd', 4, 70) end - it "sorts by latency" do + it "sorts by latency", intermittent: true do search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[0]) results = search.do_search - expect(results[0].id).to eq(@user1.id) + expect(results[0].id).to eq(@user1.id) # HAS FAILED HERE TOO expect(results[1].id).to eq(@user2.id) - expect(results[2].id).to eq(@user3.id) + expect(results[2].id).to eq(@user3.id) # HAS FAILED INTERMITTENTLY expect(results[3].id).to eq(@user4.id) end @@ -286,28 +290,28 @@ describe 'Musician Search Model' do selections.each do |hash| search.update_json_value(hash[:key], hash[:value]) json_val = search.json_value(hash[:key]) - expect(search.description).to match(/; #{hash[:description]} = #{hash[:lookup][json_val]}/) + expect(search.description).to match(/ #{hash[:description]} = #{hash[:lookup][json_val]}/) end end it 'has correct description for genres' do search.update_json_value(MusicianSearch::KEY_GENRES, [Genre.first.id, Genre.last.id]) - expect(search.description).to match(/; Genres = #{Genre.first.description}, #{Genre.last.description}/) + expect(search.description).to match(/ Genres = #{Genre.first.description}, #{Genre.last.description}/) end it 'has correct description for ages' do search.update_json_value(MusicianSearch::KEY_AGES, [MusicianSearch::AGE_COUNTS[0],MusicianSearch::AGE_COUNTS[1]]) - expect(search.description).to match(/; Ages = #{MusicianSearch::AGES[MusicianSearch::AGE_COUNTS[0]]}, #{MusicianSearch::AGES[MusicianSearch::AGE_COUNTS[1]]}/) + expect(search.description).to match(/ Ages = #{MusicianSearch::AGES[MusicianSearch::AGE_COUNTS[0]]}, #{MusicianSearch::AGES[MusicianSearch::AGE_COUNTS[1]]}/) end it 'has correct description for instruments' do instrs = Instrument.limit(2).order(:description) - instjson = [{ instrument_id: instrs[0].id, proficiency_level: 2 }, - { instrument_id: instrs[1].id, proficiency_level: 1 } + instjson = [{ id: instrs[0].id, level: 2 }, + { id: instrs[1].id, level: 1 } ] search.update_json_value(MusicianSearch::KEY_INSTRUMENTS, instjson) - instr_descrip = "#{instrs[0].description} (#{MusicianSearch::INSTRUMENT_PROFICIENCY[2]}), #{instrs[1].description} (#{MusicianSearch::INSTRUMENT_PROFICIENCY[1]})" - expect(search.description).to match(/; Instruments = #{Regexp.escape(instr_descrip)}/) + instr_descrip = "#{instrs[0].description} / #{MusicianSearch::INSTRUMENT_PROFICIENCY[2]}, #{instrs[1].description} / #{MusicianSearch::INSTRUMENT_PROFICIENCY[1]}" + expect(search.description).to match(/ Instruments = #{Regexp.escape(instr_descrip)}/) end end diff --git a/ruby/spec/jam_ruby/models/notification_spec.rb b/ruby/spec/jam_ruby/models/notification_spec.rb index 1f201c142..fcfae98a1 100644 --- a/ruby/spec/jam_ruby/models/notification_spec.rb +++ b/ruby/spec/jam_ruby/models/notification_spec.rb @@ -18,6 +18,13 @@ describe Notification do @session = FactoryGirl.create(:music_session) @band = FactoryGirl.create(:band) + @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('electric guitar')) + @slot1.save + + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('drums')) + @slot2.save + + @friend_request = FactoryGirl.create(:friend_request, user: @sender, friend: @receiver) end @@ -199,7 +206,7 @@ describe Notification do it "does not send email when user is offline and opts out of emails" do FactoryGirl.create(:friendship, :user => @receiver, :friend => @recording.owner) FactoryGirl.create(:friendship, :user => @recording.owner, :friend => @receiver) - + @receiver.subscribe_email = false @receiver.save! @@ -284,7 +291,7 @@ describe Notification do @recording.band = @band @recording.save! - + follower.subscribe_email = false follower.save! @@ -671,35 +678,14 @@ describe Notification do end end - describe "send scheduled session reminder" do - # it "sends email when user is offline and subscribes to emails" do - # session.creator = sender - # session.save! - - # calls = count_publish_to_user_calls - # notification = Notification.send_scheduled_session_cancelled(session) - - # UserMailer.deliveries.length.should == 1 - # calls[:count].should == 1 - # end - - # it "does not send email when user is offline and opts out of emails" do - # session.creator = sender - # session.save! - - # receiver.subscribe_email = false - # receiver.save! - - # calls = count_publish_to_user_calls - # notification = Notification.send_scheduled_session_cancelled(session) - - # UserMailer.deliveries.length.should == 0 - # calls[:count].should == 1 - # end - + describe "reminders" do + let(:mail) { UserMailer.deliveries[0] } + before :each do + UserMailer.deliveries.clear + end it "sends no notification if session is nil" do calls = count_publish_to_user_calls - notification = Notification.send_scheduled_session_reminder(nil) + notification = Notification.send_session_reminders() UserMailer.deliveries.length.should == 0 calls[:count].should == 0 @@ -707,12 +693,65 @@ describe Notification do it "sends no notification if there are no rsvp requests" do calls = count_publish_to_user_calls - notification = Notification.send_scheduled_session_reminder(@session) + notification = Notification.send_session_reminders() UserMailer.deliveries.length.should == 0 calls[:count].should == 0 end - end + + it "sends email 24 hours before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 23.hours + @session.save! + + notification = Notification.send_session_reminders() + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + mail.html_part.body.include?("starts in 1 hour").should be_false + mail.text_part.body.include?("starts in 1 hour").should be_false + end + + it "sends email 1 hour before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 59.minutes + @session.save! + + + notification = Notification.send_session_reminders() + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + mail.html_part.body.include?("is scheduled for tomorrow").should be_false + mail.text_part.body.include?("is scheduled for tomorrow").should be_false + + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + end + + it "sends notice 5 minutes before" do + UserMailer.deliveries.length.should == 0 + receiver_connection = FactoryGirl.create(:connection, user: @receiver) + @receiver.reload + + rsvp = RsvpRequest.create({:session_id => @session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @receiver) + UserMailer.deliveries.clear + calls = count_publish_to_user_calls + @session.creator = @sender + @session.scheduled_start = Time.now + 4.minutes + @session.save! + notification = Notification.send_session_reminders() + calls[:count].should == 1 + UserMailer.deliveries.length.should == 0 + end + end # reminders describe "send scheduled session comment" do # it "sends email when user is offline and subscribes to emails" do diff --git a/ruby/spec/jam_ruby/models/online_presence_spec.rb b/ruby/spec/jam_ruby/models/online_presence_spec.rb index 883917f25..db05c67aa 100644 --- a/ruby/spec/jam_ruby/models/online_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/online_presence_spec.rb @@ -162,6 +162,10 @@ describe OnlinePresence do let(:player1) { FactoryGirl.create(:user) } let(:player2) { FactoryGirl.create(:user) } end + + after(:all) { + Band.delete_all + } end describe "with a band" do @@ -169,5 +173,10 @@ describe OnlinePresence do let(:player1) { FactoryGirl.create(:band) } let(:player2) { FactoryGirl.create(:band) } end + + after(:all) { + Band.delete_all + } + end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/performance_sample_spec.rb b/ruby/spec/jam_ruby/models/performance_sample_spec.rb index 5cf592a2b..63f03f244 100644 --- a/ruby/spec/jam_ruby/models/performance_sample_spec.rb +++ b/ruby/spec/jam_ruby/models/performance_sample_spec.rb @@ -125,6 +125,15 @@ describe PerformanceSample do let(:player1) { FactoryGirl.create(:user) } let(:player2) { FactoryGirl.create(:user) } end + + after(:all) { + User.delete_all + RecordedTrack.delete_all + ClaimedRecording.delete_all + Recording.delete_all + ActiveMusicSession.delete_all + MusicSession.delete_all + } end describe "with a band" do @@ -132,5 +141,14 @@ describe PerformanceSample do let(:player1) { FactoryGirl.create(:band) } let(:player2) { FactoryGirl.create(:band) } end + + after(:all) { + Band.delete_all + RecordedTrack.delete_all + ClaimedRecording.delete_all + Recording.delete_all + ActiveMusicSession.delete_all + MusicSession.delete_all + } end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb index f93501647..9da353746 100644 --- a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb +++ b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb @@ -30,10 +30,10 @@ describe RsvpRequest do @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar')) @slot1.save - + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('drums')) @slot2.save - + @invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session) @invitation.save end @@ -53,12 +53,12 @@ describe RsvpRequest do @music_session.save RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee) - expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) + expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) end it "should allow invitee to RSVP to session with closed RSVPs" do rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @session_invitee) - + # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "We be jammin!" @@ -373,12 +373,14 @@ describe RsvpRequest do comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "Let's Jam!" - # cancel - expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error + calendar_count = Calendar.find(:all).count + # cancel & check that calendar has been added: + expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error rsvp = RsvpRequest.find_by_id(rsvp.id) rsvp.canceled.should == true rsvp.cancel_all.should == true + (Calendar.find(:all).count - calendar_count).should eq(1) # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index a0d4acf48..685d8dfbd 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -195,7 +195,9 @@ describe Sale do end - it "for a normally priced jam track" do + it "for a normally priced jam track", intermittent: true do + # intermittent: sometimes recurly won't mark it 'collected' soon enough for the test to pass + user.has_redeemable_jamtrack = false user.save! shopping_cart = ShoppingCart.create user, jamtrack, 1, false @@ -236,6 +238,7 @@ describe Sale do sale_line_item.recurly_adjustment_credit_uuid.should be_nil sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + # verify subscription is in Recurly recurly_account = client.get_account(user) adjustments = recurly_account.adjustments diff --git a/ruby/spec/jam_ruby/resque/active_music_session_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/active_music_session_cleaner_spec.rb index 7bae67c59..2b9196f6f 100644 --- a/ruby/spec/jam_ruby/resque/active_music_session_cleaner_spec.rb +++ b/ruby/spec/jam_ruby/resque/active_music_session_cleaner_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe "ActiveMusicSessionCleaner" do before(:all) do + MusicSession.delete_all + ActiveMusicSession.delete_all @cleaner = ActiveMusicSessionCleaner.new @cleaner.interval = "INTERVAL '1 second'" end diff --git a/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb new file mode 100644 index 000000000..35c8ae845 --- /dev/null +++ b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe 'MusicSessionReminder' do + let(:mail) { UserMailer.deliveries[0] } + before :each do + UserMailer.deliveries.clear + MusicSession.delete_all + User.delete_all + + @receiver = FactoryGirl.create(:user) + @sender = FactoryGirl.create(:user) + @session = FactoryGirl.create(:music_session) + + @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('electric guitar')) + @slot1.save + + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('drums')) + @slot2.save + end + + it "sends email 24 hours before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 23.hours + @session.save! + + JamRuby::MusicSessionReminder.perform + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + mail.html_part.body.include?("starts in 1 hour").should be_false + mail.text_part.body.include?("starts in 1 hour").should be_false + end + + it "sends email 1 hour before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 59.minutes + @session.save! + + + JamRuby::MusicSessionReminder.perform + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + mail.html_part.body.include?("is scheduled for tomorrow").should be_false + mail.text_part.body.include?("is scheduled for tomorrow").should be_false + + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + end + + it "sends notice 5 minutes before" do + UserMailer.deliveries.length.should == 0 + receiver_connection = FactoryGirl.create(:connection, user: @receiver) + @receiver.reload + + rsvp = RsvpRequest.create({:session_id => @session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @receiver) + UserMailer.deliveries.clear + calls = count_publish_to_user_calls + @session.creator = @sender + @session.scheduled_start = Time.now + 4.minutes + @session.save! + JamRuby::MusicSessionReminder.perform + calls[:count].should == 1 + UserMailer.deliveries.length.should == 0 + end + + def count_publish_to_user_calls + result = {count: 0} + MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg| + result[:count] += 1 + result[:msg] = msg + end + result + end +end #spec diff --git a/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb b/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb index bc142cfda..77a0b4265 100644 --- a/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb +++ b/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe "MusicSessionScheduler" do before(:all) do + MusicSession.delete_all @scheduler = MusicSessionScheduler.new end diff --git a/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb new file mode 100644 index 000000000..b53fdbca5 --- /dev/null +++ b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'DailyJob' do + describe "calendar cleanup" do + shared_examples_for :calendar_cleanup do |trigger_delete, end_count| + before :each do + Calendar.destroy_all + @creator = FactoryGirl.create(:user) + @creator.calendars << Calendar.new( + :name=>"Test Cal", + :description=>"This is a test", + :start_at=>(Time.now), + :end_at=>Time.now, + :trigger_delete=>trigger_delete, + :target_uid=>"2112" + ) + end + + it "properly purges old 'delete' calendars" do + @creator.reload + @creator.calendars.should have(1).items + + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(1).items + + Timecop.travel(Time.now + 5.weeks) + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(end_count).items + Timecop.return + end + end + + describe "whacks old 'delete' calendars" do + it_behaves_like :calendar_cleanup, true, 0 + end + + describe "doesn't whacks non 'delete' calendars" do + it_behaves_like :calendar_cleanup, false, 1 + end + end # calendar cleanpu +end #spec diff --git a/ruby/spec/jam_ruby/resque/scheduled_music_session_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/scheduled_music_session_cleaner_spec.rb index 0913561ac..6d08c366e 100644 --- a/ruby/spec/jam_ruby/resque/scheduled_music_session_cleaner_spec.rb +++ b/ruby/spec/jam_ruby/resque/scheduled_music_session_cleaner_spec.rb @@ -2,14 +2,18 @@ require 'spec_helper' describe 'ScheduledMusicSessionCleaner' do + before(:all) { + MusicSession.delete_all + } + it "purges old music sessions" do [MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE, MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE_RECUR].each do |interval| dd = Time.now - (interval.to_i + 1).days - Timecop.travel(dd) + Timecop.travel(dd) FactoryGirl.create(:music_session, scheduled_start: dd) Timecop.return - end + end msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now) expect(MusicSession.count).to be(3) diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index c3d041060..320615020 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -123,7 +123,7 @@ describe UserMailer do before(:each) do user.update_email = "my_new_email@jamkazam.com" - UserMailer.updating_email(user).deliver + UserMailer.updating_email(user).deliver end it { UserMailer.deliveries.length.should == 1 } @@ -137,6 +137,48 @@ describe UserMailer do it { mail.text_part.body.include?("to confirm your change in email").should be_true } end + describe "notifications" do + + let(:mail) { UserMailer.deliveries[0] } + let(:music_session) { FactoryGirl.create(:music_session) } + + it "should send upcoming email" do + user.update_email = "my_new_email@jamkazam.com" + UserMailer.scheduled_session_reminder_upcoming(music_session.creator, music_session).deliver + UserMailer.deliveries.length.should == 1 + + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + mail['to'].to_s.should == music_session.creator.email# rsvp_requests.first.user.email + mail.multipart?.should == true # because we send plain + htm + + # verify that the messages are correctly configured + mail.html_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.text_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + + end + + it "should send 1-day reminder" do + user.update_email = "my_new_email@jamkazam.com" + UserMailer.scheduled_session_reminder_day(music_session.creator, music_session).deliver + UserMailer.deliveries.length.should == 1 + + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + mail['to'].to_s.should == music_session.creator.email# rsvp_requests.first.user.email + mail.multipart?.should == true # because we send plain + htm + + # verify that the messages are correctly configured + mail.html_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.text_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + end + end + + # describe "sends new musicians email" do diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 16ac84514..93ec5dda7 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -14,7 +14,7 @@ require 'resque_failed_job_mailer' # to prevent embedded resque code from forking ENV['FORK_PER_JOB'] = 'false' - +IS_BUILD_SERVER = !ENV['BUILD_SERVER'].nil? # recreate test database and migrate it SpecDb::recreate_database @@ -85,9 +85,13 @@ end config.run_all_when_everything_filtered = true config.filter_run :focus + #config.formatter = :documentation + + # you can mark a test as slow so that developers won't commonly hit it, but build server will http://blog.davidchelimsky.net/2010/06/14/filtering-examples-in-rspec-2/ config.filter_run_excluding slow: true unless run_tests? :slow config.filter_run_excluding aws: true unless run_tests? :aws + config.filter_run_excluding intermittent: true if IS_BUILD_SERVER config.before(:suite) do DatabaseCleaner.strategy = :transaction @@ -108,7 +112,6 @@ end config.before(:each) do stub_const("APP_CONFIG", app_config) - end config.after(:each) do diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 8d16dffbb..2717f63dc 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -206,6 +206,10 @@ def app_config 1 end + def google_public_server_key + "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" + end + private def audiomixer_workspace_path diff --git a/web/Gemfile b/web/Gemfile index 0339472b2..924910f1b 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -18,10 +18,11 @@ else end end +#gem 'license_finder' gem 'oj', '2.10.2' gem 'builder' -gem 'rails', '~>3.2.11' -gem 'railties', '~>3.2.11' +gem 'rails', '~>3.2.22' +gem 'railties', '~>3.2.22' gem 'jquery-rails' gem 'jquery-ui-rails', '4.2.1' gem 'bootstrap-sass', '2.0.4' @@ -36,7 +37,7 @@ gem 'pg', '0.17.1' gem 'compass-rails', '1.1.3' # 1.1.4 throws an exception on startup about !initialize on nil gem 'rabl', '0.11.0' # for JSON API development gem 'gon', '~>4.1.0' # for passthrough of Ruby variables to Javascript variables -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'faraday', '~>0.9.0' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' @@ -85,7 +86,7 @@ gem 'slim' gem 'htmlentities' gem 'sanitize' gem 'recurly' -gem 'guard', '2.7.3' +#gem 'guard', '2.7.3' gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' gem 'sitemap_generator' @@ -95,18 +96,23 @@ gem 'react-rails', '~> 1.0' source 'https://rails-assets.org' do gem 'rails-assets-reflux' + gem 'rails-assets-classnames' end +#group :development, :production do +# gem 'rack-timeout' +#end + group :development, :test do gem 'rspec-rails', '2.14.2' gem "activerecord-import", "~> 0.4.1" - gem 'guard-rspec', '0.5.5' +# gem 'guard-rspec', '0.5.5' # gem 'jasmine', '1.3.1' gem 'pry' gem 'execjs', '1.4.0' gem 'factory_girl_rails', '4.1.0' # in dev because in use by rake task gem 'database_cleaner', '1.3.0' #in dev because in use by rake task - + gem 'test-unit' # gem 'teaspoon' # gem 'teaspoon-jasmine' gem 'puma' @@ -136,8 +142,8 @@ group :test, :cucumber do #end gem 'capybara-screenshot', '0.3.22' # 1.0.0 broke compat with rspec. maybe we need newer rspec gem 'selenium-webdriver' - gem 'cucumber-rails', :require => false #, '1.3.0', :require => false - gem 'guard-spork', '0.3.2' +# gem 'cucumber-rails', :require => false #, '1.3.0', :require => false +# gem 'guard-spork', '0.3.2' gem 'spork', '0.9.0' gem 'launchy', '2.1.1' gem 'rack-test' diff --git a/web/app/assets/images/content/bkg_slider_gain_horiz_24.png b/web/app/assets/images/content/bkg_slider_gain_horiz_24.png new file mode 100644 index 000000000..aad8fb7b9 Binary files /dev/null and b/web/app/assets/images/content/bkg_slider_gain_horiz_24.png differ diff --git a/web/app/assets/images/content/icon_email.png b/web/app/assets/images/content/icon_email.png new file mode 100644 index 000000000..a3163f217 Binary files /dev/null and b/web/app/assets/images/content/icon_email.png differ diff --git a/web/app/assets/images/content/icon_equalizer.png b/web/app/assets/images/content/icon_equalizer.png new file mode 100644 index 000000000..9c63c8dcc Binary files /dev/null and b/web/app/assets/images/content/icon_equalizer.png differ diff --git a/web/app/assets/images/content/icon_instrument_chat45.png b/web/app/assets/images/content/icon_instrument_chat45.png new file mode 100644 index 000000000..4d0309da4 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_chat45.png differ diff --git a/web/app/assets/images/content/icon_instrument_headphones21.png b/web/app/assets/images/content/icon_instrument_headphones21.png new file mode 100644 index 000000000..79ef3c944 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_headphones21.png differ diff --git a/web/app/assets/images/content/icon_instrument_headphones45.png b/web/app/assets/images/content/icon_instrument_headphones45.png new file mode 100644 index 000000000..82533c0d2 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_headphones45.png differ diff --git a/web/app/assets/images/content/icon_instrument_metronome21.png b/web/app/assets/images/content/icon_instrument_metronome21.png new file mode 100644 index 000000000..362f08665 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_metronome21.png differ diff --git a/web/app/assets/images/content/icon_instrument_metronome45.png b/web/app/assets/images/content/icon_instrument_metronome45.png new file mode 100644 index 000000000..3c06b7544 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_metronome45.png differ diff --git a/web/app/assets/images/content/icon_leave.png b/web/app/assets/images/content/icon_leave.png new file mode 100644 index 000000000..1e2a53ff2 Binary files /dev/null and b/web/app/assets/images/content/icon_leave.png differ diff --git a/web/app/assets/images/content/icon_mixer.png b/web/app/assets/images/content/icon_mixer.png new file mode 100644 index 000000000..2de1cb8e2 Binary files /dev/null and b/web/app/assets/images/content/icon_mixer.png differ diff --git a/web/app/assets/images/content/icon_mute_new.png b/web/app/assets/images/content/icon_mute_new.png new file mode 100644 index 000000000..f198ae33d Binary files /dev/null and b/web/app/assets/images/content/icon_mute_new.png differ diff --git a/web/app/assets/images/content/icon_mute_sm.png b/web/app/assets/images/content/icon_mute_sm.png new file mode 100644 index 000000000..e3d953460 Binary files /dev/null and b/web/app/assets/images/content/icon_mute_sm.png differ diff --git a/web/app/assets/images/content/icon_open_folder.png b/web/app/assets/images/content/icon_open_folder.png new file mode 100644 index 000000000..d4b9851ad Binary files /dev/null and b/web/app/assets/images/content/icon_open_folder.png differ diff --git a/web/app/assets/images/content/icon_pan.png b/web/app/assets/images/content/icon_pan.png new file mode 100644 index 000000000..3599333cd Binary files /dev/null and b/web/app/assets/images/content/icon_pan.png differ diff --git a/web/app/assets/images/content/icon_record.png b/web/app/assets/images/content/icon_record.png new file mode 100644 index 000000000..4ad12a2ae Binary files /dev/null and b/web/app/assets/images/content/icon_record.png differ diff --git a/web/app/assets/images/content/icon_resync.png b/web/app/assets/images/content/icon_resync.png index 86ed62630..b9c1b410f 100644 Binary files a/web/app/assets/images/content/icon_resync.png and b/web/app/assets/images/content/icon_resync.png differ diff --git a/web/app/assets/images/content/icon_settings_sm.png b/web/app/assets/images/content/icon_settings_sm.png index 6f43ad376..b686ebecc 100644 Binary files a/web/app/assets/images/content/icon_settings_sm.png and b/web/app/assets/images/content/icon_settings_sm.png differ diff --git a/web/app/assets/images/content/icon_share.png b/web/app/assets/images/content/icon_share.png index 5c73cce88..9f2ba73a7 100644 Binary files a/web/app/assets/images/content/icon_share.png and b/web/app/assets/images/content/icon_share.png differ diff --git a/web/app/assets/images/content/icon_sound.png b/web/app/assets/images/content/icon_sound.png new file mode 100644 index 000000000..ececf76d5 Binary files /dev/null and b/web/app/assets/images/content/icon_sound.png differ diff --git a/web/app/assets/images/content/icon_video.png b/web/app/assets/images/content/icon_video.png new file mode 100644 index 000000000..f6da5cc1c Binary files /dev/null and b/web/app/assets/images/content/icon_video.png differ diff --git a/web/app/assets/images/content/icon_volume.png b/web/app/assets/images/content/icon_volume.png new file mode 100644 index 000000000..339f21fd6 Binary files /dev/null and b/web/app/assets/images/content/icon_volume.png differ diff --git a/web/app/assets/images/content/icon_volume_lg.png b/web/app/assets/images/content/icon_volume_lg.png new file mode 100644 index 000000000..a99ca1b98 Binary files /dev/null and b/web/app/assets/images/content/icon_volume_lg.png differ diff --git a/web/app/assets/images/web/button_cta_jamtrack_free.png b/web/app/assets/images/web/button_cta_jamtrack_free.png new file mode 100644 index 000000000..0ffc3f32c Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamtrack_free.png differ diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 61b889eb2..ee570a513 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -198,50 +198,56 @@ function loggedIn(header, payload) { - server.signedIn = true; - server.clientID = payload.client_id; - server.publicIP = payload.public_ip; + // reason for setTimeout: + // loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen + // but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace... - if (context.jamClient !== undefined) { - context.jamClient.connected = true; - context.jamClient.clientID = server.clientID; - } + setTimeout(function() { + server.signedIn = true; + server.clientID = payload.client_id; + server.publicIP = payload.public_ip; - clearConnectTimeout(); + if (context.jamClient !== undefined) { + context.jamClient.connected = true; + context.jamClient.clientID = server.clientID; + } - heartbeatStateReset(); + clearConnectTimeout(); - app.clientId = payload.client_id; + heartbeatStateReset(); - if(isClientMode()) { - // tell the backend that we have logged in - context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION - $.cookie('client_id', payload.client_id); - } + app.clientId = payload.client_id; - // this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios - // where there is no device on startup for the current profile. - // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want* - // it to go through unless we've passed through .OnLoggedIn - server.connected = true; - server.reconnecting = false; - server.connecting = false; - initialConnectAttempt = false; + if (isClientMode()) { + // tell the backend that we have logged in + context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION + $.cookie('client_id', payload.client_id); + } - heartbeatMS = payload.heartbeat_interval * 1000; - connection_expire_time = payload.connection_expire_time * 1000; - logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); - lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat - connectDeferred.resolve(); - $self.triggerHandler(EVENTS.CONNECTION_UP) + // this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios + // where there is no device on startup for the current profile. + // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want* + // it to go through unless we've passed through .OnLoggedIn + server.connected = true; + server.reconnecting = false; + server.connecting = false; + initialConnectAttempt = false; - activeElementEvent('afterConnect', payload); + heartbeatMS = payload.heartbeat_interval * 1000; + connection_expire_time = payload.connection_expire_time * 1000; + logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + connectDeferred.resolve(); + $self.triggerHandler(EVENTS.CONNECTION_UP) - if(payload.client_update && context.JK.ClientUpdateInstance) { - context.JK.ClientUpdateInstance.runCheck(payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size) - } + activeElementEvent('afterConnect', payload); + + if (payload.client_update && context.JK.ClientUpdateInstance) { + context.JK.ClientUpdateInstance.runCheck(payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size) + } + }, 0) } function heartbeatAck(header, payload) { diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 3d52d2139..c04a6b329 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -130,7 +130,7 @@ $('#account-content-scroller').on('click', '#account-my-jamtracks-link', function(evt) { evt.stopPropagation(); navToMyJamTracks(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-identity-link', function(evt) { evt.stopPropagation(); navToEditIdentity(); return false; } ); - $('#account-content-scroller').on('click', '#account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); + $('#account-content-scroller').on('click', '.account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-payments-link', function(evt) { evt.stopPropagation(); navToEditPayments(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } ); diff --git a/web/app/assets/javascripts/accounts_affiliate.js b/web/app/assets/javascripts/accounts_affiliate.js index 21d0b8b1c..f9b5ec02a 100644 --- a/web/app/assets/javascripts/accounts_affiliate.js +++ b/web/app/assets/javascripts/accounts_affiliate.js @@ -207,7 +207,7 @@ rest.getLinks(type) .done(populateLinkTable) .fail(function() { - app.notify({message: 'Unable to fetch links. Please try again later.' }) + app.notify({text: 'Unable to fetch links. Please try again later.' }) }) } } diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 74706efe1..362a2e4d6 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -88,8 +88,8 @@ context.JK.AccountJamTracks = class AccountJamTracks context.location = '/client#/session/' + newSessionId # Re-loading the session settings will cause the form to reset with the right stuff in it. # This is an extra xhr call, but it keeps things to a single codepath - loadSessionSettings() - context.JK.GA.trackSessionCount data.musician_access, data.fan_access, invitationCount + #loadSessionSettings() + context.JK.GA.trackSessionCount data.musician_access, data.fan_access, 0 context.JK.GA.trackSessionMusicians context.JK.GA.SessionCreationTypes.create ).fail (jqXHR) => handled = false diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 6a4a65e2c..84b205777 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -145,15 +145,18 @@ $traditionalTouringOption.val(userDetail.traditional_band_touring ? '1' : '0') context.JK.dropdown($traditionalTouringOption) - $hourlyRate.val(userDetail.paid_sessions_hourly_rate) - $dailyRate.val(userDetail.paid_sessions_daily_rate) - + // convert the value to cents + $hourlyRate.val(profileUtils.normalizeMoneyForDisplay(userDetail.paid_sessions_hourly_rate)); + $dailyRate.val(profileUtils.normalizeMoneyForDisplay(userDetail.paid_sessions_daily_rate)); + $cowritingPurpose.val(userDetail.cowriting_purpose) context.JK.dropdown($cowritingPurpose) + + renderOptionalControls() } - function bindGenreSelector(type, $btnSelect, $genreList) { - $btnSelect.unbind('click').bind('click', function(e) { + function bindGenreSelector(type, $btnSelect, $genreList) { + $btnSelect.unbind('click').bind('click', function(e) { e.preventDefault() var genreText = $genreList.html() var genres = [] @@ -161,7 +164,7 @@ genres = genreText.split(GENRE_LIST_DELIMITER) } - ui.launchGenreSelectorDialog(type, genres, function(selectedGenres) { + ui.launchGenreSelectorDialog(type, genres, function(selectedGenres) { $genreList.html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED) }) @@ -193,7 +196,8 @@ context.JK.dropdown($virtualBandCommitment) context.JK.dropdown($traditionalBandCommitment) - context.JK.dropdown($cowritingPurpose) + context.JK.dropdown($cowritingPurpose) + $screen.on('ifToggled', 'input[type="radio"].dependent-master', renderOptionalControls); } function enableSubmits() { @@ -218,6 +222,52 @@ }) } + function isChecked(val) { + return (val && val != "false"); + } + + function renderOptionalControls(e) { + if(e){e.stopPropagation()} + + + // Is virtual band selected? + if (isChecked($screen.find($('input[name="virtual_band"]:checked')).val())) { + $screen.find($(".virtual-band-dependent")).removeClass("hidden") + } else { + $screen.find($(".virtual-band-dependent")).addClass("hidden") + } + + // Is traditional band selected? + if (isChecked($screen.find($('input[name="traditional_band"]:checked')).val())) { + $screen.find($(".traditional-band-dependent")).removeClass("hidden") + } else { + $screen.find($(".traditional-band-dependent")).addClass("hidden") + } + + // Is paid sessions selected? + if (isChecked($screen.find($('input[name="paid_sessions"]:checked')).val())) { + $screen.find($(".paid-sessions-dependent")).removeClass("hidden") + } else { + $screen.find($(".paid-sessions-dependent")).addClass("hidden") + } + + // Is free sessions selected? + if (isChecked($screen.find($('input[name="free_sessions"]:checked')).val())) { + $screen.find($(".free-sessions-dependent")).removeClass("hidden") + } else { + $screen.find($(".free-sessions-dependent")).addClass("hidden") + } + + // Is cowriting selected? + if (isChecked($screen.find($('input[name="cowriting"]:checked')).val())) { + $screen.find($(".cowriting-dependent")).removeClass("hidden") + } else { + $screen.find($(".cowriting-dependent")).addClass("hidden") + } + + return false; + } + function navigateTo(targetLocation) { context.location = targetLocation } @@ -238,11 +288,11 @@ paid_sessions: $screen.find('input[name=paid_sessions]:checked').val(), paid_session_genres: $paidSessionsGenreList.html() === NONE_SPECIFIED ? [] : $paidSessionsGenreList.html().split(GENRE_LIST_DELIMITER), - paid_sessions_hourly_rate: $hourlyRate.val(), - paid_sessions_daily_rate: $dailyRate.val(), + paid_sessions_hourly_rate: profileUtils.normalizeMoneyForSubmit($hourlyRate.val()), + paid_sessions_daily_rate: profileUtils.normalizeMoneyForSubmit($dailyRate.val()), free_sessions: $screen.find('input[name=free_sessions]:checked').val(), - free_session_genre: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + free_session_genres: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), cowriting: $screen.find('input[name=cowriting]:checked').val(), cowriting_genres: $cowritingGenreList.html() === NONE_SPECIFIED ? [] : $cowritingGenreList.html().split(GENRE_LIST_DELIMITER), @@ -263,7 +313,12 @@ var errors = JSON.parse(xhr.responseText) if(xhr.status == 422) { + context.JK.append_errors($hourlyRate, 'paid_sessions_hourly_rate', errors) + context.JK.append_errors($dailyRate, 'paid_sessions_daily_rate', errors) + if(errors.errors.length > 0) { + app.notifyServerError(xhr) + } } else { app.ajaxError(xhr, textStatus, errorMessage) @@ -279,7 +334,7 @@ app.bindScreen('account/profile/interests', screenBindings) events() - + $screen.find('.interest-options').iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', @@ -288,11 +343,11 @@ profileUtils.initializeHelpBubbles($screen) } - + this.initialize = initialize this.beforeShow = beforeShow - this.afterShow = afterShow + this.afterShow = afterShow return this } - + })(window,jQuery) \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 6cce9de0e..0962d513b 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -20,6 +20,7 @@ var ui = new context.JK.UIHelper(JK.app); var target = {}; var profileUtils = context.JK.ProfileUtils; + var $screen = $('.profile-online-sample-controls', parent); // online presences var $website = $screen.find('.website'); @@ -147,24 +148,29 @@ if (samples && samples.length > 0) { $.each(samples, function(index, val) { - recordingSources.push({ + var source = { 'url': val.url, 'recording_id': val.service_id, 'recording_title': val.description - }); - - // TODO: this code is repeated in HTML file - var recordingIdAttr = ' data-recording-id="' + val.service_id + '" '; - var recordingUrlAttr = ' data-recording-url="' + val.url + '" '; - var recordingTitleAttr = ' data-recording-title="' + val.description + '"'; - var title = formatTitle(val.description); - $sampleList.append('
' + title + '
'); - $sampleList.append('
X
'); + } + recordingSources.push(source); + buildNonJamKazamEntry($sampleList, type, source); }); } } } + function buildNonJamKazamEntry($sampleList, type, source) { + // TODO: this code is repeated in HTML file + var recordingIdAttr = ' data-recording-id="' + source.recording_id + '" '; + var recordingUrlAttr = ' data-recording-url="' + source.url + '" '; + var recordingTitleAttr = ' data-recording-title="' + source.recording_title + '"'; + var title = formatTitle(source.recording_title); + $sampleList.find(".empty").addClass("hidden") + $sampleList.append('
' + title + '
'); + $sampleList.append('
X
'); + } + function buildJamkazamEntry(recordingId, recordingName) { var title = formatTitle(recordingName); @@ -179,25 +185,22 @@ $btnAddJkRecording.click(function(evt) { evt.preventDefault(); - // retrieve recordings and pass to modal dialog - api.getClaimedRecordings() - .done(function(response) { - ui.launchRecordingSelectorDialog(response, jamkazamRecordingSources, function(selectedRecordings) { - $jamkazamSampleList.empty(); + ui.launchRecordingSelectorDialog(jamkazamRecordingSources, function(selectedRecordings) { + $jamkazamSampleList.empty(); - jamkazamRecordingSources = []; + jamkazamRecordingSources = []; - // update the list with the selected recordings - $.each(selectedRecordings, function(index, val) { - jamkazamRecordingSources.push({ - 'claimed_recording_id': val.id, - 'description': val.name - }); - - buildJamkazamEntry(val.id, val.name); - }); + // update the list with the selected recordings + $.each(selectedRecordings, function(index, val) { + jamkazamRecordingSources.push({ + 'claimed_recording_id': val.id, + 'description': val.name }); + + buildJamkazamEntry(val.id, val.name); }); + }); + return false; }); @@ -221,8 +224,8 @@ }) } - function enableSubmits() { - $btnSubmit.off("click").on("click", function(e) { + function enableSubmits() { + $btnSubmit.off("click").on("click", function(e) { e.stopPropagation(); handleUpdateProfile(); return false; @@ -287,6 +290,7 @@ disableSubmits() var player = buildPlayer() + updateFn({ website: player.website, online_presences: player.online_presences, @@ -316,8 +320,13 @@ addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); + var website = $website.val() + if (website == '') { + website = null; + } + return { - website: $website.val(), + website: website, online_presences: op, performance_samples: ps } @@ -428,8 +437,8 @@ siteSuccessCallback($inputDiv, youTubeRecordingValidator, $youTubeSampleList, 'youtube'); } - function siteSuccessCallback($inputDiv, recordingSiteValidator, sampleList, type) { - sampleList.find(".empty").addClass("hidden") + function siteSuccessCallback($inputDiv, recordingSiteValidator, $sampleList, type) { + $sampleList.find(".empty").addClass("hidden") $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); @@ -437,13 +446,7 @@ if (recordingSources && recordingSources.length > 0) { var addedRecording = recordingSources[recordingSources.length-1]; - // TODO: this code is repeated in elsewhere in this JS file: - var recordingIdAttr = ' data-recording-id="' + addedRecording.recording_id + '" '; - var recordingUrlAttr = ' data-recording-url="' + addedRecording.url + '" '; - var recordingTitleAttr = ' data-recording-title="' + addedRecording.recording_title + '"'; - var title = formatTitle(addedRecording.recording_title); - sampleList.append('
' + title + '
'); - sampleList.append('
X
'); + buildNonJamKazamEntry($sampleList, type, addedRecording); } $inputDiv.find('input').val(''); @@ -457,7 +460,7 @@ }); //}); - + } // end initializeValidators. @@ -471,7 +474,12 @@ 'afterShow': afterShow }; - app.bindScreen('account/profile/samples', screenBindings); + // We only want to bind this screen for accounts: + if (updateFn.name===api.updateUser.name) { + // A little hacky, but we are soon going to replace this: + app.bindScreen('account/profile/samples', screenBindings); + } + initializeValidators(); events(); } diff --git a/web/app/assets/javascripts/accounts_video_profile.js b/web/app/assets/javascripts/accounts_video_profile.js index fbb1a6d53..ef4d4cf63 100644 --- a/web/app/assets/javascripts/accounts_video_profile.js +++ b/web/app/assets/javascripts/accounts_video_profile.js @@ -11,12 +11,12 @@ 'beforeHide':beforeHide }; app.bindScreen('account/video', screenBindings); - - $webcamViewer.init($(".webcam-container")) + + $webcamViewer.init($("#account-video-profile .webcam-container")) } function beforeShow() { - $webcamViewer.beforeShow() + $webcamViewer.beforeShow() } function beforeHide() { diff --git a/web/app/assets/javascripts/addNewGear.js b/web/app/assets/javascripts/addNewGear.js index cbe9f7d50..ab45745fd 100644 --- a/web/app/assets/javascripts/addNewGear.js +++ b/web/app/assets/javascripts/addNewGear.js @@ -3,18 +3,16 @@ "use strict"; context.JK = context.JK || {}; - context.JK.AddNewGearDialog = function(app, sessionScreen) { + context.JK.AddNewGearDialog = function(app) { var logger = context.JK.logger; function events() { $('#btn-leave-session-test').click(function() { - sessionScreen.setPromptLeave(false); + context.SessionActions.leaveSession.trigger({location: '/client#/home'}) app.layout.closeDialog('configure-tracks'); - context.location = "/client#/home"; - app.layout.startNewFtue(); }); diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js index ba6ccd459..00dd29aa6 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -161,7 +161,7 @@ /** setTimeout(function() { - var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2); + var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 4); // this is some ugly logic coming up, here's why: // we need the id (guid) that the backend generated for the new track we just added diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index e77a3a58f..811277c13 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -38,6 +38,7 @@ //= require jquery.exists //= require jquery.payment //= require jquery.visible +//= require classnames //= require reflux //= require howler.core.js //= require jstz @@ -54,11 +55,12 @@ //= require react //= require react_ujs //= require react-init -//= require react-components //= require web/signup_helper //= require web/signin_helper //= require web/signin //= require web/tracking +//= require webcam_viewer +//= require react-components //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index da48d9138..348560630 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -37,20 +37,16 @@ } function onGenericEvent(type, text) { - context.setTimeout(function() { - var alert = ALERT_TYPES[type]; - if(alert && alert.title) { - app.notify({ - "title": ALERT_TYPES[type].title, - "text": text, - "icon_url": "/assets/content/icon_alert_big.png" - }); - } - else { - logger.debug("Unhandled Backend Event type %o, data %o", type, text) - } - }, 1); + var alert = ALERT_TYPES[type]; + + if(alert && alert.title) { + context.NotificationActions.backendNotification({msg: alert.title, detail: alert.message, backend_detail:text, help: alert.help}) + } + else { + logger.debug("Unhandled Backend Event type %o, data %o", type, text) + } + } function alertCallback(type, text) { @@ -77,8 +73,11 @@ } if (type === 2) { // BACKEND_MIXER_CHANGE - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBackendMixerChanged(type, text) + + context.MixerActions.mixersChanged(type, text) + + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBackendMixerChanged(type, text) } else if (type === ALERT_NAMES.NO_VALID_AUDIO_CONFIG) { // NO_VALID_AUDIO_CONFIG if(context.JK.GearUtilsInstance && context.JK.GearUtilsInstance.isRestartingAudio()) { @@ -101,28 +100,36 @@ onStunEvent(); } else if (type === 26) { // DEAD_USER_REMOVE_EVENT - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onDeadUserRemove(type, text); + MixerActions.deadUserRemove(text); + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onDeadUserRemove(type, text); } else if (type === 27) { // WINDOW_CLOSE_BACKGROUND_MODE - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onWindowBackgrounded(type, text); + + SessionActions.windowBackgrounded() + + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onWindowBackgrounded(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_FAIL) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastFailure(type, text); + SessionActions.broadcastFailure(text) + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBroadcastFailure(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_ACTIVE) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastSuccess(type, text); + SessionActions.broadcastSuccess(text) + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBroadcastSuccess(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_STOPPED) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastStopped(type, text); + SessionActions.broadcastStopped(text) + //if(context.JK.CurrentSessionModel) + //context.JK.CurrentSessionModel.onBroadcastStopped(type, text); } else if(type === ALERT_NAMES.RECORD_PLAYBACK_STATE) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); + context.MediaPlaybackActions.playbackStateChange(text); } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { diff --git a/web/app/assets/javascripts/bandProfile.js b/web/app/assets/javascripts/bandProfile.js index b7501373b..668154b78 100644 --- a/web/app/assets/javascripts/bandProfile.js +++ b/web/app/assets/javascripts/bandProfile.js @@ -404,9 +404,6 @@ rest.getBandMembers(bandId, false) .done(function(response) { bindMusicians(response, false); - if (isMember) { - bindPendingMembers(); - } }) .fail(function(xhr) { if(xhr.status >= 500) { @@ -437,6 +434,15 @@ } function bindMusicians(musicians, isPending) { + + if (!isPending && isMember) { + bindPendingMembers(); + } + + if(!isPending) { + $('#band-profile-members').empty(); // for race conditions seen most often in testing + } + $.each(musicians, function(index, musician) { var instrumentLogoHtml = ''; if ("instruments" in musician && musician.instruments != null) { diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 4e09a8e47..1147538a0 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -42,8 +42,9 @@ accountProfileSamples.initialize() function navBack() { + var band = buildBand() if (currentStep>0) { - saveBand(function() { + saveBand(band, function() { currentStep-- renderCurrentPage() }) @@ -57,13 +58,21 @@ } function navNext() { + var band = buildBand() + if (currentStep==GENRE_STEP) { + band.genres = getSelectedGenres(); + band.validate_genres = true + } else { + band.validate_genres = false + } + if (currentStep - @logger = context.JK.logger @gearUtils = context.JK.GearUtils @ALERT_NAMES = context.JK.ALERT_NAMES; @lastCheckedBroadcast = null @@ -21,7 +21,12 @@ context.JK.ClientInit = class ClientInit this.watchBroadcast() checkBroadcast: () => - broadcastActions.load.triggerPromise() + promise = broadcastActions.load.trigger() + if promise + promise.catch(() -> + false + ) + watchBroadcast: () => if context.JK.currentUserId diff --git a/web/app/assets/javascripts/dialog/commentDialog.js b/web/app/assets/javascripts/dialog/commentDialog.js index 958d6ec27..ef8658f01 100644 --- a/web/app/assets/javascripts/dialog/commentDialog.js +++ b/web/app/assets/javascripts/dialog/commentDialog.js @@ -93,15 +93,24 @@ // $("#spnCommentCount").html(parseInt($("#spnCommentCount").text()) + 1); renderComment(comment, context.JK.currentUserId, context.JK.currentUserName, context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false); - }); + }) + .fail(function(jqXHR) { + logger.error("unable to add recording comment: " + jqXHR.responseText) + app.notify({text: 'unable to add comment, please try again later.'}) + }) } else if (entityType === 'recording') { + logger.debug("recording comment logged! " + comment) rest.addRecordingComment(recordingId, JK.currentUserId, comment) .done(function(response) { // $("#spnCommentCount", $scope).html(parseInt($("#spnCommentCount").text()) + 1); renderComment(comment, context.JK.currentUserId, context.JK.currentUserName, context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false); - }); + }) + .fail(function(jqXHR) { + logger.error("unable to add recording comment: " + jqXHR.responseText) + app.notify({text: 'unable to add comment, please try again later.'}) + }) } } } diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index e4174df90..20801cfa6 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -112,10 +112,14 @@ $voiceChatTabSelector.click(function () { // validate audio settings if (validateAudioSettings()) { + logger.debug("initializing voice chat helper") configureTracksHelper.reset(); voiceChatHelper.reset(); showVoiceChatPanel(); } + else { + logger.debug("invalid audio settings; ignoring") + } }); $btnCancel.click(function() { diff --git a/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 10fdae9a1..2402c0023 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -120,11 +120,11 @@ openingRecording = true; // tell the server we are about to start a recording - rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + rest.startPlayClaimedRecording({id: context.SessionStore.id(), claimed_recording_id: claimedRecording.id}) .done(function(response) { // update session info - context.JK.CurrentSessionModel.updateSession(response); + context.SessionActions.updateSession.trigger(response); var recordingId = $(this).attr('data-recording-id'); var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); @@ -138,7 +138,7 @@ "icon_url": "/assets/content/icon_alert_big.png" }); - rest.stopPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + rest.stopPlayClaimedRecording({id: context.SessionStore.id(), claimed_recording_id: claimedRecording.id}) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Stop Recording Playback", diff --git a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js index bd0d136aa..7474c6a8a 100644 --- a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js @@ -85,7 +85,7 @@ 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}) + rest.openBackingTrack({id: context.SessionStore.id(), backing_track_path: backingTrack.name}) .done(function(response) { var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false); @@ -99,7 +99,7 @@ // else { // logger.error("unable to open backing track") // } - context.JK.CurrentSessionModel.refreshCurrentSession(true); + context.SessionActions.syncWithServer() }) .fail(function(jqXHR) { diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 867314632..15e46d6dc 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -86,10 +86,10 @@ var jamTrack = $(this).data('server-model'); // tell the server we are about to open a jamtrack - rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) + rest.openJamTrack({id: context.SessionStore.id(), jam_track_id: jamTrack.id}) .done(function(response) { $dialog.data('result', {success:true, jamTrack: jamTrack}) - context.JK.CurrentSessionModel.updateSession(response); + context.SessionActions.updateSession.trigger(response); app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { diff --git a/web/app/assets/javascripts/dialog/rateSessionDialog.js b/web/app/assets/javascripts/dialog/rateSessionDialog.js index 644585bb5..6f501f645 100644 --- a/web/app/assets/javascripts/dialog/rateSessionDialog.js +++ b/web/app/assets/javascripts/dialog/rateSessionDialog.js @@ -54,6 +54,7 @@ function events() { $('#btn-rate-session-cancel', $scopeSelector).click(function(evt) { closeDialog(); + return false; }); $('#btn-rate-session-up', $scopeSelector).click(function(evt) { if ($(this).hasClass('selected')) { diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index 6152927a2..cceaaedc2 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -57,7 +57,7 @@ }); } else if (localResult.aggregate_state == 'PARTIALLY_MISSING') { - logger.error("unable to open recording due to some missing tracks: %o", localResults); + logger.error("unable to open recording due to some missing tracks: %o", recording, localResults); app.notify({ title: "Unable to Open Recording for Playback", text: "Some of your tracks associated with the recording are missing. This is a bug in the application.", diff --git a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js index cc9e5dcf9..10e76595b 100644 --- a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js @@ -2,7 +2,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.RecordingSelectorDialog = function(app, recordings, selectedRecordings, selectCallback) { + context.JK.RecordingSelectorDialog = function(app, selectedRecordings, selectCallback) { var logger = context.JK.logger; var rest = context.JK.Rest(); var recordingUtils = context.JK.RecordingUtils; @@ -10,173 +10,26 @@ var dialogId = 'recording-selector-dialog'; var $screen = $('#' + dialogId); var $btnSelect = $screen.find(".btn-select-recordings"); - var $instructions = $screen.find('#instructions'); var $recordings = $screen.find('.recordings'); + var $paginatorHolder = null; var feedHelper = new context.JK.Feed(app); + var $scroller = $recordings; + var $content = $recordings; + var $noMoreFeeds = $screen.find('.end-of-list'); + var $empty = $(); + feedHelper.initialize($screen, $scroller, $content, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); function beforeShow(data) { + } function afterShow(data) { - $recordings.empty(); - - $.each(recordings, function(index, val) { - bindRecordingItem(val); - }); + feedHelper.setUser(context.JK.currentUserId) + feedHelper.refresh() // hide the avatars - $screen.find('.avatar-small.ib').hide(); - } - - /********* THE FOLLOWING BLOCK IS REPEATED IN feedHelper.js **********/ - function startRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_pausebutton.png'); - $controls.trigger('play.listenRecording'); - $feedItem.data('playing', true); - } - - function stopRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_playbutton.png'); - $controls.trigger('pause.listenRecording'); - $feedItem.data('playing', false); - } - - function toggleRecordingPlay() { - - var $playLink = $(this); - var $feedItem = $playLink.closest('.feed-entry'); - var playing = $feedItem.data('playing'); - - if(playing) { - stopRecordingPlay($feedItem); - } - else { - startRecordingPlay($feedItem); - } - return false; - } - - function toggleRecordingDetails() { - var $detailsLink = $(this); - var $feedItem = $detailsLink.closest('.feed-entry'); - var $musicians = $feedItem.find('.musician-detail'); - var $description = $feedItem.find('.description'); - var $name = $feedItem.find('.name'); - var toggledOpen = $detailsLink.data('toggledOpen'); - - if(toggledOpen) { - toggleClose($feedItem, $name, $description, $musicians) - } - else { - toggleOpen($feedItem, $name, $description, $musicians) - } - - toggledOpen = !toggledOpen; - $detailsLink.data('toggledOpen', toggledOpen); - - return false; - } - - function stateChangeRecording(e, data) { - var $controls = data.element; - var $feedItem = $controls.closest('.feed-entry'); - - var $sliderBar = $('.recording-position', $feedItem); - var $statusBar = $('.recording-status', $feedItem); - var $currentTime = $('.recording-current', $feedItem); - var $status = $('.status-text', $feedItem); - var $playButton = $('.play-button', $feedItem); - - if(data.isEnd) stopRecordingPlay($feedItem); - if(data.isError) { - $sliderBar.hide(); - $playButton.hide(); - $currentTime.hide(); - $statusBar.show(); - $status.text(data.displayText); - } - } - - function toggleOpen($feedItem, $name, $description, $musicians) { - $description.trigger('destroy.dot'); - $description.data('original-height', $description.css('height')).css('height', 'auto'); - $name.trigger('destroy.dot'); - $name.data('original-height', $name.css('height')).css('height', 'auto'); - $musicians.show(); - $feedItem.animate({'max-height': '1000px'}); - } - - function toggleClose($feedItem, $name, $description, $musicians, immediate) { - $feedItem.css('height', $feedItem.height() + 'px') - $feedItem.animate({'height': $feedItem.data('original-max-height')}, immediate ? 0 : 400).promise().done(function() { - $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); - - $musicians.hide(); - $description.css('height', $description.data('original-height')); - $description.dotdotdot(); - $name.css('height', $name.data('original-height')); - $name.dotdotdot(); - }); - } - /**********************************************************/ - - function bindRecordingItem(claimedRecording) { - claimedRecording.recording.mix_info = recordingUtils.createMixInfo({state: claimedRecording.recording.mix_state}); - var options = { - feed_item: claimedRecording.recording, - candidate_claimed_recording: claimedRecording, - mix_class: claimedRecording['has_mix?'] ? 'has-mix' : 'no-mix', - }; - - var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); - var $controls = $feedItem.find('.recording-controls'); - - var $titleText = $feedItem.find('.title .title-text'); - - // if this item will be discarded, tack on a * to the RECORDING NAME - var discardTime = claimedRecording.recording['when_will_be_discarded?']; - if(discardTime) { - context.JK.helpBubble($titleText, 'recording-discarded-soon', {discardTime: discardTime}, {}); - $titleText.text($titleText.text() + '*'); - } - - $controls.data('mix-state', claimedRecording.recording.mix_info); // for recordingUtils helper methods - $controls.data('server-info', claimedRecording.recording.mix); // for recordingUtils helper methods - $controls.data('view-context', 'feed'); - - $('.timeago', $feedItem).timeago(); - context.JK.prettyPrintElements($('.duration', $feedItem)); - context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); - $('.details', $feedItem).click(toggleRecordingDetails); - $('.details-arrow', $feedItem).click(toggleRecordingDetails); - $('.play-button', $feedItem).click(toggleRecordingPlay); - - var checked = ''; - - var match = $.grep(selectedRecordings, function(obj, index) { - return obj.claimed_recording_id === claimedRecording.id; - }); - - if (match && match.length > 0) { - checked = 'checked'; - } - - // put the item on the page - $recordings.append("
"); - $recordings.append($feedItem); - - // these routines need the item to have height to work (must be after renderFeed) - $controls.listenRecording({recordingId: claimedRecording.recording.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'}); - $controls.bind('statechange.listenRecording', stateChangeRecording); - $('.dotdotdot', $feedItem).dotdotdot(); - $feedItem.data('original-max-height', $feedItem.css('height')); - context.JK.bindHoverEvents($feedItem); - context.JK.bindProfileClickEvents($feedItem); + //$screen.find('.avatar-small.ib').hide(); } function afterHide() { @@ -187,10 +40,10 @@ } function events() { - $btnSelect.click(function(evt) { + $btnSelect.off('click').on('click', function(evt) { evt.preventDefault(); var preSelectedRecordings = []; - $recordings.find('input[type=checkbox]:checked').each(function(index) { + $recordings.find('.select-box input[type=checkbox]:checked').each(function(index) { preSelectedRecordings.push({ "id": $(this).attr('data-recording-id'), "name": $(this).attr('data-recording-title') @@ -198,6 +51,7 @@ }); if (selectCallback) { + console.log("calling selectCallback", preSelectedRecordings) selectCallback(preSelectedRecordings); } @@ -217,8 +71,6 @@ app.bindDialog(dialogId, dialogBindings); - $instructions.html('Select one or more recordings and click ADD to add JamKazam recordings to your performance samples.'); - events(); } diff --git a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js index 9b29fcb28..d81b1de38 100644 --- a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js @@ -10,6 +10,7 @@ var dialogId = 'rsvp-submit-dialog'; var $btnSubmit = $("#btnSubmitRsvp"); + function beforeShow(data) { $('.error', $dialog).hide(); } @@ -56,7 +57,6 @@ $btnSubmit.unbind('click'); $btnSubmit.click(function(e) { e.preventDefault(); - var error = false; var slotIds = []; var selectedSlots = []; @@ -96,7 +96,11 @@ if (!error) { $dialog.triggerHandler(EVENTS.RSVP_SUBMITTED); - app.layout.closeDialog(dialogId); + + // Show confirmation & calendar; hide regular buttons. + $(".rsvp-options").addClass("hidden") + $(".rsvp-confirm").removeClass("hidden") + $(".buttons").addClass("hidden") } }) .fail(function(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee b/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee new file mode 100644 index 000000000..781031b95 --- /dev/null +++ b/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee @@ -0,0 +1,36 @@ +$ = jQuery +context = window +context.JK ||= {} +MIX_MODES = context.JK.MIX_MODES + +context.JK.SessionMasterMixDialog = class SessionMasterMixDialog + constructor: (@app) -> + @rest = context.JK.Rest() + @logger = context.JK.logger + @screen = null + @dialogId = 'session-master-mix-dialog' + @dialog = null + @closeBtn = null + + initialize:() => + dialogBindings = + 'beforeShow' : @beforeShow + 'afterShow' : @afterShow + 'afterHide' : @afterHide + + + @dialog = $('[layout-id="' + @dialogId + '"]') + @app.bindDialog(@dialogId, dialogBindings) + @content = @dialog.find(".dialog-inner") + + beforeShow:() => + @logger.debug("session-master-mix-dlg: beforeShow") + context.jamClient.SetMixerMode(MIX_MODES.MASTER) + + afterShow:() => + @logger.debug("session-master-mix-dlg: afterShow") + + afterHide:() => + context.jamClient.SetMixerMode(MIX_MODES.PERSONAL) + + diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index 0c47eb20e..960d44cfd 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -1,16 +1,17 @@ (function(context,$) { context.JK = context.JK || {}; - context.JK.SessionSettingsDialog = function(app, sessionScreen) { + context.JK.SessionSettingsDialog = function(app) { var logger = context.JK.logger; var gearUtils = context.JK.GearUtilsInstance; var $dialog; var $screen = $('#session-settings'); - var $selectedFilenames = $screen.find('#selected-filenames'); - var $uploadSpinner = $screen.find('.upload-spinner'); - var $selectedFilenames = $('#settings-selected-filenames'); + //var $selectedFilenames = $screen.find('#selected-filenames'); + var $uploadSpinner = $screen.find('.spinner-small'); + //var $selectedFilenames = $('#settings-selected-filenames'); var $inputFiles = $screen.find('#session-select-files'); var $btnSelectFiles = $screen.find('.btn-select-files'); + var $inputBox = $screen.find('.inputbox') var rest = new JK.Rest(); var sessionId; @@ -21,7 +22,7 @@ context.JK.GenreSelectorHelper.render('#session-settings-genre'); $dialog = $('[layout-id="session-settings"]'); - var currentSession = sessionScreen.getCurrentSession(); + var currentSession = context.SessionStore.currentSession; sessionId = currentSession.id; // id @@ -65,13 +66,21 @@ $('#session-settings-fan-access').val('listen-chat-band'); } - // notation files + /** + // notation files in the account screen. ugh. $selectedFilenames.empty(); for (var i=0; i < currentSession.music_notations.length; i++) { var notation = currentSession.music_notations[i]; $selectedFilenames.append('' + notation.file_name + ' '); + }*/ + + $inputBox.empty(); + for (var i=0; i < currentSession.music_notations.length; i++) { + var notation = currentSession.music_notations[i]; + addNotation(notation) } + context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); @@ -81,6 +90,29 @@ $('#session-settings-fan-access').easyDropDown(easyDropDownState) } + function addNotation(notation) { + + var $notation = $('
' + notation.file_name + '
X
') + $notation.find('a').on('click', function(e) { + + if($(this).attr('data-deleting')) { + // ignore duplicate delete attempts + return false; + } + + $(this).attr('data-deleting', true) + var $notationEntry = $(this).closest('.notation-entry').find('div').text('deleting...') + + rest.deleteMusicNotation({id: notation.id}) + .done(function() { + $notation.remove() + }) + .fail(app.ajaxError) + return false; + }) + $inputBox.append($notation); + } + function saveSettings(evt) { var data = {}; @@ -111,16 +143,14 @@ data.fan_access = false; data.fan_chat = false; } - else if (fanAccess == 'listen-chat-each') { - data.fan_access = true; - data.fan_chat = false; - } - else if (fanAccess == 'listen-chat-band') { + else if (fanAccess == 'listen-chat') { data.fan_access = true; data.fan_chat = true; } rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved); + + return false; } function uploadNotations(notations) { @@ -177,7 +207,7 @@ } }) .always(function() { - $btnSelectFiles.text('SELECT FILES...').data('uploading', null) + $btnSelectFiles.text('ADD FILES...').data('uploading', null) $uploadSpinner.hide(); }); } @@ -203,10 +233,9 @@ else { // upload as soon as user picks their files. uploadNotations($inputFiles.get(0).files) - .done(function() { - context._.each(fileNames, function(fileName) { - var $text = $('').text(fileName); - $selectedFilenames.append($text); + .done(function(response) { + context._.each(response, function(notation) { + addNotation(notation) }) }) } @@ -225,13 +254,13 @@ function settingsSaved(response) { // No response returned from this call. 204. - sessionScreen.refreshCurrentSession(true); + context.SessionActions.syncWithServer() app.layout.closeDialog('session-settings'); } function events() { $('#session-settings-dialog-submit').on('click', saveSettings); - + $('#session-settings-dialog').on('submit', saveSettings) $inputFiles.on('change', changeSelectedFiles); $btnSelectFiles.on('click', toggleSelectFiles); } diff --git a/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee b/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee index 29d2670bf..14fbdd785 100644 --- a/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee +++ b/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee @@ -15,7 +15,8 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog initialize:(@url, @caption) => dialogBindings = { 'beforeShow' : @beforeShow, - 'afterShow' : @afterShow + 'afterShow' : @afterShow, + 'afterHide' : @afterHide } @dialog = $('[layout-id="' + @dialogId + '"]') @@ -27,9 +28,15 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog beforeShow:() => @player.addClass("hidden") @player.attr("src", "") - u = encodeURIComponent(@url) - src = "https://w.soundcloud.com/player/?url=#{u}&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true&loop=true" - @player.attr("src", src) + + # the Windows client does not play back correctly + if context.jamClient.IsNativeClient() + context.JK.popExternalLink(@url) + return false + else + u = encodeURIComponent(@url) + src = "https://w.soundcloud.com/player/?url=#{u}&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true&loop=true" + @player.attr("src", src) afterShow:() => @player.removeClass("hidden") @@ -37,4 +44,7 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog showDialog:() => @app.layout.showDialog(@dialogId) + afterHide: () => + @player.attr('src', '') + \ No newline at end of file diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index ba66a44ca..6d573a2c8 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -11,6 +11,7 @@ var $draggingFaderHandle = null; var $draggingFader = null; + var $floater = null; var draggingOrientation = null; var logger = g.JK.logger; @@ -20,6 +21,7 @@ e.stopPropagation(); var $fader = $(this); + var floaterConvert = $fader.data('floaterConverter') var sessionModel = window.JK.CurrentSessionModel || null; var mediaControlsDisabled = $fader.data('media-controls-disabled'); @@ -43,7 +45,7 @@ } } - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -53,6 +55,10 @@ return false; } + if(floaterConvert) { + window.JK.FaderHelpers.setFloaterValue($fader.find('.floater'), floaterConvert(faderPct)) + } + $fader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false}) setHandlePosition($fader, faderPct); @@ -61,9 +67,9 @@ function setHandlePosition($fader, value) { var ratio, position; - var $handle = $fader.find('div[control="fader-handle"]'); + var $handle = $fader.find('div[data-control="fader-handle"]'); - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var handleCssAttribute = getHandleCssAttribute($fader); // required because this method is entered directly when from a callback @@ -81,7 +87,7 @@ } function faderValue($fader, e, offset) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var getPercentFunction = getVerticalFaderPercent; var relativePosition = offset.top; if (orientation && orientation == 'horizontal') { @@ -92,7 +98,7 @@ } function getHandleCssAttribute($fader) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); return (orientation === 'horizontal') ? 'left' : 'top'; } @@ -134,12 +140,34 @@ return false; } + // simple snap feature to stick to the mid point + if(faderPct > 46 && faderPct < 54 && $draggingFader.data('snap')) { + faderPct = 50 + var orientation = $draggingFader.attr('data-orientation'); + if(orientation == 'horizontal') { + var width = $draggingFader.width() + var left = width / 2 + ui.position.left = left + } + else { + var height = $draggingFader.height() + var top = height / 2 + ui.position.top = top + } + } + + var floaterConvert = $draggingFaderHandle.data('floaterConverter') + + if(floaterConvert && $floater) { + window.JK.FaderHelpers.setFloaterValue($floater, floaterConvert(faderPct)) + } $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: true}) } function onFaderDragStart(e, ui) { $draggingFaderHandle = $(this); - $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); + $draggingFader = $draggingFaderHandle.closest('div[data-control="fader"]'); + $floater = $draggingFaderHandle.find('.floater') draggingOrientation = $draggingFader.attr('orientation'); var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); @@ -210,12 +238,12 @@ selector.html(g._.template(templateSource, options)); - selector.find('div[control="fader"]') + selector.find('div[data-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({ + selector.find('div[data-control="fader-handle"]').draggable({ drag: onFaderDrag, start: onFaderDragStart, stop: onFaderDragStop, @@ -233,6 +261,43 @@ } }, + renderFader2: function (selector, userOptions, floaterConverter) { + selector = $(selector); + if (userOptions === undefined) { + throw ("renderFader: userOptions is required"); + } + var renderDefaults = { + faderType: "vertical" + }; + var options = $.extend({}, renderDefaults, userOptions); + + selector.find('div[data-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')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + selector.find('div[data-control="fader-handle"]').draggable({ + drag: onFaderDrag, + start: onFaderDragStart, + 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('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + // Embed any custom styles, applied to the .fader below selector + if ("style" in options) { + for (var key in options.style) { + selector.find(' .fader').css(key, options.style[key]); + } + } + }, + convertLinearToDb: function (input) { // deal with extremes better @@ -263,27 +328,48 @@ // composite function resembling audio taper if (input <= 1) { return -80; } - if (input <= 28) { return (2 * input - 80); } - if (input <= 79) { return (0.5 * input - 38); } - if (input < 99) { return (0.875 * input - 67.5); } + if (input <= 28) { return Math.round((2 * input - 80)); } // -78 to -24 db + if (input <= 79) { return Math.round((0.5 * input - 38)); } // -24 to 1.5 db + if (input < 99) { return Math.round((0.875 * input - 67.5)); } // 1.625 - 19.125 db if (input >= 99) { return 20; } }, + convertAudioTaperToPercent: function(db) { + if(db <= -78) { return 0} + if(db <= -24) { return (db + 80) / 2 } + if(db <= 1.5) { return (db + 38) / .5 } + if(db <= 19.125) { return (db + 67.5) / 0.875 } + return 100; + }, - setFaderValue: function (faderId, faderValue) { - var $fader = $('[fader-id="' + faderId + '"]'); + + setFaderValue: function (faderId, faderValue, floaterValue) { + var $fader = $('[data-fader-id="' + faderId + '"]'); this.setHandlePosition($fader, faderValue); + if(floaterValue !== undefined) { + var $floater = $fader.find('.floater') + this.setFloaterValue($floater, floaterValue) + } + }, + + showFader: function(faderId) { + var $fader = $('[data-fader-id="' + faderId + '"]'); + $fader.find('div[data-control="fader-handle"]').show() }, setHandlePosition: function ($fader, faderValue) { - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); setHandlePosition($fader, faderValue); draggingOrientation = null; }, + setFloaterValue: function($floater, floaterValue) { + $floater.text(floaterValue) + }, + initialize: function () { - $('body').on('click', 'div[control="fader"]', faderClick); + $('body').on('click', 'div[data-control="fader"]', faderClick); } }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 8347edae5..81826b5eb 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -4,7 +4,7 @@ context.JK = context.JK || {}; context.JK.FakeJamClient = function(app, p2pMessageFactory) { - var ChannelGroupIds = context.JK.ChannelGroupIds; + var ChannelGroupIds = context.JK.ChannelGroupIds var logger = context.JK.logger; logger.info("*** Fake JamClient instance initialized. ***"); @@ -170,22 +170,22 @@ function FTUEGetMusicInputs() { dbg('FTUEGetMusicInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetMusicOutputs() { dbg('FTUEGetMusicOutputs'); return { - "o~11~Multichannel (FW AP Multi)~0^o~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "o~11~Multichannel (FWAPMulti)~0^o~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChatInputs() { dbg('FTUEGetChatInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChannels() { @@ -450,7 +450,7 @@ } function GetASIODevices() { - var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; + var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FWAPMulti","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; } @@ -475,8 +475,8 @@ } function SessionGetControlState(mixerIds, isMasterOrPersonal) { dbg("SessionGetControlState"); - var groups = [ - ChannelGroupIds.MasterGroup, + var groups = + [ChannelGroupIds.MasterGroup, ChannelGroupIds.MonitorGroup, ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.AudioInputChatGroup, @@ -485,13 +485,12 @@ ChannelGroupIds.UserChatInputGroup, ChannelGroupIds.PeerMediaTrackGroup, ChannelGroupIds.JamTrackGroup, - ChannelGroupIds.MetronomeGroup - ] + ChannelGroupIds.MetronomeGroup]; var names = [ - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", "", "", "", @@ -545,6 +544,7 @@ stereo: true, volume_left: -40, volume_right:-40, + pan: 0, instrument_id:50, // see globals.js mode: isMasterOrPersonal, rid: mixerIds[i] @@ -554,10 +554,10 @@ } function SessionGetIDs() { return [ - "FW AP Multi_0_10000", - "FW AP Multi_1_10100", - "FW AP Multi_2_10200", - "FW AP Multi_3_10500", + "FWAPMulti_0_10000", + "FWAPMulti_1_10100", + "FWAPMulti_2_10200", + "FWAPMulti_3_10500", "User@208.191.152.98#", "User@208.191.152.98_*" ]; @@ -624,9 +624,9 @@ function doCallbacks() { var names = ["vu"]; - //var ids = ["FW AP Multi_2_10200", "FW AP Multi_0_10000"]; - var ids= ["i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1", - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~2"]; + //var ids = ["FWAPMulti_2_10200", "FWAPMulti_0_10000"]; + var ids= ["i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1", + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~2"]; var args = []; for (var i=0; iclick here."}, // PACKET_JTR, - 4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here." }, // PACKET_LOSS - 5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // PACKET_LATE, - 6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // JTR_QUEUE_DEPTH, - 7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // NETWORK_JTR, - 8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click here." }, // NETWORK_PING, - 9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click here."}, // BITRATE_THROTTLE_WARN, - 10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, click here." }, // BANDWIDTH_LOW + 3: {"title": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // PACKET_JTR, + 4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // PACKET_LOSS + 5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // PACKET_LATE, + 6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // JTR_QUEUE_DEPTH, + 7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // NETWORK_JTR, + 8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // NETWORK_PING, + 9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // BITRATE_THROTTLE_WARN, + 10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // BANDWIDTH_LOW // IO related events - 11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // INPUT_IO_RATE - 12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // INPUT_IO_JTR, - 13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // OUTPUT_IO_RATE - 14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // OUTPUT_IO_JTR, + 11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // INPUT_IO_RATE + 12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // INPUT_IO_JTR, + 13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // OUTPUT_IO_RATE + 14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // OUTPUT_IO_JTR, // CPU load related - 15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click here." }, // CPU_LOAD + 15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // CPU_LOAD 16: {"title": "Decode Violations", "message": ""}, // DECODE_VIOLATIONS, 17: {"title": "", "message": ""}, // LAST_THRESHOLD 18: {"title": "Wifi Alert", "message": ""}, // WIFI_NETWORK_ALERT, //user or peer is using wifi @@ -162,10 +162,10 @@ 33: {"title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast."}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user 34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message - 35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click here."}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable. + 35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable. //indicates problem with user computer stack or network itself (wifi, antivirus etc) - 36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click here."}, // LOCAL_NETWORK_LATENCY_HIGH, + 36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // LOCAL_NETWORK_LATENCY_HIGH, 37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end 38: {"title": "No Audio Sent", "message": ""}, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end 39: {"title": "", "message": ""}, // SHOW_PREFERENCES, //show preferences dialog @@ -314,19 +314,17 @@ MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix' } - // Recreate ChannelGroupIDs ENUM from C++ - context.JK.ChannelGroupIds = - { + context.JK.ChannelGroupIds = { "MasterGroup": 0, "MonitorGroup": 1, - "MasterCatGroup": 2, - "MonitorCatGroup": 3, + "MasterCatGroup" : 2, + "MonitorCatGroup" : 3, "AudioInputMusicGroup": 4, "AudioInputChatGroup": 5, "MediaTrackGroup": 6, "StreamOutMusicGroup": 7, "StreamOutChatGroup": 8, - "StreamOutMediaGroup": 9, + "StreamOutMediaGroup" : 9, "UserMusicInputGroup": 10, "UserChatInputGroup": 11, "UserMediaInputGroup": 12, @@ -335,4 +333,34 @@ "JamTrackGroup": 15, "MetronomeGroup": 16 }; - })(window,jQuery); \ No newline at end of file + + context.JK.ChannelGroupLookup = { + 0: "MasterGroup", + 1: "MonitorGroup", + 2: "MasterCatGroup", + 3: "MonitorCatGroup", + 4: "AudioInputMusicGroup", + 5: "AudioInputChatGroup", + 6: "MediaTrackGroup", + 7: "StreamOutMusicGroup", + 8: "StreamOutChatGroup", + 9: "StreamOutMediaGroup", + 10: "UserMusicInputGroup", + 11: "UserChatInputGroup", + 12: "UserMediaInputGroup", + 13: "PeerAudioInputMusicGroup", + 14: "PeerMediaTrackGroup", + 15: "JamTrackGroup", + 16: "MetronomeGroup" + } + context.JK.CategoryGroupIds = { + "AudioInputMusic" : "AudioInputMusic", + "AudioInputChat" : "AudioInputChat", + "UserMusic" : "UserMusic", + "UserChat" : "UserChat", + "UserMedia" : "UserMedia", + "MediaTrack" : "MediaTrack", + "Metronome" : "Metronome" + } + +})(window,jQuery); diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index c8ccdc4cf..9e2e54083 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -29,6 +29,7 @@ function bigHelpOptions(options) { return {positions: options.positions, offsetParent: options.offsetParent, + width:options.width, spikeGirth: 15, spikeLength: 20, fill: 'white', @@ -68,13 +69,13 @@ helpBubble.jamtrackLandingPreview($preview, $preview.offsetParent()) setTimeout(function() { - helpBubble.jamtrackLandingVideo($video, $video.offsetParent()) + helpBubble.jamtrackLandingVideo($video, $video.closest('.row')) setTimeout(function() { helpBubble.jamtrackLandingCta($ctaButton, $alternativeCta) }, 11000); // 5 seconds on top of 6 second show time of bubbles }, 11000); // 5 seconds on top of 6 second show time of bubbles - }, 1500) + }, 15000) }) @@ -101,18 +102,19 @@ } helpBubble.jamtrackLandingPreview = function($element, $offsetParent) { - context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right'], offsetParent: $offsetParent})) + console.log("SHOWING THE PREVIEW BUBBLE") + context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right', 'top'], offsetParent: $offsetParent, width:250})) } helpBubble.jamtrackLandingVideo = function($element, $offsetParent) { - context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['left'], offsetParent: $offsetParent})) + context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['top', 'right'], offsetParent: $offsetParent})) } helpBubble.jamtrackLandingCta = function($element, $alternativeElement) { - if ($element.visible()) { - context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['left']})) + if (!$alternativeElement || $element.visible()) { + context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['top', 'right'], width:240})) } - else { + else if($alternativeElement) { context.JK.prodBubble($alternativeElement, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['right']})) } } diff --git a/web/app/assets/javascripts/instrumentSelector.js b/web/app/assets/javascripts/instrumentSelector.js index 51e740b75..7c4197a3c 100644 --- a/web/app/assets/javascripts/instrumentSelector.js +++ b/web/app/assets/javascripts/instrumentSelector.js @@ -10,6 +10,7 @@ var rest = new context.JK.Rest(); var _instruments = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] var _rsvp = false; + var _noICheck = false; if (typeof(_parentSelector)=="undefined") {_parentSelector=null} var _parentSelector = parentSelector; var deferredInstruments = null; @@ -100,7 +101,7 @@ selectedInstruments.push({id: id, name: name, level: level}); } }); - + return selectedInstruments; } @@ -109,13 +110,15 @@ return; } $.each(instrumentList, function (index, value) { - $('input[type="checkbox"][session-instrument-id="' + value.id + '"]') + var $item = $('input[type="checkbox"][session-instrument-id="' + value.id + '"]') .attr('checked', 'checked') - .iCheck({ + if(!_noICheck) { + $item.iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true - }); + }) + } if (_rsvp) { $('select[session-instrument-id="' + value.id + '"].rsvp-count', _parentSelector).val(value.count); $('select[session-instrument-id="' + value.id + '"].rsvp-level', _parentSelector).val(value.level); @@ -126,8 +129,9 @@ }); } - function initialize(rsvp) { + function initialize(rsvp, noICheck) { _rsvp = rsvp; + _noICheck = noICheck; // XXX; _instruments should be populated in a template, rather than round-trip to server if(!context.JK.InstrumentSelectorDeferred) { // this dance is to make sure there is only one server request instead of InstrumentSelector instances * diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index e155a0aba..1a6c2c59a 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -95,6 +95,14 @@ }); } + + function deleteMusicNotation(options) { + return $.ajax({ + type: "DELETE", + url: "/api/music_notations/" +options.id + }); + } + function legacyJoinSession(options) { var sessionId = options["session_id"]; delete options["session_id"]; @@ -478,6 +486,14 @@ }); } + function deleteParticipant(clientId) { + var url = "/api/participants/" + clientId; + return $.ajax({ + type: "DELETE", + url: url + }); + } + function login(options) { var url = '/api/auths/login'; @@ -507,16 +523,12 @@ function getUserProfile(options) { var id = getId(options); - var profile = null; - if (id != null && typeof(id) != 'undefined') { - profile = $.ajax({ + return $.ajax({ type: "GET", dataType: "json", url: "/api/users/" + id + "/profile", processData: false }); - } - return profile; } function createAffiliatePartner(options) { @@ -1840,6 +1852,7 @@ this.createScheduledSession = createScheduledSession; this.uploadMusicNotations = uploadMusicNotations; this.getMusicNotation = getMusicNotation; + this.deleteMusicNotation = deleteMusicNotation; this.getBroadcastNotification = getBroadcastNotification; this.quietBroadcastNotification = quietBroadcastNotification; this.legacyJoinSession = legacyJoinSession; @@ -1895,6 +1908,7 @@ this.addRecordingLike = addRecordingLike; this.addPlayablePlay = addPlayablePlay; this.getSession = getSession; + this.deleteParticipant = deleteParticipant; this.getClientDownloads = getClientDownloads; this.createEmailInvitations = createEmailInvitations; this.createMusicianInvite = createMusicianInvite; diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index ac25cc4c8..92c514be6 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -308,7 +308,6 @@ context.JK.JamTrackScreen=class JamTrackScreen rest.addJamtrackToShoppingCart(params).done((response) => if(isFree) if context.JK.currentUserId? - alert("TODO") context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices context.location = '/client#/redeemComplete' else diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index d1b41d451..e32b0f041 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -226,7 +226,7 @@ var errors = JSON.parse(jqXHR.responseText); var $errors = context.JK.format_all_errors(errors); logger.debug("Unprocessable entity sent from server:", JSON.stringify(errors)) - this.notify({title: title, text: $errors, icon_url: "/assets/content/icon_alert_big.png"}) + this.notify({title: title || "Validation Error", text: $errors, icon_url: "/assets/content/icon_alert_big.png"}) } else if(jqXHR.status == 403) { logger.debug("permission error sent from server:", jqXHR.responseText) diff --git a/web/app/assets/javascripts/jquery.metronomePlaybackMode.js b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js index 720cea539..222c4f2e4 100644 --- a/web/app/assets/javascripts/jquery.metronomePlaybackMode.js +++ b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js @@ -24,7 +24,7 @@ $.fn.metronomePlaybackMode = function(options) { - options = options || {mode: 'self'} + options = $.extend(false, {mode: 'self', positions: ['top']}, options); return this.each(function(index) { @@ -78,8 +78,8 @@ spikeLength:0, width:180, closeWhenOthersOpen: true, - offsetParent: $parent.offsetParent(), - positions:['top'], + offsetParent: options.offsetParent || $parent.offsetParent(), + positions: options.positions, preShow: function() { $parent.find('.down-arrow').removeClass('down-arrow').addClass('up-arrow') }, diff --git a/web/app/assets/javascripts/minimal/minimal.js b/web/app/assets/javascripts/minimal/minimal.js new file mode 100644 index 000000000..70bfd5f0b --- /dev/null +++ b/web/app/assets/javascripts/minimal/minimal.js @@ -0,0 +1,25 @@ +//= require bugsnag +//= require bind-polyfill +//= require jquery +//= require jquery.monkeypatch +//= require jquery_ujs +//= require jquery.ui.draggable +//= require jquery.ui.droppable +//= require jquery.bt +//= require jquery.icheck +//= require jquery.easydropdown +//= require jquery.metronomePlaybackMode +//= require classnames +//= require reflux +//= require AAC_underscore +//= require AAA_Log +//= require globals +//= require jam_rest +//= require ga +//= require utils +//= require playbackControls +//= require webcam_viewer +//= require react +//= require react_ujs +//= require react-init +//= require react-components \ No newline at end of file diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index 61273a2db..7481672b7 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -426,7 +426,7 @@ backendGuardTimeout = setTimeout(function () { clearBackendGuard(); backendTimedOut() - }, (gon.ftue_network_test_duration + 1) * 1000); + }, (gon.ftue_network_test_duration + 5) * 1000); } function attemptTestPass() { diff --git a/web/app/assets/javascripts/panHelpers.js.coffee b/web/app/assets/javascripts/panHelpers.js.coffee new file mode 100644 index 000000000..3347d86b3 --- /dev/null +++ b/web/app/assets/javascripts/panHelpers.js.coffee @@ -0,0 +1,37 @@ +context = window +$ = jQuery + +panHelper = class PanHelper + + ### + Convert the pan value that comes from a backend mixer + to a 0-100 % usable by a draggable panner element + ### + convertPanToPercent: (mixerPan) -> + value = (((mixerPan + 90) / 90) * 100) / 2 + + if value < 0 + 0 + else if value > 100 + 100 + else + value + + ### + Convert the % value of a draggable panner element + to a mixer-ready pan value + ### + convertPercentToPan: (percent) -> + value = 2 * percent / 100 * 90 - 90 + + if value < -90 + -90 + else if value > 90 + 90 + else + Math.round(value) + + convertPercentToPanForDisplay: (percent) -> + Math.abs(context.JK.PanHelpers.convertPercentToPan(percent)) + +context.JK.PanHelpers = new panHelper() \ No newline at end of file diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index 84ec5aed6..882c847c5 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -1,416 +1,498 @@ /** * Playback widget (play, pause , etc) */ -(function(context, $) { - "use strict"; +(function (context, $) { - var PlaybackMode = { - NoPlayback: 0, - EveryWhere: 1, - PrivatePreview: 2, - PreviewToAll: 3, - LastPbMode: 4 - }; + "use strict"; + var PlaybackMode = { + NoPlayback: 0, + EveryWhere: 1, + PrivatePreview: 2, + PreviewToAll: 3, + LastPbMode: 4 + }; - context.JK = context.JK || {}; - context.JK.PlaybackControls = function($parentElement, options){ + context.JK = context.JK || {}; + context.JK.PlaybackControls = function ($parentElement, options) { - options = $.extend(false, {playmodeControlsVisible:false}, options); + options = $.extend(false, {playmodeControlsVisible: false, mediaActions: null}, options); - var logger = context.JK.logger; - if($parentElement.length == 0) { - logger.debug("no $parentElement specified in PlaybackControls"); - } - - var PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE; - - var $playButton = $('.play-button img.playbutton', $parentElement); - var $pauseButton = $('.play-button img.pausebutton', $parentElement); - var $stopButton = $('.stop-button img.stopbutton', $parentElement); - var $currentTime = $('.recording-current', $parentElement); - var $duration = $('.duration-time', $parentElement); - var $sliderBar = $('.recording-playback', $parentElement); - var $slider = $('.recording-slider', $parentElement); - var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement); - var $jamTrackGetReady = $('.jam-track-get-ready', $parentElement); - - var $self = $(this); - - var playbackPlaying = false; - var playbackDurationMs = 0; - var playbackPositionMs = 0; - var durationChanged = false; - var seenActivity = false; - - var endReached = false; - var dragging = false; - var playingWhenDragStart = false; - var draggingUpdateTimer = null; - var canUpdateBackend = false; - var playbackMode = PlaybackMode.EveryWhere; - var monitorPlaybackTimeout = null; - var playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; - var monitoring = false; - - function init() { - updateSliderPosition(0); - updateDurationTime(0); - updateCurrentTime(0); - seenActivity = false; - } - - function startPlay() { - seenActivity = false; - updateIsPlaying(true); - if(endReached) { - update(0, playbackDurationMs, playbackPlaying); - } - $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); - - - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - var sessionModel = context.JK.CurrentSessionModel || null; - context.JK.GA.trackJamTrackPlaySession(sessionModel.id(), true) - } - } - - function stopPlay(endReached) { - updateIsPlaying(false); - $self.triggerHandler('stop', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); - } - - function pausePlay(endReached) { - updateIsPlaying(false); - $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); - } - - function updateOffsetBasedOnPosition(offsetLeft) { - var sliderBarWidth = $sliderBar.width(); - - playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); - updateCurrentTimeText(playbackPositionMs); - if(canUpdateBackend) { - $self.triggerHandler('change-position', {positionMs: playbackPositionMs, playbackMonitorMode: playbackMonitorMode}); - canUpdateBackend = false; - } - } - - function startDrag(e, ui) { - dragging = true; - playingWhenDragStart = playbackPlaying; - //draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging - //if(playingWhenDragStart) { - //stopPlay(); - //} - } - - function stopDrag(e, ui) { - dragging = false; - - clearInterval(draggingUpdateTimer); - - canUpdateBackend = true; - updateOffsetBasedOnPosition(ui.position.left); - updateSliderPosition(playbackPositionMs); - - //if(playingWhenDragStart) { - // playingWhenDragStart = false; - // startPlay(); - //} - } - - function onDrag(e, ui) { - updateOffsetBasedOnPosition(ui.position.left); - } - - $playButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $playButton}) - // return false; - //} - - startPlay(); - return false; - }); - - $pauseButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) - // return false; - //} - - pausePlay(); - return false; - }); - - $stopButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) - // return false; - //} - - stopPlay(); - return false; - }); - - $sliderBar.on('click', function(e) { - var offset = e.pageX - $(this).offset().left; - canUpdateBackend = true; - updateOffsetBasedOnPosition(offset); - updateSliderPosition(playbackPositionMs); - return false; - }); - - $slider.draggable({ - axis: 'x', - containment: $sliderBar, - start: startDrag, - stop: stopDrag, - drag: onDrag - }); - - - if(options.playmodeControlsVisible) { - $('.playback-mode-buttons.icheckbuttons', $parentElement).show(); - } - - $playmodeButton.iCheck({ - checkboxClass: 'icheckbox_minimal', - radioClass: 'iradio_minimal', - inheritClass: true - }); - - - $playmodeButton.on('ifChecked', function(e) { - var playmode = $(this).val(); - logger.debug("set new playmode", playmode); - setPlaybackMode(playmode); - }); - - function styleControls() { - $jamTrackGetReady.attr('data-mode', playbackMonitorMode); - - $parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode'); - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) { - $parentElement.addClass('mediafile-mode'); - } - else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - $parentElement.addClass('jamtrack-mode'); - } - else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { - $parentElement.addClass('metronome-mode'); - } - else - { - throw "unknown playbackMonitorMode: " + playbackMonitorMode; - } - } - function monitorRecordingPlayback() { - if(!monitoring) { - return; - } - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); - var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); - var durationMs = duration.media_len; - var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins - } - else { - var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); - var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); - } - - var isPlaying = context.jamClient.isSessionTrackPlaying(); - - if(positionMs < 0) { - // bug in backend? - positionMs = 0; - } - - if(positionMs > 0) { - seenActivity = true; - } - - - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { - updateIsPlaying(isPlaying); - } - else { - update(positionMs, durationMs, isPlaying); - } - - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - - if(playbackPlaying) { - $jamTrackGetReady.attr('data-current-time', positionMs) - } - else { - // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing - $jamTrackGetReady.attr('data-current-time', -1) - } - - } - - monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); - } - - function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) { - - if(dragging) { - return; - } - - // at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to - //logger.debug("currentTimeMs, durationTimeMs, mode", currentTimeMs, durationTimeMs, playbackMonitorMode); - if(currentTimeMs == 0 && seenActivity) { - if(playbackPlaying) { - isPlaying = false; - durationTimeMs = playbackDurationMs; - currentTimeMs = playbackDurationMs; - stopPlay(true); - endReached = true; - logger.debug("end reached"); - } - else { - // make sure slide shows '0' - updateCurrentTime(currentTimeMs); - return; - } - } - - - if(currentTimeMs < offsetStart) { - currentTimeMs = 0; // this is to squelch movement during tap-in period - } - updateDurationTime(durationTimeMs); - updateCurrentTime(currentTimeMs); - updateIsPlaying(isPlaying); - - durationChanged = false; - } - - function updateDurationTime(timeMs) { - if(timeMs != playbackDurationMs) { - $duration.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); - playbackDurationMs = timeMs; - durationChanged = true; - } - } - - function updateCurrentTimeText(timeMs) { - $currentTime.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); - } - - function updateSliderPosition(timeMs) { - - var slideWidthPx = $sliderBar.width(); - var xPos = Math.ceil(timeMs / playbackDurationMs * slideWidthPx); - $slider.css('left', xPos); - } - - function updateCurrentTime(timeMs) { - if(timeMs != playbackPositionMs || durationChanged) { - - updateCurrentTimeText(timeMs); - updateSliderPosition(timeMs); - - playbackPositionMs = timeMs; - } - } - - function updateIsPlaying(isPlaying) { - if(isPlaying != playbackPlaying) { - if(isPlaying) { - $playButton.hide(); - $pauseButton.show(); - } - else { - $playButton.show(); - $pauseButton.hide(); - } - - logger.debug("updating is playing: " + isPlaying) - playbackPlaying = isPlaying; - } - } - - function setPlaybackMode(mode) { - if(mode == 'preview-to-all') { - playbackMode = PlaybackMode.PreviewToAll; - } - else if(mode == 'preview-to-me') { - playbackMode = PlaybackMode.PrivatePreview; - } - else if(mode == 'eveywhere') { - playbackMode = PlaybackMode.EveryWhere; - } - else { - logger.error("unable to set playback mode", mode); - } - - // let the mode change immediately affect the behavior of the stream - if(playbackPlaying) { - stopPlay(); - startPlay(); - } - } - - function startMonitor(_playbackMonitorMode) { - monitoring = true; - // resets everything to zero - init(); - - if(_playbackMonitorMode === undefined || _playbackMonitorMode === null) { - playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; - } - else { - playbackMonitorMode = _playbackMonitorMode; - } - - logger.debug("playbackControl.startMonitor " + playbackMonitorMode + "") - - styleControls(); - monitorRecordingPlayback(); - } - - function stopMonitor() { - monitoring = false; - logger.debug("playbackControl.stopMonitor") - if(monitorPlaybackTimeout!= null) { - clearTimeout(monitorPlaybackTimeout); - monitorPlaybackTimeout = null; - } - } - - function onPlayStartEvent() { - updateIsPlaying(true); - playbackPlaying = true; - seenActivity = false; - } - - function onPlayStopEvent() { - updateIsPlaying(false); - playbackPlaying = false; - } - - function onPlayPauseEvent() { - playbackPlaying = false; - } - - this.update = update; - this.setPlaybackMode = setPlaybackMode; - this.startMonitor = startMonitor; - this.stopMonitor = stopMonitor; - this.onPlayStopEvent = onPlayStopEvent; - this.onPlayStartEvent = onPlayStartEvent; - this.onPlayPauseEvent = onPlayPauseEvent; - - return this; + var logger = context.JK.logger; + if ($parentElement.length == 0) { + logger.debug("no $parentElement specified in PlaybackControls"); } + + var PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE; + + var $playButton = $('.play-button img.playbutton', $parentElement); + var $pauseButton = $('.play-button img.pausebutton', $parentElement); + var $stopButton = $('.stop-button img.stopbutton', $parentElement); + var $currentTime = $('.recording-current', $parentElement); + var $duration = $('.duration-time', $parentElement); + var $sliderBar = $('.recording-playback', $parentElement); + var $slider = $('.recording-slider', $parentElement); + var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement); + var $jamTrackGetReady = $('.jam-track-get-ready', $parentElement); + + var $self = $(this); + + var playbackPlaying = false; + var playbackDurationMs = 0; + var playbackPositionMs = 0; + var durationChanged = false; + var seenActivity = false; + + var endReached = false; + var dragging = false; + var playingWhenDragStart = false; + var draggingUpdateTimer = null; + var canUpdateBackend = false; + var playbackMode = PlaybackMode.EveryWhere; + var monitorPlaybackTimeout = null; + var playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; + var monitoring = false; + + function init() { + updateSliderPosition(0); + updateDurationTime(0); + updateCurrentTime(0); + seenActivity = false; + } + + function startPlay() { + seenActivity = false; + updateIsPlaying(true); + if (endReached) { + update(0, playbackDurationMs, playbackPlaying); + } + + if (options.mediaActions) { + options.mediaActions.mediaStartPlay({playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}) + } + else { + $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); + + } + + if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + context.JK.GA.trackJamTrackPlaySession(context.SessionStore.id(), true) + } + } + + function stopPlay(endReached) { + logger.debug("STOP PLAY CLICKED") + updateIsPlaying(false); + + if (options.mediaActions) { + logger.debug("mediaStopPlay", endReached) + options.mediaActions.mediaStopPlay({ + playbackMode: playbackMode, + playbackMonitorMode: playbackMonitorMode, + endReached: endReached + }) + } + else { + $self.triggerHandler('stop', { + playbackMode: playbackMode, + playbackMonitorMode: playbackMonitorMode, + endReached: endReached + }); + } + } + + function pausePlay(endReached) { + updateIsPlaying(false); + + if (options.mediaActions) { + options.mediaActions.mediaPausePlay({ + playbackMode: playbackMode, + playbackMonitorMode: playbackMonitorMode, + endReached: endReached + }) + } + else { + $self.triggerHandler('pause', { + playbackMode: playbackMode, + playbackMonitorMode: playbackMonitorMode, + endReached: endReached + }); + } + } + + function updateOffsetBasedOnPosition(offsetLeft) { + var sliderBarWidth = $sliderBar.width(); + + playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); + updateCurrentTimeText(playbackPositionMs); + if (canUpdateBackend) { + if (options.mediaActions) { + options.mediaActions.mediaChangePosition({ + positionMs: playbackPositionMs, + playbackMonitorMode: playbackMonitorMode + }) + } + else { + $self.triggerHandler('change-position', { + positionMs: playbackPositionMs, + playbackMonitorMode: playbackMonitorMode + }); + } + canUpdateBackend = false; + } + } + + function startDrag(e, ui) { + dragging = true; + playingWhenDragStart = playbackPlaying; + //draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging + //if(playingWhenDragStart) { + //stopPlay(); + //} + } + + function stopDrag(e, ui) { + dragging = false; + + clearInterval(draggingUpdateTimer); + + canUpdateBackend = true; + updateOffsetBasedOnPosition(ui.position.left); + updateSliderPosition(playbackPositionMs); + + //if(playingWhenDragStart) { + // playingWhenDragStart = false; + // startPlay(); + //} + } + + function onDrag(e, ui) { + updateOffsetBasedOnPosition(ui.position.left); + } + + $playButton.on('click', function (e) { + startPlay(); + return false; + }); + + $pauseButton.on('click', function (e) { + pausePlay(); + return false; + }); + + $stopButton.on('click', function (e) { + stopPlay(); + return false; + }); + + $sliderBar.on('click', function (e) { + var offset = e.pageX - $(this).offset().left; + canUpdateBackend = true; + updateOffsetBasedOnPosition(offset); + updateSliderPosition(playbackPositionMs); + return false; + }); + + $slider.draggable({ + axis: 'x', + containment: $sliderBar, + start: startDrag, + stop: stopDrag, + drag: onDrag + }); + + + if (options.playmodeControlsVisible) { + $('.playback-mode-buttons.icheckbuttons', $parentElement).show(); + } + + $playmodeButton.iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); + + + $playmodeButton.on('ifChecked', function (e) { + var playmode = $(this).val(); + logger.debug("set new playmode", playmode); + setPlaybackMode(playmode); + }); + + function styleControls() { + try { + $jamTrackGetReady.attr('data-mode', playbackMonitorMode); + + $parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode'); + if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) { + $parentElement.addClass('mediafile-mode'); + } + else if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + $parentElement.addClass('jamtrack-mode'); + } + else if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { + $parentElement.addClass('metronome-mode'); + } + else { + throw "unknown playbackMonitorMode: " + playbackMonitorMode; + } + } + catch (e) { + logger.error("playbackControls: unable to style controls", e) + } + } + + function executeMonitor(positionMs, durationMs, isPlaying) { + + if (positionMs < 0) { + // bug in backend? + positionMs = 0; + } + + if (positionMs > 0) { + seenActivity = true; + } + + if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { + updateIsPlaying(isPlaying); + } + else { + update(positionMs, durationMs, isPlaying); + } + + if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + + if (playbackPlaying) { + $jamTrackGetReady.attr('data-current-time', positionMs) + } + else { + // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing + $jamTrackGetReady.attr('data-current-time', -1) + } + } + + monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + } + + function monitorRecordingPlayback() { + if (!monitoring) { + return; + } + if (options.mediaActions) { + options.mediaActions.positionUpdate(playbackMonitorMode) + } + else { + if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); + var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + var durationMs = duration.media_len; + } + else { + var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + } + + var isPlaying = context.jamClient.isSessionTrackPlaying(); + + executeMonitor(positionMs, durationMs, isPlaying) + } + } + + function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) { + + if (dragging) { + return; + } + + // at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to + //logger.debug("currentTimeMs, durationTimeMs, mode", currentTimeMs, durationTimeMs, playbackMonitorMode); + if (currentTimeMs == 0 && seenActivity) { + if (playbackPlaying) { + isPlaying = false; + durationTimeMs = playbackDurationMs; + currentTimeMs = playbackDurationMs; + stopPlay(true); + endReached = true; + logger.debug("end reached"); + } + else { + // make sure slide shows '0' + updateCurrentTime(currentTimeMs); + return; + } + } + + + if (currentTimeMs < offsetStart) { + currentTimeMs = 0; // this is to squelch movement during tap-in period + } + updateDurationTime(durationTimeMs); + updateCurrentTime(currentTimeMs); + updateIsPlaying(isPlaying); + + durationChanged = false; + } + + function updateDurationTime(timeMs) { + try { + if (timeMs != playbackDurationMs) { + $duration.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); + playbackDurationMs = timeMs; + durationChanged = true; + } + } + catch (e) { + logger.error("playbackControls: updateDurationTime error", e) + } + } + + function updateCurrentTimeText(timeMs) { + try { + var time = context.JK.prettyPrintSeconds(parseInt(timeMs / 1000)) + $currentTime.text(time); + if (options.mediaActions) { + options.mediaActions.currentTimeChanged(time) + } + } + catch (e) { + logger.error("playbackControls: updateCurrentTimeText error", e) + } + } + + function updateSliderPosition(timeMs) { + + try { + var slideWidthPx = $sliderBar.width(); + var xPos = Math.ceil(timeMs / playbackDurationMs * slideWidthPx); + $slider.css('left', xPos); + } + catch (e) { + logger.error("playbackControls: updateSliderPosition error", e) + } + } + + function updateCurrentTime(timeMs) { + try { + if (timeMs != playbackPositionMs || durationChanged) { + + updateCurrentTimeText(timeMs); + updateSliderPosition(timeMs); + + playbackPositionMs = timeMs; + } + } + catch (e) { + logger.error("playbackControls: updateCurrentTime err", e) + } + + } + + function updateIsPlaying(isPlaying) { + try { + if (isPlaying != playbackPlaying) { + if (isPlaying) { + $playButton.hide(); + $pauseButton.show(); + } + else { + $playButton.show(); + $pauseButton.hide(); + } + + logger.debug("updating is playing: " + isPlaying) + playbackPlaying = isPlaying; + } + } + catch (e) { + logger.error("playbackControls: updateIsPlaying error", e) + } + } + + function setPlaybackMode(mode) { + if (mode == 'preview-to-all') { + playbackMode = PlaybackMode.PreviewToAll; + } + else if (mode == 'preview-to-me') { + playbackMode = PlaybackMode.PrivatePreview; + } + else if (mode == 'eveywhere') { + playbackMode = PlaybackMode.EveryWhere; + } + else { + logger.error("unable to set playback mode", mode); + } + + // let the mode change immediately affect the behavior of the stream + if (playbackPlaying) { + stopPlay(); + startPlay(); + } + } + + function startMonitor(_playbackMonitorMode) { + logger.debug("startMonitor: " + _playbackMonitorMode) + + if (monitoring && _playbackMonitorMode == playbackMonitorMode) { + return; + } + + monitoring = true; + // resets everything to zero + init(); + + if (_playbackMonitorMode === undefined || _playbackMonitorMode === null) { + playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; + } + else { + playbackMonitorMode = _playbackMonitorMode; + } + + logger.debug("playbackControl.startMonitor " + playbackMonitorMode + "") + + styleControls(); + + if (monitorPlaybackTimeout != null) { + clearTimeout(monitorPlaybackTimeout); + monitorPlaybackTimeout = null; + } + monitorRecordingPlayback(); + } + + function stopMonitor() { + monitoring = false; + logger.debug("playbackControl.stopMonitor") + if (monitorPlaybackTimeout != null) { + clearTimeout(monitorPlaybackTimeout); + monitorPlaybackTimeout = null; + } + } + + function onPlayStartEvent() { + updateIsPlaying(true); + playbackPlaying = true; + seenActivity = false; + } + + function onPlayStopEvent() { + updateIsPlaying(false); + playbackPlaying = false; + } + + function onPlayPauseEvent() { + playbackPlaying = false; + } + + this.update = update; + this.setPlaybackMode = setPlaybackMode; + this.startMonitor = startMonitor; + this.stopMonitor = stopMonitor; + this.executeMonitor = executeMonitor; + this.onPlayStopEvent = onPlayStopEvent; + this.onPlayStartEvent = onPlayStartEvent; + this.onPlayPauseEvent = onPlayPauseEvent; + + return this; + } })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index fe616b7d7..d99bb421f 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -23,7 +23,7 @@ var $biography = $screen.find('#biography'); // musical experience - var $instruments = $screen.find('#instruments'); + var $instruments = $screen.find('.instruments-holder'); var $musicianStatus = $screen.find('#musician-status'); var $genres = $screen.find('#genres'); var $concertCount = $screen.find('#concert-count'); @@ -36,7 +36,7 @@ var $youTubeSamples = $screen.find('.youtube-samples'); // online presence - var $noOnlinePresence = $screen.find('.no-online-presence'); + var $userWebsite = $screen.find('.user-website'); var $soundCloudPresence = $screen.find('.soundcloud-presence'); var $reverbNationPresence = $screen.find('.reverbnation-presence'); @@ -95,7 +95,7 @@ var $age = $screen.find('#age'); // buttons - var $btnEdit = $screen.find('#btn-edit'); + var $btnEdit = $screen.find('.edit-profile-btn'); var $btnAddFriend = $screen.find('#btn-add-friend'); var $btnFollowUser = $screen.find('#btn-follow-user'); var $btnMessageUser = $screen.find('#btn-message-user'); @@ -128,7 +128,7 @@ } function resetForm() { - $instruments.empty(); + //$instruments.empty(); $aboutContent.show(); $historyContent.hide(); @@ -411,7 +411,7 @@ /****************** ABOUT TAB *****************/ function renderAbout() { - $instruments.empty(); + //$instruments.empty(); $aboutContent.show(); $historyContent.hide(); @@ -477,7 +477,7 @@ function renderBio() { $biography.html(user.biography ? user.biography : NOT_SPECIFIED_TEXT); - if (isCurrentUser() && !user.biography) { + if (isCurrentUser()) { $btnEditBio.show(); } else { $btnEditBio.hide(); @@ -485,19 +485,26 @@ } function renderMusicalExperience() { - profileUtils.renderMusicalExperience(user, $screen) + profileUtils.renderMusicalExperience(user, $screen, isCurrentUser()) } function renderPerformanceSamples() { - profileUtils.renderPerformanceSamples(user, $screen) + profileUtils.renderPerformanceSamples(user, $screen, isCurrentUser()) } function renderOnlinePresence() { - profileUtils.renderOnlinePresence(user, $screen) + profileUtils.renderOnlinePresence(user, $screen, isCurrentUser()) } function renderInterests() { // current interests + if (isCurrentUser()) { + $btnAddInterests.show(); + } + else { + $btnAddInterests.hide(); + } + var noInterests = !user.paid_sessions && !user.free_sessions && !user.cowriting && !user.virtual_band && !user.traditional_band; if (noInterests) { $noInterests.show(); @@ -506,12 +513,7 @@ $cowritingSection.hide(); $traditionalBandSection.hide(); $virtualBandSection.hide(); - - if (isCurrentUser()) { - $btnAddInterests.show(); - } } else { - $btnAddInterests.hide(); $noInterests.hide(); // paid sessions diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index 6c226b9e8..c25035936 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -59,19 +59,17 @@ profileUtils.gigMap = { "": "not specified", - "0": "zero", - "1": "under 10", - "2": "10 to 50", - "3": "50 to 100", - "4": "over 100" + "0": "under 10", + "1": "10 to 50", + "2": "50 to 100", + "3": "over 100" }; profileUtils.studioMap = { - "0": "zero", - "1": "under 10", - "2": "10 to 50", - "3": "50 to 100", - "4": "over 100" + "0": "under 10", + "1": "10 to 50", + "2": "50 to 100", + "3": "over 100" }; profileUtils.cowritingPurposeMap = { @@ -99,6 +97,39 @@ return list; } + // the server stores money in cents; display it as such + profileUtils.normalizeMoneyForDisplay = function(serverValue) { + if (!serverValue || serverValue==="") { + // Blank value is valid: + return "" + } + if(serverValue || serverValue == 0) { + return (new Number(serverValue) / 100).toFixed(2) + } + else { + return 0; + } + } + + // the server stores money in cents; normalize it from what user entered + profileUtils.normalizeMoneyForSubmit = function(clientValue) { + if (!clientValue || clientValue==="") { + // Blank value is valid: + return "" + } + var money = new Number(clientValue); + + if(!context._.isNaN(money)) { + money = Math.round(money * 100) + } + else { + // restore original value to allow server to reject with validation error + money = clientValue; + } + return money; + } + + // Initialize standard profile help bubbles (topics stored as attributes on element): profileUtils.initializeHelpBubbles = function(parentElement) { $(".help", parentElement).each(function( index ) { @@ -307,18 +338,25 @@ } function formatTitle(title) { - return title && title.length > 30 ? title.substring(0, 30) + "..." : title; + return title; } - profileUtils.renderMusicalExperience = function(player, $root) { - var $instruments = $root.find('#instruments'); + profileUtils.renderMusicalExperience = function(player, $root, isOwner) { + var $instruments = $root.find('.instruments-holder'); var $musicianStatus = $root.find('#musician-status'); var $genres = $root.find('#genres'); var $concertCount = $root.find('#concert-count'); var $studioCount = $root.find('#studio-count'); + var $btnAddExperiences = $root.find('.add-experiences') - $instruments.empty(); + $instruments.find('.profile-instrument').remove() + if(isOwner) { + $btnAddExperiences.show() + } + else { + $btnAddExperiences.hide() + } if (player.instruments) { for (var i = 0; i < player.instruments.length; i++) { var instrument = player.instruments[i]; @@ -335,7 +373,7 @@ proficiency_level_css: proficiencyCssMap[proficiency] }); - $instruments.append(instrumentHtml); + $instruments.prepend(instrumentHtml); } } @@ -367,16 +405,22 @@ var $youTubeSamples = $root.find('.youtube-samples'); var $btnAddRecordings = $root.find('.add-recordings'); + $jamkazamSamples.find('.playable').remove() + $soundCloudSamples.find('.playable').remove() + $youTubeSamples.find('.playable').remove() + + if (isOwner) { + $btnAddRecordings.show(); + } + else { + $btnAddRecordings.hide(); + } if (!performanceSamples || performanceSamples.length === 0) { $noSamples.show() $jamkazamSamples.hide() $soundCloudSamples.hide() $youTubeSamples.hide() - if (isOwner) { - $btnAddRecordings.show(); - } } else { - $btnAddRecordings.hide(); $noSamples.hide(); // show samples section @@ -402,15 +446,15 @@ } $.each(jamkazamSamples, function(index, sample) { - $jamkazamSamples.append("" + formatTitle(sample.claimed_recording.name) + "
"); + $jamkazamSamples.append("" + formatTitle(sample.claimed_recording.name) + ""); }); $.each(soundCloudSamples, function(index, sample) { - $soundCloudSamples.append("" + formatTitle(sample.description) + "
"); + $soundCloudSamples.append("" + formatTitle(sample.description) + ""); }); $.each(youTubeSamples, function(index, sample) { - $youTubeSamples.append("" + formatTitle(sample.description) + "
"); + $youTubeSamples.append("" + formatTitle(sample.description) + ""); }); } }// function renderPerformanceSamples @@ -425,13 +469,17 @@ var $youTubePresence = $root.find('.youtube-presence'); var $facebookPresence = $root.find('.facebook-presence'); var $twitterPresence = $root.find('.twitter-presence'); - var $btnAddSites = $root.find('.add-sites'); - + var $btnAddSites = $root.find('.add-presences'); + if (isOwner) { + $btnAddSites.show(); + } else { + $btnAddSites.hide(); + } // online presences var onlinePresences = player.online_presences; - if ((!onlinePresences || onlinePresences.length === 0) && !player.website) { + if (onlinePresences.length == 0 && !player.website) { $noOnlinePresence.show() $userWebsite.show() $soundCloudPresence.show() @@ -441,18 +489,19 @@ $youTubePresence.show() $facebookPresence.show() $twitterPresence.show() - - if (isOwner) { - $btnAddSites.show(); - } else { - $btnAddSites.hide(); - } } else { - $btnAddSites.hide(); $noOnlinePresence.hide(); if (player.website) { - $userWebsite.find('a').attr('href', player.website); + // make sure website is rooted + var website = player.website; + if(website.indexOf('http') == -1) { + website = 'http://' + website; + } + $userWebsite.removeClass('hidden').find('a').attr('href', website) + } + else { + $userWebsite.addClass('hidden').find('a').attr('href', '') } var soundCloudPresences = profileUtils.soundCloudPresences(onlinePresences); diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index c16b9faa7..a3f79c86e 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -1,3 +1,16 @@ -//= require ./react-components/actions/BroadcastActions -//= require ./react-components/stores/BroadcastStore -//= require_directory ./react-components \ No newline at end of file +//= require_directory ./react-components/helpers +//= require_directory ./react-components/actions +//= require ./react-components/stores/AppStore +//= require ./react-components/stores/RecordingStore +//= require ./react-components/stores/SessionStore +//= require ./react-components/stores/MixerStore +//= require ./react-components/stores/JamTrackStore +//= require ./react-components/stores/SessionNotificationStore +//= require ./react-components/stores/MediaPlaybackStore +//= require ./react-components/stores/SessionMyTracksStore +//= require ./react-components/stores/SessionOtherTracksStore +//= require ./react-components/stores/SessionMediaTracksStore +//= require_directory ./react-components/stores +//= require_directory ./react-components/mixins +//= require_directory ./react-components +//= require_directory ./react-components/landing \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee new file mode 100644 index 000000000..024f11103 --- /dev/null +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -0,0 +1,204 @@ +context = window +PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE +EVENTS = context.JK.EVENTS +logger = context.JK.logger + +mixins = [] + +# this check ensures we attempt to listen if this component is created in a popup +reactContext = if window.opener? then window.opener else window + +MixerStore = reactContext.MixerStore +MixerActions = reactContext.MixerActions +MediaPlaybackStore = reactContext.MediaPlaybackStore +SessionActions = reactContext.SessionActions +MediaPlaybackActions = reactContext.MediaPlaybackActions + +mixins.push(Reflux.listenTo(MixerStore,"onInputsChanged")) +mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) + + +@MediaControls = React.createClass({ + + mixins: mixins + 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 ] + + onMediaStateChanged: (changes) -> + if changes.playbackStateChanged + if @state.controls? + if changes.playbackState == 'play_start' + @state.controls.onPlayStartEvent() + else if changes.playbackState == 'play_stop' + @state.controls.onPlayStopEvent() + else if changes.playbackState == 'play_pause' + @state.controls.onPlayPauseEvent(); + else if changes.positionUpdateChanged + if @state.controls? + @state.controls.executeMonitor(changes.positionMs, changes.durationMs, changes.isPlaying) + + onInputsChanged: (sessionMixers) -> + + session = sessionMixers.session + mixers = sessionMixers.mixers + + if @state.controls? + mediaSummary = mixers.mediaSummary + metro = mixers.metro + + @monitorControls(@state.controls, mediaSummary) + @setState({mediaSummary: mediaSummary, metro: metro}) + + @updateMetronomeDetails(metro, @state.initializedMetronomeControls) + + updateMetronomeDetails: (metro, initializedMetronomeControls) -> + logger.debug("MediaControls: setting tempo/sound/cricket", metro) + $root = jQuery(this.getDOMNode()) + $root.find("select.metro-tempo").val(metro.tempo) + $root.find("select.metro-sound").val(metro.sound) + + if initializedMetronomeControls + mode = if metro.cricket then 'cricket' else 'self' + logger.debug("settingcricket", mode) + $root.find('#metronome-playback-select').metronomeSetPlaybackMode(mode) + + monitorControls: (controls, mediaSummary) -> + + if mediaSummary.mediaOpen + if mediaSummary.jamTrackOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK) + else if mediaSummary.backingTrackOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else if mediaSummary.metronomeOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.METRONOME) + else if mediaSummary.recordingOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else + logger.debug("unable to determine mediaOpen type", mediaSummary) + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else + controls.stopMonitor() + + metronomePlaybackModeChanged: (e, data) -> + + mode = data.playbackMode # will be either 'self' or 'cricket' + + logger.debug("setting metronome playback mode: ", mode) + isCricket = mode == 'cricket'; + SessionActions.metronomeCricketChange(isCricket) + + + onMetronomeChanged: () -> + + @setMetronomeFromForm() + + setMetronomeFromForm: () -> + $root = jQuery(this.getDOMNode()) + tempo = $root.find("select.metro-tempo:visible option:selected").val() + sound = $root.find("select.metro-sound:visible option:selected").val() + + t = parseInt(tempo) + s = null + if tempo == NaN || tempo == 0 || tempo == null + t = 120 + + if sound == null || typeof(sound)=='undefined' || sound == "" + s = "Beep" + else + s = sound + + logger.debug("Setting tempo and sound:", t, s) + MixerActions.metronomeChanged(t, s, 1, 0) + + render: () -> + + + tempo_options = [] + for tempo in @tempos + tempo_options.push(``) + + `
+ +
+
+ Get Ready! +
+ + + +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
0:00
+
+
+
+
0:00
+ +
0:00
+ +
+ + +
+
` + + + getInitialState: () -> + {controls: null, mediaSummary: {}, initializedMetronomeControls: false} + + tryPrepareMetronome: (metro) -> + if @state.mediaSummary.metronomeOpen && !@state.initializedMetronomeControls + $root = jQuery(this.getDOMNode()) + $root.on("change", ".metronome-select", @onMetronomeChanged) + $root.find('#metronome-playback-select').metronomePlaybackMode({positions:['bottom'], offsetParent:$('#minimal-container')}).on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, @metronomePlaybackModeChanged) + @updateMetronomeDetails(metro, true) + @setState({initializedMetronomeControls: true}) + + + componentDidUpdate: (prevProps, prevState) -> + @tryPrepareMetronome(@state.metro) + + componentDidMount: () -> + + + $root = jQuery(this.getDOMNode()) + controls = context.JK.PlaybackControls($root, {mediaActions: MediaPlaybackActions}) + + mediaSummary = MixerStore.mixers.mediaSummary + metro = MixerStore.mixers.metro + + @monitorControls(controls, mediaSummary) + + @tryPrepareMetronome(metro) + + @setState({mediaSummary: mediaSummary, controls: controls, metro: metro}) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee new file mode 100644 index 000000000..235fc1fd2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -0,0 +1,126 @@ +context = window +logger = context.JK.logger + +mixins = [] + +if window.opener? + SessionActions = window.opener.SessionActions + MediaPlaybackStore = window.opener.MediaPlaybackStore + MixerActions = window.opener.MixerActions + +mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) + +@PopupMediaControls = React.createClass({ + + mixins: mixins + + onMediaStateChanged: (changes) -> + if changes.currentTimeChanged && @root? + @setState({time: changes.time}) + + showMetronome: (e) -> + e.preventDefault() + + SessionActions.showNativeMetronomeGui() + + getInitialState: () -> + {time: '0:00'} + + close: () -> + window.close() + + render: () -> + + closeLinkText = null + header = null + extraControls = null + + # give the users options to close it + if @props.mediaSummary.recordingOpen + mediaType = "Recording" + mediaName = @props.recordedTracks[0].recordingName + closeLinkText = 'close recording' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + else if @props.mediaSummary.jamTrackOpen + mediaType = "JamTrack" + mediaName = @props.jamTracks[0].name + closeLinkText = 'close JamTrack' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + else if @props.mediaSummary.backingTrackOpen + mediaType = "Audio File" + mediaName = context.JK.getNameOfFile(@props.backingTracks[0].shortFilename) + closeLinkText = 'close audio file' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + extraControls = + `
+
+ +
+
+
` + else if @props.mediaSummary.metronomeOpen + mediaType = "Metronome" + closeLinkText = 'close metronome' + header = `

Metronome

` + extraControls = + `` + else + mediaType = "" + + `
+ {header} + + {extraControls} + {closeLinkText} +
` + + windowUnloaded: () -> + SessionActions.closeMedia(false) unless window.DontAutoCloseMedia + + componentDidMount: () -> + + $(window).unload(@windowUnloaded) + + @root = jQuery(this.getDOMNode()) + + $loop = @root.find('input[name="loop"]') + context.JK.checkbox($loop) + + $loop.on('ifChecked', () => + logger.debug("@props", @props) + # it doesn't matter if you do personal or master, because backend just syncs both + MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.mixer, true) + ) + $loop.on('ifUnchecked', () => + # it doesn't matter if you do personal or master, because backend just syncs both + MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.mixer, false) + ) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + #mysteryTopMargin = 20 + mysteryTopMargin = 0 + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee new file mode 100644 index 000000000..7cb29df6e --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -0,0 +1,135 @@ +context = window + +mixins = [] + +# this check ensures we attempt to listen if this component is created in a popup +if window.opener + mixins.push(Reflux.listenTo(window.opener.RecordingStore,"onRecordingStateChanged")) + +@PopupRecordingStartStop = React.createClass({ + + mixins: mixins + + onRecordingStateChanged: (recordingState) -> + this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording) + + startStopRecording: () -> + if this.state.isRecording + window.opener.RecordingActions.stopRecording() + else + window.opener.RecordingActions.startRecording() + + onNoteShowHide: () -> + this.setState(showNote: !this.state.showNote) + + getInitialState: () -> + {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false} + + render: () -> + + recordingVerb = if this.state.isRecording then 'Stop' else 'Start' + + recordingBtnClasses = classNames({ + "currently-recording" : this.state.isRecording, + "control" : true + }) + + noteJSX = `
+
+ Important Note +
+
+ While playing in your session, you are listening to your own personal mix. This recording will use the master mix, + which may sound very different. To hear and adjust your master mix settings, click the MIXER button in the session toolbar. +
+
` + + recordingJSX = `
+
+ + +
+
+
+ + +
+
+
` + + if this.state.showNote + noteText = 'hide note' + else + noteText = 'show note' + + noteShowHideJSX = `{noteText}` + + note = null + recordingOptions = null + noteShowHide = null + + if this.state.showNote && !this.state.isRecording && !this.state.recordedOnce + # should we show the note itself? Only if not recording, too + note = noteJSX + + if !this.state.isRecording && !this.state.recordedOnce + noteShowHide = noteShowHideJSX + + if gon.global.video_available == "full" + recordingOptions = recordingJSX + + + `
+ + + {recordingOptions} + + {note} + + {noteShowHide} + +
` + + windowUnloaded: () -> + window.opener.RecordingActions.recordingControlsClosed() + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $recordingType = $root.find('input[type="radio"]') + context.JK.checkbox($recordingType) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + mysteryTopMargin = 20 + + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee new file mode 100644 index 000000000..d75cb5b8e --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee @@ -0,0 +1,13 @@ +context = window +logger = context.JK.logger + +@PopupWrapper = React.createClass({ + + getInitialState: () -> + {ready: false} + + render: () -> + logger.debug("PopupProps", window.PopupProps) + return React.createElement(window[this.props.component], window.PopupProps) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee new file mode 100644 index 000000000..0f6f656da --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee @@ -0,0 +1,92 @@ +context = window +MIX_MODES = context.JK.MIX_MODES +MixerActions = @MixerActions + +@SessionBackingTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "backing-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.shortFilename}
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + if @props.isOpener || @props.mode == MIX_MODES.MASTER + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + else + context.JK.helpBubble($mute, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + context.JK.helpBubble($pan, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) +}) diff --git a/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee new file mode 100644 index 000000000..36e047005 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee @@ -0,0 +1,75 @@ +context = window + +@SessionChatMixer= React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@props.mixers.mixer], muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer + vuMixer = @props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? then @props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
Session Voice Chat Output
+
+
+ +
+
+
+
+
+
+
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee new file mode 100644 index 000000000..d5ffb91fd --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee @@ -0,0 +1,26 @@ +context = window + +@SessionInviteMusiciansBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onAppInit: (app) -> + @app = app + + @inviteMusiciansUtil = new JK.InviteMusiciansUtil(@app) + @inviteMusiciansUtil.initialize(JK.FriendSelectorDialogInstance) + + openInviteDialog : (e) -> + e.preventDefault() + + friendInput = @inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', context.SessionStore.currentSessionId) + @inviteMusiciansUtil.loadFriends() + $(friendInput).show() + @app.layout.showDialog('select-invites') + + render: () -> + ` + + Invite Musicians + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee new file mode 100644 index 000000000..c7b2db5c6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee @@ -0,0 +1,92 @@ +context = window +MIX_MODES = context.JK.MIX_MODES +MixerActions = @MixerActions + +@SessionJamTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "jam-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.trackName}
+
+ +
+
+
+
+
+
+
+
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + if @props.isOpener || @props.mode == MIX_MODES.MASTER + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + else + context.JK.helpBubble($mute, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + context.JK.helpBubble($pan, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) +}) diff --git a/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee new file mode 100644 index 000000000..d6ce1da06 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee @@ -0,0 +1,81 @@ +context = window + +MixerActions = @MixerActions + +@SessionJamTrackCategory = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "jam-track-category" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
JamTrack:
+
{this.props.jamTrackName}
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee new file mode 100644 index 000000000..a11467e65 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee @@ -0,0 +1,23 @@ +context = window + +@SessionLeaveBtn = React.createClass({ + + onLeave: (e) -> + e.preventDefault() + @rateSession() + + SessionActions.leaveSession.trigger({location: '/client#/home'}) + + rateSession: () -> + unless @rateSessionDialog? + @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); + @rateSessionDialog.initialize(); + + @rateSessionDialog.showDialog(); + + render: () -> + ` + + LEAVE + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee new file mode 100644 index 000000000..80dd34036 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee @@ -0,0 +1,53 @@ +context = window +rest = context.JK.Rest() +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES + +@SessionMasterCategoryControls = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + mixers = sessionMixers.mixers + inputGroupMixers = mixers.simulatedMusicCategoryMixers[MIX_MODES.MASTER] + chatGroupMixers = mixers.simulatedChatCategoryMixers[MIX_MODES.MASTER] + + @setState({inputGroupMixers: inputGroupMixers, chatGroupMixers: chatGroupMixers}) + + render: () -> + + categoryControls = [] + + if @state.inputGroupMixers? + input = + mixers: @state.inputGroupMixers + + categoryControls.push(``) + + if @state.chatGroupMixers? + input = + mixers: @state.chatGroupMixers + + categoryControls.push(``) + + + `
+

master output

+
+ {categoryControls} +
+
` + + + getInitialState:() -> + {inputGroupMixers: null, chatGroupMixers: null} + + + onAppInit: (app) -> + @app = app + + + + + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee new file mode 100644 index 000000000..3dbf9f5b2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee @@ -0,0 +1,62 @@ +context = window +rest = context.JK.Rest() +SessionActions = @SessionActions +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES +EVENTS = context.JK.EVENTS +ChannelGroupIds = context.JK.ChannelGroupIds + +@SessionMasterMediaTracks = React.createClass({ + + mixins: [@SessionMediaTracksMixin, Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + render: () -> + + mediaTracks = [] + + if this.state.mediaSummary.mediaOpen + + if this.state.mediaSummary.backingTrackOpen + + for backingTrack in @state.backingTracks + backingTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + + else if this.state.mediaSummary.jamTrackOpen + + mediaTracks.push(``) + for jamTrack in @state.jamTracks + jamTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + + else if this.state.mediaSummary.recordingOpen + + mediaTracks.push(``) + for recordedTrack in @state.recordedTracks + recordedTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + + else if this.state.mediaSummary.metronomeOpen + + @state.metronome.mode = MIX_MODES.MASTER + mediaTracks.push(``) + + `
+

recorded audio

+
+ {mediaTracks} +
+
` + + + getInitialState:() -> + {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null} + + onAppInit: (app) -> + @app = app + + + + + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee new file mode 100644 index 000000000..8227a6e06 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee @@ -0,0 +1,13 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@SessionMasterMix = React.createClass({ + + render: () -> + `
+ + + + +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee new file mode 100644 index 000000000..44f879eed --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee @@ -0,0 +1,40 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES +logger = context.JK.logger + +@SessionMasterMyTracks = React.createClass({ + + mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + render: () -> + + content = null + tracks = [] + + if this.state.tracks.length > 0 + for track in this.state.tracks + track.mode = MIX_MODES.MASTER + tracks.push(``) + + if @state.chat + @state.chat.mode = @props.mode + tracks.push(``) + + else if this.state.session? && this.state.session.inSession() + logger.debug("no 'my inputs' for master mix") + + `
+

my live tracks

+
+ {content} + {tracks} +
+
` + + getInitialState:() -> + {tracks:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee new file mode 100644 index 000000000..707c2fb0c --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee @@ -0,0 +1,93 @@ +context = window + +MixerActions = @MixerActions + +@SessionMasterOtherTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer? + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@props.mixers.mixer], muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer + vuMixer = @props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? then @props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + #
+ + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:this.props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:this.props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.mixers.mixer? + $mute.off("click", false) + $pan.off("click", false) + else + $mute.on("click", false) + $pan.on("click", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee new file mode 100644 index 000000000..a441c6679 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee @@ -0,0 +1,66 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup + +@SessionMasterOtherTracks = React.createClass({ + + mixins: [Reflux.listenTo(@SessionOtherTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + session = sessionMixers.session + mixers = sessionMixers.mixers + noAudioUsers = mixers.noAudioUsers + + tracks = [] + + if session.inSession() + + for participant in session.otherParticipants() + + name = participant.user.name; + + firstTrack = participant.tracks[0] + + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url) + + for track in participant.tracks + + mixerData = mixers.findMixerForTrack(participant.client_id, track, false, @props.mode) + + instrumentIcon = context.JK.getInstrumentIcon45(firstTrack.instrument_id) + + trackState = { + participant: participant, + track: track, + mixers: mixerData, + name: name, + instrumentIcon: instrumentIcon, + photoUrl: photoUrl, + hasMixer: mixerData.mixer? , + noAudio: noAudioUsers[participant.client_id] + } + + tracks.push(trackState) + # todo: sessionModel.setAudioEstablished + + this.setState(tracks: tracks, session: session) + + render: () -> + + tracks = [] + + for track in @state.tracks + tracks.push(``) + + `
+

other live tracks

+
+ {tracks} +
+
` + + getInitialState:() -> + {tracks:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee new file mode 100644 index 000000000..fdbaa40e9 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -0,0 +1,346 @@ +context = window +rest = context.JK.Rest() +SessionActions = @SessionActions +JamTrackActions = @JamTrackActions +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES +EVENTS = context.JK.EVENTS +ChannelGroupIds = context.JK.ChannelGroupIds + +@SessionMediaTracks = React.createClass({ + + mixins: [@SessionMediaTracksMixin, + Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), + Reflux.listenTo(@AppStore,"onAppInit"), + Reflux.listenTo(@JamTrackStore, "onJamTrackStateChanged")] + + onJamTrackStateChanged: (jamTrack) -> + if jamTrack? + @loadJamTrack(jamTrack) + else + SessionActions.closeMedia(true) + + #inputsChangedProcessed: (state) -> + + + closeWindow: () -> + if @childWindow? + @childWindow.DontAutoCloseMedia = true + @childWindow.close() + + closeAudio: (e) -> + e.preventDefault() + + SessionActions.closeMedia(false) + + cancelDownloadJamTrack: (e) -> + e.preventDefault() + + logger.debug("closing DownloadJamTrack widget") + @state.downloadJamTrack.root.remove() + @state.downloadJamTrack.destroy() + + SessionActions.downloadingJamTrack(false) + + @setState({downloadJamTrack: null}) + + openRecording: (e) -> + e.preventDefault() + + # just ignore the click if they are currently recording for now + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a recording while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }) + return + + @app.layout.showDialog('localRecordings') unless @app.layout.isDialogShowing('localRecordings') + + openBackingTrack: (e) -> + e.preventDefault() + if @state.backingTrackDialogOpen + logger.debug("backing track dialog already open") + return + + + # just ignore the click if they are currently recording for now + if @state.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 + + @setState({backingTrackDialogOpen: true}) + context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback2"); + + openMetronome: (e) -> + + if @state.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 + + SessionActions.openMetronome() + + openJamTrack: (e) -> + e.preventDefault() + + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a jam track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }) + return + + @app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, (e, data) => + # once the dialog is closed, see if the user has a jamtrack selected + if !data.canceled && data.result.jamTrack + JamTrackActions.open(data.result.jamTrack) + else + logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) + ) + + loadJamTrack: (jamTrack) -> + if @state.downloadJamTrack + # if there was one showing before somehow, destroy it. + logger.warn("destroying existing JamTrack") + @state.downloadJamTrack.root.remove() + @state.downloadJamTrack.destroy() + #set to null + + + downloadJamTrack = new context.JK.DownloadJamTrack(@app, jamTrack, 'large'); + + # the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, (e, data) => + if data.state == downloadJamTrack.states.synchronized + logger.debug("jamtrack synchronized; hide widget and show tracks") + downloadJamTrack.root.remove() + downloadJamTrack.destroy() + downloadJamTrack = null + + this.setState({downloadJamTrack: null}) + + # XXX: test with this removed; it should be unnecessary + context.jamClient.JamTrackStopPlay(); + + sampleRate = context.jamClient.GetSampleRate() + sampleRateForFilename = if sampleRate == 48 then '48' else '44' + fqId = jamTrack.id + '-' + sampleRateForFilename + + if jamTrack.jmep + logger.debug("setting jmep data") + + context.jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep) + else + logger.debug("no jmep data for jamtrack") + + # JamTrackPlay means 'load' + result = context.jamClient.JamTrackPlay(fqId); + + SessionActions.downloadingJamTrack(false) + + console.log("JamTrackPlay: result", ) + if !result + @app.notify( + { + title: "JamTrack Can Not Open", + text: "Unable to open your JamTrack. Please contact support@jamkazam.com" + } + , null, true) + else + participantCnt = context.SessionStore.participants().length + rest.playJamTrack(jamTrack.id) + .done(() => + @app.refreshUser(); + ) + + context.stats.write('web.jamtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) + ) + + + @setState({downloadJamTrack: downloadJamTrack}) + + render: () -> + + scrollerClassData = {'session-tracks-scroller': true} + mediaOptions = `
+
+
+ + Open: +
+ + +
` + + contents = null + mediaTracks = [] + + if this.state.downloadJamTrack? + closeOptions = + `` + + contents = closeOptions + + else if this.state.mediaSummary.mediaOpen + + # give the users options to close it + if this.state.mediaSummary.recordingOpen + mediaType = "Recording" + else if this.state.mediaSummary.jamTrackOpen + mediaType = "JamTrack" + else if this.state.mediaSummary.backingTrackOpen + mediaType = "Audio File" + else if this.state.mediaSummary.metronomeOpen + mediaType = "Metronome" + + else + mediaType = "" + + closeOptions = ` + + Close {mediaType} + ` + + + if this.state.mediaSummary.backingTrackOpen + + for backingTrack in @state.backingTracks + backingTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + else if this.state.mediaSummary.recordingOpen + + mediaTracks.push(``) + for recordedTrack in @state.recordedTracks + recordedTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + # if a JamTrack is also open (meaning the Recording was made with a Jamtrack), show it's mixers as well + if this.state.mediaSummary.jamTrackOpen + for jamTrack in @state.jamTracks + jamTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + else if this.state.mediaSummary.jamTrackOpen + + # JamTrack shows in this order: + # The Category mixer + # The Metronome + # All the JamTracks + mediaTracks.push(``) + + + if @state.metronome? + @state.metronome.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + + for jamTrack in @state.jamTracks + jamTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + else if this.state.mediaSummary.metronomeOpen + + @state.metronome.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + + contents = closeOptions + else + + scrollerClassData['media-options-showing'] = true + contents = mediaOptions + + scrollerClasses = classNames(scrollerClassData) + + `
+

recorded audio

+ {contents} +
+ + {mediaTracks} + +
+
` + + + getInitialState:() -> + {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null} + + onAppInit: (app) -> + @app = app + $(context.AppStore).on('SessionEnded', @onSessionEnded) + + onSessionEnded: () -> + @closeWindow() + + handleBackingTrackSelectedCallback: (result) -> + + @setState({backingTrackDialogOpen: false}) + + SessionActions.openBackingTrack(result) + + componentDidMount: () -> + context.JK.HandleBackingTrackSelectedCallback2 = @handleBackingTrackSelectedCallback + + + componentDidUpdate: () -> + + if @state.downloadJamTrack? + $holder = $(@getDOMNode()).find('.download-jamtrack-holder') + + if $holder.find('.download-jamtrack').length == 0 + + SessionActions.downloadingJamTrack(true) + $holder.append(@state.downloadJamTrack.root) + + # kick off the download JamTrack process + @state.downloadJamTrack.init() + + @handlePopup() + + handlePopup: () -> + if @state.mediaSummary.mediaOpen + unless @childWindow? + logger.debug("opening media control window") + @childWindow = window.open("/popups/media-controls", 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350') + @childWindow.PopupProps = @state + else + if @childWindow? + @childWindow.DontAutoCloseMedia = true + @childWindow.close() + @childWindow = null +}) diff --git a/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee new file mode 100644 index 000000000..afd9567da --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee @@ -0,0 +1,93 @@ +context = window +logger = context.JK.logger +MixerActions = @MixerActions +MIX_MODES = context.JK.MIX_MODES + +@SessionMetronome = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@mixer()], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @mixers() + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "metronome" : true + "in-jam-track" : @props.location == 'jam-track' + "no-mixer" : @props.mode == MIX_MODES.MASTER # show it as disabled if in master mode + "in-jam-track" : @props.location == 'jam-track' + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
Metronome
+
+ +
+
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $topParent = $root.closest('.top-parent') + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + + if @props.mode + context.JK.helpBubble($root.find('.disabled-track-overlay'), 'master-metronome-notice', {}, {positions:['top'], offsetParent: $topParent}) + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$topParent}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$topParent}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee new file mode 100644 index 000000000..c19e4d24f --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee @@ -0,0 +1,14 @@ +context = window + +@SessionMixerBtn = React.createClass({ + + openDialog: (e) -> + e.preventDefault() + context.JK.app.layout.showDialog('session-master-mix-dialog') + + render: () -> + ` + + MIXER + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee new file mode 100644 index 000000000..19c385a26 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee @@ -0,0 +1,74 @@ +context = window + +@SessionMusicMixer = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute(@props.mixers.mixer, muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer[0] + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? && @props.mixers.mixer.length > 0 then @props.mixers.mixer[0].pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
Session Music Output
+
+
+ +
+
+
+
+
+
+
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers, gainType : 'music'} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee new file mode 100644 index 000000000..1fad123f2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee @@ -0,0 +1,69 @@ +context = window + +MixerActions = @MixerActions + +@SessionMyChat = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + + handleMute: (e) -> + e.preventDefault() + + mixers = @mixers() + unless mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixers.mixer, mixers.oppositeMixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + + #
+ + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee new file mode 100644 index 000000000..e36502a3c --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee @@ -0,0 +1,119 @@ +context = window + +MixerActions = @MixerActions + +@SessionMyTrack = React.createClass({ + + + handleMute: (e) -> + e.preventDefault() + + unless this.props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer, this.props.mixers.oppositeMixer], muting) + + render: () -> + + muteMixer = this.props.mixers.muteMixer + vuMixer = this.props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + trackClasses = classNames({ + 'session-track' : true + 'my-track' : true + 'has-mixer' : this.props.hasMixer + 'no-mixer' : !this.props.hasMixer + }) + + pan = if this.props.mixers.mixer? then this.props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + #
+ + `
+
+
+
{this.props.trackName}
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + context.jamClient.SessionSetUserName(this.props.clientId, this.props.name) + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:this.props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:this.props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + unless this.props.hasMixer + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) + + context.JK.helpBubble($root.find('.disabled-track-overlay'), 'missing-my-tracks', {}, {positions:['top'], offsetParent: $root.closest('.top-parent')}) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.mixers.mixer? + $mute.off("click", false) + $pan.off("click", false) + $mute.off("mouseenter", false) + $mute.off("mouseleave", false) + $pan.off("mouseenter", false) + $pan.off("mouseleave", false) + else + $mute.on("click", false) + $pan.on("click", false) + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee new file mode 100644 index 000000000..80b91a403 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -0,0 +1,55 @@ +context = window +MIX_MODES = context.JK.MIX_MODES +SessionActions = context.SessionActions +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + +@SessionMyTracks = React.createClass({ + + mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + goToFtue: (e) -> + e.preventDefault() + + SessionActions.leaveSession.trigger({location: '/client#/account/audio'}) + + render: () -> + + content = null + tracks = [] + + if @state.tracks.length > 0 + for track in @state.tracks + track.mode = MIX_MODES.PERSONAL + tracks.push(``) + + if @state.chat + @state.chat.mode = @props.mode + tracks.push(``) + + else if @state.session? && @state.session.inSession() + content = `
+

+ You have not set up any inputs for your instrument or vocals. + If you want to hear yourself play through the JamKazam app, + and let the app mix your live playing with JamTracks, or with other musicians in online sessions, + click here now. +

+
` + + `
+

my live tracks

+ +
+ {content} + + {tracks} + +
+
` + + getInitialState:() -> + {tracks:[], session: null, chat:null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee new file mode 100644 index 000000000..b1decbf08 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee @@ -0,0 +1,29 @@ +context = window + +@SessionNotification = React.createClass({ + + render: () -> + + classes = classNames({ + 'session-notification' : true + 'has-details' : @props.detail? + }) + + help = `?` if @props.help? + + title = `
{this.props.title}{help}
` + extra = `
{this.props.extra}
` if @props.extra? + + `
+ {title} + {extra} +
` + + componentDidMount: () -> + + $root = $(@getDOMNode()) + context.JK.popExternalLinks($root) + + if @props.detail? + context.JK.hoverBubble($root, @props.detail, {offsetParent:$root.closest('.top-parent'), positions: ['left', 'bottom']}) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee new file mode 100644 index 000000000..96941368b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee @@ -0,0 +1,41 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +NotificationActions = @NotificationActions + +@SessionNotifications = React.createClass({ + + mixins: [Reflux.listenTo(@SessionNotificationStore,"onNotificationsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onNotificationsChanged: (notifications) -> + @setState({notifications: notifications}) + + getInitialState: () -> + {notifications: []} + + clearNotifications: (e) -> + e.preventDefault() + + NotificationActions.clear() + + render: () -> + + notifications = [] + for notification in @state.notifications + notifications.push(``) + + `
+

notifications

+ + + Clear Notifications + +
+ + {notifications} + +
+
` + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee new file mode 100644 index 000000000..68a6ccf70 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee @@ -0,0 +1,121 @@ +context = window + +MixerActions = @MixerActions + +@SessionOtherTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless this.props.hasMixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + + MixerActions.mute([mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "my-track" : true + "has-mixer" : this.props.hasMixer + "no-mixer" : !this.props.hasMixer + "has-audio" : this.props.noAudio != true + "no-audio" : this.props.noAudio == true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
{this.props.trackName}
+
+
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + if this.props.participant.client_id? + context.jamClient.SessionSetUserName(this.props.participant.client_id, this.props.name) + else + logger.error("no participant client ID") + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + {mixers:mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + {mixers:mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + unless this.props.hasMixer + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.hasMixer + $mute.off("mouseenter", false) + $mute.off("mouseleave", false) + $pan.off("mouseenter", false) + $pan.off("mouseleave", false) + else + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee new file mode 100644 index 000000000..311f0402a --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee @@ -0,0 +1,89 @@ +context = window +MixerActions = context.MixerActions +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup + +@SessionOtherTracks = React.createClass({ + + mixins: [Reflux.listenTo(@SessionOtherTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + session = sessionMixers.session + mixers = sessionMixers.mixers + noAudioUsers = mixers.noAudioUsers + + participants = [] + + if session.inSession() + + for participant in session.otherParticipants() + + name = participant.user.name + tracks = [] + firstTrack = participant.tracks[0] + hasMixer = false + + for track in participant.tracks + # try to find mixer info for this track + mixerFinder = [participant.client_id, track, false] # so that other callers can re-find their mixer data + + mixerData = mixers.findMixerForTrack(participant.client_id, track, false, @props.mode) + if mixerData.mixer? + hasMixer = true + + tracks.push(track: track, mixers: mixerData, mixerFinder: mixerFinder) + # todo: sessionModel.setAudioEstablished + + instrumentIcon = context.JK.getInstrumentIcon45(firstTrack.instrument_id) + instrumentDescription = firstTrack.instrument + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url) + + + name = "#{name}: #{instrumentDescription}" + + participantState = { + participant: participant, + tracks: tracks, + name: name, + trackName: name + instrumentIcon: instrumentIcon, + photoUrl: photoUrl, + hasMixer: hasMixer, + noAudio: noAudioUsers[participant.client_id] + } + + MixerActions.missingPeerMixer(participant.client_id) unless hasMixer + + participants.push(participantState) + + this.setState(participants: participants, session: session) + + render: () -> + + content = null + participants = [] + + noOthers = `
No other musicians are in your session.
` + + if this.state.participants.length > 0 + for participant in this.state.participants + participants.push(``) + else if this.state.session? && this.state.session.inSession() + content = noOthers + + `
+

other live tracks

+ +
+ {content} + + {participants} + +
+
` + + getInitialState:() -> + {participants:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee new file mode 100644 index 000000000..c6d75af93 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee @@ -0,0 +1,24 @@ +context = window +RecordingActions = @RecordingActions + +@SessionRecordBtn = React.createClass({ + + mixins: [Reflux.listenTo(@MixerStore,"onSessionMixerChange")] + + onSessionMixerChange: (sessionMixers) -> + + this.setState({isRecording: sessionMixers.session.isRecording}) + + getInitialState: () -> + {childWindow: null, isRecording: false} + + openRecording: () -> + + RecordingActions.openRecordingControls() + + render: () -> + ` + + RECORD + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee new file mode 100644 index 000000000..31528bac4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee @@ -0,0 +1,84 @@ +context = window + +MixerActions = @MixerActions + +@SessionRecordedCategory = React.createClass({ + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "recorded-category" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.recordingName}
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee new file mode 100644 index 000000000..d182b6450 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee @@ -0,0 +1,93 @@ +context = window +MIX_MODES = context.JK.MIX_MODES +MixerActions = @MixerActions + +@SessionRecordedTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "recorded-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.userName}
+
+ +
+
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + if @props.isOpener || @props.mode == MIX_MODES.MASTER + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + else + context.JK.helpBubble($mute, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + context.JK.helpBubble($pan, 'personal-media-track', {}, {positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee new file mode 100644 index 000000000..67b26884e --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionResyncBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + resync: (e) -> + e.preventDefault() + + SessionActions.audioResync() + + render: () -> + ` + + RESYNC + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee new file mode 100644 index 000000000..46288400d --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee @@ -0,0 +1,86 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +SessionActions = @SessionActions + +@SessionScreen = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit"), Reflux.listenTo(@SessionActions.allowLeaveSession, "onAllowLeaveSession")] + + + render: () -> + + videoBtn = null + if gon.global.video_available != 'none' + videoBtn = `` + + + `
+
+ + + + + {videoBtn} + + + +
+
+ + + + +
+
` + + componentDidMount: () -> + @logger = context.JK.logger + + beforeShow: (data) -> + @logger.debug("session beforeShow") + @allowLeave = false + + afterShow: (data) -> + @logger.debug("session afterShow") + + SessionActions.joinSession.trigger(data.id) + + beforeHide: () -> + context.JK.HelpBubbleHelper.clearJamTrackGuide(); + + beforeLeave: (data) -> + @logger.debug("session beforeLeave", @allowLeave) + + if @allowLeave + return true + else + leaveSessionWarningDialog = new context.JK.LeaveSessionWarningDialog(context.JK.app, + () => + @allowLeave = true + SessionActions.leaveSession(hash: data.hash) + #context.location.hash = data.hash + ) + + leaveSessionWarningDialog.initialize() + @app.layout.showDialog('leave-session-warning') + return false + + beforeDisconnect: () -> + @logger.debug("session beforeDisconnect") + + onAllowLeaveSession: () -> + @allowLeave = true + + onAppInit: (@app) -> + + screenBindings = { + 'beforeShow': @beforeShow, + 'afterShow': @afterShow, + 'beforeHide': @beforeHide, + 'beforeLeave' : @beforeLeave, + 'beforeDisconnect' : @beforeDisconnect, + }; + + @app.bindScreen('session', screenBindings); +}) diff --git a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee new file mode 100644 index 000000000..68ddde797 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee @@ -0,0 +1,169 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +MixerActions = @MixerActions + +@SessionSelfVolumeHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + inputGroupMixers = mixers.simulatedMusicCategoryMixers[MIX_MODES.PERSONAL] + chatGroupMixers = mixers.simulatedChatCategoryMixers[MIX_MODES.PERSONAL] + + @setState({inputGroupMixers: inputGroupMixers, chatGroupMixers: chatGroupMixers}) + + getInitialState: () -> + {inputGroupMixers: @props.inputGroupMixers, chatGroupMixers: @props.chatGroupMixers} + + handleAudioInputMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute(@state.inputGroupMixers.muteMixer, muting) + + handleChatInputMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute(@state.chatGroupMixers.muteMixer, muting) + + handleAudioInputMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute(@state.inputGroupMixers.muteMixer, muting) + + handleChatMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute(@state.chatGroupMixers.muteMixer, muting) + + render: () -> + + monitorMuteMixer = @state.inputGroupMixers.muteMixer[0] + monitorMuteMixerId = monitorMuteMixer?.id + monitorVolumeLeft = @state.inputGroupMixers.mixer[0].volume_left + monitorMuteClasses = classNames({ + 'track-icon-mute': true + 'enabled' : !monitorMuteMixer?.mute + 'muted' : monitorMuteMixer?.mute + }) + + chatMuteMixer = @state.chatGroupMixers.muteMixer[0] + chatMuteMixerId = chatMuteMixer?.id + chatVolumeLeft = @state.chatGroupMixers.mixer?[0].volume_left + chatMuteClasses = classNames({ + 'track-icon-mute': true + 'enabled' : !chatMuteMixer?.mute + 'muted' : chatMuteMixer?.mute + }) + + `
+
+

Music

+
+
+ +
+
+ +
+
+
Volume
+
{monitorVolumeLeft}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of all the music in the session in your headphones or speakers.

+

This will not affect the volume for other musicians in the session.

+

To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.

+
+
+ +
+

Chat

+
+
+ +
+
+ +
+
+
Volume
+
{chatVolumeLeft}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of all the voice chat in the session in your headphones or speakers.

+

This will not affect the volume for other musicians in the session.

+
+
+ +
+ close +
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + + # initialize icheck + $chatMuteCheckbox = $root.find('.chat-mixer input') + context.JK.checkbox($chatMuteCheckbox) + $chatMuteCheckbox.on('ifChanged', @handleChatMuteCheckbox); + + if @state.chatGroupMixers.muteMixer[0].mute + $chatMuteCheckbox.iCheck('check').attr('checked', true) + else + $chatMuteCheckbox.iCheck('uncheck').attr('checked', false) + + $audioInputMuteCheckbox = $root.find('.monitor-mixer input') + context.JK.checkbox($audioInputMuteCheckbox) + $audioInputMuteCheckbox.on('ifChanged', @handleAudioInputMuteCheckbox); + + if @state.inputGroupMixers.muteMixer[0].mute + $audioInputMuteCheckbox.iCheck('check').attr('checked', true) + else + $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false) + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # re-initialize icheck + $chatMuteCheckbox = $root.find('.chat-mixer input') + + if nextState.chatGroupMixers.muteMixer[0].mute + $chatMuteCheckbox.iCheck('check').attr('checked', true) + else + $chatMuteCheckbox.iCheck('uncheck').attr('checked', false) + + $audioInputMuteCheckbox = $root.find('.monitor-mixer input') + + if nextState.inputGroupMixers.muteMixer[0].mute + $audioInputMuteCheckbox.iCheck('check').attr('checked', true) + else + $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..0719f136b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + openSettings: (e) -> + e.preventDefault() + + @app.layout.showDialog('session-settings') + + render: () -> + ` + + SETTINGS + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee new file mode 100644 index 000000000..00af66683 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionShareBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onShare: (e) -> + e.preventDefault() + + @app.layout.showDialog('share-dialog') + + render: () -> + ` + + SHARE + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee new file mode 100644 index 000000000..ec02cbe67 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee @@ -0,0 +1,56 @@ +context = window +logger = context.JK.logger +CategoryGroupIds = context.JK.CategoryGroupIds + +@SessionTrackGain = React.createClass({ + + propTypes: { + gainType: React.PropTypes.string + } + + getInitialState: () -> + { + mixers: @props.mixers, + behaviors: @props.behaviors || {} + } + + faderChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixers = @state.mixers.mixer + + MixerActions.faderChanged(data, mixers, @props.gainType) + + render: () -> + # mixer can be a single item or array + mixer = @state.mixers?.mixer + if mixer && $.isArray(mixer) + mixer = mixer[0] + + mixerId = mixer?.id + + `
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-gain') + logger.error("unknown root node") + + context.JK.FaderHelpers.renderFader2($root, {faderType: 'vertical'}); + + # Initialize gain position + mixer = @state.mixers?.mixer + if mixer && $.isArray(mixer) + mixer = mixer[0] + + MixerActions.initGain(mixer) + + # watch for fader change events + $root.on('fader_change', @faderChanged); +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee new file mode 100644 index 000000000..c1e569566 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee @@ -0,0 +1,57 @@ +context = window +logger = context.JK.logger + +@SessionTrackPan = React.createClass({ + + getInitialState: () -> + { + mixers: this.props.mixers, + behaviors: this.props.behaviors || {} + } + + panChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixers = @state.mixers.mixer + + MixerActions.panChanged(data, mixers, groupId) + + render: () -> + + # mixer can be a single item or array + mixer = @state.mixers?.mixer + if mixer && $.isArray(mixer) + mixer = mixer[0] + + mixerId = mixer?.id + + `
+
Left
+
Right
+
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-pan') + logger.error("unknown root node") + + context.JK.FaderHelpers.renderFader2($root, {faderType: 'horizontal', snap:true}, context.JK.PanHelpers.convertPercentToPanForDisplay) + + + # Initialize panposition + mixer = @state.mixers?.mixer + if mixer && $.isArray(mixer) + mixer = mixer[0] + + MixerActions.initPan(mixer) + + # watch for fader change events + $root.on('fader_change', this.panChanged) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee new file mode 100644 index 000000000..8244991f4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee @@ -0,0 +1,51 @@ +context = window + +MixerActions = @MixerActions + +@SessionTrackPanHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore, "onInputsChanged")] + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + mixers = sessionMixers.mixers + newMixers = mixers.refreshMixer(@state.mixers) + + this.setState({mixers: newMixers}) + + + getInitialState: () -> + {mixers: this.props.mixers} + + render: () -> + + `
+
+

+ Use this slider to pan the audio of this track left or right in your personal mix. + This will not pan audio for other musicians in the session. + To pan audio in the master mix for recordings and broadcasts, use the Mixer button in the toolbar. +

+
+ +
+ +
+
+ close +
+
` + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # if the mixers go dead, whack our selves out of existence + unless nextState.mixers? + $container = $root.closest('.react-holder') + $container.data('bt').btOff() + return +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..530df4c93 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee @@ -0,0 +1,22 @@ +context = window + +logger = context.JK.logger + +@SessionTrackSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onConfigureSettings: (e) -> + e.preventDefault(); + + @app.layout.showDialog('configure-tracks') + + onAppInit: (app) -> + @app = app + + render: () -> + ` + + Settings + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee new file mode 100644 index 000000000..6bca85131 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee @@ -0,0 +1,87 @@ +context = window +ptrCount = 0 + +@SessionTrackVU = React.createClass({ + + + render: () -> + lights = [] + redSwitch = Math.round(this.props.lightCount * 0.66); + lightClass = 'vu-red-off' + + if this.props.orientation == 'horizontal' + + for i in [0..this.props.lightCount-1] + lightClass = if i >= redSwitch then 'vu-red-off' else 'vu-green-off' + + lightClasses = classNames('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = classNames('vu', 'horizontal') + + ` + + + {lights} + + +
` + else + + for i in [0..this.props.lightCount-1].reverse() + lightClass = if (i >= redSwitch) then "vu-red-off" else "vu-green-off" + + lightClasses = classNames('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = classNames('vu', 'vertical') + + ` + + {lights} + +
` + + getInitialState: () -> + {registered: null, ptr: @props.ptr || "STV#{ptrCount++}"} + + registerVU: (mixer) -> + + mixerChanged = false + if @state.registered? && mixer? + + # see if the mixer ID changed; if so, we need to unregister and re-register + if @state.registered.mixer.id != mixer.id + logger.debug("unregistering vu due to mixer change", @state.registered.mixer) + context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr) + mixerChanged = true + + if !mixerChanged && (@state.registered? || !mixer?) + return + + $root = $(this.getDOMNode()) + + if mixerChanged + logger.debug("re-registering VU #{context.JK.groupIdDisplay(mixer)}", mixer) + #else + # logger.debug("registered VU #{context.JK.groupIdDisplay(mixer)}", mixer) + + context.JK.VuHelpers.registerVU(@props.side, mixer, @state.ptr, @props.orientation == 'horizontal', @props.lightCount, $root.find('td')) + + @setState(registered: {mixer: mixer, ptr: @state.ptr}) + + + componentWillReceiveProps: (nextProps) -> + @registerVU(nextProps.mixers?.vuMixer) + + componentDidMount: () -> + @registerVU(@props.mixers?.vuMixer) + + componentWillUnmount: () -> + if @state.registered? + #logger.debug("unregistered VU #{context.JK.groupIdDisplay(@state.registered.mixer)}") + context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee new file mode 100644 index 000000000..63c02f9bb --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee @@ -0,0 +1,147 @@ +context = window +ChannelGroupIds = context.JK.ChannelGroupIds +MixerActions = @MixerActions +ptrCount = 0 + +@SessionTrackVolumeHover = React.createClass({ + + # example: 'music' when it represent the gainer that controls all non-chat inputs + propTypes: { + gainType: React.PropTypes.string + } + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + iCheckMaint: false + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + newMixers = mixers.refreshMixer(@state.mixers) + + newMixers = {} unless newMixers? + this.setState({mixers: newMixers}) + + getInitialState: () -> + {mixers: this.props.mixers, ptr: "STVH#{ptrCount++}" } + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + else + MixerActions.mute(this.state.mixers.mixer, muting) + + + handleMuteCheckbox: (e) -> + + return if @iCheckMaint + + muting = $(e.target).is(':checked') + + if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + else + MixerActions.mute(this.state.mixers.mixer, muting) + + + render: () -> + + muteMixer = @state.mixers?.muteMixer + if muteMixer && $.isArray(muteMixer) + muteMixer = muteMixer[0] + muteMixerId = muteMixer?.id + + mixer = @state.mixers?.mixer + if mixer && $.isArray(mixer) + mixer = mixer[0] + + volume_left = mixer.volume_left + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + `
+
+
+ +
+
+ +
+
+
Volume
+
{volume_left}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of this track in your personal mix.

+

This will not affect the volume of this track for other musicians in the session.

+

To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.

+
+ +
+ close +
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + + muteMixer = this.state.mixers?.muteMixer + if $.isArray(muteMixer) + muteMixer = muteMixer[0] + + # initialize icheck + $checkbox = $root.find('input') + context.JK.checkbox($checkbox) + $checkbox.on('ifChanged', this.handleMuteCheckbox); + + # using iCheck causes a 'ifChanged' event, so we need to swallow this up + @iCheckMaint = true + if muteMixer.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) + @iCheckMaint = false + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # if the mixers go dead, whack our selves out of existence + unless nextState.mixers? + $container = $root.closest('.react-holder') + $container.data('bt').btOff() + return + + # re-initialize icheck + $checkbox = $root.find('input') + + muteMixer = nextState.mixers?.muteMixer + if muteMixer? && $.isArray(muteMixer) + muteMixer = muteMixer[0] + + # using iCheck causes a 'ifChanged' event, so we need to swallow this up + @iCheckMaint = true + if muteMixer.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) + @iCheckMaint = false +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee new file mode 100644 index 000000000..d89f7ae29 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee @@ -0,0 +1,16 @@ +context = window +SessionActions = @SessionActions + +@SessionVideoBtn = React.createClass({ + + sessionWebCam: (e) -> + e.preventDefault(); + + SessionActions.toggleSessionVideo() + + render: () -> + ` + + VIDEO + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..ee60d7a76 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee @@ -0,0 +1,49 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@SessionVolumeSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@MixerStore,"onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + + musicMixers = sessionMixers.mixers.simulatedMusicCategoryMixers[ MIX_MODES.PERSONAL ] + chatMixers = sessionMixers.mixers.simulatedChatCategoryMixers[ MIX_MODES.PERSONAL ] + + @setState(musicMixers: musicMixers, chatMixers: chatMixers) + + getInitialState: () -> + { musicMixers: null, chatMixers: null} + + render: () -> + + musicMixers = @state.musicMixers + chatMixers = @state.chatMixers + + # find out if the first mixer on the music mixers or chat mixers is muted; if so, switch to a muted icon + + isMuted = musicMixers?.first.mute || chatMixers?.first.mute + + volumeIconClasses = classNames({ + 'volume-icon' : true + 'enabled' : !isMuted + 'muted' : isMuted + }) + + ` +
+ VOLUME +
` + + componentDidMount: () -> + $root = $(this.getDOMNode()) + + context.JK.interactReactBubble( + $root, + 'SessionSelfVolumeHover', + () => + {inputGroupMixers: @state.musicMixers, chatGroupMixers: @state.chatMixers} + , + {width:470, positions:['right', 'bottom', 'left'], offsetParent:$root.closest('.screen')}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/Test.js.jsx.coffee b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee new file mode 100644 index 000000000..fbdc502a7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee @@ -0,0 +1,19 @@ +context = window + +@TestComponent = React.createClass({ + + getInitialState: () -> + {something: 1} + + tick: () -> + console.log("tick") + this.setState({something: this.state.something + 1}) + + componentDidMount: () -> + console.log("here") + setInterval(@tick, 1000) + + render: () -> + console.log("render") + `
{this.state.something}
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee new file mode 100644 index 000000000..6c054e3ec --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@AppActions = Reflux.createActions({ + appInit: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee index 7761257d2..e4bc43707 100644 --- a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee @@ -1,6 +1,6 @@ context = window -BroadcastActions = Reflux.createActions({ +@BroadcastActions = Reflux.createActions({ load: {asyncResult: true}, hide: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee new file mode 100644 index 000000000..adcb9467a --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee @@ -0,0 +1,7 @@ +context = window + +@JamTrackActions = Reflux.createActions({ + open: {} + close: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee new file mode 100644 index 000000000..0996e5745 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee @@ -0,0 +1,11 @@ +context = window + +@MediaPlaybackActions = Reflux.createActions({ + playbackStateChange: {} + positionUpdate:{} + mediaStartPlay: {} + mediaStopPlay: {} + mediaPausePlay: {} + mediaChangePosition: {} + currentTimeChanged: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee new file mode 100644 index 000000000..68c71dfba --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee @@ -0,0 +1,17 @@ +context = window + +@MixerActions = Reflux.createActions({ + mute: {} + faderChanged: {} + initGain: {} + panChanged: {} + initPan: {} + mixersChanged: {} + syncTracks: {} + mixerModeChanged: {} + loopChanged: {} + openMetronome: {} + metronomeChanged: {} + deadUserRemove: {} + missingPeerMixer: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee new file mode 100644 index 000000000..481dc8ff8 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@NotificationActions = Reflux.createActions({ + clear:{} + backendNotification: {} + frontendNotification: {} + sessionEnded: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee new file mode 100644 index 000000000..f6b111ac9 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee @@ -0,0 +1,14 @@ +context = window + +@RecordingActions = Reflux.createActions({ + initModel: {} + startRecording: {} + stopRecording: {} + startingRecording:{} + startedRecording: {} + stoppingRecording: {} + stoppedRecording: {} + abortedRecording: {} + openRecordingControls: {} + recordingControlsClosed: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee new file mode 100644 index 000000000..634e7d419 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee @@ -0,0 +1,22 @@ +context = window + +@SessionActions = Reflux.createActions({ + joinSession: {} + leaveSession: {} + mixersChanged: {} + allowLeaveSession: {} + syncWithServer: {} + toggleSessionVideo : {} + audioResync: {} + openBackingTrack: {} + closeMedia: {} + updateSession: {} + downloadingJamTrack : {} + openMetronome: {} + showNativeMetronomeGui: {} + metronomeCricketChange: {} + windowBackgrounded: {} + broadcastFailure: {} + broadcastSuccess: {} + broadcastStopped: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee new file mode 100644 index 000000000..568ba3b29 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@SessionMyTracksActions = Reflux.createActions({ + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee new file mode 100644 index 000000000..7089f1af6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -0,0 +1,1004 @@ +context = window + +logger = context.JK.logger +ChannelGroupIds = context.JK.ChannelGroupIds +CategoryGroupIds = context.JK.CategoryGroupIds +MIX_MODES = context.JK.MIX_MODES; + + +@MixerHelper = class MixerHelper + + constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) -> + @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions + @app = @session.app + @mixersByResourceId = {} + @mixersByTrackId = {} + @allMixers = {} + @currentMixerRangeMin = null + @currentMixerRangeMax = null + @mediaSummary = {} + @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, + ChannelGroupIds.MetronomeGroup] + @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, + ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup] + @vuStats = {} + @shouldCollectVuStats = false + @simulatedMusicCategoryMixers = {} + @simulatedChatCategoryMixers = {} + @organize() + + organize: () -> + for masterMixer in @masterMixers + @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id + + # populate mixer pair + mixerPair = {} + @mixersByResourceId[masterMixer.rid] = mixerPair + @mixersByTrackId[masterMixer.id] = mixerPair + mixerPair.master = masterMixer; + + for personalMixer in @personalMixers + + @allMixers['P' + personalMixer.id] = personalMixer + + # populate other side of mixer pair + + mixerPair = @mixersByResourceId[personalMixer.rid] + unless mixerPair + if personalMixer.group_id != ChannelGroupIds.MonitorGroup + logger.warn("there is no master version of ", personalMixer) + + mixerPair = {} + @mixersByResourceId[personalMixer.rid] = mixerPair + + @mixersByTrackId[personalMixer.id] = mixerPair; + mixerPair.personal = personalMixer; + + @groupTypes() + @chatMixer = @resolveChatMixer() + + groupTypes: () -> + localMediaMixers = @mixersForGroupIds(@mediaTrackGroups, MIX_MODES.MASTER) + peerLocalMediaMixers = @mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER) + + #logger.debug("localMediaMixers", localMediaMixers) + #logger.debug("peerLocalMediaMixers", peerLocalMediaMixers) + + # get the server data regarding various media tracks + recordedBackingTracks = @session.recordedBackingTracks() + backingTracks = @session.backingTracks() + recordedJamTracks = @session.recordedJamTracks() + jamTracks = @session.jamTracks() + + ### + with mixer info, we use these to decide what kind of tracks are open in the backend + + each mixer has a media_type field, which describes the type of media track it is. + * JamTrack + * BackingTrack + * RecordingTrack + * MetronomeTrack + * "" - adhoc track (not supported visually) + + it is supposed to be the case that there are only one type of track open at a time, however, that's a business policy/logic + constraint; and may be buggy. **So, we should render whatever we have, so that it's obvious what's really going on.** + + so, let's group up all mixers by type, and then ask them to be rendered + ### + + @recordingTrackMixers = [] + @backingTrackMixers = [] + @jamTrackMixers = [] + @metronomeTrackMixers = [] + @adhocTrackMixers = [] + + groupByType = (mixers, isLocalMixer) => + for mixer in mixers + mediaType = mixer.media_type + groupId = mixer.group_id + + if mediaType == 'MetronomeTrack' || groupId == ChannelGroupIds.MetronomeGroup + # Metronomes come across with a blank media type, so check group_id: + @metronomeTrackMixers.push(mixer) + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # additional check; if we can match an id in backing tracks or recorded backing track, + # we need to remove it from the recorded track set, but move it to the backing track set + + isJamTrack = false; + + if jamTracks + # check if the ID matches that of an open jam track + for jamTrack in jamTracks + if mixer.id == jamTrack.id + isJamTrack = true; + break + + if !isJamTrack && recordedJamTracks + # then check if the ID matches that of a open, recorded jam track + for recordedJamTrack in recordedJamTracks + if mixer.id == recordedJamTrack.id + isJamTrack = true + break + + if isJamTrack + @jamTrackMixers.push(mixer) + else + isBackingTrack = false + if recordedBackingTracks + for recordedBackingTrack in recordedBackingTracks + if mixer.id == 'L' + recordedBackingTrack.client_track_id + isBackingTrack = true + break + + if backingTracks + for backingTrack in backingTracks + if mixer.id == 'L' + backingTrack.client_track_id + isBackingTrack = true + break + + if isBackingTrack + @backingTrackMixers.push(mixer) + else + # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file + @recordingTrackMixers.push(mixer) + + else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack' + @backingTrackMixers.push(mixer) + else if mediaType == 'JamTrack' + @jamTrackMixers.push(mixer); + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # mediaType == null is for backwards compat with older clients. Can be removed soon + @recordingTrackMixers.push(mixer) + else + logger.warn("Unknown track type: " + mediaType) + @adhocTrackMixers.push(mixer) + + groupByType(localMediaMixers, true); + groupByType(peerLocalMediaMixers, false); + + ### + if recordingTrackMixers.length > 0 + renderRecordingTracks(recordingTrackMixers) + + if backingTrackMixers.length > 0 + renderBackingTracks(backingTrackMixers) + + if jamTrackMixers.length > 0 + renderJamTracks(jamTrackMixers); + + if metronomeTrackMixers.length > 0 && @session.jamTracks() == null && @session.recordedJamTracks() == null + renderMetronomeTracks(metronomeTrackMixers); + + checkMetronomeTransition(); + ### + + @backingTracks = @resolveBackingTracks() + @jamTracks = @resolveJamTracks() + @recordedTracks = @resolveRecordedTracks() + @metronome = @resolveMetronome() + + if @adhocTrackMixers.length > 0 + logger.warn("some tracks are open that we don't know how to show") + + @mediaSummary = + recordingOpen: @recordedTracks.length > 0 + jamTrackOpen: @jamTracks.length > 0 + backingTrackOpen: @backingTracks.length > 0 + metronomeOpen: @session.isMetronomeOpen() + + # figure out if any media is open + mediaOpenSummary = false + for mediaType, mediaOpen of @mediaSummary + mediaOpenSummary = true if mediaOpen + + @mediaSummary.mediaOpen = mediaOpenSummary + + + # figure out if we opened any media + isOpener = false + + if @mediaSummary.recordingOpen + isOpener = @recordedTracks[0].isOpener + else if @mediaSummary.jamTrackOpen + isOpener = @jamTracks[0].isOpener + else if @mediaSummary.backingTrackOpen + isOpener = @backingTracks[0].isOpener + + @mediaSummary.isOpener = isOpener + + @prepareSimulatedMixers() + + + # 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) + + resolveBackingTracks: () -> + backingTracks = [] + + return backingTracks unless @backingTrackMixers.length > 0 + + # find both client and server representation of the backing track + serverBackingTracks = [] + backingTrackMixers = @backingTrackMixers + + if @session.isPlayingRecording() + backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return mixer.managed || !mixer.managed?) + serverBackingTracks = @session.recordedBackingTracks() + else + serverBackingTracks = @session.backingTracks(); + backingTrackMixers = context._.filter(backingTrackMixers, (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 backingTracks; + + # we don't render backing tracks unless we have server data to accompany + if !serverBackingTracks? || serverBackingTracks.length == 0 + return backingTracks + + noCorrespondingTracks = false + for mixer in backingTrackMixers + # find the track or tracks that correspond to the mixer + correspondingTracks = [] + noCorrespondingTracks = false + if @session.isPlayingRecording() + for backingTrack in serverBackingTracks + # occurs if this client is the one that opened the track, # occurs if this client is a remote participant + if mixer.persisted_track_id == backingTrack.client_track_id || mixer.id == 'L' + backingTrack.client_track_id + correspondingTracks.push(backingTrack) + else + # if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours + correspondingTracks.push(serverBackingTracks[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" + }); + break + + # now we have backing track and mixer in hand; we can render + serverBackingTrack = correspondingTracks[0] + + oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + + isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup + data = + isOpener: isOpener + shortFilename: context.JK.getNameOfFile(serverBackingTrack.filename) + instrumentIcon: context.JK.getInstrumentIcon45(serverBackingTrack.instrument_id) + photoUrl: "/assets/content/icon_recording.png" + showLoop: isOpener && !@session.isPlayingRecording() + track: serverBackingTrack + mixers: @mediaMixers(mixer, isOpener) + + backingTracks.push(data) + + backingTracks + + resolveJamTracks: () -> + _jamTracks = [] + + return _jamTracks unless @jamTrackMixers.length > 0 + + + jamTrackMixers = @jamTrackMixers.slice(); + jamTracks = [] + jamTrackName = null; + + if @session.isPlayingRecording() + # only return managed mixers for recorded backing tracks + jamTracks = @session.recordedJamTracks() + jamTrackName = @session.recordedJamTrackName() + else + # only return un-managed (ad-hoc) mixers for normal backing tracks + jamTracks = @session.jamTracks() + jamTrackName = @session.jamTrackName() + + # 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 (JamTrackGroup), then we can say this person is the opener + isOpener = jamTrackMixers[0].group_id == ChannelGroupIds.JamTrackGroup; + + if jamTracks + noCorrespondingTracks = false + for jamTrack in jamTracks + mixer = null + preMasteredClass = "" + # find the track or tracks that correspond to the mixer + correspondingTracks = [] + + for matchMixer in @jamTrackMixers + if matchMixer.id == jamTrack.id + correspondingTracks.push(jamTrack) + mixer = matchMixer + + if correspondingTracks.length == 0 + noCorrespondingTracks = true + logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks) + @app.notify({ + title: "Unable to Open JamTrack", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png"}) + return _jamTracks + + #jamTracks = $.grep(jamTracks, (value) => + # $.inArray(value, correspondingTracks) < 0 + #) + + # prune found mixers + jamTrackMixers.splice(mixer); + + oneOfTheTracks = correspondingTracks[0]; + instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id); + + part = oneOfTheTracks.part + + instrumentName = oneOfTheTracks.instrument.description + + if part? + trackName = "#{instrumentName}: #{part}" + else + trackName = instrumentName + + data = + name: jamTrackName + trackName: trackName + part: part + isOpener: isOpener + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: @mediaMixers(mixer, isOpener) + + _jamTracks.push(data) + + _jamTracks + + resolveRecordedTracks: () -> + recordedTracks = [] + + return recordedTracks unless @recordingTrackMixers.length > 0 + + serverRecordedTracks = @session.recordedTracks() + + isOpener = @recordingTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup + + # using the server's info in conjuction with the client's, draw the recording tracks + if serverRecordedTracks + recordingName = @session.recordingName() + noCorrespondingTracks = false + for mixer in @recordingTrackMixers + preMasteredClass = "" + correspondingTracks = [] + for recordedTrack in serverRecordedTracks + if mixer.id.indexOf("L") == 0 + if mixer.id.substring(1) == recordedTrack.client_track_id + correspondingTracks.push(recordedTrack) + else if mixer.id.indexOf("C") == 0 + if mixer.id.substring(1) == recordedTrack.client_id + correspondingTracks.push(recordedTrack) + preMasteredClass = "pre-mastered-track" + else + # this should not be possible + alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id") + + if correspondingTracks.length == 0 + noCorrespondingTracks = true + logger.debug("unable to correlate all recorded tracks", recordingMixers, serverRecordedTracks) + @app.notify({ + title: "Unable to Open Recording", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png"}); + return recordedTracks + + serverRecordedTracks = $.grep(serverRecordedTracks, + (value) => + $.inArray(value, correspondingTracks) < 0 + ) + + oneOfTheTracks = correspondingTracks[0] + instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument_id) + userName = oneOfTheTracks.user.name + userName = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name unless userName? + + data = + recordingName: recordingName + isOpener: isOpener + userName: userName + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: @mediaMixers(mixer, isOpener) + + recordedTracks.push(data) + + recordedTracks + + resolveMetronome: () -> + + return null if @metronomeTrackMixers.length == 0 + + mixer = @metronomeTrackMixers[0] + + instrumentIcon = "/assets/content/icon_metronome.png" + + metronome = + instrumentIcon: instrumentIcon + mixers: @mediaMixers(mixer, true) + + metronome + + resolveChatMixer: () -> + masterChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.MASTER); + + return null if masterChatMixers.length == 0 + + personalChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.PERSONAL); + + if personalChatMixers.length == 0 + logger.warn("unable to find personal mixer for voice chat"); + return null + + + masterChatMixer = masterChatMixers[0]; + personalChatMixer = personalChatMixers[0]; + + { + master: { + mixer: masterChatMixer + muteMixer: masterChatMixer + vuMixer: masterChatMixer + oppositeMixer: personalChatMixer + } + personal: { + mixer: personalChatMixer + muteMixer: personalChatMixer + vuMixer: personalChatMixer + oppositeMixer: masterChatMixer + } + } + + # supply the master mixer of a media track, and this function will harvest out the rest + mediaMixers:(masterMixer, isOpener) -> + personalMixer = if isOpener then @getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL) else null + personalVuMixer = if isOpener then personalMixer else masterMixer + { + isOpener: isOpener + + master: { + mixer: masterMixer + muteMixer: masterMixer + vuMixer: masterMixer + } + personal: { + mixer: personalMixer + muteMixer: personalMixer + vuMixer: personalVuMixer + } + } + + + mixersForGroupIds: (groupIds, mixMode) -> + foundMixers = [] + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + for groupId in groupIds + if mixer.group_id == groupId + foundMixers.push(mixer) + + foundMixers + + mixersForGroupId: (groupId, mixMode) -> + foundMixers = []; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + for mixer in mixers + if mixer.group_id == groupId + foundMixers.push(mixer) + + foundMixers + + mixerForGroupId: (groupId, mixMode) -> + mixers = @mixersForGroupId(groupId, mixMode) + if mixers? && mixers.length > 0 + mixers[0] + else + null + + getMixer: (mixerId, mode) -> + mode = @mixMode unless mode? + @allMixers[(if mode then 'M' else 'P') + mixerId] + + getMixerByTrackId: (trackId, mode) -> + mixerPair = @mixersByTrackId[trackId] + + return null unless mixerPair + + if mode == undefined + return mixerPair + + else + if mode == MIX_MODES.MASTER + return mixerPair.master + else + return mixerPair.personal + + + groupedMixersForClientId: (clientId, groupIds, usedMixers, mixMode) -> + foundMixers = {}; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + unless mixer? + #logger.debug("empty mixer: ", mixers) + continue + + if mixer.client_id == clientId + for groupId in groupIds + if mixer.group_id == groupId + if (mixer.groupId != ChannelGroupIds.UserMusicInputGroup) && !(mixer.id of usedMixers) + mixers = foundMixers[mixer.group_id] + if !mixers + mixers = [] + foundMixers[mixer.group_id] = mixers + mixers.push(mixer) + + foundMixers + + getMixerByResourceId:(resourceId, mode) -> + mixerPair = @mixersByResourceId[resourceId]; + + return null if(!mixerPair) + + if !mode? + return mixerPair; + else + if mode == MIX_MODES.MASTER + return mixerPair.master + else + return mixerPair.personal + + + findMixerForTrack: (client_id, track, myTrack, mode = MIX_MODES.PERSONAL) -> + mixer = null # what is the best mixer for this track/client ID? + oppositeMixer = null # what is the corresponding mixer in the opposite mode? + vuMixer = null + muteMixer = null + + + if myTrack + # when it's your track, look it up by the backend resource ID + mixer = @getMixerByTrackId(track.client_track_id, mode) + vuMixer = mixer + muteMixer = mixer + + # sanity checks + if mixer && mixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer) + + if mixer + # find the matching AudioInputMusicGroup for the opposite mode + oppositeMixer = @getMixerByTrackId(track.client_track_id, !mode) + + if mode == MIX_MODES.PERSONAL + muteMixer = oppositeMixer; # make the master mixer the mute mixer + + # sanity checks + if !oppositeMixer + logger.error("unable to find opposite mixer for local mixer", mixer) + else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer) + else + logger.debug("local track is not present: ", track, @allMixers) + else + switch mode + when MIX_MODES.MASTER + + # when it's a remote track and in master mode, we should find the PeerAudioInputMusicGroup + mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + + # sanity check + if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + vuMixer = mixer + muteMixer = mixer + + if mixer + # we should be able to find a UserMusicInputGroup for this clientId in personal mode + oppositeMixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if oppositeMixers[ChannelGroupIds.UserMusicInputGroup] + oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] + + if !oppositeMixer + logger.error("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer ) + + when MIX_MODES.PERSONAL + mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if mixers[ChannelGroupIds.UserMusicInputGroup] + mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0] + + vuMixer = mixer + muteMixer = mixer + + if mixer + # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer + oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + if !oppositeMixer + logger.debug("unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id) + else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs + + { + mixer: mixer, + oppositeMixer: oppositeMixer, + vuMixer: vuMixer, + muteMixer: muteMixer + } + + mute: (mixerId, mode, muting) -> + + mode = @mixMode unless mode? + + @fillTrackVolumeObject(mixerId, mode) + + context.trackVolumeObject.mute = muting + + context.jamClient.SessionSetControlState(mixerId, mode) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixerId, mode) + mixer.mute = muting + + getOriginalVolume: (mixers, gainType) -> + originalVolume = null + if gainType == 'music' + + # find a non media volL to compare against for later 'relative move' + for mixer in mixers + if mixer.name != CategoryGroupIds.UserMedia && mixer.name != CategoryGroupIds.MediaTrack + originalVolume = mixer.volume_left + break + else + originalVolume = mixers[0].volume_left + + originalVolume + + faderChanged: (data, mixers, gainType) -> + mixers = [mixers] unless $.isArray(mixers) + + originalVolume = @getOriginalVolume(mixers, gainType) + + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + + relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack) + + @setMixerVolume(mixer, data.percentage, relative, originalVolume) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.volume_left = context.trackVolumeObject.volL + + #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 + # context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) + + initGain: (mixer) -> + if $.isArray(mixer) + mixer = mixer[0] + + gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left) + context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent) + context.JK.FaderHelpers.showFader(mixer.id) + + panChanged: (data, mixers, groupId) -> + mixers = [mixers] unless $.isArray(mixers) + # media tracks are the only controls that sometimes set two mixers right now + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + + @setMixerPan(mixer, data.percentage) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.pan = context.trackVolumeObject.pan + + initPan: (mixer) -> + panPercent= context.JK.PanHelpers.convertPanToPercent(mixer.pan) + context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan)) + context.JK.FaderHelpers.showFader(mixer.id) + + setMixerPan: (mixer, panPercent) -> + + context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent); + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + loopChanged: (mixer, shouldLoop) -> + + @fillTrackVolumeObject(mixer.id, mixer.mode) + context.trackVolumeObject.loop = shouldLoop + context.jamClient.SessionSetControlState(mixer.id, mixer.mode) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.loop = context.trackVolumeObject.loop + + setMixerVolume: (mixer, volumePercent, relative, originalVolume) -> + ### + // 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. + // All that needs doing is to translate the incoming percent + // into the real value ont the sliders range. Set Left/Right + // volumes on trackVolumeObject, and call SetControlState to stick. + ### + + newVolume = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); + if relative + context.trackVolumeObject.volL = context.trackVolumeObject.volL + (newVolume - originalVolume) + context.trackVolumeObject.volR = context.trackVolumeObject.volR + (newVolume - originalVolume) + + # keep within range + if context.trackVolumeObject.volL < -80 + context.trackVolumeObject.volL = -80 + else if context.trackVolumeObject.volL > 20 + context.trackVolumeObject.volL = 20 + + if context.trackVolumeObject.volR < -80 + context.trackVolumeObject.volR = -80 + else if context.trackVolumeObject.volR > 20 + context.trackVolumeObject.volR = 20 + + + else + context.trackVolumeObject.volL = newVolume + context.trackVolumeObject.volR = newVolume + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + percentFromMixerValue: (min, max, value) -> + try + range = Math.abs(max - min) + magnitude = value - min + percent = Math.round(100*(magnitude/range)) + percent + catch err + 0 + + + percentToMixerValue:(min, max, percent) -> + range = Math.abs(max - min); + multiplier = percent/100; # Change 85 into 0.85 + value = min + (multiplier * range); + + # Protect against percents < 0 and > 100 + if value < min + value = min; + + if value > max + value = max; + + return value; + + fillTrackVolumeObject: (mixerId, mode, broadcast) -> + _broadcast = true + if broadcast? + _broadcast = broadcast + + mixer = @getMixer(mixerId, mode) + context.trackVolumeObject.clientID = mixer.client_id + context.trackVolumeObject.broadcast = _broadcast + context.trackVolumeObject.master = mixer.master + context.trackVolumeObject.monitor = mixer.monitor + context.trackVolumeObject.mute = mixer.mute + context.trackVolumeObject.name = mixer.name + context.trackVolumeObject.record = mixer.record + context.trackVolumeObject.volL = mixer.volume_left + context.trackVolumeObject.pan = mixer.pan + + # today we treat all tracks as mono, but this is required to make a stereo track happy + # context.trackVolumeObject.volR = mixer.volume_right; + context.trackVolumeObject.volR = mixer.volume_left; + + context.trackVolumeObject.loop = mixer.loop; + # trackVolumeObject doesn't have a place for range min/max + @currentMixerRangeMin = mixer.range_low; + @currentMixerRangeMax = mixer.range_high; + mixer + + collectStats: (mixer) -> + mixerStats = @vuStats[mixer.id] + + unless mixerStats? + mixerStats = {count: 0, group_name: context.JK.groupIdDisplay(mixer)} + @vuStats[mixer.id] = mixerStats + + mixerStats.count++ + + dumpVUStats: () -> + + # to use: check MixerStore for setInterval in cstr + logger.debug("VU STAT DUMP") + for mixerId, mixerStat of @vuStats + logger.debug("VU STAT: #{mixerState.group_name} count=#{mixerStat.count}") + + updateVU: (mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) -> + mixer = @getMixer(mixerId, mode) + + if mixer? + @collectStats(mixer) if @shouldCollectVuStats + context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping) + + ### + if mixer + if mixer.stereo # // stereo track + if mixerId.substr(-4) == "_vul" + context.JK.VuHelpers.updateVU2('vul', mixer, value) + else + context.JK.VuHelpers.updateVU2('vur', mixer, value) + else + if mixerId.substr(-4) == "_vul" + # Do the left + context.JK.VuHelpers.updateVU2('vul', mixer, value) + # Do the right + context.JK.VuHelpers.updateVU2('vur', mixer, value) + ### + getTrackInfo: () -> + context.JK.TrackHelpers.getTrackInfo(context.jamClient, @masterMixers) + + getGroupMixer: (categoryId, mode) -> + groupId = if mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup + mixers = @mixersForGroupId(groupId, mode) + + if mixers.length == 0 + #logger.warn("could not find mixer with group ID: " + groupId + ', mode:' + mode) + return null + + found = null + for mixer in mixers + if mixer.name == categoryId + found = mixer + break + + unless found? + logger.warn("could not find mixer with categoryId: " + categoryId) + return null + else + { + mixer: found, + muteMixer : found, + vuMixer: found, + oppositeMixer: found + } + + prepareSimulatedMixers: () -> + @simulatedMusicCategoryMixers[MIX_MODES.MASTER] = @getSimulatedMusicCategoryMixer(MIX_MODES.MASTER) + @simulatedMusicCategoryMixers[MIX_MODES.PERSONAL] = @getSimulatedMusicCategoryMixer(MIX_MODES.PERSONAL) + @simulatedChatCategoryMixers[MIX_MODES.MASTER] = @getSimulatedChatCategoryMixer(MIX_MODES.MASTER) + @simulatedChatCategoryMixers[MIX_MODES.PERSONAL] = @getSimulatedChatCategoryMixer(MIX_MODES.PERSONAL) + + getSimulatedMusicCategoryMixer: (mode) -> + myInputs = @getAudioInputCategoryMixer(mode)?.mixer + peerInputs = @getUserMusicCategoryMixer(mode)?.mixer + myMedia= @getMediaCategoryMixer(mode)?.mixer + peerMedia = @getUserMediaCategoryMixer(mode)?.mixer + metronome = @getMetronomeCategoryMixer(mode)?.mixer + output = @getOutputMixer(mode) + oppositeOutput = @getOutputMixer(!mode) + + # if myInputs category is missing, all categories are missing (seen when audio is first starting) + if myInputs + { + first: myInputs + mixer: [myInputs, peerInputs, myMedia, peerMedia, metronome] + muteMixer: [myInputs, peerInputs, myMedia, peerMedia, metronome] + vuMixer: output + } + else + null + + + getSimulatedChatCategoryMixer: (mode) -> + myChats = @getChatCategoryMixer(mode)?.mixer + peerChats = @getUserChatCategoryMixer(mode)?.mixer + if myChats + { + first: myChats + mixer: [myChats, peerChats] + muteMixer: [myChats, peerChats] + vuMixer: myChats + } + else + null + + + getAudioInputCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputMusic, mode) + + getChatCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputChat, mode) + + getUserChatCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.UserChat, mode) + + getMediaCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.MediaTrack, mode) + + getUserMediaCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.UserMedia, mode) + + getUserMusicCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.UserMusic, mode) + + getMetronomeCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.Metronome, mode) + + getOutputCategoryMixer: (mode) -> + if mode == MIX_MODES.MASTER + @getGroupMixer(CategoryGroupIds.MasterCatGroup, mode) + else + @getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode) + + getOutputMixer: (mode) -> + if mode == MIX_MODES.MASTER + @mixerForGroupId(ChannelGroupIds.MasterGroup, mode) + else + @mixerForGroupId(ChannelGroupIds.MonitorGroup, mode) + + refreshMixer: (mixers) -> + return null unless mixers? && mixers.mixer? + + updateMixers = null + if $.isArray(mixers.mixer) + if mixers.mixer.length > 0 + updateMixers = [] + for mixer in mixers.mixer + updateMixers.push(@getMixer(mixer.id, mixer.mode)) + else + updateMixers = @getMixer(mixers.mixer.id, mixers.mixer.mode) + + updatedVUMixers = null + if $.isArray(mixers.vuMixer) + updatedVUMixers = [] + for vuMixer in mixers.vuMixer + updateVUMixers.push(@getMixer(vuMixer.id, vuMixer.mode)) + else + updateVUMixers = @getMixer(mixers.vuMixer.id, mixers.vuMixer.mode) + + updateMuteMixers = null + if $.isArray(mixers.muteMixer) + updateMuteMixers = [] + for muteMixer in mixers.muteMixer + updateMuteMixers.push(@getMixer(muteMixer.id, muteMixer.mode)) + else + updateMuteMixers = @getMixer(mixers.muteMixer.id, mixers.muteMixer.mode) + + oppositeMixer = if mixers.oppositeMixer then @getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode) else null + + if updateMixers + { + mixer: updateMixers + vuMixer: updateVUMixers + muteMixer: updateMuteMixers + oppositeMixer: oppositeMixer + } + else + return null + + + recordingName: () -> + @session.recordingName() + + jamTrackName: () -> + @session.jamTrackName() diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee new file mode 100644 index 000000000..1cb5023c7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -0,0 +1,106 @@ +context = window + +@SessionHelper = class SessionHelper + + constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack) -> + @app = app + @session = session + @participantsEverSeen = participantsEverSeen + @isRecording = isRecording + @downloadingJamTrack = downloadingJamTrack + + inSession: () -> + @session? + + participants: () -> + if @session + return @session.participants + else + [] + + otherParticipants: () -> + others = [] + for participant in @participants() + myTrack = @app.clientId == participant.client_id + + others.push(participant) unless myTrack + others + + # if any participant has the metronome open, then we say this session has the metronome open + isMetronomeOpen: () -> + @session? && @session.metronome_active + + 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 !!(@session && @session.claimed_recording); + + recordedTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_tracks + else + null + + recordedBackingTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_backing_tracks + else + null + + backingTracks: () -> + 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... + + for participant in @participants() + if participant.backing_tracks.length > 0 + backingTracks = participant.backing_tracks + break + + backingTracks + + backingTrack: () -> + result = null + if @session + # TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668 + result = + path: @session.backing_track_path + result + + jamTracks: () -> + if @session && @session.jam_track + @session.jam_track.tracks.filter((track)-> + track.track_type == 'Track' + ) + else + null + + jamTrackName: () -> + @session?.jam_track?.name + + recordedJamTracks:() -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_jam_track_tracks + else + null + + recordedJamTrackName: () -> + jam_track = @session?.claimed_recording?.recording?.jam_track + + if jam_track? then jam_track.name else null + + recordingName: () -> + @session?.claimed_recording?.name + + getParticipant: (clientId) -> + found = null + for participant in @participants() + if participant.client_id == clientId + found = participant + break + + logger.warn('unable to find participant with clientId: ' + clientId) unless found + found + + id: () -> + @session.id \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee new file mode 100644 index 000000000..809a273a6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee @@ -0,0 +1,52 @@ +context = window + +@IndividualJamTrackPage = React.createClass({ + + watchVideo: (e) -> + e.preventDefault() + window.open("/popups/youtube/player?id=askHvcCoNfw", 'What Are JamTracks?', 'scrollbars=yes,toolbar=no,status=no,height=282,width=500') + + render: () -> + + header = null + if @props.band + header = "#{@props.jam_track.original_artist} Backing Tracks - Complete Multitracks" + else if @props.generic? + header = "Backing Tracks + Free Amazing App = Unmatched Experience" + else + header = "#{@props.jam_track.name} Backing Track by #{@props.jam_track.original_artist}" + + + `
+
+

{header}

+
+
+
+

Here's Why 20,000 Musicians Love Our Backing Tracks

+

JamKazam gives you a better backing track experience:

+
    +
  • Full multitrack recordings with isolated track for each part
  • +
  • Free JamKazam app to: +
      +
    • Hear just the part you want to play to learn it
    • +
    • Mute the part you want to play, and play live with other parts
    • +
    • Record and mix your live play with unmuted tracks
    • +
    +
  • +
  • Free Internet Service to play this track live online with others
  • +
+ Watch A Video To See How It Works +
+
+

Preview "{this.props.jam_track.name}" Backing Track by {this.props.jam_track.original_artist}

+

Click the play buttons below to preview the master mix and fully isolated tracks of the professional backing track recording. All are included in your backing track.

+
+
+
+
+
+ +
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee new file mode 100644 index 000000000..de888990e --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee @@ -0,0 +1,60 @@ +context = window +rest = context.JK.Rest() + +@JamTrackCta = React.createClass({ + + redeem: (e) -> + e.preventDefault() + + return if @state.processing + + isFree = context.JK.currentUserFreeJamTrack + + rest.addJamtrackToShoppingCart({id: @props.jam_track.id}).done((response) => + if(isFree) + if context.JK.currentUserId? + context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices + context.location = '/client#/redeemComplete' + else + # now make a rest call to buy it + context.location = '/client#/redeemSignup' + + else + context.location = '/client#/shoppingCart' + + ).fail((jqXHR, textStatus, errorMessage) => + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + cart_errors = errors?.errors?.cart_id + if cart_errors?.length == 1 && cart_errors[0] == 'has already been taken' + context.location = '/client#/shoppingCart' + else + context.JK.app.ajaxError(jqXHR, textStatus, errorMessage) + @setState({processing:false}) + ) + + @setState({processing:true}) + + getInitialState:() -> + {processing: false} + + render: () -> + bandBrowseUrl = "/client?artist=#{this.props.jam_track.original_artist}#/jamtrackBrowse" + + `` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee new file mode 100644 index 000000000..2d7ef7ce1 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee @@ -0,0 +1,11 @@ +context = window + +@PopupYoutubePlayer = React.createClass({ + + render: () -> + video_url = "//www.youtube.com/embed/#{this.props.video_id}" + + `
+ +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee new file mode 100644 index 000000000..553aa3a5e --- /dev/null +++ b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee @@ -0,0 +1,17 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@MasterPersonalMixersMixin = { + + mixer: () -> + if @props.mode == MIX_MODES.MASTER + @props.mixers['master'].mixer + else + @props.mixers['personal'].mixer + + mixers: () -> + if @props.mode == MIX_MODES.MASTER + @props.mixers['master'] + else + @props.mixers['personal'] +} \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee new file mode 100644 index 000000000..92dc025f7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee @@ -0,0 +1,39 @@ +context = window +MIX_MODES = context.JK.MIX_MODES +logger = context.JK.logger + +@SessionMediaTracksMixin = { + + metronomeTrulyGoneCheck: () -> + + logger.debug("metronome is completely gone") + @setState({metronomeFlickerTimeout: null}) + + onInputsChanged: (sessionMixers) -> + + session = sessionMixers.session + mixers = sessionMixers.mixers + + # the backend delete/adds the metronome rapidly when the user hits play. this is custom code to deal with that + + if mixers.mediaSummary.isOpener + mediaCategoryMixer = mixers.getMediaCategoryMixer(@props.mode) + else + mediaCategoryMixer = mixers.getUserMediaCategoryMixer(@props.mode) + + state = + isRecording: session.isRecording + mediaSummary: mixers.mediaSummary + backingTracks: mixers.backingTracks + jamTracks: mixers.jamTracks + recordedTracks: mixers.recordedTracks + metronome: mixers.metronome + mediaCategoryMixer: mediaCategoryMixer + recordingName: mixers.recordingName() + jamTrackName: mixers.jamTrackName() + + @inputsChangedProcessed(state) if @inputsChangedProcessed? + + @setState(state) + +} \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee new file mode 100644 index 000000000..9d808485c --- /dev/null +++ b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee @@ -0,0 +1,48 @@ +context = window + +@SessionMyTracksMixin = { + + onInputsChanged: (sessionMixers) -> + + + session = sessionMixers.session + mixers = sessionMixers.mixers + + tracks = [] + + if session.inSession() + participant = session.getParticipant(@app.clientId) + + if participant + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); + + chatMixer = mixers.chatMixer + chat = null + if chatMixer + chat = + mixers: chatMixer + mode: @props.mode + photoUrl: photoUrl + + name = participant.user.name; + + for track in participant.tracks + # try to find mixer info for this track + mixerFinder = [participant.client_id, track, true] # so that other callers can re-find their mixer data + mixerData = mixers.findMixerForTrack(participant.client_id, track, true, @props.mode) + + hasMixer = mixerData.mixer? + + # todo: sessionModel.setAudioEstablished + + instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); + + trackName = "#{name}: #{track.instrument}" + tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id}) + + + else + logger.warn("SessionMyTracks: unable to find participant") + + this.setState(tracks: tracks, session:session, chat: chat) +} \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee new file mode 100644 index 000000000..adda6eac3 --- /dev/null +++ b/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee @@ -0,0 +1,6 @@ +context = window + +@SessionOtherTracksMixin = { + + +} \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee new file mode 100644 index 000000000..bb5a11d98 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee @@ -0,0 +1,12 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@AppStore = Reflux.createStore( + { + listenables: @AppActions + + onAppInit: (app) -> + @trigger(app) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee new file mode 100644 index 000000000..b06d128f5 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -0,0 +1,34 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +EVENTS = context.JK.EVENTS + + +JamTrackActions = @JamTrackActions + +@JamTrackStore = Reflux.createStore( + { + listenables: JamTrackActions + jamTrack: null + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (app) -> + @app = app + + onOpen: (jamTrack) -> + if @jamTrack? + @app.notify({text: 'Unable to open JamTrack because another one is already open.'}) + return + + @jamTrack = jamTrack + this.trigger(@jamTrack) + + onClose: () -> + @jamTrack = null + this.trigger(@jamTrack) + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee new file mode 100644 index 000000000..051e0047e --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee @@ -0,0 +1,120 @@ +$ = jQuery +context = window +logger = context.JK.logger +PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE +RecordingActions = @RecordingActions + +@MediaPlaybackStore = Reflux.createStore( + { + listenables: @MediaPlaybackActions + + playbackStateChanged: false + positionUpdateChanged: false + currentTimeChanged: false + playbackState: null + positionMs: 0 + durationMs: 0 + isRecording: false + sessionHelper: null + + + init: () -> + this.listenTo(context.SessionStore, this.onSessionChanged); + + onCurrentTimeChanged: (time) -> + @time = time + @currentTimeChanged = true + @issueChange() + + onSessionChanged: (session) -> + @isRecording = session.isRecording + @sessionHelper = session + + onMediaStartPlay: (data) -> + logger.debug("calling jamClient.SessionStartPlay"); + context.jamClient.SessionStartPlay(data.playbackMode); + + onMediaStopPlay: (data) -> + # if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording + if @sessionHelper.jamTracks() && @isRecording + logger.debug("preemptive jamtrack stop") + @startStopRecording(); + + if !data.endReached + logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached) + context.jamClient.SessionStopPlay() + + onMediaPausePlay: (data) -> + # if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording + if @sessionHelper.jamTracks() && @isRecording + logger.debug("preemptive jamtrack stop") + @startStopRecording(); + + + if !data.endReached + logger.debug("calling jamClient.SessionPausePlay. endReached:", data.endReached) + context.jamClient.SessionPausePlay() + + startStopRecording: () -> + if @isRecording + RecordingActions.stopRecording.trigger() + else + RecordingActions.startRecording.trigger() + + onMediaChangePosition: (data) -> + seek = data.positionMs; + + if data.playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK + # if positionMs == 0, then seek it back to whatever the earliest play start is to catch all the prelude + + if(seek == 0) + duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + seek = duration.start; + + logger.debug("calling jamClient.SessionTrackSeekMs(" + seek + ")"); + + if data.playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK + context.jamClient.SessionJamTrackSeekMs(seek); + else + context.jamClient.SessionTrackSeekMs(seek); + + + issueChange: () -> + + @state = + playbackState: @playbackState + playbackStateChanged: @playbackStateChanged + positionUpdateChanged: @positionUpdateChanged + currentTimeChanged: @currentTimeChanged + positionMs: @positionMs + durationMs: @durationMs + isPlaying: @isPlaying + time: @time + + this.trigger(@state) + @playbackStateChanged = false + @positionUpdateChanged = false + @currentTimeChanged = false + + onPlaybackStateChange: (text) -> + @playbackState = text + @playbackStateChanged = true + + @issueChange() + + onPositionUpdate: (playbackMode) -> + if playbackMode == PLAYBACK_MONITOR_MODE.JAMTRACK + @positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs() + duration = context.jamClient.SessionGetJamTracksPlayDurationMs() + @durationMs = duration.media_len + else + @positionMs = context.jamClient.SessionCurrrentPlayPosMs() + @durationMs = context.jamClient.SessionGetTracksPlayDurationMs() + + @isPlaying = context.jamClient.isSessionTrackPlaying() + + @positionUpdateChanged = true + @issueChange() + + } +) diff --git a/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee new file mode 100644 index 000000000..d98600ca5 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee @@ -0,0 +1,282 @@ +context = window +logger = context.JK.logger +MIX_MODES = context.JK.MIX_MODES +rest = context.JK.Rest() + +@MixerStore = Reflux.createStore( + { + METRO_SOUND_LOOKUP: { + 0 : "BuiltIn", + 1 : "SineWave", + 2 : "Beep", + 3 : "Click", + 4 : "Kick", + 5 : "Snare", + 6 : "MetroFile" + } + + metro: {tempo: 120, cricket: false, sound: "Beep" } + noAudioUsers : {} + checkingMissingPeers : {} + missingMixerPeers : {} + recheckTimeout : null + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.SessionStore, this.onSessionChange) + this.listenTo(context.MixerActions.mute, this.onMute) + this.listenTo(context.MixerActions.faderChanged, this.onFaderChanged) + this.listenTo(context.MixerActions.initGain, this.onInitGain) + this.listenTo(context.MixerActions.initPan, this.onInitPan) + this.listenTo(context.MixerActions.panChanged, this.onPanChanged) + this.listenTo(context.MixerActions.mixersChanged, this.onMixersChanged) + this.listenTo(context.MixerActions.syncTracks, this.onSyncTracks) + this.listenTo(context.MixerActions.mixerModeChanged, this.onMixerModeChanged) + this.listenTo(context.MixerActions.loopChanged, this.onLoopChanged) + this.listenTo(context.MixerActions.openMetronome, this.onOpenMetronome) + this.listenTo(context.MixerActions.metronomeChanged, this.onMetronomeChanged) + this.listenTo(context.MixerActions.deadUserRemove, this.onDeadUserRemove) + this.listenTo(context.MixerActions.missingPeerMixer, this.onMissingPeerMixer) + + context.JK.HandleVolumeChangeCallback2 = @handleVolumeChangeCallback + context.JK.HandleMetronomeCallback2 = @handleMetronomeCallback + context.JK.HandleBridgeCallback2 = @handleBridgeCallback + context.JK.HandleBackingTrackSelectedCallback2 = @handleBackingTrackSelectedCallback + + #setInterval(@dumpVUStats, 5000) + + dumpVUStats: () -> + @mixers.dumpVUStats() if @mixers? + + issueChange: () -> + @trigger({session: @session, mixers: @mixers}) + + handleVolumeChangeCallback: (mixerId, isLeft, value, isMuted) -> + # TODO + # Visually update mixer + # There is no need to actually set the back-end mixer value as the + # back-end will already have updated the audio mixer directly prior to sending + # me this event. I simply need to visually show the new fader position. + # TODO: Use mixer's range + #faderValue = percentFromMixerValue(-80, 20, value); + #context.JK.FaderHelpers.setFaderValue(mixerId, faderValue); + #var $muteControl = $('[control="mute"][mixer-id="' + mixerId + '"]'); + #_toggleVisualMuteControl($muteControl, isMuted); + logger.debug("volume change") + + + handleMetronomeCallback: (args) -> + logger.debug("MetronomeCallback: ", args) + @metro.tempo = args.bpm + @metro.cricket = args.cricket; + @metro.sound = @METRO_SOUND_LOOKUP[args.sound]; + + # This isn't actually there, so we rely on the metroSound as set from select on form: + # metroSound = args.sound + SessionActions.syncWithServer() + + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + + @issueChange() + + handleBridgeCallback: (vuData) -> + + eventName = null + mixerId = null + value = null + vuInfo = null + + for vuInfo in vuData + eventName = vuInfo[0]; + vuVal = 0.0; + if eventName == "vu" + mixerId = vuInfo[1]; + mode = vuInfo[2]; + leftValue = vuInfo[3]; + leftClipping = vuInfo[4]; + rightValue = vuInfo[5]; + rightClipping = vuInfo[6]; + # TODO - no guarantee range will be -80 to 20. Get from the + # GetControlState for this mixer which returns min/max + # value is a DB value from -80 to 20. Convert to float from 0.0-1.0 + + @mixers.updateVU(mixerId, mode, (leftValue + 80) / 80, leftClipping, (rightValue + 80) / 80, rightClipping) + #@mixers.updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping) + + + handleBackingTrackSelectedCallback: () -> + logger.debug("backing track selected") + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + context.jamClient.SetVURefreshRate(150) + context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback2") + context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback2") + + + sessionEnded: () -> + @checkingMissingPeers = {} + @noAudioUsers = {} + @missingMixerPeers = {} + clearTimeout(@recheckTimeout) if @recheckTimeout? + + onSessionChange: (session) -> + + @sessionEnded() unless session.inSession() + + @session = session + + @masterMixers = context.jamClient.SessionGetAllControlState(true); + @personalMixers = context.jamClient.SessionGetAllControlState(false); + + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + + @issueChange() + + onMute: (mixers, muting) -> + + mixers = [mixers] unless $.isArray(mixers) + + for mixer in mixers + @mixers.mute(mixer.id, mixer.mode, muting); + + # simulate a state change to cause a UI redraw + @issueChange() + + onFaderChanged: (data, mixers, gainType) -> + + @mixers.faderChanged(data, mixers, gainType) + + @issueChange() + + onPanChanged: (data, mixers, groupId) -> + @mixers.panChanged(data, mixers, groupId) + + @issueChange() + + onLoopChanged: (mixer, shouldLoop) -> + @mixers.loopChanged(mixer, shouldLoop) + + onOpenMetronome: () -> + context.jamClient.SessionStopPlay() + context.jamClient.SessionOpenMetronome(@mixers.metro.tempo, @mixers.metro.sound, 1, 0) + + onMetronomeChanged: (tempo, sound) -> + logger.debug("onMetronomeChanged", tempo, sound) + + @metro.tempo = tempo + @metro.sound = sound + context.jamClient.SessionSetMetronome(@metro.tempo, @metro.sound, 1, 0); + + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + @issueChange() + + onDeadUserRemove: (clientId) -> + return unless @session.inSession() + + participant = @session.participantsEverSeen[clientId]; + + if participant? + logger.debug("todo :notify dead user") + # XXX TODO trigger some notification store + + #app.notify({ + # "title": ALERT_TYPES[type].title, + # "text": participant.user.name + " is no longer sending audio.", + # "icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url) + #}); + + @noAudioUsers[clientId] = true + + @issueChange() + + onMissingPeerMixer: (clientId) -> + + missingPeerAttempts = @missingMixerPeers[clientId] + + # check for 5 tries (10 seconds if setTimeout is 2000ms) + if !missingPeerAttempts? || missingPeerAttempts < 5 + @checkingMissingPeers[clientId] = true + @recheckTimeout = setTimeout(@recheckForMixers, 2000) unless @recheckTimeout? + else + logger.debug("ignoring missing peer recheck. missingPeerAttempts: #{missingPeerAttempts}") + + recheckForMixers: () -> + # increment how many times we've checked for this particular peer + for clientId, meh of @checkingMissingPeers + missingPeerAttempts = @missingMixerPeers[clientId] + missingPeerAttempts = 0 unless missingPeerAttempts? + missingPeerAttempts++ + @missingMixerPeers[clientId] = missingPeerAttempts + + # reset the peers we are looking for + @checkingMissingPeers = {} + + @recheckTimeout = null + @masterMixers = context.jamClient.SessionGetAllControlState(true); + @personalMixers = context.jamClient.SessionGetAllControlState(false); + logger.debug("MixerStore: recheckForMixers") + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + @issueChange() + + onInitGain: (mixer) -> + @mixers.initGain(mixer) + + onInitPan: (mixer) -> + @mixers.initPan(mixer) + + onMixersChanged: (type, text) -> + @masterMixers = context.jamClient.SessionGetAllControlState(true); + @personalMixers = context.jamClient.SessionGetAllControlState(false); + + logger.debug("MixerStore: onMixersChanged") + + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + + SessionActions.mixersChanged.trigger(type, text, @mixers.getTrackInfo()) + + @issueChange() + + onMixerModeChanged: (mode) -> + if mode == MIX_MODES.MASTER + @app.layout.showDialog('session-master-mix-dialog') unless @app.layout.isDialogShowing('session-master-mix-dialog') + else + @app.layout.closeDialog('session-master-mix-dialog') if @app.layout.isDialogShowing('session-master-mix-dialog') + + onSyncTracks: () -> + logger.debug("MixerStore: onSyncTracks") + unless @session.inSession() + logger.debug("dropping sync tracks because no longer in session") + return + + allTracks = @mixers.getTrackInfo() + + inputTracks = allTracks.userTracks; + backingTracks = allTracks.backingTracks; + metronomeTracks = allTracks.metronomeTracks; + + # create a trackSync request based on backend data + syncTrackRequest = {} + syncTrackRequest.client_id = @app.clientId + syncTrackRequest.tracks = inputTracks + syncTrackRequest.backing_tracks = backingTracks + syncTrackRequest.metronome_open = metronomeTracks.length > 0 + syncTrackRequest.id = @session.id() + + rest.putTrackSyncChange(syncTrackRequest) + .fail((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.") + ) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee new file mode 100644 index 000000000..f68e4d091 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee @@ -0,0 +1,75 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@RecordingStore = Reflux.createStore( + { + listenables: @RecordingActions + recordingWindow: null + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + + onSessionEnded: () -> + if @recordingWindow? + @recordingWindow.close() + + onAppInit: (app) -> + @app = app + $(context.AppStore).on('SessionEnded', @onSessionEnded) + + onInitModel: (recordingModel) -> + @recordingModel = recordingModel + this.trigger({isRecording: @recordingModel.isRecording()}) + + onStartRecording: () -> + @recordingModel.startRecording() + + onStopRecording: () -> + @recordingModel.stopRecording() + + onStartingRecording: (details) -> + details.cause = 'starting' + this.trigger(details) + + @popupRecordingControls() unless @recordingWindow? + + onStartedRecording: (details) -> + details.cause = 'started' + this.trigger(details) + + @popupRecordingControls() unless @recordingWindow? + + onStoppingRecording: (details) -> + details.cause = 'stopping' + this.trigger(details) + + onStoppedRecording: (details) -> + details.cause = 'stopped' + this.trigger(details) + + onAbortedRecording: (details) -> + details.cause = 'aborted' + this.trigger(details) + + onOpenRecordingControls: () -> + logger.debug("recording controls opening") + + if @recordingWindow? + @recordingWindow.close() + + @popupRecordingControls() + + onRecordingControlsClosed: () -> + logger.debug("recording controls closed") + @recordingWindow = null + + popupRecordingControls: () -> + logger.debug("poupRecordingControls") + @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=350') + @recordingWindow.ParentRecordingStore = context.RecordingStore + @recordingWindow.ParentIsRecording = @recordingModel.isRecording() + + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee new file mode 100644 index 000000000..08e0544ce --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee @@ -0,0 +1,21 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@SessionMediaTracksStore = Reflux.createStore( + { + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.MixerStore, this.onSessionMixerChange) + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + onSessionMixerChange: (sessionMixers) -> + + this.trigger(sessionMixers) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee new file mode 100644 index 000000000..d50dce041 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee @@ -0,0 +1,21 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@SessionMyTracksStore = Reflux.createStore( + { + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.MixerStore, this.onSessionMixerChange) + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + onSessionMixerChange: (sessionMixers) -> + + this.trigger(sessionMixers) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee new file mode 100644 index 000000000..54b42c1b7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee @@ -0,0 +1,61 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() + +@SessionNotificationStore = Reflux.createStore( + { + listenables: @NotificationActions + + notifications: [] + count: 0 + + issueChange: () -> + @trigger(@notifications) + + onClear: () -> + @notifications = [] + @issueChange() + + onSessionEnded: () -> + @notifications = [] + @issueChange() + + processNotification: (notification) -> + notification.id = ++@count + + title = 'n/a' + extra = null + + if notification.backend_detail? + if notification.backend_detail == 'Network Issues' + title = 'Network Issues' + extra = notification.msg + else + title = notification.msg + extra = notification.backend_detail + else + title = notification.msg + + detail = if notification.detail? && notification.detail != "" then notification.detail else null + + data = + title: title + extra: extra + detail: detail + help: notification.help + + @notifications.unshift(data) + + if @notifications.length > 100 + @notifications.pop(); + @issueChange() + + onBackendNotification: (notification) -> + @processNotification(notification) + + onFrontendNotification: (notification) -> + @processNotification(notification) + } +) + diff --git a/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee new file mode 100644 index 000000000..703d65305 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee @@ -0,0 +1,21 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@SessionOtherTracksStore = Reflux.createStore( + { + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.MixerStore, this.onSessionMixerChange) + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + onSessionMixerChange: (sessionMixers) -> + + this.trigger(sessionMixers) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee new file mode 100644 index 000000000..65d06211b --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -0,0 +1,1070 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +EVENTS = context.JK.EVENTS +MIX_MODES = context.JK.MIX_MODES + +JamTrackActions = @JamTrackActions +SessionActions = @SessionActions +RecordingActions = @RecordingActions +NotificationActions = @NotificationActions + +@SessionStore = Reflux.createStore( + { + listenables: SessionActions + + userTracks: null # comes from the backend + currentSessionId: null + currentSession: null + currentOrLastSession: null + startTime: null + currentParticipants: {} + participantsEverSeen: {} + users: {} # // User info for session participants + requestingSessionRefresh: false + pendingSessionRefresh: false + sessionPageEnterTimeout: null + sessionPageEnterDeferred: null + gearUtils: null + sessionUtils: null + joinDeferred: null + recordingModel: null + currentTrackChanges: 0 + isRecording: false + previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []} + webcamViewer: null + openBackingTrack: null + helper: null + downloadingJamTrack: false + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + this.listenTo(context.RecordingStore, this.onRecordingChanged) + + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + @recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient); + RecordingActions.initModel(@recordingModel) + @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack) + + if gon.global.video_available && gon.global.video_available!="none" && context.JK.WebcamViewer? + @webcamViewer = new context.JK.WebcamViewer() + @webcamViewer.init($("#create-session-layout")) + @webcamViewer.setVideoOff() + + issueChange: () -> + @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack) + this.trigger(@helper) + + onWindowBackgrounded: () -> + @app.user() + .done((userProfile) => + if userProfile.show_whats_next && + window.location.pathname.indexOf(gon.client_path) == 0 && + !@app.layout.isDialogShowing('getting-started') + @app.layout.showDialog('getting-started') + ) + + return unless @inSession() + + # the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen + logger.debug("leaving session because window was closed") + SessionActions.leaveSession({location: '/client#/home'}) + + onBroadcastFailure: (text) -> + logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text); + + if @currentSession? && @currentSession.mount? + rest.createSourceChange({ + mount_id: @currentSession.mount.id, + source_direction: true, + success: false, + reason: text, + client_id: @app.clientId + }) + else + logger.debug("unable to report source change because no mount seen on session") + + onBroadcastSuccess: (text) -> + logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text); + + if @currentSession? && @currentSession.mount? + rest.createSourceChange({ + mount_id: @currentSession.mount.id, + source_direction: true, + success: true, + reason: text, + client_id: @app.clientId + }) + else + logger.debug("unable to report source change because no mount seen on session") + + onBroadcastStopped: (text) -> + logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text); + + if @currentSession? && @currentSession.mount? + rest.createSourceChange({ + mount_id: @currentSession.mount.id, + source_direction: false, + success: true, + reason: text, + client_id: @app.clientId + }) + else + logger.debug("unable to report source change because no mount seen on session") + + onShowNativeMetronomeGui: () -> + context.jamClient.SessionShowMetronomeGui() + + onOpenMetronome: () -> + unstable = @unstableNTPClocks() + if @participants().length > 1 && unstable.length > 0 + names = unstable.join(", ") + logger.debug("Unstable clocks: ", names, unstable) + context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' })); + else + data = + value: 1 + session_size: @participants().length + user_id: context.JK.currentUserId + user_name: context.JK.currentUserName + + context.stats.write('web.metronome.open', data) + rest.openMetronome({id: @currentSessionId}) + .done((response) => + MixerActions.openMetronome() + @updateSessionInfo(response, true) + ) + .fail((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" + }) + ) + + onMetronomeCricketChange: (isCricket) -> + context.jamClient.setMetronomeCricketTestState(isCricket); + + unstableNTPClocks: () -> + unstable = [] + # This should be handled in the below loop, actually: + myState = context.jamClient.getMyNetworkState() + map = null + for participant in @participants() + isSelf = participant.client_id == @app.clientId + + if isSelf + isStable = myState.ntp_stable + else + map = context.jamClient.getPeerState(participant.client_id) + isStable = map.ntp_stable + + if !isStable + name = participant.user.name + + if isSelf + name += " (this computer)" + + unstable.push(name) + unstable + + + + onDownloadingJamTrack: (downloading) -> + @downloadingJamTrack = downloading + + @issueChange() + + onToggleSessionVideo: () -> + logger.debug("toggle session video") + @webcamViewer.toggleWebcam() if @webcamViewer? + + onAudioResync: () -> + logger.debug("audio resyncing") + response = context.jamClient.SessionAudioResync() + if response? + @app.notify({ + "title": "Error", + "text": response, + "icon_url": "/assets/content/icon_alert_big.png"}) + + onSyncWithServer: () -> + @refreshCurrentSession(true) + + onWatchedInputs: (inputTracks) -> + + logger.debug("obtained tracks at start of session") + @sessionPageEnterDeferred.resolve(inputTracks); + @sessionPageEnterDeferred = null + + + # codeInitiated means the user did not initiate this + onCloseMedia: (codeInitiated) -> + + logger.debug("SessionStore: onCloseMedia") + if @helper.recordedTracks() + @closeRecording() + else if @helper.jamTracks() || @downloadingJamTrack + @closeJamTrack() + else if @helper.backingTrack() && @helper.backingTrack().path + @closeBackingTrack() + else if @helper.isMetronomeOpen() + @closeMetronomeTrack() + else + logger.error("don't know how to close open media") unless codeInitiated + + closeJamTrack: () -> + logger.debug("closing jam track"); + + if @isRecording + logger.debug("can't close jamtrack while recording") + @app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'}) + return + + unless @selfOpenedJamTracks() + logger.debug("can't close jamtrack if not the opener") + @app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'}) + return + + rest.closeJamTrack({id: @currentSessionId}) + .done(() => + @refreshCurrentSession(true) + ) + .fail((jqXHR) => + @app.notify({ + "title": "Couldn't Close JamTrack", + "text": "Couldn't inform the server to close JamTrack. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }) + ) + + context.jamClient.JamTrackStopPlay() + JamTrackActions.close() + + + onOpenBackingTrack: (result) -> + unless @inSession() + logger.debug("ignoring backing track selected callback (not in session)") + return + + if result.success + logger.debug("backing track selected: " + result.file); + + rest.openBackingTrack({id: @currentSessionId, backing_track_path: result.file}) + .done(() => + + openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false); + + if openResult + # storing session state in memory, not in response of Session server response. bad. + @openBackingTrack = result.file + else + @app.notify({ + "title": "Couldn't Open Backing Track", + "text": "Is the file a valid audio file?", + "icon_url": "/assets/content/icon_alert_big.png" + }); + @closeBackingTrack() + ) + .fail((jqXHR) => + @app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback"); + ) + + closeRecording: () -> + logger.debug("closing recording"); + + rest.stopPlayClaimedRecording({id: @currentSessionId, claimed_recording_id: @currentSession.claimed_recording.id}) + .done((response) => + #sessionModel.refreshCurrentSession(true); + # update session info + @onUpdateSession(response) + ) + .fail((jqXHR) => + @app.notify({ + "title": "Couldn't Stop Recording Playback", + "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }) + ) + + context.jamClient.CloseRecording() + + closeMetronomeTrack:() -> + logger.debug("SessionStore: closeMetronomeTrack") + rest.closeMetronome({id: @currentSessionId}) + .done(() => + context.jamClient.SessionCloseMetronome() + @refreshCurrentSession(true) + ) + .fail((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" + }) + ) + + closeBackingTrack: () -> + if @isRecording + logger.debug("can't close backing track while recording") + return + + rest.closeBackingTrack({id: @currentSessionId}) + .done(() => + ) + .fail(() => + @app.notify({ + "title": "Couldn't Close Backing Track", + "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + ) + + # '' closes all open backing tracks + context.jamClient.SessionStopPlay(); + context.jamClient.SessionCloseBackingTrackFile(''); + + + onMixersChanged: (type, text, trackInfo) -> + + return unless @inSession() + + if text == 'RebuildAudioIoControl' + + if @backendMixerAlertThrottleTimer + clearTimeout(@backendMixerAlertThrottleTimer) + + @backendMixerAlertThrottleTimer = + setTimeout(() => + @backendMixerAlertThrottleTimer = null + if @sessionPageEnterDeferred + # this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready + + # we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much + # so we need to check that we actaully have userTracks before considering ourselves done + if trackInfo.userTracks.length > 0 + logger.debug("obtained tracks at start of session") + @sessionPageEnterDeferred.resolve(trackInfo.userTracks) + @sessionPageEnterDeferred = null + + return + + # wait until we are fully in session before trying to sync tracks to server + if @joinDeferred + @joinDeferred + .done(()=> + MixerActions.syncTracks() + ) + , 100) + else if text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl' + + backingTracks = trackInfo.backingTracks + previousBackingTracks = @previousAllTracks.backingTracks + metronomeTracks = trackInfo.metronomeTracks + previousMetronomeTracks = @previousAllTracks.metronomeTracks + + # 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.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks + logger.debug("backing tracks changed", previousBackingTracks, backingTracks) + MixerActions.syncTracks() + else if !(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks + #logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks) + MixerActions.syncTracks() + else + @refreshCurrentSession(true) + + @previousAllTracks = trackInfo + + else if text == 'Global Peer Input Mixer Mode' + MixerActions.mixerModeChanged(MIX_MODES.MASTER) + + else if text == 'Local Peer Stream Mixer Mode' + MixerActions.mixerModeChanged(MIX_MODES.PERSONAL) + + onRecordingChanged: (details) -> + logger.debug("SessionStore.onRecordingChanged: " + details.cause) + @isRecording = details.isRecording + + switch details.cause + when 'started' + + if details.reason + reason = details.reason; + detail = details.detail; + title = "Could Not Start Recording"; + + switch reason + when 'client-no-response' + @notifyWithUserInfo(title, 'did not respond to the start signal.', detail) + when 'empty-recording-id' + @app.notifyAlert(title, "No recording ID specified.") + when 'missing-client' + @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail) + when 'already-recording' + @app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.') + when 'recording-engine-unspecified' + @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail) + when 'recording-engine-create-directory' + @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail) + when 'recording-engine-create-file' + @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail) + when 'recording-engine-sample-rate' + @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail) + when 'rest' + jqXHR = detail[0]; + @app.notifyServerError(jqXHR); + else + @notifyWithUserInfo(title, 'Error Reason: ' + reason) + else + @displayWhoCreatedRecording(details.clientId) + + when 'stopped' + if @selfOpenedJamTracks() + timeline = context.jamClient.GetJamTrackTimeline(); + + rest.addRecordingTimeline(details.recordingId, timeline) + .fail(()=> + @app.notify({ + title: "Unable to Add JamTrack Volume Data", + text: "The volume of the JamTrack will not be correct in the recorded mix." + }, null, true) + ) + + if details.reason + logger.warn("Recording Discarded: ", details) + reason = details.reason + detail = details.detail + title = "Recording Discarded" + + switch reason + when 'client-no-response' + @notifyWithUserInfo(title, 'did not respond to the stop signal.', detail) + when 'missing-client' + @notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail) + when 'empty-recording-id' + @app.notifyAlert(title, "No recording ID specified.") + when 'wrong-recording-id' + @app.notifyAlert(title, "Wrong recording ID specified.") + when 'not-recording' + @app.notifyAlert(title, "Not currently recording.") + when 'already-stopping' + @app.notifyAlert(title, "Already stopping the current recording.") + when 'start-before-stop' + @notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail) + else + @app.notifyAlert(title, "Error reason: " + reason) + else + @promptUserToSave(details.recordingId, timeline); + + when 'abortedRecording' + reason = details.reason + detail = details.detail + + title = "Recording Cancelled" + + switch reason + when 'client-no-response' + @notifyWithUserInfo(title, 'did not respond to the start signal.', detail) + when 'missing-client' + @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail) + when 'populate-recording-info' + @notifyWithUserInfo(title, 'could not synchronize with the server.', detail) + when 'recording-engine-unspecified' + @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail) + when 'recording-engine-create-directory' + @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail) + when 'recording-engine-create-file' + @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail) + when 'recording-engine-sample-rate' + @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail) + else + @app.notifyAlert(title, "Error reason: " + reason) + + @issueChange() + + notifyWithUserInfo: (title , text, clientId) -> + @findUserBy({clientId: clientId}) + .done((user)=> + @app.notify({ + "title": title, + "text": user.name + " " + text, + "icon_url": context.JK.resolveAvatarUrl(user.photo_url) + }); + ) + .fail(()=> + @app.notify({ + "title": title, + "text": 'Someone ' + text, + "icon_url": "/assets/content/icon_alert_big.png" + }) + ) + + findUserBy: (finder) -> + if finder.clientId + foundParticipant = null + for participant in @participants() + if participant.client_id == finder.clientId + foundParticipant = participant + break + + if foundParticipant + return $.Deferred().resolve(foundParticipant.user).promise(); + + # TODO: find it via some REST API if not found? + return $.Deferred().reject().promise(); + + displayWhoCreatedRecording: (clientId) -> + if @app.clientId != clientId # don't show to creator + @findUserBy({clientId: clientId}) + .done((user) => + @app.notify({ + "title": "Recording Started", + "text": user.name + " started a recording", + "icon_url": context.JK.resolveAvatarUrl(user.photo_url) + }) + ) + .fail(() => + @app.notify({ + "title": "Recording Started", + "text": "Oops! Can't determine who started this recording", + "icon_url": "/assets/content/icon_alert_big.png" + }) + ) + + promptUserToSave: (recordingId, timeline) -> + rest.getRecording( {id: recordingId} ) + .done((recording) => + if timeline + recording.timeline = timeline.global + + context.JK.recordingFinishedDialog.setRecording(recording) + @app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, (e, data) => + if data.result && data.result.keep + context.JK.prodBubble($('#recording-manager-viewer'), 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $('#session-screen').parent()}) + ) + ) + .fail(@app.ajaxError) + + onJoinSession: (sessionId) -> + + # poke ShareDialog + shareDialog = new JK.ShareDialog(@app, sessionId, "session"); + shareDialog.initialize(context.JK.FacebookHelperInstance); + + # initialize webcamViewer + if gon.global.video_available && gon.global.video_available != "none" + @webcamViewer.beforeShow() + + # double-check that we are connected to the server via websocket + + return unless @ensureConnected() + + # just make double sure a previous session state is cleared out + @sessionEnded() + + # update the session data to be empty + @updateCurrentSession(null) + + # start setting data for this new session + @currentSessionId = sessionId + @startTime = new Date().getTime() + + # let's find out the public/private nature of this session, + # so that we can decide whether we need to validate the audio profile more aggressively + rest.getSessionHistory(@currentSessionId) + .done((musicSession)=> + musicianAccessOnJoin = musicSession.musician_access + + shouldVerifyNetwork = musicSession.musician_access; + + @gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() => + SessionActions.leaveSession.trigger({location: '/client#/home'}) + ).done(() => + result = @sessionUtils.SessionPageEnter(); + + @gearUtils.guardAgainstActiveProfileMissing(@app, result) + .fail((data) => + leaveBehavior = {} + + if data && data.reason == 'handled' + if data.nav == 'BACK' + leaveBehavior.location = -1 + else + leaveBehavior.location = data.nav + else + leaveBehavior.location = '/client#/home'; + + SessionActions.leaveSession.trigger(leaveBehavior) + ).done(() => + @waitForSessionPageEnterDone() + .done((userTracks) => + @userTracks = userTracks + + @ensureAppropriateProfile(musicianAccessOnJoin) + .done(() => + logger.debug("user has passed all session guards") + @joinSession() + ) + .fail((result) => + unless result.controlled_location + SessionActions.leaveSession.trigger({location: "/client#/home"}) + ) + ).fail((data) => + if data == "timeout" + context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') + else if data == 'session_over' + # do nothing; session ended before we got the user track info. just bail + logger.debug("session is over; bailing") + else + context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) + + SessionActions.leaveSession.trigger({location: '/client#/home'}) + ) + ) + ) + ) + .fail(() => + logger.error("unable to fetch session history") + ) + + waitForSessionPageEnterDone: () -> + @sessionPageEnterDeferred = $.Deferred() + + # see if we already have tracks; if so, we need to run with these + inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient) + + logger.debug("isNoInputProfile", @gearUtils.isNoInputProfile()) + if inputTracks.length > 0 || @gearUtils.isNoInputProfile() + logger.debug("on page enter, tracks are already available") + @sessionPageEnterDeferred.resolve(inputTracks) + deferred = @sessionPageEnterDeferred + @sessionPageEnterDeferred = null + return deferred + + @sessionPageEnterTimeout = setTimeout(()=> + if @sessionPageEnterTimeout + if @sessionPageEnterDeferred + @sessionPageEnterDeferred.reject('timeout') + @sessionPageEnterDeferred = null + @sessionPageEnterTimeout = null + , 5000) + + @sessionPageEnterDeferred + + ensureAppropriateProfile: (musicianAccess) -> + deferred = new $.Deferred(); + if musicianAccess + deferred = context.JK.guardAgainstSinglePlayerProfile(@app) + else + deferred.resolve(); + deferred + + joinSession: () -> + context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); + context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); + context.jamClient.SessionSetConnectionStatusRefreshRate(1000); + #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) + + # subscribe to events from the recording model + @recordingRegistration() + + # tell the server we want to join + + @joinDeferred = rest.joinSession({ + client_id: @app.clientId, + ip_address: context.JK.JamServer.publicIP, + as_musician: true, + tracks: @userTracks, + session_id: @currentSessionId, + audio_latency: context.jamClient.FTUEGetExpectedLatency().latency + }) + .done((response) => + + unless @inSession() + # the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out + logger.debug("user left before fully joined to session. telling server again that they have left") + @leaveSessionRest(@currentSessionId) + return + + @updateSessionInfo(response, true) + @issueChange() + + logger.debug("calling jamClient.JoinSession"); + # on temporary disconnect scenarios, a user may already be in a session when they enter this path + # so we avoid double counting + unless @alreadyInSession() + if @participants().length == 1 + context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create); + else + context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join); + + @recordingModel.reset(@currentSessionId); + + context.jamClient.JoinSession({sessionID: @currentSessionId}); + + #@refreshCurrentSession(true); + + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); + + $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document + + @handleAutoOpenJamTrack() + ) + .fail((xhr) => + @updateCurrentSession(null) + + if xhr.status == 404 + # we tried to join the session, but it's already gone. kick user back to join session screen + leaveBehavior = + location: "/client#/findSession" + notify: + title: "Unable to Join Session", + text: " The session you attempted to join is over." + SessionActions.leaveSession.trigger(leaveBehavior) + else if xhr.status == 422 + response = JSON.parse(xhr.responseText); + if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track" + @app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')) + + else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"] + + leaveBehavior = + location: "/client#/findSession" + notify: + title: "Unable to Join Session" + text: "The session is currently recording." + SessionActions.leaveSession.trigger(leaveBehavior) + else + @app.notifyServerError(xhr, 'Unable to Join Session'); + else + @app.notifyServerError(xhr, 'Unable to Join Session'); + ) + + trackChanges: (header, payload) -> + if @currentTrackChanges < payload.track_changes_counter + # we don't have the latest info. try and go get it + logger.debug("track_changes_counter = stale. refreshing...") + @refreshCurrentSession(); + + else + if header.type != 'HEARTBEAT_ACK' + # don't log if HEARTBEAT_ACK, or you will see this log all the time + logger.info("track_changes_counter = fresh. skipping refresh...", header, payload) + + handleAutoOpenJamTrack: () -> + jamTrack = @sessionUtils.grabAutoOpenJamTrack(); + if jamTrack + # give the session to settle just a little (call a timeout of 1 second) + setTimeout(()=> + # tell the server we are about to open a jamtrack + rest.openJamTrack({id: @currentSessionId, jam_track_id: jamTrack.id}) + .done((response) => + logger.debug("jamtrack opened") + # now actually load the jamtrack + context.SessionActions.updateSession.trigger(response); + # context.JK.CurrentSessionModel.updateSession(response); + # loadJamTrack(jamTrack); + JamTrackActions.open(jamTrack) + ) + .fail((jqXHR) => + @app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback") + ) + , 1000) + + inSession: () -> + !!@currentSessionId + + alreadyInSession: () -> + inSession = false + for participant in @participants() + if participant.user.id == context.JK.currentUserId + inSession = true + break + + participants: () -> + if @currentSession + @currentSession.participants; + else + [] + + refreshCurrentSession: (force) -> + logger.debug("refreshCurrentSession(force=true)") if force + + @refreshCurrentSessionRest(force) + + refreshCurrentSessionRest: (force) -> + unless @inSession() + logger.debug("refreshCurrentSession skipped: ") + return + + if @requestingSessionRefresh + # if someone asks for a refresh while one is going on, we ask for another to queue up + logger.debug("queueing refresh") + @pendingSessionRefresh = true; + else + @requestingSessionRefresh = true + rest.getSession(@currentSessionId) + .done((response) => + try + @updateSessionInfo(response, force) + catch e + logger.error("unable to updateSessionInfo in session refresh", e) + #setTimeout(() => + # @updateSessionInfo(response, force) + #, 5000) + ) + .fail((jqXHR) => + if jqXHR.status != 404 + @app.notifyServerError(jqXHR, "Unable to refresh session data") + else + logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone") + ) + .always(() => + @requestingSessionRefresh = false + if @pendingSessionRefresh + # and when the request is done, if we have a pending, fire it off again + @pendingSessionRefresh = false + @refreshCurrentSessionRest(force) + ) + + onUpdateSession: (session) -> + @updateSessionInfo(session, true) + + updateSessionInfo: (session, force) -> + if force == true || @currentTrackChanges < session.track_changes_counter + logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter) + @currentTrackChanges = session.track_changes_counter; + @sendClientParticipantChanges(@currentSession, session); + @updateCurrentSession(session); + #if(callback != null) { + # callback(); + #} + else + logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter); + + + leaveSessionRest: () -> + rest.deleteParticipant(@app.clientId); + + sendClientParticipantChanges: (oldSession, newSession) -> + joins = [] + leaves = [] + leaveJoins = []; # Will hold JamClientParticipants + + oldParticipants = []; # will be set to session.participants if session + oldParticipantIds = {}; + newParticipants = []; + newParticipantIds = {}; + + if oldSession && oldSession.participants + for oldParticipant in oldSession.participants + oldParticipantIds[oldParticipant.client_id] = oldParticipant + + if newSession && newSession.participants + for newParticipant in newSession.participants + newParticipantIds[newParticipant.client_id] = newParticipant + + for client_id, participant of newParticipantIds + # grow the 'all participants seen' list + unless (client_id of @participantsEverSeen) + @participantsEverSeen[client_id] = participant; + + + if client_id of oldParticipantIds + # if the participant is here now, and here before, there is still a chance we missed a + # very fast leave/join. So check if joined_session_at is different + if oldParticipantIds[client_id].joined_session_at != participant.joined_session_at + leaveJoins.push(participant) + else + # new participant id that's not in old participant ids: Join + joins.push(participant); + + for client_id, participant of oldParticipantIds + unless (client_id of newParticipantIds) + # old participant id that's not in new participant ids: Leave + leaves.push(participant); + + for i, v of joins + if v.client_id != @app.clientId + @participantJoined(newSession, v) + + for i,v of leaves + if v.client_id != @app.clientId + @participantLeft(newSession, v) + + for i,v of leaveJoins + if v.client_id != @app.clientId + logger.debug("participant had a rapid leave/join") + @participantLeft(newSession, v) + @participantJoined(newSession, v) + + participantJoined: (newSession, participant) -> + logger.debug("jamClient.ParticipantJoined", participant.client_id) + context.jamClient.ParticipantJoined(newSession, @toJamClientParticipant(participant)); + @currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}} + + participantLeft: (newSession, participant) -> + logger.debug("jamClient.ParticipantLeft", participant.client_id) + context.jamClient.ParticipantLeft(newSession, @toJamClientParticipant(participant)); + delete @currentParticipants[participant.client_id] + + toJamClientParticipant: (participant) -> + { + userID: "", + clientID: participant.client_id, + tcpPort: 0, + udpPort: 0, + localIPAddress: participant.ip_address, # ? + globalIPAddress: participant.ip_address, # ? + latency: 0, + natType: "" + } + + recordingRegistration: () -> + logger.debug("recording registration not hooked up yet") + + updateCurrentSession: (sessionData) -> + if sessionData != null + @currentOrLastSession = sessionData + + @currentSession = sessionData + + #logger.debug("session changed") + + @issueChange() + + ensureConnected: () -> + unless context.JK.JamServer.connected + leaveBehavior = + location: '/client#/home' + notify: + title: "Not Connected" + text: 'To create or join a session, you must be connected to the server.' + + SessionActions.leaveSession.trigger(leaveBehavior) + + context.JK.JamServer.connected + + # called by anyone wanting to leave the session with a certain behavior + onLeaveSession: (behavior) -> + logger.debug("attempting to leave session", behavior) + if behavior.notify + @app.layout.notify(behavior.notify) + + SessionActions.allowLeaveSession.trigger() + + if behavior.location + if jQuery.isNumeric(behavior.location) + window.history.go(behavior.location) + else + window.location = behavior.location + else if behavior.hash + window.location.hash = behavior.hash + else + logger.warn("no location specified in leaveSession action", behavior) + window.location = '/client#/home' + + if gon.global.video_available && gon.global.video_available != "none" + @webcamViewer.setVideoOff() + + @leaveSession() + + @sessionUtils.SessionPageLeave() + + leaveSession: () -> + + if !@joinDeferred? || @joinDeferred?.state() == 'resolved' + deferred = new $.Deferred() + + @recordingModel.stopRecordingIfNeeded() + .always(()=> + @performLeaveSession(deferred) + ) + + performLeaveSession: (deferred) -> + + logger.debug("SessionModel.leaveCurrentSession()") + # TODO - sessionChanged will be called with currentSession = null\ + + # leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long + # time, for that entire duration you'll still be sending voice data to the other users. + # this may be bad if someone decides to badmouth others in the left-session during this time + logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + @app.clientId) + context.jamClient.LeaveSession({ sessionID: @currentSessionId }) + @leaveSessionRest(@currentSessionId) + .done(=> + deferred.resolve(arguments[0], arguments[1], arguments[2])) + .fail(=> + deferred.reject(arguments[0], arguments[1], arguments[2]); + ) + + # 'unregister' for callbacks + context.jamClient.SessionRegisterCallback(""); + #context.jamClient.SessionSetAlertCallback(""); + context.jamClient.SessionSetConnectionStatusRefreshRate(0); + + @sessionEnded() + + @issueChange() + + selfOpenedJamTracks: () -> + @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId) + + sessionEnded: () -> + # cleanup + + context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); + context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges); + context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); + context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); + + if @sessionPageEnterDeferred? + @sessionPageEnterDeferred.reject('session_over') + @sessionPageEnterDeferred = null + + if @backendMixerAlertThrottleTimer + clearTimeout(@backendMixerAlertThrottleTimer) + @backendMixerAlertThrottleTimer = null + + @userTracks = null; + @startTime = null; + + if @joinDeferred?.state() == 'resolved' + $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}}) + + @currentTrackChanges = 0 + @currentSession = null + @joinDeferred = null + @isRecording = false + @currentSessionId = null + @currentParticipants = {} + @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} + @openBackingTrack = null + @shownAudioMediaMixerHelp = false + @controlsLockedForJamTrackRecording = false + @openBackingTrack = null + @downloadingJamTrack = false + @sessionUtils.setAutoOpenJamTrack(null) + + JamTrackActions.close() + NotificationActions.sessionEnded() + + $(context.AppStore).triggerHandler('SessionEnded') + + id: () -> + @currentSessionId + + getCurrentOrLastSession: () -> + @currentOrLastSession + + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index 4f0b84e00..0aec59c6a 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -18,7 +18,7 @@ context.JK = context.JK || {}; var logger = context.JK.logger; - context.JK.RecordingModel = function(app, sessionModel, _rest, _jamClient) { + context.JK.RecordingModel = function(app, _rest, _jamClient) { var currentRecording = null; // the JSON response from the server for a recording var currentOrLastRecordingId = null; var currentRecordingId = null; @@ -31,7 +31,7 @@ var waitingOnStopTimer = null; var jamClient = _jamClient; - var sessionModel = sessionModel; + var sessionId = null; var $self = $(this); function isRecording (recordingId) { @@ -46,7 +46,7 @@ } /** called every time a session is joined, to ensure clean state */ - function reset() { + function reset(_sessionId) { currentlyRecording = false; waitingOnServerStop = false; waitingOnClientStop = false; @@ -57,9 +57,11 @@ currentRecording = null; currentRecordingId = null; stoppingRecording = false; + sessionId = _sessionId } + function groupTracksToClient(recording) { // group N tracks to the same client Id var groupedTracks = {}; @@ -84,7 +86,9 @@ currentlyRecording = true; stoppingRecording = false; - currentRecording = rest.startRecording({"music_session_id": sessionModel.id()}) + context.RecordingActions.startingRecording({isRecording: false}) + + currentRecording = rest.startRecording({"music_session_id": sessionId}) .done(function(recording) { currentRecordingId = recording.id; currentOrLastRecordingId = recording.id; @@ -94,8 +98,10 @@ jamClient.StartRecording(recording["id"], groupedTracks); }) .fail(function(jqXHR) { - $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments }); + var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } + $self.triggerHandler('startedRecording', details); currentlyRecording = false; + context.RecordingActions.startedRecording(details); }) @@ -116,6 +122,7 @@ waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000); $self.triggerHandler('stoppingRecording', {reason: reason, detail: detail}); + context.RecordingActions.stoppingRecording({reason: reason, detail: detail, isRecording:true}) // this path assumes that the currentRecording info has, or can be, retrieved // failure for currentRecording is handled elsewhere @@ -145,7 +152,9 @@ else { logger.error("unable to stop recording %o", arguments); transitionToStopped(); - $self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments}); + var details = {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) } }); }); @@ -168,7 +177,9 @@ if(!waitingOnClientStop && !waitingOnServerStop) { transitionToStopped(); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail}); + var details = {recordingId: recordingId, reason: errorReason, detail: errorDetail, isRecording: false} + $self.triggerHandler('stoppedRecording', details) + context.RecordingActions.stoppedRecording(details) } } @@ -198,12 +209,16 @@ if(success) { - $self.triggerHandler('startedRecording', {clientId: app.clientId}) + var details = {clientId: app.clientId, isRecording:true} + $self.triggerHandler('startedRecording', details) + context.RecordingActions.startedRecording(details) } else { currentlyRecording = false; logger.error("unable to start the recording %o, %o", reason, detail); - $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: reason, detail: detail}); + var details = { clientId: app.clientId, reason: reason, detail: detail, isRecording: false} + $self.triggerHandler('startedRecording', details); + context.RecordingActions.startedRecording(details) } } @@ -221,7 +236,9 @@ else { transitionToStopped(); logger.error("backend unable to stop the recording %o, %o", reason, detail); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail : detail}); + var details = {recordingId: recordingId, reason: reason, detail : detail, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) } } @@ -242,9 +259,14 @@ currentOrLastRecordingId = recording.id; }); - $self.triggerHandler('startingRecording', {recordingId: recordingId}); + var details = {recordingId: recordingId, isRecording: false} + $self.triggerHandler('startingRecording', details); + context.RecordingActions.startingRecording(details) currentlyRecording = true; - $self.triggerHandler('startedRecording', {clientId: clientId, recordingId: recordingId}); + + details = {clientId: clientId, recordingId: recordingId, isRecording: true} + $self.triggerHandler('startedRecording', details); + context.RecordingActions.startedRecording(details) } function handleRecordingStopped(recordingId, result) { @@ -253,7 +275,10 @@ var detail = result.detail; - $self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: reason, detail: detail }); + var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: true } + $self.triggerHandler('stoppingRecording', details); + context.RecordingActions.stoppingRecording(details) + // the backend says the recording must be stopped. // tell the server to stop it too rest.stopRecording({ @@ -265,18 +290,26 @@ .fail(function(jqXHR, textStatus, errorMessage) { if(jqXHR.status == 422) { logger.debug("recording already stopped %o", arguments); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); + var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) } else if(jqXHR.status == 404) { logger.debug("recording is already deleted %o", arguments); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); + var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) } else { - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage}); + var details = {recordingId: recordingId, reason: textStatus, detail: errorMessage, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) } }) .done(function() { - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); + var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false} + $self.triggerHandler('stoppedRecording', details); + context.RecordingActions.stoppedRecording(details) }) } @@ -287,7 +320,9 @@ stoppingRecording = false; - $self.triggerHandler('abortedRecording', {recordingId: recordingId, reason: reason, detail: detail }); + var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false } + $self.triggerHandler('abortedRecording', details); + context.RecordingActions.abortedRecording(details) // the backend says the recording must be stopped. // tell the server to stop it too rest.stopRecording({ diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 1827758af..7fb4f7058 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1409,13 +1409,13 @@ var metronome = {} $('.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); - + 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({ @@ -1941,7 +1941,7 @@ // Given a mixerID and a value between 0.0-1.0, // light up the proper VU lights. - function _updateVU(mixerId, value, isClipping) { + function _updateVU(mixerId, value, isClipping) { // Special-case for mono tracks. If mono, and it's a _vul id, // update both sides, otherwise do nothing. @@ -2142,8 +2142,8 @@ setFormFromMetronome(); // 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); + // metroSound = args.sound + context.JK.CurrentSessionModel.refreshCurrentSession(true); } function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) { @@ -2177,8 +2177,8 @@ // TODO - no guarantee range will be -80 to 20. Get from the // GetControlState for this mixer which returns min/max // value is a DB value from -80 to 20. Convert to float from 0.0-1.0 - _updateVU(mixerId + "_vul", (leftValue + 80) / 100, leftClipping); - _updateVU(mixerId + "_vur", (rightValue + 80) / 100, rightClipping); + _updateVU(mixerId + "_vul", (leftValue + 80) / 80, leftClipping); + _updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping); } else if(eventName === 'connection_status') { var mixerId = vuInfo[1]; @@ -3009,6 +3009,7 @@ function closeMetronomeTrack() { rest.closeMetronome({id: sessionModel.id()}) .done(function() { + logger.debug("session: SessionCloseMetronome") context.jamClient.SessionCloseMetronome(); sessionModel.refreshCurrentSession(true); }) @@ -3248,6 +3249,7 @@ $metronomePlaybackSelect.metronomePlaybackMode().on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, metronomePlaybackModeChanged) context.JK.helpBubble($metronomePlaybackHelp, 'metromone-playback-modes', {} , {offsetParent: $screen, width:'400px'}); $(document).on('layout_resized', function() { + console.log("RESIZE FLUID") resizeFluid(); }); } @@ -3269,10 +3271,10 @@ 'beforeLeave' : beforeLeave, 'beforeDisconnect' : beforeDisconnect, }; - app.bindScreen('session', screenBindings); + //app.bindScreen('session', screenBindings); $recordingManagerViewer = $('#recording-manager-viewer'); - $screen = $('#session-screen'); + $screen = $('#session-screen-old'); $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); $otherAudioContainer = $('#session-recordedtracks-container'); @@ -3292,7 +3294,7 @@ $voiceChat = $screen.find('#voice-chat'); $tracksHolder = $screen.find('#tracks') if(gon.global.video_available && gon.global.video_available!="none") { - webcamViewer.init($(".webcam-container")) + webcamViewer.init($("#create-session-layout .webcam-container")) webcamViewer.setVideoOff() } events(); diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index cf733c451..19c43bc94 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -263,8 +263,8 @@ var recordingId = payload.recording_id; - if(recordingId && context.JK.CurrentSessionModel.recordingModel.isRecording(recordingId)) { - context.JK.CurrentSessionModel.recordingModel.onServerStopRecording(recordingId); + if(recordingId && context.RecordingStore.recordingModel.isRecording(recordingId)) { + context.RecordingStore.recordingModel.onServerStopRecording(recordingId); } else { app.notify({ @@ -305,11 +305,11 @@ logger.debug("Handling SOURCE_UP_REQUESTED msg " + JSON.stringify(payload)); - var current_session_id = context.JK.CurrentSessionModel.id(); + var current_session_id = context.SessionStore.id(); if (!current_session_id) { // we are not in a session - var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + var last_session = context.SessionStore.getCurrentOrLastSession(); if(last_session && last_session.id == payload.music_session) { // the last session we were in was responsible for this message. not that odd at all logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") @@ -328,7 +328,7 @@ '', payload.bitrate) } else { - var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + var last_session = context.SessionStore.getCurrentOrLastSession(); if(last_session && last_session.id == payload.music_session) { // the last session we were in was responsible for this message. not that odd at all logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") @@ -346,11 +346,11 @@ function registerSourceDownRequested() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_DOWN_REQUESTED, function(header, payload) { logger.debug("Handling SOURCE_DOWN_REQUESTED msg " + JSON.stringify(payload)); - var current_session_id = context.JK.CurrentSessionModel.id(); + var current_session_id = context.SessionStore.id(); if (!current_session_id) { // we are not in a session - var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + var last_session = context.SessionStore.getCurrentOrLastSession(); if(last_session && last_session.id == payload.music_session) { // the last session we were in was responsible for this message. not that odd at all logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") @@ -367,7 +367,7 @@ context.jamClient.SessionLiveBroadcastStop(); } else { - var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + var last_session = context.SessionStore.getCurrentOrLastSession(); if(last_session && last_session.id == payload.music_session) { // the last session we were in was responsible for this message. not that odd at all logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 0bf819d2e..a3122282e 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -672,7 +672,7 @@ context.JK.SyncViewer = class SyncViewer sendCommand: ($retry, cmd) => - if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() + if context.SessionStore.inSession() context.JK.ackBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')}) else context.jamClient.OnTrySyncCommand(cmd) @@ -817,7 +817,7 @@ context.JK.SyncViewer = class SyncViewer exportRecording: (e) => $export = $(e.target) - if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() + if context.SessionStore.inSession() context.JK.ackBubble($export, 'sync-viewer-paused', {}, {offsetParent: $export.closest('.dialog')}) return @@ -837,7 +837,7 @@ context.JK.SyncViewer = class SyncViewer deleteRecording: (e) => $delete = $(e.target) - if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession() + if context.SessionStore.inSession() context.JK.ackBubble($delete, 'sync-viewer-paused', {}, {offsetParent: $delete.closest('.dialog')}) return diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index b6c61385e..f2be21ea5 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -7,6 +7,8 @@ "use strict"; + var ChannelGroupIds = context.JK.ChannelGroupIds + context.JK = context.JK || {}; // As these are helper functions, just have a single @@ -14,13 +16,15 @@ // take all necessary arguments to complete its work. context.JK.TrackHelpers = { - getTrackInfo: function(jamClient) { + getTrackInfo: function(jamClient, masterTracks) { - var allTracks = context.jamClient.SessionGetAllControlState(true); + if(masterTracks === undefined) { + masterTracks = context.jamClient.SessionGetAllControlState(true); + } - var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, allTracks); - var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, allTracks); - var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, 16); + var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, masterTracks); + var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, masterTracks); + var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MetronomeGroup); return { userTracks: userTracks, @@ -51,7 +55,7 @@ // allTracks is the result of SessionGetAllControlState; as an optimization getBackingTracks: function(jamClient, allTracks) { - var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 6, allTracks); + var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MediaTrackGroup, allTracks); var backingTracks = [] context._.each(mediaTracks, function(mediaTrack) { @@ -80,7 +84,7 @@ var localMusicTracks = []; var i; - localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 4, allTracks); + localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.AudioInputMusicGroup, allTracks); var trackObjects = []; diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 11fb08af1..5ba6362af 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -74,8 +74,8 @@ return genreSelectorDialog.showDialog(); } - function launchRecordingSelectorDialog(recordings, selectedRecordings, callback) { - var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, recordings, selectedRecordings, callback); + function launchRecordingSelectorDialog(selectedRecordings, callback) { + var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, selectedRecordings, callback); recordingSelectorDialog.initialize(); return recordingSelectorDialog.showDialog(); } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 8af882715..3d8359137 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -21,6 +21,7 @@ var os = null; + var reactHovers = [] context.JK.getGenreList = function() { return context.JK.Rest().getGenres(); } @@ -209,6 +210,84 @@ }) return $element; } + + /** Creates a hover element that does not dissappear when the user mouses over the hover. + * + * @param $element + * @param text + * @param options + */ + context.JK.interactReactBubble = function($element, reactElementName, reactPropsCallback, options) { + + if(!options) options = {}; + + context._.each(reactHovers, function(react) { + reactHovers.btOff(); + }) + reactHovers = [] + var reactElement = null + var reactDomNode = null; + + function cleanupReact() { + if(reactDomNode) { + logger.debug() + React.unmountComponentAtNode(reactDomNode) + } + } + function waitForBubbleHover($bubble) { + $bubble.hoverIntent({ + over: function() { + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + }, + out: function() { + //$element.btOff(); + }}); + } + + var timeout = null; + + options.postHide = cleanupReact; + options.trigger = 'none' + options.clickAnywhereToClose = true + options.closeWhenOthersOpen = true + options.preShow = function(container) { + var reactElement = context[reactElementName] + if(!reactElementName) { + throw "unknown react element" + reactElementName + } + reactElement= React.createElement(reactElement, reactPropsCallback()); + var $container = $(container) + reactDomNode = $container.find('.react-holder').get(0) + $(reactDomNode).data('bt', $element) + React.render(reactElement, reactDomNode) + } + options.postShow = function(container) { + + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + waitForBubbleHover($(container)) + timeout = setTimeout(function() {/**$element.btOff()*/}, 3000) + } + + $element.hoverIntent({ + over: function() { + $element.btOn(); + }, + out: function() { + + }}); + + options.cssStyles = {} + options.padding = 0; + context.JK.hoverBubble($element, '
', options) + return $element; + } + /** * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the bubble when hovered @@ -263,6 +342,15 @@ } + context.JK.groupIdDisplay = function(mixer) { + if(mixer && mixer.group_id) { + return context.JK.ChannelGroupLookup[mixer.group_id] + } + else { + return "?group?" + } + } + context.JK.bindProfileClickEvents = function($parent, dialogsToClose) { if (!$parent) { $parent = $('body'); @@ -804,6 +892,19 @@ return ul; } + context.JK.reset_errors = function($container) { + $container.find('.error-text').remove() + $container.find('.error').removeClass("error") + } + + context.JK.append_errors = function($field, fieldName, errors_data) { + var $ul = context.JK.format_errors(fieldName, errors_data); + if($ul != null) { + delete errors_data['errors'][fieldName]; + $field.closest('div.field').addClass('error').end().after($ul); + } + } + context.JK.format_all_errors = function (errors_data) { var errors = errors_data["errors"]; if (errors == null) return $('
  • unknown error
'); @@ -1353,7 +1454,7 @@ /** validates that no changes are being made to tracks while recording */ context.JK.verifyNotRecordingForTrackChange = function (app) { - if (context.JK.CurrentSessionModel.recordingModel.isRecording()) { + if (context.RecordingStore.recordingModel.isRecording()) { app.notify({ title: "Currently Recording", text: "Tracks cannot be modified while recording.", diff --git a/web/app/assets/javascripts/voiceChatHelper.js b/web/app/assets/javascripts/voiceChatHelper.js index e4d744178..d9e14d73f 100644 --- a/web/app/assets/javascripts/voiceChatHelper.js +++ b/web/app/assets/javascripts/voiceChatHelper.js @@ -336,14 +336,14 @@ // renders volumes based on what the backend says function renderVolumes() { - var $fader = $voiceChatFader.find('[control="fader"]'); + var $fader = $voiceChatFader.find('[data-control="fader"]'); var db = context.jamClient.FTUEGetChatInputVolume(); var faderPct = db + 80; context.JK.FaderHelpers.setHandlePosition($fader, faderPct); } function renderNoVolume() { - var $fader = $voiceChatFader.find('[control="fader"]'); + var $fader = $voiceChatFader.find('[data-control="fader"]'); context.JK.FaderHelpers.setHandlePosition($fader, 50); context.JK.VuHelpers.updateVU($voiceChatVuLeft, 0); context.JK.VuHelpers.updateVU($voiceChatVuRight, 0); @@ -384,7 +384,7 @@ renderVolumes(); uniqueCallbackName = 'voiceChatHelperChatInputVUCallback' + caller; - context.JK[uniqueCallbackName] = function(dbValue) { + context.JK[uniqueCallbackName] = function(dbValue, leftClip, rightClip) { context.JK.ftueVUCallback(dbValue, $voiceChatVuLeft); context.JK.ftueVUCallback(dbValue, $voiceChatVuRight); } diff --git a/web/app/assets/javascripts/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js index e9b4c70ce..5962fe3b5 100644 --- a/web/app/assets/javascripts/vuHelpers.js +++ b/web/app/assets/javascripts/vuHelpers.js @@ -14,6 +14,8 @@ // take all necessary arguments to complete its work. context.JK.VuHelpers = { + registeredMixers: [], + /** * Render a VU meter into the provided selector. * vuType can be either "horizontal" or "vertical" @@ -93,7 +95,154 @@ } }) - } + }, + + createQualifiedId: function(mixer) { + return (mixer.mode ? 'M' : 'P') + mixer.id + }, + + // type can be 'single' or 'double', meaning how the VU is represented (one set of lights, two) + // mixerId is the ID of the mixer + // and someFunction is used to make the registration (equality check). + registerVU: function(type, mixer, someFunction, horizontal, lightCount, lights) { + + var fqId = this.createQualifiedId(mixer) + var registrations = this.registeredMixers[fqId] + if (!registrations) { + registrations = [] + this.registeredMixers[fqId] = registrations + } + + if(type == 'best') { + registrations.push({type:type, ptr: someFunction, ptrCount: 1, horizontal: horizontal, lightCount: lightCount, lights:lights}) + } + else { + // find the right registration and add left lights or right lights to it + var found = null + context._.each(registrations, function(registration) { + if(registration.ptr == someFunction) { + found = registration; + return false; + } + }) + + if(!found) { + found = {type:type, ptr: someFunction, ptrCount: 1, horizontal: horizontal, lightCount: lightCount} + registrations.push(found); + } + else { + found.ptrCount++; + } + + if(type == 'left') { + //logger.debug("adding left lights") + found.leftLights = lights; + } + else { + //logger.debug("adding right lights"); + found.rightLights = lights; + } + + } + }, + + unregisterVU: function(mixer, someFunction) { + var fqId = this.createQualifiedId(mixer) + var registrations = this.registeredMixers[fqId] + if (!registrations || registrations.length == 0) { + logger.debug("no registration found for:" + fqId, registrations, this.registeredMixers) + return + } + else { + //logger.debug("unregistering " + fqId + ", " + registrations.length) + } + + var origLength = registrations.length; + registrations = registrations.filter(function(element) { + var isMatch = element.ptr == someFunction; + + if(isMatch) { + // found a registration that matches + //logger.debug("removing matching ptr", element.ptr) + element.ptrCount--; + + // keep the registration if any ptr's still left + var keepRegistration = element.ptrCount > 0; + if(!keepRegistration) { + //logger.debug("getting rid of the registration; no more ptrs"); + } + return keepRegistration; + } + else { + // keep the registration if this does not match the ptr + return true; + } + }) + + this.registeredMixers[fqId] = registrations + }, + + updateSingleVU: function(horizontal, lightCount, $lights, value, isClipping) { + + var i = 0; + var state = 'on'; + var lights = Math.round(value * lightCount); + var redSwitch = Math.round(lightCount * 0.6666667); + + var $light = null; + var colorClass = 'vu-green-'; + var thisLightSelector = null; + + // Remove all light classes from all lights + $lights.removeClass('vu-green-off vu-green-on vu-red-off vu-red-on'); + + // Set the lights + for (i = 0; i < lightCount; i++) { + colorClass = 'vu-green-'; + state = 'on'; + if (i >= redSwitch) { + colorClass = 'vu-red-'; + } + if (i >= lights) { + state = 'off'; + } + + var lightIndex = horizontal ? i : lightCount - i - 1; + $lights.eq(lightIndex).addClass(colorClass + state); + } + }, + + // sentMixerId ends with vul or vur + updateVU3: function(mixer, leftValue, leftClipping, rightValue, rightClipping) { + + var fqId = this.createQualifiedId(mixer) + + var registrations = this.registeredMixers[fqId] + if (registrations) { + var j; + for(j = 0; j < registrations.length; j++) { + var registration = registrations[j] + var horizontal = registration.horizontal; + var lightCount = registration.lightCount; + + if(registration.type == 'best') { + // TODO: find 'active' VU ... is it left value, or right value? + var $lights = registration.lights; + this.updateSingleVU(horizontal, lightCount, $lights, leftValue, leftClipping) + } + else { + if(mixer.stereo) { + this.updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping) + this.updateSingleVU(horizontal, lightCount, registration.rightLights, rightValue, rightClipping) + } + else { + this.updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping) + this.updateSingleVU(horizontal, lightCount, registration.rightLights, leftValue, leftClipping) + } + } + } + } + }, }; diff --git a/web/app/assets/javascripts/web/downloads.js b/web/app/assets/javascripts/web/downloads.js index 6f01037e5..1d7b2ffad 100644 --- a/web/app/assets/javascripts/web/downloads.js +++ b/web/app/assets/javascripts/web/downloads.js @@ -141,9 +141,9 @@ downloadUris[platform] = item.uri; }); }) - .fail(function() { + .fail(function(jqXHR) { removeSpinner(); - alert("Currently unable to list client software downloads due to error."); + context.JK.app.notify({text: "Currently unable to list client software downloads due to error."}); }) .always(function() { selectPlatform(currentOS == null ? 'Win32' : currentOS); diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js index ebb720f2c..6b0b8fd78 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack.js +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -8,35 +8,13 @@ var rest = context.JK.Rest(); var logger = context.JK.logger; var $page = null; - var $jamtrack_name = null; - var $jamtrack_band = null; var $previews = null; var $jamTracksButton = null; - var $genericHeader = null; - var $individualizedHeader = null; var $ctaJamTracksButton = null; function fetchJamTrack() { rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) .done(function (jam_track) { - logger.debug("jam_track", jam_track) - - if(!gon.just_previews) { - if (gon.generic) { - $genericHeader.removeClass('hidden'); - $jamTracksButton.attr('href', '/client#/jamtrackBrowse') - $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks") - - } - else { - $individualizedHeader.removeClass('hidden') - $jamtrack_name.text('"' + jam_track.name + '"'); - $jamtrack_band.text(jam_track.original_artist) - $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') - $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") - $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') - } - } context._.each(jam_track.tracks, function (track) { @@ -44,10 +22,10 @@ $previews.append($element); - new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true}) + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: true, color:'black', master_adds_line_break: false, preload_master:true}) if(track.track_type =='Master') { - context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.one_by_two .watch-video'), $page.find('.checkout')); } }) @@ -60,13 +38,9 @@ function initialize() { $page = $('body') - $jamtrack_name = $page.find('.jamtrack_name') - $jamtrack_band = $page.find('.jamtrack_band') $previews = $page.find('.previews') $jamTracksButton = $page.find('.browse-jamtracks') $ctaJamTracksButton = $page.find('.cta-free-jamtrack'); - $genericHeader = $page.find('h1.generic') - $individualizedHeader = $page.find('h1.individualized') context.JK.Tracking.adTrack(app) fetchJamTrack(); diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js similarity index 100% rename from web/app/assets/javascripts/web/individual_jamtrack_band.js rename to web/app/assets/javascripts/web/individual_jamtrack_band_v1.js diff --git a/web/app/assets/javascripts/web/individual_jamtrack_v1.js b/web/app/assets/javascripts/web/individual_jamtrack_v1.js new file mode 100644 index 000000000..a9fc6ff38 --- /dev/null +++ b/web/app/assets/javascripts/web/individual_jamtrack_v1.js @@ -0,0 +1,77 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.IndividualJamTrackv1 = function (app) { + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $page = null; + var $jamtrack_name = null; + var $jamtrack_band = null; + var $previews = null; + var $jamTracksButton = null; + var $genericHeader = null; + var $individualizedHeader = null; + var $ctaJamTracksButton = null; + + function fetchJamTrack() { + rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) + .done(function (jam_track) { + logger.debug("jam_track", jam_track) + + if(!gon.just_previews) { + if (gon.generic) { + $genericHeader.removeClass('hidden'); + $jamTracksButton.attr('href', '/client#/jamtrackBrowse') + $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks") + + } + else { + $individualizedHeader.removeClass('hidden') + $jamtrack_name.text('"' + jam_track.name + '"'); + $jamtrack_band.text(jam_track.original_artist) + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") + $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + } + } + + context._.each(jam_track.tracks, function (track) { + + var $element = $('
') + + $previews.append($element); + + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true}) + + if(track.track_type =='Master') { + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + } + }) + + $previews.append('
') + }) + .fail(function () { + app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) + }) + } + function initialize() { + + $page = $('body') + $jamtrack_name = $page.find('.jamtrack_name') + $jamtrack_band = $page.find('.jamtrack_band') + $previews = $page.find('.previews') + $jamTracksButton = $page.find('.browse-jamtracks') + $ctaJamTracksButton = $page.find('.cta-free-jamtrack'); + $genericHeader = $page.find('h1.generic') + $individualizedHeader = $page.find('h1.individualized') + + context.JK.Tracking.adTrack(app) + fetchJamTrack(); + } + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 89f49d6fe..561e44bf9 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -66,7 +66,8 @@ //= require web/home //= require web/tracking //= require web/individual_jamtrack -//= require web/individual_jamtrack_band +//= require web/individual_jamtrack_v1 +//= require web/individual_jamtrack_band_v1 //= require web/affiliate_program //= require web/affiliate_links //= require fakeJamClient @@ -75,3 +76,9 @@ //= require JamServer //= require_directory ../dialog //= require everywhere/everywhere +//= require classnames +//= require reflux +//= require react +//= require react_ujs +//= require react-init +//= require react-components diff --git a/web/app/assets/javascripts/webcam_viewer.js.coffee b/web/app/assets/javascripts/webcam_viewer.js.coffee index ed2b1d772..a5538167a 100644 --- a/web/app/assets/javascripts/webcam_viewer.js.coffee +++ b/web/app/assets/javascripts/webcam_viewer.js.coffee @@ -14,39 +14,46 @@ context.JK.WebcamViewer = class WebcamViewer @resolution=null init: (root) => + # the session usage of webcamViewer does not actually pass in anything + root = $() unless root? + @root = root @toggleBtn = @root.find(".webcam-test-btn") @webcamSelect = @root.find(".webcam-select-container select") @resolutionSelect = @root.find(".webcam-resolution-select-container select") @webcamSelect.on("change", this.selectWebcam) - @toggleBtn.on 'click', @toggleWebcam + @toggleBtn.on('click', @toggleWebcam) + @resolutionSelect.on("change", this.selectResolution) + #logger.debug("Initialed with (unique) select",@webcamSelect) - beforeShow:() => + beforeShow:() => this.loadWebCams() this.selectWebcam() this.loadResolutions() this.selectResolution() @initialScan = true - @client.SessStopVideoSharing() - #client.SessSetInsetPosition(5) + # protect against non-video clients pointed at video-enabled server from getting into a session + if @client.SessStopVideoSharing + @client.SessStopVideoSharing() + #client.SessSetInsetPosition(5) #client.SessSetInsetSize(1) #client.FTUESetAutoSelectVideoLayout(false) - #client.SessSelectVideoDisplayLayoutGroup(1) - - + #client.SessSelectVideoDisplayLayoutGroup(1) + + selectWebcam:(e, data) => - device = @webcamSelect.val() + device = @webcamSelect.val() if device? caps = @client.FTUEGetVideoCaptureDeviceCapabilities(device) @logger.debug("Got capabilities from device", caps, device) - @client.FTUESelectVideoCaptureDevice(device, caps) - + @client.FTUESelectVideoCaptureDevice(device, caps) + selectResolution:() => - @logger.debug 'Selecting res control: ', @resolutionSelect + @logger.debug 'Selecting from res control: ', @resolutionSelect @resolution = @resolutionSelect.val() if @resolution? - @logger.debug 'Selecting res: ', @resolution - @client.FTUESetVideoEncodeResolution @resolution + @logger.debug 'Selecting webcam resolution: ', @resolution + @client.FTUESetVideoEncodeResolution @resolution # if @isVideoShared # this.setVideoOff() # this.toggleWebcam() @@ -54,18 +61,18 @@ context.JK.WebcamViewer = class WebcamViewer setVideoOff:() => if this.isVideoShared() @client.SessStopVideoSharing() - + isVideoShared:() => @videoShared - + setToggleState:() => available = @webcamSelect.find('option').size() > 0 shared = this.isVideoShared() @toggleBtn.prop 'disabled', true @toggleBtn.prop 'disabled', !available - + toggleWebcam:() => - @logger.debug 'Toggling webcam from: ', this.isVideoShared() + @logger.debug 'Toggling webcam from: ', this.isVideoShared(), @toggleBtn if this.isVideoShared() @toggleBtn.removeClass("selected") @client.SessStopVideoSharing() @@ -77,14 +84,16 @@ context.JK.WebcamViewer = class WebcamViewer selectedDeviceName:() => webcamName="None Configured" - webcam = @client.FTUECurrentSelectedVideoDevice() - if (webcam? && Object.keys(webcam).length>0) - webcamName = _.values(webcam)[0] + # protect against non-video clients pointed at video-enabled server from getting into a session + webcam = if @client.FTUECurrentSelectedVideoDevice? then @client.FTUECurrentSelectedVideoDevice() else null + if (webcam? && Object.keys(webcam).length>0) + webcamName = _.values(webcam)[0] webcamName - + loadWebCams:() => - devices = @client.FTUEGetVideoCaptureDeviceNames() + # protect against non-video clients pointed at video-enabled server from getting into a session + devices = if @client.FTUEGetVideoCaptureDeviceNames? then @client.FTUEGetVideoCaptureDeviceNames() else [] selectedDevice = this.selectedDeviceName() selectControl = @webcamSelect context._.each devices, (device) -> @@ -95,21 +104,22 @@ context.JK.WebcamViewer = class WebcamViewer text: device) selectControl.append option selectControl.val selectedDevice - + if devices.length == 0 @root.find('.no-webcam-msg').removeClass 'hidden' else - @root.find('.no-webcam-msg').addClass 'hidden' + @root.find('.no-webcam-msg').addClass 'hidden' loadResolutions:() => - resolutions = @client.FTUEGetAvailableEncodeVideoResolutions() + # protect against non-video clients pointed at video-enabled server from getting into a session + resolutions = if @client.FTUEGetAvailableEncodeVideoResolutions? then @client.FTUEGetAvailableEncodeVideoResolutions() else {} selectControl = @resolutionSelect @logger.debug 'FOUND THESE RESOLUTIONS', resolutions, selectControl context._.each resolutions, (value, key, obj) -> option = $('