This commit is contained in:
Seth Call 2015-02-19 14:31:00 -06:00
commit 9345d65f06
143 changed files with 5536 additions and 723 deletions

View File

@ -49,7 +49,7 @@ gem 'unf', '0.1.3' #optional fog dependency
gem 'country-select'
gem 'aasm', '3.0.16'
gem 'postgres-copy', '0.6.0'
gem 'aws-sdk' #, '1.29.1'
gem 'aws-sdk', '~> 1'
gem 'bugsnag'
gem 'gon'
gem 'cocoon'

View File

@ -223,7 +223,6 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
bpm 100.1
time_signature '4/4'
status 'Production'
recording_type 'Cover'

View File

@ -242,4 +242,14 @@ active_jam_track.sql
bpms_on_tap_in.sql
jamtracks_job.sql
text_messages.sql
text_message_migration.sql
text_message_migration.sql
user_model_about_changes.sql
performance_samples.sql
user_presences.sql
discard_scores_optimized.sql
backing_tracks.sql
metronome.sql
recorded_backing_tracks.sql
recorded_backing_tracks_add_filename.sql
user_syncs_include_backing_tracks.sql
remove_bpm_from_jamtracks.sql

2
db/up/backing_tracks.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN backing_track_path VARCHAR(1024);
ALTER TABLE active_music_sessions ADD COLUMN backing_track_initiator_id VARCHAR(64);

View File

@ -0,0 +1,89 @@
DROP FUNCTION IF EXISTS discard_scores();
CREATE FUNCTION discard_scores (keep INTEGER) RETURNS VOID AS $$
BEGIN
DELETE FROM scores WHERE score_dt <
(SELECT score_dt FROM scores s WHERE s.alocidispid = scores.alocidispid AND s.blocidispid = scores.blocidispid ORDER BY score_dt DESC LIMIT 1 OFFSET (keep - 1));
RETURN;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION update_current_network_scores(aloc BIGINT, bloc BIGINT) RETURNS VOID
STRICT VOLATILE AS $$
DECLARE
newscore INTEGER;
newscore_dt TIMESTAMP;
newscore_limited BOOL;
sum INTEGER;
kount INTEGER;
r RECORD;
avgscore INTEGER;
maxscore INTEGER;
minscore INTEGER;
BEGIN
-- find the 6 most recent scores
-- (supposedly newscore is the first...)
-- hybrid scheme: compute the average of some recent scores, then limit newscore to be between 4/5 and 6/5 of the average
newscore := NULL;
newscore_dt := NULL;
newscore_limited := FALSE;
sum := 0;
kount := 0;
FOR r IN SELECT score, score_dt FROM scores WHERE alocidispid = aloc AND blocidispid = bloc ORDER BY score_dt DESC LIMIT 6 LOOP
IF newscore IS NULL THEN
newscore := r.score;
newscore_dt := r.score_dt;
ELSE
sum := sum + r.score;
kount := kount + 1;
END IF;
END LOOP;
-- if no scores in query at all, then delete any current entry
IF newscore IS NULL THEN
DELETE FROM current_network_scores WHERE alocidispid = aloc AND blocidispid = bloc;
IF aloc != bloc THEN
DELETE FROM current_network_scores WHERE alocidispid = bloc AND blocidispid = aloc;
END IF;
END IF;
-- if there are scores older than newscore, then use their average to limit the range of newscore
IF kount > 0 THEN
avgscore := sum / kount;
maxscore := avgscore*6/5;
minscore := avgscore*4/5;
-- the score newscore will be inserted as the current value in current_network_scores, but we will limit it
-- to be no greater than 120% of the average and no less than 80% of the average. this will dampen wild
-- swings in the scores.
IF newscore > maxscore THEN
newscore := maxscore;
newscore_limited := TRUE;
ELSEIF newscore < minscore THEN
newscore := minscore;
newscore_limited := TRUE;
END IF;
END IF;
UPDATE current_network_scores SET score = newscore, limited = newscore_limited, score_dt = newscore_dt WHERE alocidispid = aloc AND blocidispid = bloc;
IF NOT FOUND THEN
INSERT INTO current_network_scores (alocidispid, blocidispid, score, limited, score_dt) VALUES (aloc, bloc, newscore, newscore_limited, newscore_dt);
END IF;
IF aloc != bloc THEN
UPDATE current_network_scores SET score = newscore, limited = newscore_limited, score_dt = newscore_dt WHERE alocidispid = bloc AND blocidispid = aloc;
IF NOT FOUND THEN
INSERT INTO current_network_scores (alocidispid, blocidispid, score, limited, score_dt) VALUES (bloc, aloc, newscore, newscore_limited, newscore_dt);
END IF;
END IF;
-- keep the scores table clean, meaning only up to the most 5 recent scores per group & direction (scorer)
DELETE FROM scores WHERE alocidispid = aloc AND blocidispid = bloc AND scorer = 0 AND score_dt <
(SELECT score_dt FROM scores s WHERE s.alocidispid = aloc AND s.blocidispid = bloc AND s.scorer = 0 ORDER BY score_dt DESC LIMIT 1 OFFSET 4);
DELETE FROM scores WHERE alocidispid = bloc AND blocidispid = aloc AND scorer = 1 AND score_dt <
(SELECT score_dt FROM scores s WHERE s.alocidispid = bloc AND s.blocidispid = aloc AND s.scorer = 1 ORDER BY score_dt DESC LIMIT 1 OFFSET 4);
END;
$$ LANGUAGE plpgsql;

2
db/up/metronome.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN metronome_active BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE active_music_sessions ADD COLUMN metronome_initiator_id VARCHAR(64);

View File

@ -0,0 +1,10 @@
CREATE TABLE performance_samples (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
url VARCHAR(4000) NULL,
type VARCHAR(100) NOT NULL,
claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id) ON DELETE CASCADE,
service_id VARCHAR(100) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,38 @@
CREATE UNLOGGED TABLE backing_tracks (
id VARCHAR(64) NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
filename VARCHAR(1024) NOT NULL,
connection_id VARCHAR(64) NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
client_track_id VARCHAR(64) NOT NULL,
client_resource_id VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE recorded_backing_tracks (
id BIGINT PRIMARY KEY,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
backing_track_id VARCHAR(64),
recording_id VARCHAR(64) NOT NULL,
client_track_id VARCHAR(64) NOT NULL,
is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE,
next_part_to_upload INTEGER NOT NULL DEFAULT 0,
upload_id CHARACTER VARYING(1024),
part_failures INTEGER NOT NULL DEFAULT 0,
discard BOOLEAN,
download_count INTEGER NOT NULL DEFAULT 0,
md5 CHARACTER VARYING(100),
length BIGINT,
client_id VARCHAR(64) NOT NULL,
file_offset BIGINT,
url VARCHAR(1024) NOT NULL,
fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE,
upload_failures INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE recorded_backing_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');

View File

@ -0,0 +1,2 @@
ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL;
ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE;

View File

@ -0,0 +1 @@
ALTER TABLE jam_tracks DROP COLUMN bpm;

View File

@ -0,0 +1,25 @@
ALTER TABLE users ADD COLUMN website varchar(4000) NULL;
ALTER TABLE users ADD COLUMN skill_level smallint NULL;
ALTER TABLE users ADD COLUMN concert_count smallint NULL;
ALTER TABLE users ADD COLUMN studio_session_count smallint NULL;
-- virtual band
ALTER TABLE users ADD COLUMN virtual_band boolean NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN virtual_band_commitment smallint NULL;
-- traditional band
ALTER TABLE users ADD COLUMN traditional_band boolean NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN traditional_band_commitment smallint NULL;
ALTER TABLE users ADD COLUMN traditional_band_touring boolean NULL;
-- paid sessions
ALTER TABLE users ADD COLUMN paid_sessions boolean NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN paid_sessions_hourly_rate int NULL;
ALTER TABLE users ADD COLUMN paid_sessions_daily_rate int NULL;
-- free sessions
ALTER TABLE users ADD COLUMN free_sessions boolean NOT NULL DEFAULT FALSE;
-- cowriting
ALTER TABLE users ADD COLUMN cowriting boolean NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN cowriting_purpose smallint NULL;

8
db/up/user_presences.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE user_presences (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,47 @@
DROP VIEW user_syncs;
CREATE VIEW user_syncs AS
SELECT DISTINCT b.id AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
b.id AS unified_id,
a.user_id AS user_id,
b.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id
UNION ALL
SELECT CAST(NULL AS BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
a.id AS recorded_backing_track_id,
a.id AS unified_id,
a.user_id AS user_id,
a.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
mixes.id AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
mixes.id AS unified_id,
claimed_recordings.user_id AS user_id,
NULL as fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
quick_mixes.id AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
quick_mixes.id AS unified_id,
quick_mixes.user_id,
quick_mixes.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE;

View File

@ -50,6 +50,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
end
it "is possible for #{user3} to sign in and not get disconnected within 30 seconds" do
pending "continual failures - need to debug - try using Selenium instead of PhantomJS"
as_monitor(user3) do
sign_in_poltergeist(user3)
repeat_for(30.seconds) do

View File

@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[])
users.each do |user|
in_client(user) do
users.reject {|u| u==user}.each do |other|
find('div.track-label', text: other.name)
find('div.track-label > span', text: other.name)
#puts user.name + " is able to see " + other.name + "\'s track"
end
end

View File

@ -28,7 +28,7 @@ gem 'amqp', '1.0.2'
gem 'will_paginate'
gem 'actionmailer', '3.2.13'
gem 'sendgrid', '1.2.0'
gem 'aws-sdk' #, '1.29.1'
gem 'aws-sdk', '~> 1'
gem 'carrierwave', '0.9.0'
gem 'aasm', '3.0.16'
gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError)

View File

@ -42,6 +42,7 @@ require "jam_ruby/resque/resque_hooks"
require "jam_ruby/resque/audiomixer"
require "jam_ruby/resque/quick_mixer"
require "jam_ruby/resque/icecast_config_writer"
require "jam_ruby/resque/stress_job"
require "jam_ruby/resque/scheduled/audiomixer_retry"
require "jam_ruby/resque/scheduled/icecast_config_retry"
require "jam_ruby/resque/scheduled/icecast_source_check"
@ -86,6 +87,7 @@ require "jam_ruby/lib/stats.rb"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"
require "jam_ruby/models/backing_track"
require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer"
#require "jam_ruby/models/max_mind_geo"
@ -131,8 +133,11 @@ require "jam_ruby/models/search"
require "jam_ruby/models/recording"
require "jam_ruby/models/recording_comment"
require "jam_ruby/models/recording_liker"
require "jam_ruby/models/recorded_backing_track"
require "jam_ruby/models/recorded_backing_track_observer"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/recorded_track_observer"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/quick_mix"
require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token"
@ -196,7 +201,6 @@ require "jam_ruby/models/score_history"
require "jam_ruby/models/jam_company"
require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/text_message"
require "jam_ruby/jam_tracks_manager"

View File

@ -11,7 +11,7 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded.
def extension_white_list
%w(ogg)
%w(ogg wav)
end
def store_dir

View File

@ -1,45 +1,64 @@
<% provide(:title, 'Welcome to JamKazam!') %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p>
<p> We're delighted that you have decided to try the JamKazam service,
and we hope that you will enjoy using JamKazam to play music with others.
Following are links to some resources that can help to get you up and running quickly.
and we hope that you will enjoy using JamKazam to play
music with others.
Following are some resources that can help you get oriented and get the most out of JamKazam.
</p>
<p>
<p><b style="color: white;">Getting Started</b><br/>
There are basically three kinds of setups you can use to play on JamKazam.<br/>
<ul>
<li><b style="color: white;">Built-In Audio on Your Computer</b> - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to
handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly,
creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.</li>
<li><b style="color: white;">Computer with External Audio Interface</b> - You can use a Windows or Mac computer with an external audio interface that you
already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer
to our <a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements">Minimum System Requirements</a>
to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=DBo--aj_P1w">Getting Started Video</a> to learn more about your options here.</li>
<li><b style="color: white;">The JamBlaster</b> - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster.
It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians
who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the
system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only
through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=gAJAIHMyois">JamBlaster Video</a> to learn more about this amazing new product.</li>
</ul>
</p>
<p><b style="color: white;">JamKazam Features</b><br/>
JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch
to easily get up to speed on some of the things you can do with JamKazam:<br/>
<ul>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=EZZuGcDUoWk">Creating a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=xWponSJo-GU">Finding a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=zJ68hA8-fLA">Playing in a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=4KWklSZZxRc">Connecting with Other Musicians</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=Gn-dOqnNLoY">Working with Recordings</a></li>
</ul>
</p>
<p><b style="color: white;">Getting Help</b><br/>
If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running.
You can visit our
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/">Support Portal</a>
to find knowledge base articles and post questions that have
not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers,
you can visit our <a style="color: #ffcc00;" href="http://forums.jamkazam.com/">Community Forum</a>
.
</p>
<p>
Getting Started Video<br/>
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=DBo--aj_P1w">https://www.youtube.com/watch?v=DBo--aj_P1w</a>
Again, welcome to JamKazam, and we look forward to seeing and hearing you online soon!
</p>
<p>
Other Great Tutorial Videos<br />
There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles">https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles</a>
</p>
<p>
Knowledge Base Articles<br />
You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles">https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles</a>
</p>
<p>
JamKazam Support Portal<br />
If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/">https://jamkazam.desk.com/</a>
</p>
<p>
JamKazam Community Forum<br />
And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here:
<a style="color: #ffcc00;" href="http://forums.jamkazam.com/">http://forums.jamkazam.com/</a>
</p>
<p>
Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing and hearing you online soon!
</p>
&nbsp;&nbsp;-- Team JamKazam
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -1,27 +1,43 @@
Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly.
We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam.
Getting Started Video
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
https://www.youtube.com/watch?v=DBo--aj_P1w
Other Great Tutorial Videos
There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles
Getting Started
---------------
Knowledge Base Articles
You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here:
https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles
There are basically three kinds of setups you can use to play on JamKazam.
JamKazam Support Portal
If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here:
https://jamkazam.desk.com
* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
JamKazam Community Forum
And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here:
http://forums.jamkazam.com
* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here.
Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing and hearing you online soon!
* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product.
JamKazam Features
-----------------
JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam:
* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk
* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU
* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA
* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc
* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY
Getting Help
------------
If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/.
Again, welcome to JamKazam, and we look forward to seeing and hearing you online soon!
Best Regards,
Team JamKazam
-- Team JamKazam

View File

@ -311,14 +311,19 @@ SQL
end
end
else
# there are still people in the session
conn.exec("UPDATE active_music_sessions set backing_track_initiator_id = NULL, backing_track_path = NULL where backing_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set metronome_initiator_id = NULL, metronome_active = FALSE where metronome_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
#ensure that there is no active claimed recording if the owner of that recording left the session
conn.exec("UPDATE active_music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
[user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
[user_id, previous_music_session_id])
end
end

View File

@ -82,6 +82,8 @@ module ValidationMessages
MUST_BE_KNOWN_TIMEZONE = "not valid"
JAM_TRACK_ALREADY_OPEN = 'another jam track already open'
RECORDING_ALREADY_IN_PROGRESS = "recording being made"
METRONOME_ALREADY_OPEN = 'another metronome already open'
BACKING_TRACK_ALREADY_OPEN = 'another audio file already open'
# notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'

View File

@ -25,6 +25,9 @@ module JamRuby
Dir.mktmpdir do |tmp_dir|
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ
# use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata
nm = jam_track_track.id + File.extname(jam_track_track.filename)
track_filename = File.join(tmp_dir, nm)

View File

@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions'
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
@ -15,6 +15,9 @@ module JamRuby
belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions
belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id"
belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id"
belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id"
has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id'
has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id'
belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id
@ -27,6 +30,8 @@ module JamRuby
validate :creator_is_musician
validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track
validate :validate_opening_backing_track, :if => :opening_backing_track
validate :validate_opening_metronome, :if => :opening_metronome
after_create :started_session
@ -73,22 +78,52 @@ module JamRuby
if is_jam_track_open?
errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
if is_backing_track_open?
errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
if is_metronome_open?
errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN)
end
end
def validate_opening_jam_track
validate_other_audio(:jam_track)
end
def validate_opening_backing_track
validate_other_audio(:backing_track)
end
def validate_opening_metronome
validate_other_audio(:metronome)
end
def validate_other_audio(error_key)
# validate that there is no metronome already open in this session
if metronome_active_was
errors.add(error_key, ValidationMessages::METRONOME_ALREADY_OPEN)
end
# validate that there is no backing track already open in this session
if backing_track_path_was.present?
errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
# validate that there is no jam track already open in this session
unless jam_track_id_was.nil?
errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
if jam_track_id_was.present?
errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
# validate that there is no recording being made
if is_recording?
errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
end
# validate that there is no recording being played back to the session
if is_playing_recording?
errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
end
end
@ -593,6 +628,14 @@ module JamRuby
!self.jam_track.nil?
end
def is_backing_track_open?
self.backing_track_path.present?
end
def is_metronome_open?
self.metronome_active.present?
end
# is this music session currently recording?
def is_recording?
recordings.where(:duration => nil).count > 0
@ -742,6 +785,35 @@ module JamRuby
self.save
end
# @param backing_track_path is a relative path:
def open_backing_track(user, backing_track_path)
self.backing_track_path = backing_track_path
self.backing_track_initiator = user
self.opening_backing_track = true
self.save
self.opening_backing_track = false
end
def close_backing_track
self.backing_track_path = nil
self.backing_track_initiator = nil
self.save
end
def open_metronome(user)
self.metronome_active = true
self.metronome_initiator = user
self.opening_metronome = true
self.save
self.opening_metronome = false
end
def close_metronome
self.metronome_active = false
self.metronome_initiator = nil
self.save
end
def self.sync(session_history)
music_session = MusicSession.find_by_id(session_history.id)

View File

@ -0,0 +1,19 @@
module JamRuby
class BackingTrack < ActiveRecord::Base
self.table_name = "backing_tracks"
self.primary_key = 'id'
default_scope order('created_at ASC')
belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id'
validates :connection, presence: true
validates :client_track_id, presence: true
validates :filename, presence: true
def user
self.connection.user
end
end
end

View File

@ -18,6 +18,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id
has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
validates :as_musician, :inclusion => {:in => [true, false, nil]}

View File

@ -20,7 +20,6 @@ module JamRuby
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :description, length: {maximum: 1000}
validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/
validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES}
validates :status, inclusion: {in: [nil] + STATUS}
validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE}

View File

@ -138,11 +138,17 @@ module JamRuby
manifest = { "files" => [], "timeline" => [] }
mix_params = []
recording.recorded_tracks.each do |recorded_track|
manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
recording.recorded_backing_tracks.each do |recorded_backing_track|
manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
manifest["output"] = { "codec" => "vorbis" }
manifest["recording_id"] = self.recording.id

View File

@ -0,0 +1,196 @@
module JamRuby
# BackingTrack analog to JamRuby::RecordedTrack
class RecordedBackingTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin
attr_accessor :marking_complete
attr_writer :current_user
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks
validates :filename, :presence => true
validates :client_id, :presence => true # not a connection relation on purpose
validates :backing_track_id, :presence => true # not a track relation on purpose
validates :client_track_id, :presence => true
validates :md5, :presence => true, :if => :upload_starting?
validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely...
validates :user, presence: true
validates :download_count, presence: true
before_destroy :delete_s3_files
validate :validate_fully_uploaded
validate :validate_part_complete
validate :validate_too_many_upload_failures
validate :verify_download_count
def self.create_from_backing_track(backing_track, recording)
recorded_backing_track = self.new
recorded_backing_track.recording = recording
recorded_backing_track.client_id = backing_track.connection.client_id
recorded_backing_track.backing_track_id = backing_track.id
recorded_backing_track.client_track_id = "R" + backing_track.client_track_id # Matches behavior in RecordingManager.cpp#getWavComment
recorded_backing_track.user = backing_track.connection.user
recorded_backing_track.filename = backing_track.filename
recorded_backing_track.next_part_to_upload = 0
recorded_backing_track.file_offset = 0
recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id)
recorded_backing_track.save
recorded_backing_track
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def can_download?(some_user)
claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user }
if claimed_recording
!claimed_recording.discarded
else
false
end
end
def too_many_upload_failures?
upload_failures >= APP_CONFIG.max_track_upload_failures
end
def too_many_downloads?
(self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
end
def upload_starting?
next_part_to_upload_was == 0 && next_part_to_upload == 1
end
def validate_too_many_upload_failures
if upload_failures >= APP_CONFIG.max_track_upload_failures
errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED)
end
end
def validate_fully_uploaded
if marking_complete && fully_uploaded && fully_uploaded_was
errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED)
end
end
def validate_part_complete
# if we see a transition from is_part_uploading from true to false, we validate
if is_part_uploading_was && !is_part_uploading
if next_part_to_upload_was + 1 != next_part_to_upload
errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED)
end
if file_offset > length
errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH)
end
elsif next_part_to_upload_was + 1 == next_part_to_upload
# this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start
if next_part_to_upload_was != 0
# we see that the part number was ticked--but was is_part_upload set to true before this transition?
if !is_part_uploading_was && !is_part_uploading
errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED)
end
end
end
end
def verify_download_count
if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
errors.add(:download_count, "must be less than or equal to 100")
end
end
def upload_start(length, md5)
#self.upload_id set by the observer
self.next_part_to_upload = 1
self.length = length
self.md5 = md5
save
end
# if for some reason the server thinks the client can't carry on with the upload,
# this resets everything to the initial state
def reset_upload
self.upload_failures = self.upload_failures + 1
self.part_failures = 0
self.file_offset = 0
self.next_part_to_upload = 0
self.upload_id = nil
self.md5 = nil
self.length = 0
self.fully_uploaded = false
self.is_part_uploading = false
save :validate => false # skip validation because we need this to always work
end
def upload_next_part(length, md5)
self.marking_complete = true
if next_part_to_upload == 0
upload_start(length, md5)
end
self.is_part_uploading = true
save
end
def upload_sign(content_md5)
s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id)
end
def upload_part_complete(part, offset)
# validated by :validate_part_complete
self.marking_complete = true
self.is_part_uploading = false
self.next_part_to_upload = self.next_part_to_upload + 1
self.file_offset = offset.to_i
self.part_failures = 0
save
end
def upload_complete
# validate from happening twice by :validate_fully_uploaded
self.fully_uploaded = true
self.marking_complete = true
save
end
def increment_part_failures(part_failure_before_error)
self.part_failures = part_failure_before_error + 1
RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'")
end
def stored_filename
# construct a path from s3
RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id)
end
def update_download_count(count=1)
self.download_count = self.download_count + count
self.last_downloaded_at = Time.now
end
def delete_s3_files
s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url])
end
def mark_silent
destroy
# check if we have all the files we need, now that the recorded_backing_track is out of the way
recording.preconditions_for_mix?
end
private
def self.construct_filename(created_at, recording_id, client_track_id)
raise "unknown ID" unless client_track_id
"recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg"
end
end
end

View File

@ -0,0 +1,91 @@
module JamRuby
class RecordedBackingTrackObserver < ActiveRecord::Observer
# if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here
observe JamRuby::RecordedBackingTrack
def before_validation(recorded_backing_tracks)
# if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded
if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading
begin
aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1)
# calling size on a part that does not exist will throw an exception... that's what we want
aws_part.size
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later on 500.
rescue Exception => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue RuntimeError => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
end
end
# if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together
if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded
multipart_success = false
begin
recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id)
multipart_success = true
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later.
rescue Exception => e
#recorded_track.reload
recorded_backing_tracks.reset_upload
recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD)
end
# unlike RecordedTracks, only the person who uploaded can download it, so no need to notify
# tell all users that a download is available, except for the user who just uploaded
# recorded_backing_tracks.recording.users.each do |user|
#Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user
# end
end
end
def after_commit(recorded_backing_track)
end
# here we tick upload failure counts, or revert the state of the model, as needed
def after_rollback(recorded_backing_track)
# if fully uploaded, don't increment failures
if recorded_backing_track.fully_uploaded
return
end
# increment part failures if there is a part currently being uploaded
if recorded_backing_track.is_part_uploading_was
#recorded_track.reload # we don't want anything else that the user set to get applied
recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was)
if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures
# save upload id before we abort this bad boy
upload_id = recorded_backing_track.upload_id
begin
recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id)
rescue => e
puts e.inspect
end
recorded_backing_track.reset_upload
if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures
# do anything?
end
end
end
end
def before_save(recorded_backing_track)
# if we are on the 1st part, then we need to make sure we can save the upload_id
if recorded_backing_track.next_part_to_upload == 1
recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url])
end
end
end
end

View File

@ -11,6 +11,7 @@ module JamRuby
has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy
has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
@ -179,6 +180,14 @@ module JamRuby
recorded_tracks.where(:user_id => user.id)
end
def recorded_backing_tracks_for_user(user)
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
end
recorded_backing_tracks.where(:user_id => user.id)
end
def has_access?(user)
users.exists?(user)
end
@ -209,6 +218,10 @@ module JamRuby
connection.video_sources.each do |video|
recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording)
end
connection.backing_tracks.each do |backing_track|
recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording)
end
end
end
end
@ -321,8 +334,7 @@ module JamRuby
}
)
end
latest_recorded_track = downloads[-1][:next] if downloads.length > 0
latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0
Mix.joins(:recording).joins(:recording => :claimed_recordings)
.order('mixes.id')
@ -345,16 +357,31 @@ module JamRuby
}
)
end
latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0
latest_mix = downloads[-1][:next] if downloads.length > 0
if !latest_mix.nil? && !latest_recorded_track.nil?
next_date = [latest_mix, latest_recorded_track].max
elsif latest_mix.nil?
next_date = latest_recorded_track
else
next_date = latest_mix
RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings)
.order('recorded_backing_tracks.id')
.where('recorded_backing_tracks.fully_uploaded = TRUE')
.where('recorded_backing_tracks.id > ?', since)
.where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back
.where('all_discarded = false')
.where('deleted = false')
.where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track|
downloads.push(
{
:type => "recorded_backing_track",
:id => recorded_backing_track.client_track_id,
:recording_id => recorded_backing_track.recording_id,
:length => recorded_backing_track.length,
:md5 => recorded_backing_track.md5,
:url => recorded_backing_track[:url],
:next => recorded_backing_track.id
}
)
end
latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0
next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max
if next_date.nil?
next_date = since # echo back to the client the same value they passed in, if there are no results
@ -417,6 +444,20 @@ module JamRuby
Arel::Nodes::As.new('stream_mix', Arel.sql('item_type'))
]).reorder("")
# Select fields for quick mix. Note that it must include
# the same number of fields as the track or video in order for
# the union to work:
backing_track_arel = RecordedBackingTrack.select([
:id,
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
Arel::Nodes::As.new('backing_track', Arel.sql('item_type'))
]).reorder("")
# Glue them together:
union = track_arel.union(vid_arel)
@ -439,7 +480,25 @@ module JamRuby
])
# And repeat:
union_all = arel.union(quick_mix_arel)
union_quick = arel.union(quick_mix_arel)
utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick)
arel = arel.from(utable_quick)
arel = arel.except(:select)
arel = arel.select([
"recorded_items_quick.id",
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
# And repeat for backing track:
union_all = arel.union(backing_track_arel)
utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all)
arel = arel.from(utable_all)
@ -455,7 +514,6 @@ module JamRuby
:item_type
])
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
@ -492,6 +550,13 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
elsif recorded_item.item_type == 'backing_track'
uploads << ({
:type => "recorded_backing_track",
:recording_id => recorded_item.recording_id,
:client_track_id => recorded_item.client_track_id,
:next => recorded_item.id
})
else
end
@ -513,6 +578,11 @@ module JamRuby
recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
recorded_backing_tracks.each do |recorded_backing_track|
return false unless recorded_backing_track.fully_uploaded
end
true
end

View File

@ -55,11 +55,64 @@ module JamRuby
return query
end
def self.diff_track(track_class, existing_tracks, new_tracks, &blk)
result = []
if new_tracks.length == 0
existing_tracks.delete_all
else
# we will prune from this as we find matching tracks
to_delete = Set.new(existing_tracks)
to_add = Array.new(new_tracks)
existing_tracks.each do |existing_track|
new_tracks.each do |new_track|
if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id
to_delete.delete(existing_track)
to_add.delete(new_track)
blk.call(existing_track, new_track)
result.push(existing_track)
if existing_track.save
next
else
result = existing_track
raise ActiveRecord::Rollback
end
end
end
end
to_add.each do |new_track|
existing_track = track_class.new
blk.call(existing_track, new_track)
if existing_track.save
result.push(existing_track)
else
result = existing_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
end
result
end
# this is a bit different from a normal track synchronization in that the client just sends up all tracks,
# ... some may already exist
def self.sync(clientId, tracks)
result = []
def self.sync(clientId, tracks, backing_tracks = [])
result = {}
backing_tracks = [] unless backing_tracks
Track.transaction do
connection = Connection.find_by_client_id!(clientId)
@ -68,67 +121,28 @@ module JamRuby
msh = MusicSessionUserHistory.find_by_client_id!(clientId)
instruments = []
if tracks.length == 0
connection.tracks.delete_all
else
connection_tracks = connection.tracks
tracks.each do |track|
instruments << track[:instrument_id]
end
# we will prune from this as we find matching tracks
to_delete = Set.new(connection_tracks)
to_add = Array.new(tracks)
result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.instrument_id = track_info[:instrument_id]
track_record.sound = track_info[:sound]
end
tracks.each do |track|
instruments << track[:instrument_id]
end
result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.filename = track_info[:filename]
end
connection_tracks.each do |connection_track|
tracks.each do |track|
if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id
to_delete.delete(connection_track)
to_add.delete(track)
# don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
result.push(connection_track)
if connection_track.save
next
else
result = connection_track
raise ActiveRecord::Rollback
end
end
end
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
to_add.each do |track|
connection_track = Track.new
connection_track.connection = connection
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
if connection_track.save
result.push(connection_track)
else
result = connection_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
end

View File

@ -122,6 +122,7 @@ module JamRuby
# saved tracks
has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user
has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user
has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user
# invited users
@ -342,6 +343,11 @@ module JamRuby
self.recordings.size
end
def age
now = Time.now.utc.to_date
self.birth_date.nil? ? "unspecified" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0)
end
def session_count
MusicSession.where("user_id = ? AND started_at IS NOT NULL", self.id).size
end

View File

@ -4,6 +4,7 @@ module JamRuby
belongs_to :recorded_track
belongs_to :mix
belongs_to :quick_mix
belongs_to :recorded_backing_track
def self.show(id, user_id)
self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first
@ -22,7 +23,7 @@ module JamRuby
raise 'no user id specified' if user_id.blank?
query = UserSync
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[])
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[])
.joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id")
.where(user_id: user_id)
.where(%Q{

View File

@ -35,7 +35,6 @@ Resque.before_first_fork do
end
JamRuby::Stats.init(config)
end
# https://devcenter.heroku.com/articles/forked-pg-connections
Resque.before_fork do

View File

@ -0,0 +1,29 @@
require 'resque'
module JamRuby
# this job exists as a way to manually test a bunch of jobs firing at once. It's not a real job.
class StressJob
extend JamRuby::ResqueStats
@queue = :stress_job
@@log = Logging.logger[StressJob]
def self.perform
@@log.debug("STARTING")
100.times do
user = User.first.id
diagnostic = Diagnostic.first.user_id
count = Diagnostic.all.count
end
@@log.debug("ENDING")
end
end
end

View File

@ -232,6 +232,11 @@ FactoryGirl.define do
sequence(:client_resource_id) { |n| "resource_id#{n}"}
end
factory :backing_track, :class => JamRuby::BackingTrack do
sequence(:client_track_id) { |n| "client_track_id#{n}"}
filename 'foo.mp3'
end
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
@ -250,6 +255,20 @@ FactoryGirl.define do
association :recording, factory: :recording
end
factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do
sequence(:client_id) { |n| "client_id-#{n}"}
sequence(:backing_track_id) { |n| "track_id-#{n}"}
sequence(:client_track_id) { |n| "client_track_id-#{n}"}
sequence(:filename) { |n| "filename-{#n}"}
sequence(:url) { |n| "/recordings/blah/#{n}"}
md5 'abc'
length 1
fully_uploaded true
association :user, factory: :user
association :recording, factory: :recording
end
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}
fully_uploaded true
@ -700,7 +719,6 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
bpm 100.1
time_signature '4/4'
status 'Production'
recording_type 'Cover'

View File

@ -745,6 +745,29 @@ describe ActiveMusicSession do
@music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when backing track is open" do
# open the backing track
@backing_track = "foo.mp3"
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when metronome is open" do
# open the metronome
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
end
end
@ -830,5 +853,143 @@ describe ActiveMusicSession do
music_sessions[0].connections[0].tracks.should have(1).items
end
end
describe "open_backing_track" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
@backing_track = "foo/bar.mp3"
end
it "allow a backing track to be associated" do
# simple success case; just open the backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should == @backing_track
@music_session.backing_track_initiator.should == @user1
end
it "allow a backing track to be closed" do
# simple success case; close an opened backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.close_backing_track
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should be_nil
@music_session.backing_track_initiator.should be_nil
end
it "disallow a backing track to be opened when another is already opened" do
# if a backing track is open, don't allow another to be opened
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a backing track to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a backing track to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
describe "open_metronome" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
end
it "allow a metronome to be activated" do
# simple success case; just open the metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should == true
@music_session.metronome_initiator.should == @user1
end
it "allow a metronome to be closed" do
# simple success case; close an opened metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.close_metronome
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should be_false
@music_session.metronome_initiator.should be_nil
end
it "disallow a metronome to be opened when another is already opened" do
# if a metronome is open, don't allow another to be opened
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
it "disallow a metronome to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a metronome to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
end

View File

@ -55,25 +55,6 @@ describe JamTrack do
end
describe "validations" do
describe "bpm" do
it "1" do
FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true
end
it "100" do
FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true
end
it "100.1" do
FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true
end
it "100.12" do
jam_track = FactoryGirl.build(:jam_track, bpm: 100.12)
jam_track.valid?.should be_false
jam_track.errors[:bpm].should == ['is invalid']
end
end
describe "price" do

View File

@ -0,0 +1,228 @@
require 'spec_helper'
require 'rest-client'
describe RecordedBackingTrack do
include UsesTempFiles
before do
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
end
it "should copy from a regular track properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.user.id.should == @backing_track.connection.user.id
@recorded_backing_track.filename.should == @backing_track.filename
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.fully_uploaded.should == false
@recorded_backing_track.client_id = @connection.client_id
@recorded_backing_track.backing_track_id = @backing_track.id
end
it "should update the next part to upload properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)"
@recorded_backing_track.errors[:md5][0].should == "can't be blank"
end
it "properly finds a recorded track given its upload filename" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.save.should be_true
RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track
end
it "gets a url for the track" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg"
end
it "signs url" do
stub_const("APP_CONFIG", app_config)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.sign_url.should_not be_nil
end
it "can not be downloaded if no claimed recording" do
user2 = FactoryGirl.create(:user)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.can_download?(user2).should be_false
@recorded_backing_track.can_download?(@user).should be_false
end
it "can be downloaded if there is a claimed recording" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false
@recorded_backing_track.can_download?(@user).should be_true
end
describe "aws-based operations", :aws => true do
def put_file_to_aws(signed_data, contents)
begin
RestClient.put( signed_data[:url],
contents,
{
:'Content-Type' => 'audio/ogg',
:Date => signed_data[:datetime],
:'Content-MD5' => signed_data[:md5],
:Authorization => signed_data[:authorization]
})
rescue => e
puts e.response
raise e
end
end
# create a test file
upload_file='some_file.ogg'
in_directory_with_file(upload_file)
upload_file_contents="ogg binary stuff in here"
md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp
test_config = app_config
s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
before do
stub_const("APP_CONFIG", app_config)
# this block of code will fully upload a sample file to s3
content_for_file(upload_file_contents)
s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging
end
it "cant mark a part complete without having started it" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED
end
it "no parts" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_next_part(1000, "abc")
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS
end
it "enough part failures reset the upload" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = !part_failure_rollover
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == 1
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
end
it "enough upload failures fails the upload forever" do
APP_CONFIG.stub(:max_track_upload_failures).and_return(1)
APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
APP_CONFIG.max_track_upload_failures.times do |j|
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = part_failure_rollover ? false : true
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.upload_failures.should == j + 1
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
# try to poke it and get the right kind of error back
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED]
end
describe "correctly uploaded a file" do
before do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
signed_data = @recorded_backing_track.upload_sign(md5)
@response = put_file_to_aws(signed_data, upload_file_contents)
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.marking_complete = false
end
it "can download an updated file" do
@response = RestClient.get @recorded_backing_track.sign_url
@response.body.should == upload_file_contents
end
it "can't mark completely uploaded twice" do
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for a next part if fully uploaded" do
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for mark part complete if fully uploaded" do
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
end
end
end

View File

@ -211,6 +211,20 @@ describe Recording do
user1_recorded_tracks[0].discard = true
user1_recorded_tracks[0].save!
end
it "should allow finding of backing tracks" do
user2 = FactoryGirl.create(:user)
connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session)
track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument)
backing_track = FactoryGirl.create(:backing_track, :connection => connection2)
@recording = Recording.start(@music_session, @user)
@recording.recorded_backing_tracks_for_user(@user).length.should eq(0)
user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2)
user2_recorded_tracks.length.should == 1
user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0]
end
it "should set up the recording properly when recording is started with 1 user in the session" do
@music_session.is_recording?.should be_false
@ -547,6 +561,8 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
# We should have 2 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(3).items

View File

@ -522,29 +522,30 @@ describe Score do
it "works" do
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.count.should == 2
Score.connection.execute("SELECT discard_scores()").check
Score.connection.execute("SELECT discard_scores(5)").check
Score.count.should == 2
end
it "discards over 5 items" do
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 6.days.ago)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 5.days.ago)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 4.days.ago)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 3.days.ago)
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 2.days.ago)
Score.count.should == 12
Score.connection.execute("SELECT discard_scores()").check
Score.count.should == 12
Score.count.should == 10
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 26, nil)
Score.connection.execute("UPDATE scores set created_at = TIMESTAMP '#{2.days.ago}' WHERE score = 26").cmdtuples.should == 2
Score.connection.execute("SELECT discard_scores()").check
Score.count.should == 12
Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 12
Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 6
Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 6
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 1.days.ago)
Score.count.should == 10
# make a score older than all the rest; it should get whacked
Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 26, 7.days.ago)
Score.count.should == 10
Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 5
Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 5
Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil)
@ -554,18 +555,35 @@ describe Score do
Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil)
Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil)
Score.count.should == 24
Score.connection.execute("SELECT discard_scores()").check
Score.count.should == 24
Score.count.should == 20
Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 36, nil)
Score.connection.execute("UPDATE scores set created_at = TIMESTAMP '#{2.days.ago}' WHERE score = 36").cmdtuples.should == 2
Score.connection.execute("SELECT discard_scores()").check
Score.count.should == 24
Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 12
Score.connection.execute("SELECT * FROM scores WHERE score = 22 AND scorer = 0").ntuples.should == 6
Score.connection.execute("SELECT * FROM scores WHERE score = 22 AND scorer = 1").ntuples.should == 6
Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 10
Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 36, 7.days.ago)
Score.count.should == 20
Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 10
# let's create scores between a new location, and make sure they don't distrurb the data we have now
Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil)
Score.count.should == 22
Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil)
Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil)
Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil)
Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil)
Score.count.should == 30
Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10
Score.connection.execute("SELECT * FROM scores WHERE score = 10").ntuples.should == 10
end
end
end

View File

@ -7,8 +7,10 @@ describe Track do
let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) }
let (:track) { FactoryGirl.create(:track, :connection => connection)}
let (:track2) { FactoryGirl.create(:track, :connection => connection)}
let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)}
let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) }
let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} }
before(:each) do
msuh.touch
@ -16,7 +18,8 @@ describe Track do
describe "sync" do
it "create one track" do
tracks = Track.sync(connection.client_id, [track_hash])
result = Track.sync(connection.client_id, [track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -25,7 +28,8 @@ describe Track do
end
it "create two tracks" do
tracks = Track.sync(connection.client_id, [track_hash, track_hash])
result = Track.sync(connection.client_id, [track_hash, track_hash])
tracks = result[:tracks]
tracks.length.should == 2
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -40,7 +44,8 @@ describe Track do
it "delete only track" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [])
result = Track.sync(connection.client_id, [])
tracks = result[:tracks]
tracks.length.should == 0
end
@ -49,7 +54,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -62,7 +68,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -75,7 +82,8 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -87,7 +95,8 @@ describe Track do
it "updates a single track using .client_track_id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -99,11 +108,69 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
end
describe "backing tracks" do
it "create one track and one backing track" do
result = Track.sync(connection.client_id, [track_hash], [backing_track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
track = backing_tracks[0]
track.client_track_id.should == backing_track_hash[:client_track_id]
end
it "delete only backing_track" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
connection.backing_tracks.length.should == 1
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 0
end
it "does not touch updated_at when nothing changes" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
set_updated_at(backing_track, 1.days.ago)
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
found = backing_tracks[0]
expect(found.id).to eq backing_track.id
expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i
end
end
end
end

View File

@ -25,6 +25,22 @@ describe User do
should respond_to(:can_invite)
should respond_to(:mods)
should respond_to(:last_jam_audio_latency)
should respond_to(:website)
should respond_to(:age)
should respond_to(:skill_level)
should respond_to(:concert_count)
should respond_to(:studio_session_count)
should respond_to(:virtual_band)
should respond_to(:virtual_band_commitment)
should respond_to(:traditional_band)
should respond_to(:traditional_band_commitment)
should respond_to(:traditional_band_touring)
should respond_to(:paid_sessions)
should respond_to(:paid_sessions_hourly_rate)
should respond_to(:paid_sessions_daily_rate)
should respond_to(:free_sessions)
should respond_to(:cowriting)
should respond_to(:cowriting_purpose)
should be_valid
should_not be_admin
}
@ -649,6 +665,18 @@ describe User do
end
end
describe "age" do
let(:user) {FactoryGirl.create(:user)}
it "should calculate age based on birth_date" do
user.birth_date = Time.now - 10.years
user.age.should == 10
user.birth_date = Time.now - 10.years + 3.months
user.age.should == 9
end
end
describe "mods_merge" do
let(:user) {FactoryGirl.create(:user)}

View File

@ -20,6 +20,49 @@ describe UserSync do
data[:next].should be_nil
end
describe "backing_tracks" do
let!(:recording1) {
recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false)
recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.save!
recording.reload
recording
}
let(:sorted_tracks) {
Array.new(recording1.recorded_tracks).sort! {|a, b|
if a.created_at == b.created_at
a.id <=> b.id
else
a.created_at <=> b.created_at
end
}
}
# backing tracks should only list download, or upload, for the person who opened it, for legal reasons
it "lists backing track for opener" do
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(3)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0]
end
it "does not list backing track for non-opener" do
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(2)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
end
it "one mix and quick mix" do
mix = FactoryGirl.create(:mix)
mix.recording.duration = 1

View File

@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance
ActiveRecord::Base.add_observer UserObserver.instance
ActiveRecord::Base.add_observer FeedbackObserver.instance
ActiveRecord::Base.add_observer RecordedTrackObserver.instance
ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance
ActiveRecord::Base.add_observer QuickMixObserver.instance
#RecordedTrack.observers.disable :all # only a few tests want this observer active

View File

@ -51,7 +51,7 @@ gem 'twitter'
gem 'fb_graph', '2.5.9'
gem 'sendgrid', '1.2.0'
gem 'filepicker-rails', '0.1.0'
gem 'aws-sdk' #, '1.29.1'
gem 'aws-sdk', '~> 1'
gem 'aasm', '3.0.16'
gem 'carrierwave', '0.9.0'
gem 'carrierwave_direct'
@ -75,7 +75,7 @@ gem 'netaddr'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
gem 'rest_client'
gem 'rest-client'
gem 'iso-639'
gem 'language_list'
gem 'rubyzip'

View File

@ -1,6 +1,3 @@
TODO:
====
Jasmine Javascript Unit Tests
=============================

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@ -355,7 +355,7 @@
context._.each(tracks.tracks, function(track) {
if(!track.instrument_id) {
logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument.");
context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.');
context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.');
return false;
}
});

View File

@ -112,6 +112,10 @@
// tell the server we are about to start a recording
rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id})
.done(function(response) {
// update session info
context.JK.CurrentSessionModel.updateSession(response);
var recordingId = $(this).attr('data-recording-id');
var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording);

View File

@ -0,0 +1,147 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.OpenBackingTrackDialog = function(app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var showing = false;
var perPage = 10;
var $dialog = null;
var $tbody = null;
var $paginatorHolder = null;
var $templateOpenBackingTrackRow = null;
var $downloadedTrackHelp = null;
var $whatAreBackingTracks = null;
var $displayAudioFileFolder = null;
function emptyList() {
$tbody.empty();
}
function resetPagination() {
$dialog.find('.paginator').remove();
}
function beforeShow() {
emptyList();
resetPagination();
showing = true;
getBackingTracks();
$dialog.data('result', null);
// .done(function(data, textStatus, jqXHR) {
// // initialize pagination
// var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected)
// $paginatorHolder.append($paginator);
// });
}
function afterHide() {
showing = false;
}
function onPageSelected(targetPage) {
return getBackingTracks(targetPage);
}
function getBackingTracks(page) {
var result = context.jamClient.getBackingTrackList();
console.log("result", result)
var backingTracks = result.backing_tracks;
if (!backingTracks || backingTracks.length == 0) {
$tbody.append("<tr><td colspan='100%'>No Tracks found</td></tr>");
} else {
$.each(backingTracks, function(index, backingTrack) {
var extension = backingTrack.name
var options = {
backingTrackState: null,
name: backingTrack.name,
type: getExtension(backingTrack.name),
length: displaySize(backingTrack.size)
}
var $tr = $(context._.template($templateOpenBackingTrackRow.html(), options, { variable: 'data' }));
$tr.data('server-model', backingTrack);
$tbody.append($tr);
});
}//end
}
// from http://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript
function getExtension(filename) {
return filename.substr((~-filename.lastIndexOf(".") >>> 0) + 2)
}
// from seth:
function displaySize(length) {
var size = (length==null || typeof(length)=='undefined') ? 0 : Number(length)
return (Math.round(size * 10 / (1024 * 1024) ) / 10).toString() + "M"
}
function registerStaticEvents() {
$tbody.on('click', 'tr', function(e) {
var backingTrack = $(this).data('server-model');
// tell the server we are about to open a backing track:
rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name})
.done(function(response) {
var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false);
console.log("BackingTrackPlay response: %o", result);
// TODO: Possibly actually check the result. Investigate
// what real client returns:
// // if(result) {
// let callers see which backing track was chosen
$dialog.data('result', backingTrack);
app.layout.closeDialog('open-backing-track-dialog');
// }
// else {
// logger.error("unable to open backing track")
// }
context.JK.CurrentSessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback");
})
return false;
})
context.JK.helpBubble($whatAreBackingTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog})
$whatAreBackingTracks.on('click', false) // no help yet
$displayAudioFileFolder.on('click', function(e) {
e.stopPropagation();
context.jamClient.OpenBackingTracksDirectory();
})
}
function initialize(){
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('open-backing-track-dialog', dialogBindings);
$dialog = $('#open-backing-track-dialog');
$tbody = $dialog.find('table.open-backing-tracks tbody');
$paginatorHolder = $dialog.find('.paginator-holder');
$templateOpenBackingTrackRow = $('#template-backing-track-row')
$whatAreBackingTracks = $dialog.find('.what-are-backingtracks')
$displayAudioFileFolder = $dialog.find('.display-backingtracks-folder')
registerStaticEvents();
};
this.initialize = initialize;
this.isShowing = function isShowing() { return showing; }
}
return this;
})(window,jQuery);

View File

@ -67,7 +67,7 @@
else {
// load recording
var openRecordingResult = context.jamClient.OpenRecording(recording);
var openRecordingResult = context.jamClient.PreviewRecording(recording);
logger.debug("OpenRecording response: %o", openRecordingResult);
@ -78,6 +78,23 @@
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
// hunt for missing backing tracks; if so, mark them as silent
context._.each(openRecordingResult.backing_tracks, function(backingTrack) {
if(backingTrack.local_state == "MISSING") {
// mark this as deleted
logger.debug("marking recorded track as deleted")
rest.markRecordedBackingTrackSilent({recording_id: openRecordingResult.recording_id, backing_track_id: backingTrack.client_track_id})
.fail(function() {
app.notify({
"title": "Unable to Mark Backing Track",
"text": "A backing track was never played, but we could not tell the server to remove it from the recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}
})
}
playbackControls.startMonitor();
}
@ -88,7 +105,7 @@
function afterHide() {
recording = null;
playbackControls.stopMonitor();
context.jamClient.CloseRecording();
context.jamClient.ClosePreviewRecording();
}
function discardRecording(e) {

View File

@ -28,6 +28,15 @@
return false;
}
if($fader.data('showHelpAboutMediaMixers')) {
if(window.JK.CurrentSessionModel) {
if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) {
window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')})
window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp()
}
}
}
draggingOrientation = $fader.attr('orientation');
var offset = $fader.offset();
var position = { top: e.pageY - offset.top, left: e.pageX - offset.left}
@ -137,6 +146,16 @@
}
function onFaderDragStop(e, ui) {
if($draggingFader.data('showHelpAboutMediaMixers')) {
if(window.JK.CurrentSessionModel) {
if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) {
window.JK.prodBubble($draggingFader, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $draggingFader.closest('.screen')})
window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp()
}
}
}
var faderPct = faderValue($draggingFader, e, ui.position);
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
@ -179,7 +198,10 @@
selector.html(g._.template(templateSource, options));
selector.find('div[control="fader"]').data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener'))
selector.find('div[control="fader"]')
.data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
selector.find('div[control="fader-handle"]').draggable({
drag: onFaderDrag,
@ -187,7 +209,9 @@
stop: onFaderDragStop,
containment: "parent",
axis: options.faderType === 'horizontal' ? 'x' : 'y'
}).data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener'))
}).data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
// Embed any custom styles, applied to the .fader below selector
if ("style" in options) {

View File

@ -21,6 +21,12 @@
var frameSize = 2.5;
var fakeJamClientRecordings = null;
var p2pCallbacks = null;
var metronomeActive=false;
var metronomeBPM=false;
var metronomeSound=false;
var metronomeMeter=0;
var backingTrackPath="";
var backingTrackLoop=false;
function dbg(msg) { logger.debug('FakeJamClient: ' + msg); }
@ -398,21 +404,42 @@
}
function SessionGetControlState(mixerIds, isMasterOrPersonal) {
dbg("SessionGetControlState");
var groups = [0, 1, 2, 3, 7, 9];
var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12];
var names = [
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"",
""
"",
"",
"",
"",
""
];
var media_types = [
"Master",
"Monitor",
"AudioInputMusic",
"AudioInputChat",
"StreamOutMusic",
"UserMusicInput",
"PeerAudioInputMusic",
"PeerMediaTrack",
"JamTrack",
"MetronomeTrack"
]
var clientIds = [
"",
"",
"",
"",
"3933ebec-913b-43ab-a4d3-f21dc5f8955b",
"",
"",
"",
"",
""
];
var response = [];
@ -422,6 +449,7 @@
group_id: groups[i],
id: mixerIds[i] + (isMasterOrPersonal ? 'm' : 'p'),
master: isMasterOrPersonal,
media_type: media_types[i],
monitor: !isMasterOrPersonal,
mute: false,
name: names[i],
@ -696,6 +724,55 @@
function GetScoreWorkTimingInterval() { return {interval: 1000, backoff:60000} }
function SetScoreWorkTimingInterval(knobs) {return true;}
function SessionOpenBackingTrackFile(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionSetBackingTrackFileLoop(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionCloseBackingTrackFile(path) {
backingTrackPath=""
}
function SessionOpenMetronome(bpm, click, meter, mode){
console.log("Setting metronome BPM: ", bpm)
metronomeActive =true
metronomeBPM = bpm
metronomeSound = click
metronomeMeter = meter
}
//change setting - click. Mode 0: = mono, 1, = left ear, 2= right ear
function SessionSetMetronome(bpm,click,meter, mode){
SessionOpenMetronome(bpm, click, meter, mode)
}
//close everywhere
function SessionCloseMetronome(){
metronomeActive=false
}
function setMetronomeOpenCallback(callback) {
}
function getMyNetworkState() {
return {
ntp_stable: Math.random() > 0.5
}
}
function getPeerState(clientId) {
return {
ntp_stable: Math.random() > 0.5
}
}
// stun
function NetworkTestResult() { return {remote_udp_blocked: false} }
@ -727,6 +804,14 @@
fire();
}
function getBackingTrackList() {
return {backing_tracks: [
{name:"This is a really long name for a song dude.mp3", size:4283},
{name:"foo.mp3",size:325783838}
]};
}
function ClientUpdateStartUpdate(path, successCallback, failureCallback) {}
// -------------------------------
@ -806,7 +891,11 @@
function OpenRecording(claimedRecording) {
return {success: true}
}
function PreviewRecording(claimedRecording) {
return OpenRecording(claimedRecording);
}
function CloseRecording() {}
function ClosePreviewRecording() {CloseRecording();}
function OnDownloadAvailable() {}
function SaveToClipboard(text) {}
function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ return false; }
@ -994,6 +1083,20 @@
this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval;
this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval;
// Backing tracks:
this.getBackingTrackList = getBackingTrackList;
this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile;
this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile;
this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop;
// Metronome:
this.SessionCloseMetronome = SessionCloseMetronome;
this.SessionOpenMetronome = SessionOpenMetronome;
this.SessionSetMetronome = SessionSetMetronome;
this.setMetronomeOpenCallback = setMetronomeOpenCallback;
this.getMyNetworkState = getMyNetworkState;
this.getPeerState = getPeerState;
// Client Update
this.IsAppInWritableVolume = IsAppInWritableVolume;
this.ClientUpdateVersion = ClientUpdateVersion;
@ -1017,6 +1120,8 @@
this.GetLocalRecordingState = GetLocalRecordingState;
this.OpenRecording = OpenRecording;
this.CloseRecording = CloseRecording;
this.PreviewRecording = PreviewRecording;
this.ClosePreviewRecording = ClosePreviewRecording;
this.OnDownloadAvailable = OnDownloadAvailable;
// Clipboard

View File

@ -1043,6 +1043,18 @@
})
}
function markRecordedBackingTrackSilent(options) {
var recordingId = options["recording_id"];
var trackId = options["backing_track_id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
data: {},
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + '/silent'
});
}
function getRecordedTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
@ -1055,6 +1067,18 @@
});
}
function getRecordedBackingTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId
});
}
function getRecording(options) {
var recordingId = options["id"];
@ -1157,6 +1181,32 @@
})
}
function openBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/open",
data: JSON.stringify(options)
})
}
function closeBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/close",
data: JSON.stringify(options)
})
}
function openJamTrack(options) {
var musicSessionId = options["id"];
var jamTrackId = options["jam_track_id"];
@ -1185,6 +1235,32 @@
})
}
function openMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/open",
data: JSON.stringify(options)
})
}
function closeMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/close",
data: JSON.stringify(options)
})
}
function discardRecording(options) {
var recordingId = options["id"];
@ -1389,6 +1465,15 @@
});
}
function getBackingTracks(options) {
return $.ajax({
type: "GET",
url: '/api/backing_tracks?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function addJamtrackToShoppingCart(options) {
return $.ajax({
type: "POST",
@ -1500,6 +1585,14 @@
});
}
function validateUrlSite(url, sitetype) {
return $.ajax({
type: "GET",
url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url),
contentType: 'application/json'
});
}
function initialize() {
return self;
}
@ -1577,6 +1670,7 @@
this.stopRecording = stopRecording;
this.getRecording = getRecording;
this.getRecordedTrack = getRecordedTrack;
this.getRecordedBackingTrack = getRecordedBackingTrack;
this.getClaimedRecordings = getClaimedRecordings;
this.getClaimedRecording = getClaimedRecording;
this.updateClaimedRecording = updateClaimedRecording;
@ -1585,8 +1679,13 @@
this.claimRecording = claimRecording;
this.startPlayClaimedRecording = startPlayClaimedRecording;
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
this.openJamTrack = openJamTrack;
this.openJamTrack = openJamTrack
this.openBackingTrack = openBackingTrack
this.closeBackingTrack = closeBackingTrack
this.closeMetronome = closeMetronome;
this.closeJamTrack = closeJamTrack;
this.openMetronome = openMetronome;
this.closeMetronome = closeMetronome;
this.discardRecording = discardRecording;
this.putTrackSyncChange = putTrackSyncChange;
this.createBand = createBand;
@ -1619,6 +1718,7 @@
this.getPurchasedJamTracks = getPurchasedJamTracks;
this.getJamTrackRight = getJamTrackRight;
this.enqueueJamTrack = enqueueJamTrack;
this.getBackingTracks = getBackingTracks;
this.addJamtrackToShoppingCart = addJamtrackToShoppingCart;
this.getShoppingCarts = getShoppingCarts;
this.removeShoppingCart = removeShoppingCart;
@ -1632,6 +1732,8 @@
this.resendBandInvitation = resendBandInvitation;
this.getMount = getMount;
this.createSourceChange = createSourceChange;
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
return this;
};

View File

@ -3,7 +3,8 @@
"use strict";
$(function() {
context.JK.popExternalLinks();
// commented out because JamKazam.js does this, and it's included everywhere that this file is
//scontext.JK.popExternalLinks();
})
})(window, jQuery);

View File

@ -215,6 +215,14 @@
top: childLayout.top,
left: childLayout.left
}, opts.animationDuration);
if($(this).is('.feed')) {
$('#jamblaster-notice').animate({
width: childLayout.width,
bottom: '102%',
left: childLayout.left
}, opts.animationDuration)
}
});
}

View File

@ -221,9 +221,9 @@
$.each(response, function(index, val) {
// this means the session no longer exists
if (response.fan_access == null && response.musician_access == null) {
return;
}
//if (response.fan_access == null && response.musician_access == null) {
// return;
//}
if(val.description == context.JK.MessageType.TEXT_MESSAGE) {
val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();

File diff suppressed because it is too large Load Diff

View File

@ -224,29 +224,11 @@
}
if (showJoinLink) {
// wire up the Join Link to the T&Cs dialog
// wire up the Join Link to the T&Cs dialog
$('.join-link', $parentRow).click(function(evt) {
if(!context.JK.guardAgainstBrowser(app)) {
return false;
}
if (!context.JK.JamServer.connected) {
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
return false;
}
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
app.notify(
{ title: "Unable to Join Session",
text: "You can only join a session once you have working audio gear and a tested internet connection."
})
})
.done(function(){
sessionUtils.joinSession(session.id);
})
return false;
sessionUtils.ensureValidClient(app, gearUtils, function() {
sessionUtils.joinSession(session.id);
});
});
}
}
@ -368,7 +350,7 @@
$('a.more.rsvps', $parentRow).click(toggleRsvps);
var showRsvpLink = true;
var noLinkText = '';
var sessionLinkText = '';
$('.rsvp-link-text', $parentRow).hide();
function showStartSessionButton(scheduledStart) {
@ -380,8 +362,8 @@
if (session.creator.id === context.JK.currentUserId) {
showRsvpLink = false;
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
noLinkText.find('a').click(function() {
sessionLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
sessionLinkText.find('a').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
@ -390,18 +372,18 @@
showRsvpLink = false;
if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) {
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a>&nbsp;|&nbsp;<a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
noLinkText.find('a.start').click(function() {
sessionLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a>&nbsp;|&nbsp;<a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
sessionLinkText.find('a.start').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
}
else {
noLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
sessionLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
}
// wire cancel link
noLinkText.find('a.cancel').click(function() {
sessionLinkText.find('a.cancel').click(function() {
ui.launchRsvpCancelDialog(session.id, approvedRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {
rest.getSessionHistory(session.id)
@ -419,8 +401,8 @@
showRsvpLink = false;
if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) {
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
noLinkText.find('a').click(function() {
sessionLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
sessionLinkText.find('a').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
@ -428,8 +410,8 @@
}
else if (pendingRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
noLinkText.find('a').click(function() {
sessionLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
sessionLinkText.find('a').click(function() {
ui.launchRsvpCancelDialog(session.id, pendingRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {
rest.getSessionHistory(session.id)
@ -445,11 +427,11 @@
}
else if (!openSlots) {
showRsvpLink = false;
noLinkText = '<span class="text">No more openings in this session.</span>';
sessionLinkText = '<span class="text">No more openings in this session.</span>';
}
else if (!openRsvps && !hasInvitation) {
showRsvpLink = false;
noLinkText = '<span class="text">You need an invitation to RSVP to this session.</span>';
sessionLinkText = '<span class="text">You need an invitation to RSVP to this session.</span>';
}
if (showRsvpLink) {
@ -472,7 +454,7 @@
});
}
else {
$('.rsvp-msg', $parentRow).html(noLinkText).show();
$('.rsvp-msg', $parentRow).html(sessionLinkText).show();
$('.rsvp-link', $parentRow).hide();
}
}

View File

@ -34,6 +34,9 @@
var sessionPageEnterTimeout = null;
var startTime = null;
var joinDeferred = null;
var previousBackingTracks = [];
var openBackingTrack = null;
var shownAudioMediaMixerHelp = false;
var mixerMode = MIX_MODES.PERSONAL;
@ -67,7 +70,7 @@
function isPlayingRecording() {
// this is the server's state; there is no guarantee that the local tracks
// requested from the backend will have corresponding track information
return currentSession && currentSession.claimed_recording;
return !!(currentSession && currentSession.claimed_recording);
}
function recordedTracks() {
@ -79,6 +82,28 @@
}
}
function recordedBackingTracks() {
if(currentSession && currentSession.claimed_recording) {
return currentSession.claimed_recording.recording.recorded_backing_tracks
}
else {
return null;
}
}
function backingTracks() {
var backingTracks = []
// this may be wrong if we loosen the idea that only one person can have a backing track open.
// but for now, the 1st person we find with a backing track open is all there is to find...
context._.each(participants(), function(participant) {
if(participant.backing_tracks.length > 0) {
backingTracks = participant.backing_tracks;
return false; // break
}
})
return backingTracks;
}
function jamTracks() {
if(currentSession && currentSession.jam_track) {
return currentSession.jam_track.tracks
@ -88,6 +113,27 @@
}
}
function backingTrack() {
if(currentSession) {
// TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
return {
path: currentSession.backing_track_path
}
}
else {
return null;
}
}
function metronomeActive() {
if(currentSession) {
return currentSession.metronome_active
}
else {
return null;
}
}
function creatorId() {
if(!currentSession) {
throw "creator is not known"
@ -303,6 +349,9 @@
}
currentSessionId = null;
currentParticipants = {}
previousBackingTracks = []
openBackingTrack = null
shownAudioMediaMixerHelp = false
}
// you should only update currentSession with this function
@ -321,6 +370,25 @@
}
}
function updateSession(response) {
updateSessionInfo(response, null, true);
}
function updateSessionInfo(response, callback, force) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
}
/**
* Reload the session data from the REST server, calling
* the provided callback when complete.
@ -344,18 +412,7 @@
type: "GET",
url: url,
success: function(response) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
updateSessionInfo(response, callback, force);
},
error: function(jqXHR) {
if(jqXHR.status != 404) {
@ -532,9 +589,49 @@
return mixerMode == MIX_MODES.PERSONAL;
}
function getMixMode() {
return mixerMode;
}
function getMixMode() {
return mixerMode;
}
function syncTracks(backingTracks) {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return null;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// backingTracks can be passed in as an optimization, so that we don't hit the backend excessively
if(backingTracks === undefined ) {
backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
}
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.backing_tracks = backingTracks;
syncTrackRequest.id = id();
return rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
}
function onWebsocketDisconnected(in_error) {
// kill the streaming of the session immediately
@ -678,45 +775,25 @@
// wait until we are fully in session before trying to sync tracks to server
if(joinDeferred) {
joinDeferred.done(function() {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
syncTracks();
})
}
}, 100);
}
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
refreshCurrentSession(true);
var backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
// the way we know if backing tracks changes, or recordings are opened, is via this event.
// but we want to report to the user when backing tracks change; so we need to detect change on our own
if(previousBackingTracks != backingTracks) {
logger.debug("backing tracks changed")
syncTracks(backingTracks);
}
else {
refreshCurrentSession(true);
}
}
else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
setMixerMode(MIX_MODES.MASTER);
@ -729,6 +806,10 @@
// Public interface
this.id = id;
this.start = start;
this.backingTrack = backingTrack;
this.backingTracks = backingTracks;
this.recordedBackingTracks = recordedBackingTracks;
this.metronomeActive = metronomeActive;
this.setUserTracks = setUserTracks;
this.recordedTracks = recordedTracks;
this.jamTracks = jamTracks;
@ -736,6 +817,7 @@
this.joinSession = joinSession;
this.leaveCurrentSession = leaveCurrentSession;
this.refreshCurrentSession = refreshCurrentSession;
this.updateSession = updateSession;
this.subscribe = subscribe;
this.participantForClientId = participantForClientId;
this.isPlayingRecording = isPlayingRecording;
@ -767,6 +849,19 @@
this.getParticipant = function(clientId) {
return participantsEverSeen[clientId]
};
this.setBackingTrack = function(backingTrack) {
openBackingTrack = backingTrack;
};
this.getBackingTrack = function() {
return openBackingTrack;
};
this.hasShownAudioMediaMixerHelp = function() {
return shownAudioMediaMixerHelp;
}
this.markShownAudioMediaMixerHelp = function() {
shownAudioMediaMixerHelp = true;
}
// call to report if the current user was able to establish audio with the specified clientID
this.setAudioEstablished = function(clientId, audioEstablished) {

View File

@ -125,7 +125,33 @@
}
}
sessionUtils.ensureValidClient = function(app, gearUtils, successCallback) {
if(!context.JK.guardAgainstBrowser(app)) {
return false;
}
if (!context.JK.JamServer.connected) {
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
return false;
}
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
app.notify(
{ title: "Unable to Join Session",
text: "You can only join a session once you have working audio gear and a tested internet connection."
});
})
.done(function() {
if (successCallback) {
successCallback();
}
});
}
sessionUtils.joinSession = function(sessionId) {
var hasInvitation = false;
var session = null;
// we need to do a real-time check of the session in case the settings have

View File

@ -0,0 +1,167 @@
$ = jQuery
context = window
context.JK ||= {};
context.JK.SiteValidator = class SiteValidator
constructor: (site_type) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@site_type = site_type
@input_div = $(".site_validator#"+site_type+"_validator")
@data_input = @input_div.find('input')
@logger = context.JK.logger
@spinner = @input_div.find('span.spinner-small')
@checkmark = @input_div.find('.validate-checkmark')
this.setSiteStatus(null)
this.showFormatStatus()
@is_rec_src = false
@deferred_status_check = null
@is_validating = false
init: () =>
this.renderErrors({})
@spinner.hide()
validator = this
@data_input.on 'blur', ->
validator.didBlur()
@data_input.on 'focus', ->
validator.showFormatStatus()
@data_input.on 'change', ->
@site_status = null
dataToValidate: () =>
url = @data_input.val()
if 0 < url.length
url.substring(0,2000)
else
null
showFormatStatus: () =>
data = this.dataToValidate()
yn = true
if data && ('url' == @site_type || @is_rec_src)
regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
yn = regexp.test(this.dataToValidate())
unless yn
@checkmark.hide()
yn
didBlur: () =>
if this.showFormatStatus()
this.validateSite()
validateSite: () =>
unless data = this.dataToValidate()
return null
this.setSiteStatus(null)
@spinner.show()
@rest.validateUrlSite(data, @site_type)
.done(this.processSiteCheckSucceed)
.fail(this.processSiteCheckFail)
processSiteCheckSucceed: (response) =>
@spinner.hide()
if 'Valid Site' == response.message
this.setSiteStatus(true)
this.renderErrors({})
if @deferred_status_check
@deferred_status_check.resolve()
else
this.setSiteStatus(false)
this.renderErrors(response)
if @deferred_status_check
@deferred_status_check.reject()
@deferred_status_check = null
@logger.debug("site_status = "+@site_status)
processSiteCheckFail: (response) =>
@logger.error("site check error")
this.setSiteStatus(false)
if @deferred_status_check
@deferred_status_check.reject()
@deferred_status_check = null
setSiteStatus: (status) =>
@site_status = status
@spinner.hide()
if true == status
@checkmark.show()
else
@checkmark.hide()
siteIsValid: () =>
this.setSiteStatus(true)
siteIsInvalid: () =>
this.setSiteStatus(false)
renderErrors: (errors) =>
errdiv = @input_div.find('.error')
if errmsg = context.JK.format_errors("site", errors)
errdiv.show()
errdiv.html(errmsg)
else
errdiv.hide()
errdiv.html('')
state: () =>
dfr = $.Deferred()
if null == @site_status
@deferred_status_check = dfr
this.validateSite()
else
if true == @site_status
dfr.resolve()
else
dfr.reject()
return dfr.promise()
context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator
constructor: (site_type) ->
super(site_type)
@recording_sources = []
@is_rec_src = true
@add_btn = @input_div.find('a.add-recording-source')
init: (sources) =>
super()
if sources
@recording_sources = sources
@add_btn.on 'click', =>
this.attemptAdd()
processSiteCheckSucceed: (response) =>
super(response)
@add_btn.removeClass('disabled')
@recording_sources.push({ url: response.data, recording_id: response.recording_id })
processSiteCheckFail: (response) =>
super(response)
@add_btn.removeClass('disabled')
didBlur: () =>
# do nothing, validate on add only
validateSite: () =>
@add_btn.addClass('disabled')
super()
attemptAdd: () =>
if data = this.dataToValidate()
unless this.containsRecordingUrl(data)
this.validateSite()
removeRecordingId: (recording_id) =>
start_len = @recording_sources.length
@recording_sources = $.grep @recording_sources, (src_data) ->
src_data['recording_id'] != recording_id
start_len != @recording_sources.length
containsRecordingUrl: (url) =>
vals = $.grep @recording_sources, (src_data) ->
src_data['url'] == url
0 < vals.length

View File

@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer
@list = @root.find('.list')
@logList = @root.find('.log-list')
@templateRecordedTrack = $('#template-sync-viewer-recorded-track')
@templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track')
@templateStreamMix = $('#template-sync-viewer-stream-mix')
@templateMix = $('#template-sync-viewer-mix')
@templateNoSyncs = $('#template-sync-viewer-no-syncs')
@templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details')
@templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track')
@templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track')
@templateHoverMix = $('#template-sync-viewer-hover-mix')
@templateDownloadReset = $('#template-sync-viewer-download-progress-reset')
@templateUploadReset = $('#template-sync-viewer-upload-progress-reset')
@templateGenericCommand = $('#template-sync-viewer-generic-command')
@templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command')
@templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command')
@templateLogItem = $('#template-sync-viewer-log-item')
@tabSelectors = @root.find('.dialog-tabs .tab')
@tabs = @root.find('.tab-content')
@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer
them_upload_soon: 'them-upload-soon'
missing: 'missing',
me_uploaded: 'me-uploaded',
them_uploaded: 'them-uploaded'
them_uploaded: 'them-uploaded',
not_mine: 'not-mine'
}
@clientStates = {
unknown: 'unknown',
@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer
hq: 'hq',
sq: 'sq',
missing: 'missing',
discarded: 'discarded'
discarded: 'discarded',
not_mine: 'not-mine'
}
throw "no sync-viewer" if not @root.exists()
@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer
$clientRetry.hide()
$uploadRetry.hide()
updateBackingTrackState: ($track) =>
clientInfo = $track.data('client-info')
serverInfo = $track.data('server-info')
myTrack = serverInfo.user.id == context.JK.currentUserId
# determine client state
clientStateMsg = 'UNKNOWN'
clientStateClass = 'unknown'
clientState = @clientStates.unknown
if serverInfo.mine
if serverInfo.download.should_download
if serverInfo.download.too_many_downloads
clientStateMsg = 'EXCESS DOWNLOADS'
clientStateClass = 'error'
clientState = @clientStates.too_many_uploads
else
if clientInfo?
if clientInfo.local_state == 'HQ'
clientStateMsg = 'HIGHEST QUALITY'
clientStateClass = 'hq'
clientState = @clientStates.hq
else if clientInfo.local_state == 'MISSING'
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'DISCARDED'
clientStateClass = 'discarded'
clientState = @clientStates.discarded
else
clientStateMsg = 'NOT MINE'
clientStateClass = 'not_mine'
clientState = @clientStates.not_mine
# determine upload state
uploadStateMsg = 'UNKNOWN'
uploadStateClass = 'unknown'
uploadState = @uploadStates.unknown
if serverInfo.mine
if !serverInfo.fully_uploaded
if serverInfo.upload.too_many_upload_failures
uploadStateMsg = 'UPLOAD FAILURE'
uploadStateClass = 'error'
uploadState = @uploadStates.too_many_upload_failures
else
if myTrack
if clientInfo?
if clientInfo.local_state == 'HQ'
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.me_upload_soon
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.them_upload_soon
else
uploadStateMsg = 'UPLOADED'
uploadStateClass = 'uploaded'
if myTrack
uploadState = @uploadStates.me_uploaded
else
uploadState = @uploadStates.them_uploaded
else
uploadStateMsg = 'NOT MINE'
uploadStateClass = 'not_mine'
uploadState = @uploadStates.not_mine
$clientState = $track.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
$clientStateProgress = $clientState.find('.progress')
$uploadState = $track.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
$uploadStateProgress = $uploadState.find('.progress')
$clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
$clientStateMsg.text(clientStateMsg)
$clientStateProgress.css('width', '0')
$uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
$uploadStateMsg.text(uploadStateMsg)
$uploadStateProgress.css('width', '0')
# this allows us to make styling decisions based on the combination of both client and upload state.
$track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}")
$clientRetry = $clientState.find('.retry')
$uploadRetry = $uploadState.find('.retry')
if gon.isNativeClient
# handle client state
# only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already
if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
$clientRetry.show()
else
$clientRetry.hide()
# only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it
if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon)
$uploadRetry.show()
else
$uploadRetry.hide()
else
$clientRetry.hide()
$uploadRetry.hide()
associateClientInfo: (recording) =>
for clientInfo in recording.local_tracks
$track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
for clientInfo in recording.backing_tracks
$track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
$track = @list.find(".mix[data-recording-id='#{recording.recording_id}']")
$track.data('client-info', recording.mix)
$track.data('total-size', recording.size)
@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer
uploadStateClass: uploadStateClass}
{variable: 'data'})
onHoverOfStateIndicator: () ->
displayBackingTrackHover: ($recordedTrack) =>
$clientState = $recordedTrack.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
clientStateClass = $clientState.data('custom-class')
clientState = $clientState.attr('data-state')
clientInfo = $recordedTrack.data('client-info')
$uploadState = $recordedTrack.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
uploadStateClass = $uploadState.data('custom-class')
uploadState = $uploadState.attr('data-state')
serverInfo = $recordedTrack.data('server-info')
# decide on special case strings first
summary = ''
if clientState == @clientStates.not_mine && @uploadStates.them_uploaded
# this is not our backing track
summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you."
else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon
# this is not our backing track
summary = "#{serverInfo.user.name} has not yet uploaded their backing track."
else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded
# we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing."
else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.discarded
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded )
summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made."
clientStateDefinition = switch clientState
when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed."
when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track."
when @clientStates.missing then "MISSING means you do not have this backing track anymore."
when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track."
else 'There is no help for this state'
uploadStateDefinition = switch uploadState
when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted."
when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon."
when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this backing track soon."
when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track."
when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track."
when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either."
when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track."
context._.template(@templateHoverRecordedBackingTrack.html(),
{summary: summary,
clientStateDefinition: clientStateDefinition,
uploadStateDefinition: uploadStateDefinition,
clientStateMsg: $clientStateMsg.text(),
uploadStateMsg: $uploadStateMsg.text(),
clientStateClass: clientStateClass,
uploadStateClass: uploadStateClass}
{variable: 'data'})
onTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayTrackHover($recordedTrack)
onBackingTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-backing-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayBackingTrackHover($recordedTrack)
onStreamMixHover: () ->
$streamMix = $(this).closest('.stream-mix.sync')
self = $streamMix.data('sync-viewer')
@ -512,6 +709,39 @@ context.JK.SyncViewer = class SyncViewer
return false
retryDownloadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_track',
action: 'download'
queue: 'download',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
retryUploadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_track',
action: 'upload'
queue: 'upload',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
createMix: (userSync) =>
recordingInfo = null
if userSync == 'fake'
@ -548,8 +778,26 @@ context.JK.SyncViewer = class SyncViewer
$uploadStateRetry.click(this.retryUploadRecordedTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
createBackingTrack: (userSync) =>
$track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'}))
$track.data('server-info', userSync)
$track.data('sync-viewer', this)
$clientState = $track.find('.client-state')
$uploadState = $track.find('.upload-state')
$clientStateRetry = $clientState.find('.retry')
$clientStateRetry.click(this.retryDownloadRecordedBackingTrack)
$uploadStateRetry = $uploadState.find('.retry')
$uploadStateRetry.click(this.retryUploadRecordedBackingTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
@ -687,6 +935,8 @@ context.JK.SyncViewer = class SyncViewer
for userSync in response.entries
if userSync.type == 'recorded_track'
@list.append(this.createTrack(userSync))
if userSync.type == 'recorded_backing_track'
@list.append(this.createBackingTrack(userSync))
else if userSync.type == 'mix'
@list.append(this.createMix(userSync))
else if userSync.type == 'stream_mix'
@ -707,6 +957,8 @@ context.JK.SyncViewer = class SyncViewer
for track in @list.find('.recorded-track.sync')
this.updateTrackState($(track))
for track in @list.find('.recorded-backing-track.sync')
this.updateBackingTrackState($(track))
for streamMix in @list.find('.stream-mix.sync')
this.updateStreamMixState($(streamMix))
@ -726,6 +978,18 @@ context.JK.SyncViewer = class SyncViewer
deferred.resolve(matchingTrack.data('server-info'))
return deferred
resolveBackingTrack: (commandMetadata) =>
recordingId = commandMetadata['recording_id']
clientTrackId = commandMetadata['track_id']
matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if matchingTrack.length == 0
return @rest.getRecordedBackingTrack({recording_id: recordingId, track_id: clientTrackId})
else
deferred = $.Deferred();
deferred.resolve(matchingTrack.data('server-info'))
return deferred
renderFullUploadRecordedTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
@ -738,6 +1002,18 @@ context.JK.SyncViewer = class SyncViewer
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
renderFullUploadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
$busy.empty().append($track)
@uploadProgress.find('.progress').css('width', '0%')
renderFullDownloadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'}))
$busy = @downloadProgress.find('.busy')
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
@ -756,6 +1032,23 @@ context.JK.SyncViewer = class SyncViewer
deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'download', commandMetadata)
deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderUploadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'upload', commandMetadata)
deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderGeneric: (commandId, category, commandMetadata) =>
commandMetadata.displayType = this.displayName(commandMetadata)
@ -794,6 +1087,8 @@ context.JK.SyncViewer = class SyncViewer
@downloadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'download', commandMetadata)
else if commandMetadata.queue == 'upload'
@ -803,6 +1098,8 @@ context.JK.SyncViewer = class SyncViewer
@uploadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'upload', commandMetadata)
else if commandMetadata.queue == 'cleanup'
@ -820,6 +1117,12 @@ context.JK.SyncViewer = class SyncViewer
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
else if userSync.type == 'recorded_backing_track'
$track = @list.find(".sync[data-id='#{userSync.id}']")
continue if $track.length == 0
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateBackingTrackState($track)
else if userSync.type == 'mix'
# check if there is a virtual mix 1st; if so, update it
$mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']")
@ -839,20 +1142,6 @@ context.JK.SyncViewer = class SyncViewer
updateSingleRecording: (recording_id) =>
@rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording)
updateSingleRecordedTrack: ($track) =>
serverInfo = $track.data('server-info')
@rest.getUserSync({user_sync_id: serverInfo.id})
.done((userSync) =>
# associate new server-info with this track
$track.data('server-info', userSync)
# associate new client-info with this track
clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSync.recording])
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
)
.fail(@app.ajaxError)
updateProgressOnSync: ($track, queue, percentage) =>
state = if queue == 'upload' then '.upload-state' else '.client-state'
$progress = $track.find("#{state} .progress")
@ -892,10 +1181,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @downloadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @downloadMetadata.type == 'recorded_track'
if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track'
clientTrackId = @downloadMetadata['track_id']
recordingId = @downloadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
$matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'download', percentage)
@ -903,10 +1192,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @uploadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload'
if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_track') and @uploadMetadata.action == 'upload'
clientTrackId = @uploadMetadata['track_id']
recordingId = @uploadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
$matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'upload', percentage)
else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload'
@ -977,15 +1266,15 @@ context.JK.SyncViewer = class SyncViewer
this.logResult(data.commandMetadata, false, data.commandReason, true)
displayName: (metadata) =>
if metadata.type == 'recorded_track' && metadata.action == 'download'
if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download'
return 'DOWNLOADING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'upload'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'upload'
return 'UPLOADING TRACK'
else if metadata.type == 'mix' && metadata.action == 'download'
return 'DOWNLOADING MIX'
else if metadata.type == 'recorded_track' && metadata.action == 'convert'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert'
return 'COMPRESSING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'delete'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete'
return 'CLEANUP TRACK'
else if metadata.type == 'stream_mix' && metadata.action == 'upload'
return 'UPLOADING STREAM MIX'

View File

@ -30,6 +30,28 @@
return tracks;
},
getBackingTracks: function(jamClient) {
var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4);
console.log("mediaTracks", mediaTracks)
var backingTracks = []
context._.each(mediaTracks, function(mediaTrack) {
// the check for 'not managed' means this is not a track opened by a recording, basically
// we do not try and sync these sorts of backing tracks to the server, because they
// are already encompassed by
if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) {
var track = {};
track.client_track_id = mediaTrack.persisted_track_id;
track.client_resource_id = mediaTrack.rid;
track.filename = mediaTrack.filename;
backingTracks.push(track);
}
})
return backingTracks;
},
/**
* This function resolves which tracks to configure for a user
* when creating or joining a session. By default, tracks are pulled

View File

@ -6,6 +6,7 @@
context.JK.UIHelper = function(app) {
var logger = context.JK.logger;
var rest = new context.JK.Rest();
var sessionUtils = context.JK.SessionUtils;
function addSessionLike(sessionId, userId, $likeCountSelector, $likeButtonSelector) {
rest.addSessionLike(sessionId, userId)
@ -54,9 +55,11 @@
}
function launchSessionStartDialog(session) {
var sessionStartDialog = new JK.SessionStartDialog(JK.app, session);
sessionStartDialog.initialize();
return sessionStartDialog.showDialog();
sessionUtils.ensureValidClient(app, context.JK.GearUtils, function() {
var sessionStartDialog = new JK.SessionStartDialog(JK.app, session);
sessionStartDialog.initialize();
return sessionStartDialog.showDialog();
});
}
this.addSessionLike = addSessionLike;

View File

@ -991,6 +991,15 @@
return hasFlash;
}
context.JK.getNameOfFile = function(filename) {
var index = filename.lastIndexOf('/');
if(index == -1) {
index = filename.lastIndexOf('\\');
}
return index == -1 ? filename : filename.substring(index + 1, filename.length)
}
context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result);

View File

@ -208,6 +208,10 @@ $fair: #cc9900;
background-color: $error;
}
&.not_mine {
background-color: $good;
}
&.discarded {
background-color: $unknown;
}
@ -252,6 +256,10 @@ $fair: #cc9900;
background-color: $good;
}
&.not_mine {
background-color: $good;
}
.retry {
display:none;
position:absolute;

View File

@ -45,7 +45,7 @@ body.jam, body.web, .dialog{
}
}
.help-hover-recorded-tracks, .help-hover-stream-mix {
.help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks {
font-size:12px;
padding:5px;

View File

@ -593,5 +593,16 @@ body.jam .icheckbox_minimal {
display:inline-block;
}
}
#jamblaster-notice {
position:absolute;
width:100%;
bottom:105%;
border-color:#ED3618;
border-style:solid;
border-width:1px;
padding:10px;
text-align:center;
@include border_box_sizing;
}

View File

@ -18,7 +18,7 @@
.track {
width:70px;
height:290px;
height:300px;
display:inline-block;
margin-right:8px;
position:relative;
@ -50,6 +50,9 @@
vertical-align:top;
}
.session-recordedtracks-container {
//display: block;
}
.recording-controls {
display:none;
@ -74,17 +77,23 @@
left:5px;
}
.open-media-file-header {
.open-media-file-header, .use-metronome-header {
font-size:16px;
line-height:100%;
margin:0;
float:left;
img {
position:relative;
top:3px;
}
}
.open-media-file-header {
float: left;
}
.use-metronome-header {
clear: both;
}
.open-media-file-options {
font-size:16px;
@ -110,8 +119,21 @@
.session-recording-name-wrapper{
position:relative;
white-space:nowrap;
display:none;
white-space:normal;
display:none;
.session-recording-name {
position:relative;
margin-top:9px;
margin-bottom:8px;
font-size:16px;
height: 22px;
min-height: 22px;
max-height: 22px;
display: inline-block;
width:60%;
text-overflow:ellipsis;
}
.session-add {
margin-top:9px;
@ -205,6 +227,9 @@ table.vu td {
position: absolute;
text-align:center;
width: 55px;
height: 15px;
min-height: 11px;
max-height: 33px;
max-width: 55px;
white-space:normal;
top: 3px;
@ -212,6 +237,7 @@ table.vu td {
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
font-weight: bold;
text-overflow:ellipsis;
}
.track-close {
@ -323,8 +349,6 @@ table.vu td {
color: inherit;
}
.session-add {
margin-top:9px;
margin-bottom:8px;
@ -349,7 +373,7 @@ table.vu td {
overflow-x:auto;
overflow-y:hidden;
width:100%;
height:340px;
height:370px;
float:left;
white-space:nowrap;
}
@ -486,12 +510,9 @@ table.vu td {
.track-gain {
position:absolute;
width:28px;
height:83px;
height:63px;
top:138px;
left:23px;
background-image:url('/assets/content/bkg_gain_slider.png');
background-repeat:repeat-y;
background-position:bottom;
}
.track-gain-wrapper {
@ -518,6 +539,45 @@ table.vu td {
height: 18px;
background-image:url('/assets/content/icon_mute.png');
background-repeat:no-repeat;
text-align: center;
}
.track-icon-loop {
cursor: pointer;
position:absolute;
top:250px;
left:11px;
width: 20px;
height: 18px;
text-align: center;
font-size: 8pt;
font-weight: bold;
.icheckbox_minimal {
top:5px;
margin-right:5px;
}
}
.metronome-selects {
position: absolute;
width: 52px;
top:252px;
left: 10px;
height: 18px;
text-align: center;
//display: block;
//padding: 4px;
select.metronome-select {
position: relative;
padding: 4px 0px 4px 0px;
margin: 0;
width: 100% !important;
font-size: 10px;
font-weight: normal;
}
}
.track-icon-mute.muted {
@ -528,12 +588,12 @@ table.vu td {
}
.session-livetracks .track-icon-mute, .session-recordings .track-icon-mute {
top:245px;
top:225px;
}
.track-icon-settings {
position:absolute;
top:255px;
top:235px;
left:28px;
}

View File

@ -1,7 +1,7 @@
@import "client/common";
table.findsession-table, table.local-recordings, table.open-jam-tracks, #account-session-detail {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, #account-session-detail {
.latency-unacceptable {
width: 50px;
@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, #account
text-align:center;
}
}
table.findsession-table, table.local-recordings, table.open-jam-tracks {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks {
width:98%;
height:10%;
font-size:11px;

View File

@ -0,0 +1,38 @@
@import "client/common";
.site_validator {
.validator-input {
float: left;
}
.validator-add-rec {
float: left;
}
input {
width: 100%;
padding: 5px 5px 5px 30px;
float: left;
}
.validate-checkmark {
background-image: url('/assets/content/icon_checkmark_circle.png');
background-repeat:no-repeat;
background-position:center;
width:32px;
height:32px;
background-size: 50% 50%;
display:inline-block;
vertical-align: middle;
position: relative;
margin-top: -40px;
left: 0px;
}
.error {
}
span.spinner-small {
display:inline-block;
vertical-align: middle;
position: relative;
margin-top: -40px;
}
}

View File

@ -0,0 +1,40 @@
@import "client/common";
#open-backing-track-dialog {
table.open-backing-tracks {
tbody {
tr:hover {
background-color: #777;
cursor:pointer;
}
tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] {
background-color:#777;
color:#aaa;
}
}
}
.right {
margin-right:10px;
}
.help-links {
text-align: left;
position: absolute;
margin: 0 auto;
width: 70%;
//left: 15%;
font-size: 12px;
padding-top:5px;
a {
margin:0 10px;
}
}
.paginator-holder {
padding-top:3px;
}
}

View File

@ -9,10 +9,412 @@ body.web.landing_page {
display:none;
}
&.wo_1 {
.landing-content h1 {
margin-left:45px;
.badge-number {
font-size:125%;
color:white;
background-color:$ColorScreenPrimary;
width:30px;
height:30px;
//position:absolute;
//left:-10px;
//top:4px;
margin-right:10px;
-webkit-border-radius:50%;
-moz-border-radius:50%;
border-radius:50%;
text-align:center;
display:inline-block;
border:2px solid white;
}
&.kick {
h1 {
}
p {
margin-bottom:15px;
line-height:120%;
}
.signup-wrapper {
width:75%;
text-align:center;
}
.landing-tag {
left: 50%;
}
.cta-container {
width:100%;
text-align:left;
margin-left:0% !important;
h1 {
margin: 0 0 10px 0;
}
a {margin-bottom:0 !important}
}
.column:nth-child(1) {
width:50% !important;
.cta-container {
margin-top:20px;
}
}
.column:nth-child(2) {
width:50% !important;
h2 {
margin-bottom:30px;
}
.cta-container a {
margin-bottom:8px;
}
}
}
&.kick_4 {
h1 {
}
.column h2 {
position:absolute;
font-size:12px;
margin-left:45px;
top:45px;
}
p {
margin-bottom:15px;
line-height:120%;
}
.signup-wrapper {
width:75%;
text-align:center;
}
.landing-tag {
left: 50%;
}
.cta-container {
width:100%;
text-align:left;
margin-left:0% !important;
h1 {
margin: 0 0 10px 0;
}
a {margin-bottom:0 !important}
}
.column:nth-child(1) {
width:50% !important;
.cta-container {
margin-top:20px;
}
}
.column:nth-child(2) {
width:50% !important;
h2 {
margin-bottom:30px;
}
.cta-container {
width:80%;
a {
width:90%;
text-align:center;
img {
width:65%;
margin-top:20px;
}
}
}
}
}
&.kick_2 {
.linker {
-webkit-border-radius:6px;
-moz-border-radius:6px;
border-radius:6px;
border-width:1px;
border-style:solid;
border-color:$ColorScreenPrimary;
position:absolute;
width:92%;
left:-1%;
top:60px;
height:270px;
}
h1 {
//padding-left:20px;
}
.youtube-time-tip {
font-style:italic;
}
p {
line-height:120%;
}
.signup-wrapper {
width:75%;
text-align:center;
}
.landing-tag {
left: 50%;
}
.cta-container {
width:85%;
text-align:left;
margin-left:0% !important;
h1 {
margin: 0 0 10px 0;
}
a {margin-bottom:0 !important}
}
.back-us {
margin-top:15px;
width:75%;
margin-left:30px;
img {
width:100%;
}
}
.column.one {
width:50% !important;
.cta-container {
margin-top:20px;
}
}
.column.two {
width:45% !important;
h2 {
margin-bottom:30px;
}
.cta-container a {
margin-bottom:8px;
}
}
.testimonial {
margin-top:70px;
p {
font-size:18px !important;
}
}
.signature {
margin-top:15px;
margin-left:30%;
font-size:18px;
position:relative;
.dash {
position:absolute;
width:20px;
left:-10px;
top:0;
}
}
.signature-details {
margin-top:5px;
font-style:italic;
margin-left:30%;
}
.signup-holder {
position:absolute;
top:350px;
width:45%;
}
.signup-info {
}
.signup-wrapper {
width:90%;
}
}
&.kick_3 {
.linker {
-webkit-border-radius:6px;
-moz-border-radius:6px;
border-radius:6px;
border-width:1px;
border-style:solid;
border-color:$ColorScreenPrimary;
position:absolute;
width:92%;
left:-1%;
top:56px;
height:270px;
}
.column h1 {
font-size:16px !important;
}
.youtube-time-tip {
font-style:italic;
}
p {
line-height:120%;
}
.signup-wrapper {
width:75%;
text-align:center;
}
.landing-tag {
left: 50%;
}
.cta-container {
width:85%;
text-align:left;
margin-left:0% !important;
h1 {
margin: 0 0 10px 0;
}
a {margin-bottom:0 !important}
}
.back-us {
margin-top:15px;
width:75%;
margin-left:30px;
img {
width:100%;
}
}
.column.one {
width:50% !important;
.cta-container {
margin-top:20px;
}
}
.column.two {
width:45% !important;
h2 {
margin-bottom:30px;
}
.cta-container a {
margin-bottom:8px;
}
}
.testimonial {
margin-top:70px;
p {
font-size:18px !important;
}
}
.signature {
margin-top:15px;
margin-left:30%;
font-size:18px;
position:relative;
.dash {
position:absolute;
width:20px;
left:-10px;
top:0;
}
}
.signature-details {
margin-top:5px;
font-style:italic;
margin-left:30%;
}
.signup-holder {
position:absolute;
top:350px;
width:45%;
}
.signup-info {
}
.signup-wrapper {
width:90%;
}
}
&.wo_1 {
.landing-tag {
left: 50%;
}
.cta-container {
width:75%;
text-align:center;
margin-left:0% !important;
h2 {
margin-left:0px !important;
}
}
.column:nth-child(1) {
width:50% !important;
}
.column:nth-child(2) {
width:50% !important;
h1 {
}
h2 {
margin-bottom:30px;
}
.cta-container a {
margin-bottom:8px;
}
}
}
&.wo_3 {
.landing-content h1 {

View File

@ -64,10 +64,16 @@ body.web {
}
}
&.register {
.landing-content {
min-height:460px;
}
}
.landing-content {
background-color:black;
width:100%;
min-height: 460px;
min-height: 366px;
position:relative;
padding-bottom:30px;

View File

@ -19,6 +19,15 @@ body.web {
}
}
.jamfest {
top:-70px;
position:relative;
.jamblaster {
font-weight:bold;
}
}
.follow-links {
position: absolute;
right: 0;

View File

@ -0,0 +1,32 @@
class ApiBackingTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_silent ]
respond_to :json
def index
tracks = [
{:name=>'foo',:path=>"foobar.mp3", :length=>4283},
{:name=>'bar',:path=>"foo.mp3",:length=>3257}
]
@backing_tracks, @next = tracks, nil
render "api_backing_tracks/index", :layout => nil
end
def backing_track_silent
@recorded_backing_track.mark_silent
render :json => {}, :status => 200
end
private
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
end # class ApiBackingTracksController

View File

@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [ :add_like, :show, :show_history, :add_session_info_comment ]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close, :backing_track_open, :backing_track_close, :metronome_open, :metronome_close]
skip_before_filter :api_signed_in_user, only: [:perf_upload]
respond_to :json
@ -357,7 +357,7 @@ class ApiMusicSessionsController < ApiController
end
def track_sync
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks])
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_tracks])
unless @tracks.kind_of? Array
# we have to do this because api_session_detail_url will fail with a bad @tracks
@ -597,8 +597,44 @@ class ApiMusicSessionsController < ApiController
respond_with_model(@music_session)
end
def backing_track_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
private
@backing_track_path = params[:backing_track_path]
@music_session.open_backing_track(current_user, @backing_track_path)
respond_with_model(@music_session)
end
def backing_track_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_backing_track()
respond_with_model(@music_session)
end
def metronome_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.open_metronome(current_user)
respond_with_model(@music_session)
end
def metronome_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_metronome()
respond_with_model(@music_session)
end
private
def lookup_session
@music_session = ActiveMusicSession.find(params[:id])

View File

@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ]
before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ]
before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ]
before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ]
@ -43,7 +44,11 @@ class ApiRecordingsController < ApiController
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download
def show_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download # track
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user)
@recorded_track.current_user = current_user
@ -58,6 +63,21 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_download
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user)
@recorded_backing_track.current_user = current_user
@recorded_backing_track.update_download_count
@recorded_backing_track.valid?
if !@recorded_backing_track.errors.any?
@recorded_backing_track.save!
redirect_to @recorded_backing_track.sign_url
else
render :json => { :message => "download limit surpassed" }, :status => 404
end
end
def start
music_session = ActiveMusicSession.find(params[:music_session_id])
@ -227,6 +247,61 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_upload_next_part
length = params[:length]
md5 = params[:md5]
@recorded_backing_track.upload_next_part(length, md5)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
# this is not typical, but please don't change this line unless you are sure it won't break anything
# this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's
# state doesn't cause errors to shoot out like normal.
render :json => { :errors => @recorded_backing_track.errors }, :status => 422
else
result = {
:part => @recorded_backing_track.next_part_to_upload,
:offset => @recorded_backing_track.file_offset.to_s
}
render :json => result, :status => 200
end
end
def backing_track_upload_sign
render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200
end
def backing_track_upload_part_complete
part = params[:part]
offset = params[:offset]
@recorded_backing_track.upload_part_complete(part, offset)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
else
render :json => {}, :status => 200
end
end
def backing_track_upload_complete
@recorded_backing_track.upload_complete
@recorded_backing_track.recording.upload_complete
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
return
else
render :json => {}, :status => 200
end
end
# POST /api/recordings/:id/videos/:video_id/upload_sign
def video_upload_sign
length = params[:length]
@ -314,6 +389,11 @@ class ApiRecordingsController < ApiController
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user)
end
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
def lookup_stream_mix
@quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user)

View File

@ -12,7 +12,8 @@ class ApiRsvpRequestsController < ApiController
music_session = MusicSession.find(params[:session_id])
# retrieve all requests for this session
if music_session.creator.id == current_user.id
creator = music_session.creator
if creator && creator.id == current_user.id
@rsvp_requests = RsvpRequest.index(music_session, nil, params)
# scope the response to the current user

View File

@ -1,7 +1,7 @@
require 'sanitize'
class ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump]
before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
@ -699,6 +699,37 @@ class ApiUsersController < ApiController
end
end
def validate_data
unless (data = params[:data]).present?
render(json: { message: "blank data #{data}" }, status: :unprocessable_entity) && return
end
url = nil
site = params[:sitetype]
if site.blank? || 'url'==site
url = data
elsif Utils.recording_source?(site)
rec_id = Utils.extract_recording_id(site, data)
if rec_id
render json: { message: 'Valid Site', recording_id: rec_id, data: data }, status: 200
return
else
render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200
return
end
else
url = Utils.username_url(data, site)
end
unless url.blank?
if errmsg = Utils.site_validator(url, site)
render json: { message: 'Invalid Site', data: data, errors: { site: [errmsg] } }, status: 200
else
render json: { message: 'Valid Site', data: data }, status: 200
end
else
render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity
end
end
###################### RECORDINGS #######################
# def recording_index
# @recordings = User.recording_index(current_user, params[:id])

View File

@ -22,6 +22,22 @@ class LandingsController < ApplicationController
end
end
def watch_overview_kick
render 'watch_kick', layout: 'web'
end
def watch_overview_kick2
render 'watch_kick_2', layout: 'web'
end
def watch_overview_kick3
render 'watch_kick_3', layout: 'web'
end
def watch_overview_kick4
render 'watch_kick_4', layout: 'web'
end
def watch_overview
@promo_buzz = PromoBuzz.active

View File

@ -52,4 +52,12 @@ class SpikesController < ApplicationController
render :layout => 'web'
end
def site_validate
render :layout => 'web'
end
def recording_source
render :layout => 'web'
end
end

View File

@ -190,12 +190,12 @@ class UsersController < ApplicationController
def welcome
@slides = [
Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1"),
Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1")
Slide.new("JamKazam Overview", "web/carousel_overview.png", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("The Revolutionary New JamBlaster!", "web/carousel_jamblaster.png", "http://www.youtube.com/embed/gAJAIHMyois?autoplay=1"),
Slide.new("Kudos From Our Community", "web/carousel_community.png", "http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1"),
Slide.new("JamKazam Overview", "web/carousel_overview.png", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("The Revolutionary New JamBlaster!", "web/carousel_fans.jpg", "http://www.youtube.com/embed/gAJAIHMyois?autoplay=1"),
Slide.new("Kudos From Our Community", "web/carousel_community.png", "http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1")
]
@promo_buzz = PromoBuzz.active
@ -207,8 +207,8 @@ class UsersController < ApplicationController
end
# temporary--will go away soon
@jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID
@jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID
#@jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID
#@jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID
# temporary--end
@welcome_page = true

View File

@ -91,4 +91,10 @@ module SessionsHelper
current_user.musician? ? 'Musician' : 'Fan'
end
end
def metronome_tempos
[
40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208
]
end
end

View File

@ -0,0 +1,7 @@
node :next do |page|
@next
end
node :backing_tracks do |page|
@backing_tracks
end

View File

@ -2,6 +2,8 @@
# I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata
# depictable.
# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState
object @claimed_recording
attributes :id, :name, :description, :is_public, :genre_id, :discarded
@ -36,6 +38,18 @@ child(:recording => :recording) {
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url
}
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -13,7 +13,7 @@ if !current_user
}
else
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active
node :can_join do |session|
session.can_join?(current_user, true)
@ -54,6 +54,10 @@ else
child(:tracks => :tracks) {
attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at
}
child(:backing_tracks => :backing_tracks) {
attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at
}
}
child({:invitations => :invitations}) {
@ -114,6 +118,14 @@ else
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
}
}

View File

@ -27,6 +27,12 @@ child(:recorded_tracks => :recorded_tracks) {
end
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
node do |recorded_backing_track|
partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track)
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -0,0 +1,11 @@
object @recorded_backing_track
attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url
}

View File

@ -18,7 +18,6 @@ glue :recorded_track do
partial("api_recordings/show", :object => recorded_track.recording)
end
node :upload do |recorded_track|
{
should_upload: true,
@ -35,6 +34,45 @@ glue :recorded_track do
end
glue :recorded_backing_track do
@object.current_user = current_user
node :type do |i|
'recorded_backing_track'
end
attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename
node :user do |recorded_backing_track|
partial("api_users/show_minimal", :object => recorded_backing_track.user)
end
node :recording do |recorded_backing_track|
partial("api_recordings/show", :object => recorded_backing_track.recording)
end
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
node :upload do |recorded_backing_track|
{
should_upload: true,
too_many_upload_failures: recorded_backing_track.too_many_upload_failures?
}
end
node :download do |recorded_backing_track|
{
should_download: recorded_backing_track.can_download?(current_user),
too_many_downloads: recorded_backing_track.too_many_downloads?
}
end
end
glue :mix do
@object.current_user = current_user

View File

@ -199,6 +199,10 @@ script type="text/template" id="template-help-media-controls-disabled"
| Only the person who opened the recording can control the volume levels.
| {% } %}
script type="text/template" id="template-help-volume-media-mixers"
| Audio files only expose master mix controls, so any change here will also affect everyone in the session.
script type="text/template" id="template-help-downloaded-jamtrack"
.downloaded-jamtrack
p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client.

View File

@ -1,4 +1,8 @@
.screen layout="screen" layout-id="home"
.screen layout="screen" layout-id="home"
-if Rails.configuration.show_jamblaster_notice
#jamblaster-notice
a href='https://www.youtube.com/watch?v=gAJAIHMyois' rel="external"
span Check out the amazing new JamBlaster, and learn how it can improve your sessions!
/ Layout is different if jam_tracks tile available:
-jamtracks=Rails.configuration.jam_tracks_available
-if (jamtracks)
@ -19,7 +23,7 @@
h2 find session
.homebox-info
/! 1 session invitation, 19 public sessions active
.homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed"
.homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed"
h2 feed
.homebox-info
/! 4 friends online, 2 currently in sessions

Some files were not shown because too many files have changed in this diff Show More