diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb
index 8e62bbc85..6cfb6c1a3 100644
--- a/admin/app/admin/jam_tracks.rb
+++ b/admin/app/admin/jam_tracks.rb
@@ -26,6 +26,7 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
column :id
column :name
column :description
+ column :version
column :initial_play_silence
column :time_signature
column :status
diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim
index 16e53837d..9ec25ffcf 100644
--- a/admin/app/views/admin/jam_tracks/_form.html.slim
+++ b/admin/app/views/admin/jam_tracks/_form.html.slim
@@ -7,7 +7,9 @@
| JamTrack should only be made available (to end users) if all its sub-component are in place:
= f.input :available, as: :boolean
= f.input :description, :input_html => { :rows=>5, :maxlength=>1000 }
- = f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)'
+ = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly'
+ = f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the definition of this JamTrack'
+ //= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)'
= f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: false
= f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false
= f.input :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE, include_blank: false
@@ -24,8 +26,9 @@
= f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :licensor_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :pro_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
- = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true
= f.input :url, :as => :file, :label => 'Audio File'
+ = f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 }
+ = f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly=>true }, :hint => 'readonly'
= f.semantic_fields_for :jam_track_tracks do |track|
= render 'jam_track_track_fields', f: track
diff --git a/admin/config/application.rb b/admin/config/application.rb
index 684f30d7e..102a03176 100644
--- a/admin/config/application.rb
+++ b/admin/config/application.rb
@@ -147,5 +147,8 @@ module JamAdmin
config.influxdb_hosts = ["localhost"]
config.influxdb_port = 8086
config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development']
+
+ config.jamtracks_dir = ENV['JAMTRACKS_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jamtracks"))
+ config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep"))
end
end
diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb
new file mode 100644
index 000000000..c88fc6a2c
--- /dev/null
+++ b/admin/config/initializers/jam_tracks.rb
@@ -0,0 +1,25 @@
+class JamRuby::JamTrack
+
+ # add a custom validation
+
+ before_save :jmep_json_generate
+ validate :jmep_text_validate
+
+ def jmep_text_validate
+ begin
+ JmepManager.execute(self.jmep_text)
+ rescue ArgumentError => err
+ errors.add(:jmep_text, err.to_s)
+ end
+ end
+
+ def jmep_json_generate
+ begin
+ self[:jmep_json] = JmepManager.execute(self.jmep_text)
+ rescue ArgumentError => err
+ #errors.add(:jmep_text, err.to_s)
+ end
+ end
+
+
+end
diff --git a/build b/build
index 6d408c180..db8ab33a4 100755
--- a/build
+++ b/build
@@ -66,7 +66,7 @@ DEB_SERVER=http://localhost:9010/apt-`uname -p`
GEM_SERVER=http://localhost:9000/gems
# if still going, then push all debs up
- if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* ]]; then
+ if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* || "$GIT_BRANCH" == *feature* || "$GIT_BRANCH" == *hotfix* ]]; then
echo ""
echo "PUSHING DB ARTIFACTS"
diff --git a/db/manifest b/db/manifest
index fc5d059bc..ab12b759f 100755
--- a/db/manifest
+++ b/db/manifest
@@ -258,4 +258,7 @@ user_presences_rename.sql
add_genre_type.sql
add_description_to_perf_samples.sql
widen_user_authorization_token.sql
+jam_track_version.sql
+recorded_jam_track_tracks.sql
+jam_track_jmep_data.sql
musician_search.sql
diff --git a/db/up/jam_track_jmep_data.sql b/db/up/jam_track_jmep_data.sql
new file mode 100644
index 000000000..46c6e67b5
--- /dev/null
+++ b/db/up/jam_track_jmep_data.sql
@@ -0,0 +1,2 @@
+ALTER TABLE jam_tracks ADD COLUMN jmep_text VARCHAR;
+ALTER TABLE jam_tracks ADD COLUMN jmep_json JSON;
\ No newline at end of file
diff --git a/db/up/jam_track_version.sql b/db/up/jam_track_version.sql
new file mode 100644
index 000000000..7c1efff86
--- /dev/null
+++ b/db/up/jam_track_version.sql
@@ -0,0 +1 @@
+ALTER TABLE jam_tracks ADD COLUMN version VARCHAR NOT NULL DEFAULT 0;
diff --git a/db/up/recorded_jam_track_tracks.sql b/db/up/recorded_jam_track_tracks.sql
index 246b76425..08a485643 100644
--- a/db/up/recorded_jam_track_tracks.sql
+++ b/db/up/recorded_jam_track_tracks.sql
@@ -1,15 +1,15 @@
-ALTER TABLE recordings ADD COLUMN jam_track_id VARCHAR(64) REFERENCES jam_tracks(id);
+ALTER TABLE recordings ADD COLUMN jam_track_id BIGINT REFERENCES jam_tracks(id);
ALTER TABLE recordings ADD COLUMN jam_track_initiator_id VARCHAR(64) REFERENCES users(id);
CREATE TABLE recorded_jam_track_tracks (
id BIGINT PRIMARY KEY,
- user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
- jam_track_track_id VARCHAR(64),
- recording_id VARCHAR(64) NOT NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ jam_track_track_id VARCHAR(64) REFERENCES jam_track_tracks(id) NOT NULL,
+ recording_id VARCHAR(64) REFERENCES recordings(id) NOT NULL,
discard BOOLEAN,
timeline JSON,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-ALTER TABLE recorded_jam_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');
\ No newline at end of file
+ALTER TABLE recorded_jam_track_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 51b53471a..385b530be 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -139,6 +139,7 @@ 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/recorded_jam_track_track"
require "jam_ruby/models/quick_mix"
require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token"
@@ -204,6 +205,7 @@ require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source"
require "jam_ruby/models/text_message"
require "jam_ruby/jam_tracks_manager"
+require "jam_ruby/jmep_manager"
require "jam_ruby/models/performance_sample"
require "jam_ruby/models/online_presence"
require "jam_ruby/models/json_store"
diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb
index a5c04cd61..37003d115 100644
--- a/ruby/lib/jam_ruby/jam_tracks_manager.rb
+++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb
@@ -20,7 +20,6 @@ module JamRuby
def save_jam_track_right_jkz(jam_track_right)
jam_track = jam_track_right.jam_track
- #py_root = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "jamtracks"))
py_root = APP_CONFIG.jamtracks_dir
Dir.mktmpdir do |tmp_dir|
jam_file_opts=""
@@ -42,10 +41,11 @@ module JamRuby
title=jam_track.name
output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz")
py_file = File.join(py_root, "jkcreate.py")
+ version = jam_track.version
@@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})"
# From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819:
- cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}'"
+ cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}' -V '#{version}'"
Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
pid = wait_thr.pid
exit_status = wait_thr.value
diff --git a/ruby/lib/jam_ruby/jmep_manager.rb b/ruby/lib/jam_ruby/jmep_manager.rb
new file mode 100644
index 000000000..a4d0215bc
--- /dev/null
+++ b/ruby/lib/jam_ruby/jmep_manager.rb
@@ -0,0 +1,55 @@
+require 'json'
+require 'tempfile'
+require 'open3'
+require 'fileutils'
+require 'open-uri'
+
+module JamRuby
+
+ # Interact with external python tools to create jmep json
+ class JmepManager
+
+ @@log = Logging.logger[JmepManager]
+
+ class << self
+
+ def execute(jmep_text)
+
+ json = nil
+
+ if jmep_text.blank?
+ return nil
+ end
+
+ py_root = APP_CONFIG.jmep_dir
+ Dir.mktmpdir do |tmp_dir|
+
+ output_json = File.join(tmp_dir, "jmep.json")
+ input_text = File.join(tmp_dir, "jmep.txt")
+
+ # put JMEP text into input file
+ File.open(input_text, 'w') { |file| file.write(jmep_text) }
+
+ py_file = File.join(py_root, "jmepgen.py")
+ @@log.info "Executing python source in #{py_file}, outputting to #{output_json})"
+
+ # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819:
+ cli = "python #{py_file} -i '#{input_text}' -o '#{output_json}'"
+ Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
+ pid = wait_thr.pid
+ exit_status = wait_thr.value
+ err = stderr.read(1000)
+ out = stdout.read(1000)
+
+ raise ArgumentError, "#{out} #{err}" if exit_status != 0
+
+ json = File.read(output_json)
+ end
+ end
+
+ json
+ end
+
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb
index 3d884ca3a..fa57b623f 100644
--- a/ruby/lib/jam_ruby/models/jam_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track.rb
@@ -16,7 +16,7 @@ module JamRuby
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
- :jam_track_tap_ins_attributes, :available, as: :admin
+ :jam_track_tap_ins_attributes, :available, :version, :jmep_json, :jmep_text, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :description, length: {maximum: 1000}
@@ -30,6 +30,7 @@ module JamRuby
validates :sales_region, inclusion: {in: [nil] + SALES_REGION}
validates_format_of :price, with: /^\d+\.*\d{0,2}$/
validates :initial_play_silence, numericality: true, :allow_nil => true
+ validates :version, presence: true
validates :reproduction_royalty, inclusion: {in: [nil, true, false]}
validates :public_performance_royalty, inclusion: {in: [nil, true, false]}
@@ -50,6 +51,8 @@ module JamRuby
has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession"
+ has_many :recordings, :class_name => "JamRuby::Recording"
+
accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true
accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true
@@ -149,6 +152,7 @@ module JamRuby
def sanitize_active_admin
self.genre_id = nil if self.genre_id == ''
self.licensor_id = nil if self.licensor_id == ''
+ self.jmep_json = nil if self.jmep_json = ''
end
end
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb
index affee58a5..3e82185ed 100644
--- a/ruby/lib/jam_ruby/models/jam_track_right.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_right.rb
@@ -123,8 +123,6 @@ module JamRuby
state = nil
if signed
state = 'SIGNED'
- elsif error_count > 0
- state = 'ERROR'
elsif signing_started_at
if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time
state = 'SIGNING_TIMEOUT'
@@ -137,6 +135,8 @@ module JamRuby
else
state = 'QUEUED'
end
+ elsif error_count > 0
+ state = 'ERROR'
else
state = 'QUIET' # needs to be poked to go build
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb
index 978ed29ab..c8d7c523f 100644
--- a/ruby/lib/jam_ruby/models/jam_track_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_track.rb
@@ -21,6 +21,8 @@ module JamRuby
belongs_to :instrument, class_name: "JamRuby::Instrument"
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
+ has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy
+
# create storage directory that will house this jam_track, as well as
def store_dir
"#{jam_track.store_dir}/tracks"
diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb
index 83557f4f7..f441a441f 100644
--- a/ruby/lib/jam_ruby/models/mix.rb
+++ b/ruby/lib/jam_ruby/models/mix.rb
@@ -141,12 +141,35 @@ module JamRuby
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 }
+ mix_params << { "level" => 1.0, "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 }
+ mix_params << { "level" => 1.0, "balance" => 0 }
+ end
+
+ recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
+ manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
+ # let's look for level info from the client
+ level = 1.0 # default value - means no effect
+ if recorded_jam_track_track.timeline
+
+ timeline_data = JSON.parse(recorded_jam_track_track.timeline)
+
+ # always take the 1st entry for now
+ first = timeline_data[0]
+
+ if first["mute"]
+ # mute equates to no noise
+ level = 0.0
+ else
+ # otherwise grab the left channel...
+ level = first["vol_l"]
+ end
+ end
+
+ mix_params << { "level" => level, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
diff --git a/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb
new file mode 100644
index 000000000..f77615beb
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb
@@ -0,0 +1,21 @@
+module JamRuby
+ # BackingTrack analog to JamRuby::RecordedTrack
+ class RecordedJamTrackTrack < ActiveRecord::Base
+
+ belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_jam_track_tracks
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_jam_track_tracks
+ belongs_to :jam_track_track, :class_name => "JamRuby::JamTrackTrack", :inverse_of => :recorded_jam_track_tracks
+
+ validates :user, presence: true
+ validates :jam_track_track, presence:true
+
+ def self.create_from_jam_track_track(jam_track_track, recording)
+ recorded_jam_track_track = self.new
+ recorded_jam_track_track.recording = recording
+ recorded_jam_track_track.jam_track_track = jam_track_track
+ recorded_jam_track_track.save
+ recorded_jam_track_track
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index 69d690962..c4537c94d 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -12,6 +12,7 @@ module JamRuby
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 :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :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
@@ -20,6 +21,8 @@ module JamRuby
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id'
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id
+ belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id'
+ belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id'
accepts_nested_attributes_for :recorded_tracks, :mixes, :claimed_recordings, allow_destroy: true
@@ -223,6 +226,14 @@ module JamRuby
recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording)
end
end
+
+ if music_session.jam_track
+ music_session.jam_track.jam_track_tracks.each do |jam_track_track|
+ recording.recorded_jam_track_tracks << RecordedJamTrackTrack.create_from_jam_track_track(jam_track_track, recording)
+ end
+ recording.jam_track = music_session.jam_track
+ recording.jam_track_initiator = music_session.jam_track_initiator
+ end
end
end
@@ -556,7 +567,7 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:client_track_id => recorded_item.client_track_id,
:next => recorded_item.id
- })
+ })
else
end
@@ -678,6 +689,24 @@ module JamRuby
self.save(:validate => false)
end
+ def add_timeline(timeline)
+ tracks = timeline["tracks"]
+
+ raise JamArgumentError, "tracks must be specified" unless tracks
+
+ jam_tracks = tracks.select {|track| track["type"] == "jam_track"}
+ jam_tracks.each do |client_jam_track|
+ recorded_jam_track_track = RecordedJamTrackTrack.find_by_jam_track_track_id(client_jam_track["id"])
+ if recorded_jam_track_track
+ recorded_jam_track_track.timeline = client_jam_track["timeline"].to_json
+ recorded_jam_track_track.save!
+ else
+ @@log.error("unable to find JamTrackTrack with id #{recorded_jam_track_track.id}")
+ end
+ end
+ end
+
+
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 7df12721c..ef07016c8 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -124,6 +124,10 @@ module JamRuby
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
+ has_many :recorded_jam_track_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedJamTrackTrack", :inverse_of => :user
+
+ # jam track recordings started
+ has_many :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id', :class_name => "JamRuby::Recording", :inverse_of => :jam_track_initiator
# invited users
has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser"
diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb
index eb70824c9..3bf68e991 100644
--- a/ruby/lib/jam_ruby/recurly_client.rb
+++ b/ruby/lib/jam_ruby/recurly_client.rb
@@ -111,6 +111,7 @@ module JamRuby
end
def place_order(current_user, jam_track)
+ jam_track_right = nil
account = get_account(current_user)
if (account.present?)
begin
@@ -136,7 +137,7 @@ module JamRuby
else
raise RecurlyClientError, "Could not find account to place order."
end
- account
+ jam_track_right
end
def find_or_create_account(current_user, billing_info=nil)
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index 8c940bd0b..07008dda7 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -268,7 +268,6 @@ FactoryGirl.define do
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
@@ -277,6 +276,12 @@ FactoryGirl.define do
association :recording, factory: :recording
end
+ factory :recorded_jam_track_track, :class => JamRuby::RecordedJamTrackTrack do
+ association :user, factory: :user
+ association :recording, factory: :recording
+ association :jam_track_track, factory: :jam_track_track
+ end
+
factory :instrument, :class => JamRuby::Instrument do
description { |n| "Instrument #{n}" }
end
diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb
index d2fd7c3b0..3d8515f29 100644
--- a/ruby/spec/jam_ruby/models/recording_spec.rb
+++ b/ruby/spec/jam_ruby/models/recording_spec.rb
@@ -1073,6 +1073,33 @@ describe Recording do
RecordedVideo.find_by_id(video.id).should_not be_nil
end
end
+
+ describe "add_timeline" do
+
+ let!(:recorded_jam_track_track) {FactoryGirl.create(:recorded_jam_track_track)}
+ let(:recording) {recorded_jam_track_track.recording}
+ let(:timeline_data) {{"sample" => "data"}}
+ let(:good_timeline) { {
+ "tracks" => [
+ {
+ "id" => recorded_jam_track_track.jam_track_track.id,
+ "timeline" => timeline_data,
+ "type" => "jam_track"
+ }
+ ]
+ }
+ }
+
+ it "applies timeline data correctly" do
+ recording.add_timeline good_timeline
+ recorded_jam_track_track.reload
+ JSON.parse(recorded_jam_track_track.timeline).should eq(timeline_data)
+ end
+
+ it "fails if no tracks data" do
+ expect { recording.add_timeline({}) }.to raise_error(JamRuby::JamArgumentError)
+ end
+ end
end
diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js
index 73997c0a7..61b889eb2 100644
--- a/web/app/assets/javascripts/JamServer.js
+++ b/web/app/assets/javascripts/JamServer.js
@@ -529,10 +529,12 @@
if(server.connecting) {
logger.error("server.connect should never be called if we are already connecting. cancelling.")
+ // XXX should return connectDeferred, but needs to be tested/vetted
return;
}
if(server.connected) {
logger.error("server.connect should never be called if we are already connected. cancelling.")
+ // XXX should return connectDeferred, but needs to be tested/vetted
return;
}
@@ -678,7 +680,12 @@
logger.info("server.send(" + jsMessage + ")");
}
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
- server.socket.send(jsMessage);
+ try {
+ server.socket.send(jsMessage);
+ }
+ catch(err) {
+ logger.warn("error when sending on websocket: " + err)
+ }
} else {
logger.warn("Dropped message because server connection is closed.");
}
diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js
index 21432a68e..349742ad7 100644
--- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js
+++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js
@@ -24,6 +24,7 @@
}
function beforeShow() {
+ $dialog.data('result', null)
emptyList();
resetPagination();
showing = true;
@@ -77,18 +78,8 @@
// tell the server we are about to start a recording
rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id})
.done(function(response) {
- context.jamClient.JamTrackStopPlay();
- var result = context.jamClient.JamTrackPlay(jamTrack.id);
-
- logger.debug("JamTrackPlay response: %o", result);
-
- if(result) {
- app.layout.closeDialog('open-jam-track-dialog');
- }
- else {
- logger.error("unable to open jam track")
- }
-
+ $dialog.data('result', {success:true, jamTrack: jamTrack})
+ app.layout.closeDialog('open-jam-track-dialog');
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback");
diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee
index 75e684bc6..6e09d8bc5 100644
--- a/web/app/assets/javascripts/download_jamtrack.js.coffee
+++ b/web/app/assets/javascripts/download_jamtrack.js.coffee
@@ -2,10 +2,471 @@ $ = jQuery
context = window
context.JK ||= {};
-context.JK.DownloadJamTrack = class SyncViewer
- constructor: (@app) ->
+# This is the sequence of how this widget works:
+# checkState() is the heart of the state machine; it is called to get things going, and is called whenevr a state ends
+# checkState() checks first against what the client thinks about the state of the JamTrack;
+# if it on the disk then the state machine may enter one of:
+# * synchronized
+# * keying
+#
+# if it's still on the server, then the state machine may be:
+# * packaging
+# * downloading
+#
+# errored state can be entered from @jamTrack.jam_track_right_id
+#
+# other state; you augment the error to the user by suppling @errorMessage before transitioning
+#
+# no-client is the way the widget behaves when you are in a normal browser (i.e., nothing happens other than tell the user to use the client)
+#
+# Discussion of the different states:
+# There are different states that a JamTrack can be in.
+# The final success state is that the JamTrack is on disk and loadable. (show synchronized state)
+# But there are others until you get there:
+# The JamTrack does not exist on the server, so we will create it (packaging state)
+# The JamTrack exists on the server, but not on disk, so we will download it (downloading state)
+# The JamTrack is on the disk, but does not yet have keys, so we will fetch them (keying)
+
+context.JK.DownloadJamTracks = {}
+context.JK.DownloadJamTrack = class DownloadJamTrack
+ constructor: (@app, jamTrack, size = 'large') ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
+ @logger = context.JK.logger
+ @jamTrack = jamTrack
+ @size = size
+ @attemptedEnqueue = false
+ @errorReason = null
+ @errorMessage = null
+ @transitionTimer = null
+ @downloadTimer = null
+ @trackDetail = null
+ @stateHolder = null
+ @active = false
+ @startTime = null
+ @attempts = 0
+ @tracked = false
+ @ajaxEnqueueAborted = false
+ @ajaxGetJamTrackRightAborted = false
+ throw "no JamTrack specified" unless @jamTrack?
+ throw "invalid size" if @size != 'large' && @size != 'small'
+ throw "no JamTrack version" unless @jamTrack.version?
+
+ @path = []
+ @states = {
+ no_client: { name: 'no-client', show: @showNoClient, leaf: true },
+ synchronized: { name: 'synchronized', show: @showSynchronized, leaf: true},
+ packaging: { name: 'packaging', show: @showPackaging },
+ downloading: { name: 'downloading', show: @showDownloading },
+ keying: { name: 'keying', show: @showKeying, max_time: 10000 },
+ initial: { name: 'initial', show: @showInitial },
+ quiet: { name: 'quiet', show: @showQuiet },
+ errored: { name: 'errored', show: @showError, leaf: true}
+ }
+
+ context.JK.DownloadJamTracks[@jamTrack.id] = this
+ downloadJamTrackTemplate = $('#template-download-jamtrack')
+ throw "no download jamtrack template" if not downloadJamTrackTemplate.exists()
+
+ @root = $(downloadJamTrackTemplate.html())
+ @stateHolder = @root.find('.state')
+ @root.on('remove', this.destroy) # automatically destroy self when removed from DOM
+
+ # populate in template and visual transition functions
+ for state, data of @states
+ data.template = $("#template-download-jamtrack-state-#{data.name}")
+
+ # start off in quiet state, but don't do it through transition system. The transition system expects a change, not initial state
+ @state = @states.quiet
+
+ this.showState()
+
+
+ # after you've created the DownloadJamTrack widget, call synchronize which will begin ensuring that the jamtrack
+ # is downloaded and ready to open
init: () =>
- @root = $($('#template-download-jamtrack').html())
+ @active = true
+ @root.addClass('active')
+ this.reset()
+
+ # check if we are in a browser or client
+ if !gon.isNativeClient
+ this.transition(@states.no_client)
+ else
+ this.transition(@states.initial)
+
+ # when done with the widget, call destroy; this ensures it's not still active, and tracks final metrics
+ destroy: () =>
+ $(this).off()
+ @active = false
+ @root.removeClass('active')
+ this.trackProgress()
+ # since we are not in a leave node, we need to report a state since this is effectively our end state
+ this.reset()
+
+ reset: () =>
+ @path = []
+ @attempts = 0
+ @tracked = false
+ @startTime = new Date()
+ # reset attemptedEnqueue to false, to allow one attempt to enqueue
+ @attemptedEnqueue = false
+ this.clearDownloadTimer()
+ this.clearTransitionTimer()
+ this.abortEnqueue()
+ this.abortGetJamTrackRight()
+ for state, data of @states
+ if data.timer?
+ clearInterval(data.timer)
+ data.timer = null
+
+ abortEnqueue: () =>
+ if @ajaxEnqueueAborted
+ @logger.debug("DownloadJamTrack: aborting ajax enqueue")
+ # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline
+ ajax = @ajaxEnqueueAborted
+ @ajaxEnqueueAborted = true
+ ajax.abort()
+
+ abortGetJamTrackRight: () =>
+ if @ajaxGetJamTrackRightAborted
+ @logger.debug("DownloadJamTrack: aborting ajax GetJamTrackRight")
+ # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline
+ ajax = @ajaxGetJamTrackRightAborted
+ @ajaxGetJamTrackRightAborted = true
+ ajax.abort()
+
+ showState: () =>
+ @state.stateStartTime = new Date();
+ @stateHolder.children().remove()
+ @stateHolder.append(context._.template(@state.template.html(), @jamTrack, { variable: 'data' }))
+ @stateHolder.find('.' + @size).removeClass('hidden')
+ @state.show()
+
+ # report a stat now that we've reached the end of this widget's journey
+ trackProgress: () =>
+
+ # do not double-report
+ if @tracked
+ return
+
+ if @path.length == 0
+ return
+
+ unless @state.leaf
+ # we've been asked to report at a non-leaf node, meaning the user must have cancelled
+ @path.push('user-cancelled')
+
+ flattened_path = @path.join('-')
+
+ data = {
+ value: 1,
+ path: flattened_path,
+ duration: (new Date().getTime() - @startTime.getTime()) / 1000,
+ attempts: @attempts,
+ user_id: context.JK.currentUserId,
+ user_name: context.JK.currentUserName}
+ if @state == @states.errored
+ data.result = 'error'
+ data.detail = @errorReason
+ else
+ data.result = 'success'
+
+ context.stats.write('web.jamtrack.downloader', data)
+ @tracked = true
+
+ showPackaging: () =>
+ @logger.debug("showing #{@state.name}")
+ this.expectTransition()
+
+ showDownloading: () =>
+ @logger.debug("showing #{@state.name}")
+ # while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually
+ context.jamClient.JamTrackDownload(@jamTrack.id, this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback())
+
+ showKeying: () =>
+ @logger.debug("showing #{@state.name}")
+ context.jamClient.JamTrackKeysRequest()
+ this.waitForState()
+
+ showQuiet: () =>
+ @logger.debug("showing #{@state.name}")
+
+ showInitial: () =>
+ @logger.debug("showing #{@state.name}")
+ @attempts = @attempts + 1
+ this.expectTransition()
+ context.JK.SubscriptionUtils.subscribe('jam_track_right', @jamTrack.jam_track_right_id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onJamTrackRightEvent)
+ this.checkState()
+
+ showError: () =>
+ @logger.debug("showing #{@state.name}")
+ context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id)
+
+ if @size == 'large'
+ @stateHolder.find('.msg').text(@errorMessage)
+ @stateHolder.find('.retry-button').click(this.retry)
+ else
+ @stateHolder.find('.msg').text(@jamTrack.name + ' (error)')
+ @stateHolder.find('.errormsg').text(@errorMessage)
+ @stateHolder.find('.retry-button').on('click', this.retry)
+
+ retryMsg = ''
+ if @attempts > 1
+ retryMsg = 'Continue retrying or contact support@jamkazam.com'
+
+ @stateHolder.find('.retry').text(retryMsg)
+
+ showSynchronized: () =>
+ @logger.debug("showing #{@state.name}")
+ context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id)
+
+ showNoClient: () =>
+ @logger.debug("showing #{@state.name}")
+
+ downloadCheck: () =>
+ @logger.debug "downloadCheck"
+
+ retry: () =>
+ @path = []
+ @path.push('retry')
+ # just switch to the initial state again, causing the loop to start again
+ this.transition(@states.initial)
+ return false
+
+ clearStateTimer: () =>
+ if @state.timer?
+ clearInterval(@state.timer)
+ @state.timer = null
+
+ stateIntervalCheck: () =>
+ this.checkState()
+
+ # if the timer is null now, then it must have been whacked due to a state change
+ # if not, then let's see if we have timed out
+ if @state.timer?
+ if (new Date()).getTime() - @state.stateStartTime.getTime() > @state.max_time
+ @logger.debug("The current step (#{@state.name}) took too long")
+
+ if @state == @states.keying
+ # specific message
+ this.transitionError("#{@state.name}-timeout", "It took too long for the JamTrack to be keyed.")
+ else
+ # generic message
+ this.transitionError("#{@state.name}-timeout", "The current step (#{@state.name}) took too long")
+
+
+ # sets an interval timer for every second, waiting for the status to change
+ waitForState: () =>
+ unless @active
+ @logger.error("DownloadJamTrack: ignoring waitForState because we are not active")
+
+ @state.timer = setInterval(this.stateIntervalCheck, 1000)
+
+
+ # unused atm; the backend is good about always signalling. we still should though
+ expectDownload: () =>
+ unless @active
+ @logger.error("DownloadJamTrack: ignoring expectDownload because we are not active")
+
+ # every 10 seconds, wake up and check the server and see if we missed a state transition
+ this.clearDownloadTimer()
+ @downloadTimer = setTimeout(this.downloadCheck, 10000)
+
+ clearDownloadTimer: () =>
+ if @downloadTimer?
+ clearTimeout(@downloadTimer)
+ @downloadTimer = null
+
+ transitionError: (reasonCode, errorMessage) =>
+ @errorReason = reasonCode
+ @errorMessage = errorMessage
+ this.transition(@states.errored)
+
+ transitionCheck: () =>
+ this.checkState()
+
+ # this should be called every time something changes statefully, to restart a 12 second timer to hit the server for update.
+ # if everything is moving snappily, we won't have to query the server much, because we are also getting subscription events
+ # about any changes to the status of the jam track. But, we could miss a message or there could be a path in the server where
+ # we don't get an event, so that's why, after 12 seconds, we'll still go to the server and check.
+ # exception: this should not be runngi
+ expectTransition: () =>
+ unless @active
+ @logger.error("DownloadJamTrack: ignoring expectTransition because we are not active")
+
+ # every 12 seconds, wake up and check the server and see if we missed a state transition
+ this.clearTransitionTimer()
+ @transitionTimer = setTimeout(this.transitionCheck, 12000)
+
+ clearTransitionTimer: () =>
+ if @transitionTimer?
+ clearTimeout(@transitionTimer)
+ @transitionTimer = null
+
+ transition: (newState) =>
+ unless @active
+ @logger.error("DownloadJamTrack: ignoring state change because we are not active")
+ return
+
+ if newState == @state
+ @logger.debug("DownloadJamTrack: ignoring state change #{@state.name} #{newState}")
+ return
+
+ if @state?
+ @logger.debug("DownloadJamTrack: state change: #{@state.name} => #{newState.name}")
+ # make sure there is no timer running on the old state
+ this.clearTransitionTimer()
+ this.clearStateTimer()
+ this.abortEnqueue()
+ @logger.debug("aborting getJamTrack right on state change")
+ this.abortGetJamTrackRight()
+ else
+ @logger.debug("DownloadJamTrack: initial state: #{newState.name}")
+
+ @state = newState
+
+ # track which states were taken
+ @path.push(@state.name)
+
+ if @state.leaf
+ this.trackProgress()
+
+ this.showState()
+
+ $(this).triggerHandler(@EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, {state: @state})
+
+ checkState: () =>
+ # check for the success state against the local state of the client... if it's playable, then we should be OK
+ @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id)
+
+ @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail.key_state: " + @trackDetail.key_state)
+
+ # first check if the version is not the same; if so, invalidate.
+
+ if @trackDetail.version?
+ if @jamTrack.version != @trackDetail.version
+ @logger.info("DownloadJamTrack: JamTrack on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.version}. Invalidating")
+ context.jamClient.InvalidateJamTrack(@jamTrack.id)
+ @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id)
+
+ if @trackDetail.version?
+ @logger.error("after invalidating package, the version is still wrong!")
+ throw "after invalidating package, the version is still wrong!"
+
+ switch @trackDetail.key_state
+ when 'pending'
+ this.transition(@states.keying)
+ when 'not authorized'
+ # TODO: if not authorized, do we need to re-initiate a keying attempt?
+ this.transition(@states.keying)
+ when 'ready'
+ this.transition(@states.synchronized)
+ when 'unknown'
+ @ajaxGetJamTrackRightAborted = false
+ @rest.getJamTrackRight({id: @jamTrack.id})
+ .done(this.processJamTrackRight)
+ .fail(this.processJamTrackRightFail)
+
+
+ processSigningState: (signingState) =>
+ @logger.debug("DownloadJamTrack: processSigningState: " + signingState)
+
+ switch signingState
+ when 'QUIET'
+ if @attemptedEnqueue
+ # this means we've already tried to poke the server. something is wrong
+ this.transitionError("enqueue-timeout", "The server has not begun building your JamTrack.")
+ else
+ this.expectTransition()
+
+ this.attemptToEnqueue()
+ when 'QUEUED'
+ # when it's queued, there is nothing to do except wait.
+ this.transition(@states.packaging)
+ when 'QUEUED_TIMEOUT'
+ this.transitionError("queued-timeout", "The server took too long to begin processing your JamTrack.")
+ when 'SIGNING'
+ this.transition(@states.packaging)
+ when 'SIGNING_TIMEOUT'
+ this.transitionError("signing-timeout", "The server took too long to create your JamTrack.")
+ when 'SIGNED'
+ this.transition(@states.downloading)
+ when 'ERROR'
+ if @attemptedEnqueue
+ # this means we've already tried to poke the server. something is wrong
+ this.transitionError("package-error", "The server failed to create your package.")
+ else
+ this.expectTransition()
+
+ this.attemptToEnqueue()
+ else
+ @logger.error("unknown state: " + signingState)
+ this.transitionError("unknown-state-#{signingState}", "The server sent an unknown state message: " + signingState)
+
+ attemptToEnqueue: () =>
+ @attemptedEnqueue = true
+ @ajaxEnqueueAborted = false
+ @rest.enqueueJamTrack({id: @jamTrack.id})
+ .done(this.processEnqueueJamTrack)
+ .fail(this.processEnqueueJamTrackFail)
+
+
+ processJamTrackRight: (myJamTrack) =>
+ unless @ajaxGetJamTrackRightAborted
+ this.processSigningState(myJamTrack.signing_state)
+ else
+ @logger.debug("DownloadJamTrack: ignoring processJamTrackRight response")
+
+ processJamTrackRightFail: () =>
+ unless @ajaxGetJamTrackRightAborted?
+ this.transitionError("status-check-error", "Unable to check with the server on the status of your JamTrack.")
+ else
+ @logger.debug("DownloadJamTrack: ignoring processJamTrackRightFail response")
+
+ processEnqueueJamTrack: (enqueueResponse) =>
+ unless @ajaxEnqueueAborted
+ this.expectTransition() # the act of enqueuing should send down events to the client. we wait...
+ else
+ @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response")
+
+ processEnqueueJamTrackFail: () =>
+ unless @ajaxEnqueueAborted
+ this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.")
+ else
+ @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response")
+
+ onJamTrackRightEvent: (e, data) =>
+ @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type)
+ this.expectTransition()
+ this.processSigningState(data.body.signing_state)
+
+ downloadProgressCallback: (bytesReceived, bytesTotal, downloadSpeedMegSec, timeRemaining) =>
+ bytesReceived = Number(bytesReceived)
+ bytesTotal = Number(bytesTotal)
+ # bytesTotal from Qt is not trust worthy; trust server's answer instead
+ #progressWidth = ((bytesReceived / updateSize) * 100).toString() + "%";
+ # $('#progress-bar').width(progressWidth)
+
+ downloadSuccessCallback: (updateLocation) =>
+ # is the package loadable yet?
+ @logger.debug("DownloadJamTrack: download complete - on to keying")
+ this.transition(@states.keying)
+
+ downloadFailureCallback: (errorMsg) =>
+
+ this.transitionError("download-error", errorMsg)
+
+ # makes a function name for the backend
+ makeDownloadProgressCallback: () =>
+ "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadProgressCallback"
+
+ # makes a function name for the backend
+ makeDownloadSuccessCallback: () =>
+ "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadSuccessCallback"
+
+ # makes a function name for the backend
+ makeDownloadFailureCallback: () =>
+ "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadFailureCallback"
+
diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js
index b4cb3823d..ba66a44ca 100644
--- a/web/app/assets/javascripts/faderHelpers.js
+++ b/web/app/assets/javascripts/faderHelpers.js
@@ -20,7 +20,8 @@
e.stopPropagation();
var $fader = $(this);
-
+ var sessionModel = window.JK.CurrentSessionModel || null;
+
var mediaControlsDisabled = $fader.data('media-controls-disabled');
if(mediaControlsDisabled) {
var mediaTrackOpener = $fader.data('media-track-opener');
@@ -28,11 +29,16 @@
return false;
}
+ if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') {
+ window.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $fader.closest('.screen')})
+ return false;
+ }
+
if($fader.data('showHelpAboutMediaMixers')) {
- if(window.JK.CurrentSessionModel) {
- if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) {
+ if(sessionModel) {
+ if(!sessionModel.hasShownAudioMediaMixerHelp()) {
window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')})
- window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp()
+ sessionModel.markShownAudioMediaMixerHelp()
}
}
}
@@ -138,10 +144,16 @@
var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled');
var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener');
-
+ var sessionModel = window.JK.CurrentSessionModel || null;
+
if(mediaControlsDisabled) {
return false;
}
+
+ if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $draggingFaderHandle.closest('.session-track').data('track_data').type == 'jam_track') {
+ return false;
+ }
+
return true;
}
diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js
index 4db550fcb..7a15ffbb1 100644
--- a/web/app/assets/javascripts/fakeJamClient.js
+++ b/web/app/assets/javascripts/fakeJamClient.js
@@ -707,6 +707,16 @@
function TrackSetInstrument(track, instrumentId) {}
+ function JamTrackStopPlay() {}
+ function JamTrackPlay(){return true; }
+ function JamTrackIsPlayable() {
+ return true;
+ }
+ function JamTrackGetTrackDetail() {
+ return {key_state: 'unknown'}
+ }
+ function JamTrackKeysRequest() {}
+ function JamTrackDownload() {}
// Method which sets volume
function UpdateMixer(mixerId) {}
@@ -1062,6 +1072,13 @@
this.TrackGetChatUsesMusic = TrackGetChatUsesMusic;
this.TrackSetChatUsesMusic = TrackSetChatUsesMusic;
+ this.JamTrackStopPlay = JamTrackStopPlay;
+ this.JamTrackPlay = JamTrackPlay;
+ this.JamTrackIsPlayable = JamTrackIsPlayable;
+ this.JamTrackGetTrackDetail = JamTrackGetTrackDetail;
+ this.JamTrackKeysRequest = JamTrackKeysRequest;
+ this.JamTrackDownload = JamTrackDownload;
+
// Scoring Knobs
this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval;
this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval;
diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js
index 584eba17b..01c640719 100644
--- a/web/app/assets/javascripts/globals.js
+++ b/web/app/assets/javascripts/globals.js
@@ -45,7 +45,8 @@
SUBSCRIBE_NOTIFICATION: 'subscribe_notification',
CONNECTION_UP: 'connection_up',
CONNECTION_DOWN: 'connection_down',
- SCREEN_CHANGED: 'screen_changed'
+ SCREEN_CHANGED: 'screen_changed',
+ JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state'
};
context.JK.ALERT_NAMES = {
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index c89414c64..29993f644 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -1448,6 +1448,28 @@
});
}
+ function getJamTrackRight(options) {
+ var jamTrackId = options['id'];
+
+ return $.ajax({
+ type: "GET",
+ url: '/api/jamtracks/rights/' + jamTrackId + '?' + $.param(options),
+ dataType: "json",
+ contentType: 'application/json'
+ })
+ }
+
+ function enqueueJamTrack(options) {
+ var jamTrackId = options['id'];
+
+ return $.ajax({
+ type: "POST",
+ url: '/api/jamtracks/enqueue?' + jamTrackId + '?' + $.param(options),
+ dataType: "json",
+ contentType: 'applications/json'
+ });
+ }
+
function getPurchasedJamTracks(options) {
return $.ajax({
type: "GET",
@@ -1577,13 +1599,23 @@
});
}
- function validateUrlSite(url, sitetype) {
+ function validateUrlSite(url, sitetype) {
+ return $.ajax({
+ type: "GET",
+ url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url),
+ contentType: 'application/json'
+ });
+ }
+
+ function addRecordingTimeline(recordingId, data) {
return $.ajax({
- type: "GET",
- url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url),
- contentType: 'application/json'
+ type: "POST",
+ url: '/api/recordings/' + recordingId + '/timeline',
+ dataType: "json",
+ contentType: 'application/json',
+ data: JSON.stringify(data),
});
- }
+ }
function initialize() {
return self;
@@ -1709,6 +1741,8 @@
this.updateAudioLatency = updateAudioLatency;
this.getJamtracks = getJamtracks;
this.getPurchasedJamTracks = getPurchasedJamTracks;
+ this.getJamTrackRight = getJamTrackRight;
+ this.enqueueJamTrack = enqueueJamTrack;
this.getBackingTracks = getBackingTracks;
this.addJamtrackToShoppingCart = addJamtrackToShoppingCart;
this.getShoppingCarts = getShoppingCarts;
@@ -1725,6 +1759,7 @@
this.createSourceChange = createSourceChange;
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
+ this.addRecordingTimeline = addRecordingTimeline;
return this;
};
diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js
index fb741ad31..c5eb34e65 100644
--- a/web/app/assets/javascripts/order.js
+++ b/web/app/assets/javascripts/order.js
@@ -4,9 +4,12 @@
context.JK = context.JK || {};
context.JK.OrderScreen = function(app) {
+ var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var $screen = null;
+ var $templateOrderContent = null;
+ var $templatePurchasedJamTrack = null;
var $navigation = null;
var $billingInfo = null;
var $shippingInfo = null;
@@ -16,15 +19,23 @@
var $paymentInfoPanel = null;
var $orderPanel = null;
var $thanksPanel = null;
+ var $jamTrackInBrowser = null;
+ var $purchasedJamTrack = null;
+ var $purchasedJamTrackHeader = null;
+ var $purchasedJamTracks = null;
var $orderContent = null;
var userDetail = null;
var step = null;
var billing_info = null;
var shipping_info = null;
var shipping_as_billing = null;
+ var downloadJamTracks = [];
+ var purchasedJamTracks = null;
+ var purchasedJamTrackIterator = 0;
function beforeShow() {
- beforeShowPaymentInfo();
+ beforeShowPaymentInfo();
+ resetJamTrackDownloadInfo();
}
function beforeShowPaymentInfo() {
@@ -33,6 +44,12 @@
renderAccountInfo();
}
+ function resetJamTrackDownloadInfo() {
+ $purchasedJamTrack.addClass('hidden');
+ $purchasedJamTracks.children().remove()
+ $jamTrackInBrowser.hide('hidden');
+ }
+
function renderAccountInfo() {
rest.getUserDetail()
.done(populateAccountInfo)
@@ -81,6 +98,21 @@
}
function afterShow(data) {
+ // XXX : style-test code
+ // moveToThanks({jam_tracks: [{id: 14, jam_track_right_id: 11, name: 'Back in Black'}, {id: 15, jam_track_right_id: 11, name: 'In Bloom'}, {id: 16, jam_track_right_id: 11, name: 'Love Bird Supreme'}]});
+ }
+
+ function beforeHide() {
+ if(downloadJamTracks) {
+ context._.each(downloadJamTracks, function(downloadJamTrack) {
+ downloadJamTrack.destroy();
+ downloadJamTrack.root.remove();
+ })
+
+ downloadJamTracks = [];
+ }
+ purchasedJamTracks = null;
+ purchasedJamTrackIterator = 0;
}
function next(e) {
@@ -412,7 +444,7 @@
data.shipping_as_billing = shipping_as_billing
var orderContentHtml = $(
context._.template(
- $('#template-order-content').html(),
+ $templateOrderContent.html(),
data,
{variable: 'data'}
)
@@ -430,13 +462,86 @@
beforeShowOrder();
}
- function moveToThanks() {
+ function moveToThanks(purchaseResponse) {
$("#order_error").addClass("hidden")
$paymentInfoPanel.addClass("hidden")
$orderPanel.addClass("hidden")
$thanksPanel.removeClass("hidden")
rest.clearShoppingCart()
beforeShowOrder()
+ handleJamTracksPurchased(purchaseResponse.jam_tracks)
+ }
+
+ function handleJamTracksPurchased(jamTracks) {
+ // were any JamTracks purchased?
+ var jamTracksPurchased = jamTracks && jamTracks.length > 0;
+ if(jamTracksPurchased) {
+ if(gon.isNativeClient) {
+ startDownloadJamTracks(jamTracks)
+ }
+ else {
+ $jamTrackInBrowser.removeClass('hidden');
+ }
+ }
+ }
+ function startDownloadJamTracks(jamTracks) {
+ // there can be multiple purchased JamTracks, so we cycle through them
+
+ purchasedJamTracks = jamTracks;
+
+ // populate list of jamtracks purchased, that we will iterate through graphically
+ context._.each(jamTracks, function(jamTrack) {
+ var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small');
+ var $purchasedJamTrack = $(context._.template(
+ $templatePurchasedJamTrack.html(),
+ jamTrack,
+ {variable: 'data'}
+ ));
+
+ $purchasedJamTracks.append($purchasedJamTrack)
+
+ // show it on the page
+ $purchasedJamTrack.append(downloadJamTrack.root)
+
+ downloadJamTracks.push(downloadJamTrack)
+ })
+
+ iteratePurchasedJamTracks();
+ }
+
+ function iteratePurchasedJamTracks() {
+ if(purchasedJamTrackIterator < purchasedJamTracks.length ) {
+ var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++];
+
+ // make sure the 'purchasing JamTrack' section can be seen
+ $purchasedJamTrack.removeClass('hidden');
+
+ // the widget indicates when it gets to any transition; we can hide it once it reaches completion
+ $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) {
+
+ if(data.state == downloadJamTrack.states.synchronized) {
+ logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;")
+ //downloadJamTrack.root.remove();
+ downloadJamTrack.destroy();
+
+ // go to the next JamTrack
+ iteratePurchasedJamTracks()
+ }
+ })
+
+ logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing")
+
+ // kick off the download JamTrack process
+ downloadJamTrack.init()
+
+ // XXX style-test code
+ // downloadJamTrack.transitionError("package-error", "The server failed to create your package.")
+
+ }
+ else {
+ logger.debug("done iterating over purchased JamTracks")
+ $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.')
+ }
}
function moveToPaymentInfo(e) {
@@ -506,21 +611,28 @@
function initialize() {
var screenBindings = {
'beforeShow': beforeShow,
- 'afterShow': afterShow
+ 'afterShow': afterShow,
+ 'beforeHide' : beforeHide
};
app.bindScreen('order', screenBindings);
- $screen = $("#orderScreen");
- $paymentInfoPanel = $screen.find(".checkout-payment-info");
- $orderPanel = $screen.find(".order-panel");
- $thanksPanel = $screen.find(".thanks-panel");
- $navigation = $screen.find(".checkout-navigation-bar");
- $billingInfo = $paymentInfoPanel.find(".billing-address");
- $shippingInfo = $paymentInfoPanel.find(".shipping-address");
- $paymentMethod = $paymentInfoPanel.find(".payment-method");
- $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail");
- $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing");
- $orderContent = $orderPanel.find(".order-content");
+ $screen = $("#orderScreen");
+ $templateOrderContent = $("#template-order-content");
+ $templatePurchasedJamTrack = $('#template-purchased-jam-track');
+ $paymentInfoPanel = $screen.find(".checkout-payment-info");
+ $orderPanel = $screen.find(".order-panel");
+ $thanksPanel = $screen.find(".thanks-panel");
+ $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser");
+ $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track");
+ $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header");
+ $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list")
+ $navigation = $screen.find(".checkout-navigation-bar");
+ $billingInfo = $paymentInfoPanel.find(".billing-address");
+ $shippingInfo = $paymentInfoPanel.find(".shipping-address");
+ $paymentMethod = $paymentInfoPanel.find(".payment-method");
+ $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail");
+ $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing");
+ $orderContent = $orderPanel.find(".order-content");
if($screen.length == 0) throw "$screen must be specified";
if($navigation.length == 0) throw "$navigation must be specified";
diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js
index e05c185ae..3930635e6 100644
--- a/web/app/assets/javascripts/playbackControls.js
+++ b/web/app/assets/javascripts/playbackControls.js
@@ -47,6 +47,7 @@
var canUpdateBackend = false;
var playbackMode = PlaybackMode.EveryWhere;
var monitorPlaybackTimeout = null;
+ var jamTrackMode = false; // if true, we use different APIs to determine playback info
function startPlay() {
updateIsPlaying(true);
@@ -67,7 +68,7 @@
playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs);
updateCurrentTimeText(playbackPositionMs);
if(canUpdateBackend) {
- $self.triggerHandler('change-position', {positionMs: playbackPositionMs});
+ $self.triggerHandler('change-position', {positionMs: playbackPositionMs, jamTrackMode: jamTrackMode});
canUpdateBackend = false;
}
}
@@ -100,13 +101,25 @@
}
$playButton.on('click', function(e) {
- startPlay();
+ var sessionModel = context.JK.CurrentSessionModel || null;
+ if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') {
+ context.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $playButton})
return false;
+ }
+
+ startPlay();
+ return false;
});
$pauseButton.on('click', function(e) {
- stopPlay();
+ var sessionModel = context.JK.CurrentSessionModel || null;
+ if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') {
+ context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton})
return false;
+ }
+
+ stopPlay();
+ return false;
});
$sliderBar.on('click', function(e) {
@@ -144,16 +157,31 @@
});
function monitorRecordingPlayback() {
+ if(jamTrackMode) {
+ var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs();
+ var duration = context.jamClient.SessionGetJamTracksPlayDurationMs();
+ var durationMs = duration.media_len;
+ var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins
+ //console.log("JamTrack start: " + start)
+ }
+ else {
+ var positionMs = context.jamClient.SessionCurrrentPlayPosMs();
+ var durationMs = context.jamClient.SessionGetTracksPlayDurationMs();
+ }
+
var isPlaying = context.jamClient.isSessionTrackPlaying();
- var positionMs = context.jamClient.SessionCurrrentPlayPosMs();
- var durationMs = context.jamClient.SessionGetTracksPlayDurationMs();
+
+ if(positionMs < 0) {
+ // bug in backend?
+ positionMs = 0;
+ }
update(positionMs, durationMs, isPlaying);
monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500);
}
- function update(currentTimeMs, durationTimeMs, isPlaying) {
+ function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) {
if(dragging) {
return;
@@ -175,6 +203,10 @@
}
}
+
+ if(currentTimeMs < offsetStart) {
+ currentTimeMs = 0; // this is to squelch movement during tap-in period
+ }
updateDurationTime(durationTimeMs);
updateCurrentTime(currentTimeMs);
updateIsPlaying(isPlaying);
@@ -247,7 +279,10 @@
}
}
- function startMonitor() {
+ function startMonitor(_jamTrackMode) {
+
+ jamTrackMode = !!_jamTrackMode;
+
monitorRecordingPlayback();
}
diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js
index 20efadbf6..ad663cbc5 100644
--- a/web/app/assets/javascripts/profile.js
+++ b/web/app/assets/javascripts/profile.js
@@ -39,9 +39,9 @@
var $reverbNationPresence = $screen.find('#reverbnation-presence');
var $bandCampPresence = $screen.find('#bandcamp-presence');
var $fandalismPresence = $screen.find('#fandalism-presence');
- var $soundCloudPresence = $screen.find('#youtube-presence');
- var $soundCloudPresence = $screen.find('#facebook-presence');
- var $youTubePresence = $screen.find('#twitter-presence');
+ var $youTubePresence = $screen.find('#youtube-presence');
+ var $facebookPresence = $screen.find('#facebook-presence');
+ var $twitterPresence = $screen.find('#twitter-presence');
// current interests
var $noInterests = $screen.find('#no-interests');
@@ -96,6 +96,9 @@
var $btnAddFriend = $screen.find('#btn-add-friend');
var $btnFollowUser = $screen.find('#btn-follow-user');
var $btnMessageUser = $screen.find('#btn-message-user');
+ var $btnAddRecordings = $screen.find('.add-recordings');
+ var $btnAddSites = $screen.find('.add-sites');
+ var $btnAddInterests = $screen.find('.add-interests');
// social
var $socialLeft = $screen.find('.profile-social-left');
@@ -426,15 +429,121 @@
function bindAbout() {
- $instruments.empty();
+ if (!isCurrentUser()) {
+ $btnAddRecordings.hide();
+ $btnAddSites.hide();
+ $btnAddInterests.hide();
+ }
+ renderNameLocationStats();
+ renderBio();
+ renderMusicalExperience();
+ renderPerformanceSamples();
+ renderOnlinePresence();
+ renderInterests();
+ }
+
+ function renderNameLocationStats() {
// name
$userName.html(user.name);
// avatar
$avatar.attr('src', context.JK.resolveAvatarUrl(user.photo_url));
- // instruments
+ // location
+ $location.html(user.location);
+
+ $age.html(user.age ? user.age + " years old" : "");
+
+ // stats
+ var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend";
+ $friendStats.html('' + user.friend_count + '' + text);
+
+ text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower";
+ $followerStats.html('' + user.follower_count + '' + text);
+
+ if (isMusician()) {
+ text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session";
+ $sessionStats.html(user.session_count + text);
+
+ text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording";
+ $recordingStats.html(user.recording_count + text);
+ }
+ else {
+ text = " Following";
+ $followingStats.html(user.following_count + text);
+ text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite";
+ $favoriteStats.html(user.favorite_count + text);
+ }
+ }
+
+ function renderBio() {
+
+ function initializeBioVisibility() {
+
+ $showBio.hide();
+ $noBio.hide();
+ $biographyEditor.hide();
+
+ $bioTextArea.val(user.biography);
+
+ if(user.biography) {
+
+ $showBio.show();
+ $biographyText.text(user.biography).show();
+ }
+ else {
+ if(isCurrentUser()) {
+ $noBio.show();
+ }
+ }
+ }
+
+ initializeBioVisibility();
+
+ // $addBiographyButton.unbind('click').click(function() {
+ // $biographyEditor.val(user.biography).show();
+ // return false;
+ // });
+
+ $submitBiographyButton.unbind('click').click(function() {
+ var bio = $bioTextArea.val();
+ $bioTextArea.closest('div.field').removeClass('error');
+ $('.error-text', $bioTextArea.closest('div.field')).remove();
+ userDefer = rest.updateUser({
+ biography: bio
+ })
+ .done(function(response) {
+ user = response;
+ initializeBioVisibility();
+ })
+ .fail(function(jqXHR) {
+ if(jqXHR.status == 422) {
+ var errors = JSON.parse(jqXHR.responseText)
+ var biography = context.JK.format_errors("biography", errors);
+ if(biography != null) {
+ $bioTextArea.closest('div.field').addClass('error').end().after(biography);
+ }
+ else {
+ app.notifyServerError(jqXHR, "Unable to update biography")
+ }
+ }
+ else {
+ app.notifyServerError(jqXHR, "Unable to update biography")
+ }
+ })
+ return false;
+ })
+
+ $cancelBiographyButton.unbind('click').click(function() {
+ initializeBioVisibility();
+ return false;
+ })
+ }
+
+ function renderMusicalExperience() {
+ $instruments.empty();
+
if (user.instruments) {
for (var i = 0; i < user.instruments.length; i++) {
var instrument = user.instruments[i];
@@ -471,28 +580,20 @@
// studio gigs
var studioGigCount = user.studio_session_count;
$studioCount.html(studioGigCount > 0 ? 'Has played ' + profileUtils.gigMap[studioGigCount] + ' studio session gigs' : NOT_SPECIFIED_TEXT);
+ }
- // location
- $location.html(user.location);
-
- $age.html(user.age ? user.age + " years old" : "");
-
- // stats
- var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend";
- $friendStats.html('' + user.friend_count + '' + text);
-
- text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower";
- $followerStats.html('' + user.follower_count + '' + text);
-
- // text = user.following_count > 1 || user.following_count === 0 ? " Followings" : " Following";
- // $('#profile-following-stats').html('' + user.following_count + '' + text);
-
+ function renderPerformanceSamples() {
// performance samples
var performanceSamples = user.performance_samples;
if (!performanceSamples || performanceSamples.length === 0) {
$noSamples.show();
+
+ if (isCurrentUser()) {
+ $btnAddRecordings.show();
+ }
}
else {
+ $btnAddRecordings.hide();
$noSamples.hide();
// show samples section
@@ -512,24 +613,88 @@
$youTubeSamples.append("" + sample.service_id + "");
});
}
+ }
+ function renderOnlinePresence() {
// online presences
var onlinePresences = user.online_presences;
if ((!onlinePresences || onlinePresences.length === 0) && !user.website) {
$noOnlinePresence.show();
+
+ if (isCurrentUser()) {
+ $btnAddSites.show();
+ }
+ else {
+ $btnAddSites.hide();
+ }
}
else {
- $noOnlinePresence.hide();
+ $btnAddSites.hide();
+ $noOnlinePresence.hide();
- if (user.website) {
- $userWebsite.append("");
- }
-
- $.each(onlinePresences, function(index, presence) {
-
- });
+ if (user.website) {
+ $userWebsite.find('a').attr('href', user.website);
+ }
+
+ var soundCloudPresences = profileUtils.soundCloudPresences(onlinePresences);
+ if (soundCloudPresences && soundCloudPresences.length > 0) {
+ $soundCloudPresence.find('a').attr('href', 'http://www.soundcloud.com/' + soundCloudPresences[0].username);
+ }
+ else {
+ $soundCloudPresence.hide();
+ }
+
+ var reverbNationPresences = profileUtils.reverbNationPresences(onlinePresences);
+ if (reverbNationPresences && reverbNationPresences.length > 0) {
+ $reverbNationPresence.find('a').attr('href', 'http://www.reverbnation.com/' + reverbNationPresences[0].username);
+ }
+ else {
+ $reverbNationPresence.hide();
+ }
+
+ var bandCampPresences = profileUtils.bandCampPresences(onlinePresences);
+ if (bandCampPresences && bandCampPresences.length > 0) {
+ $bandCampPresence.find('a').attr('href', 'http://' + bandCampPresences[0].username + '.bandcamp.com/');
+ }
+ else {
+ $bandCampPresence.hide();
+ }
+
+ var fandalismPresences = profileUtils.fandalismPresences(onlinePresences);
+ if (fandalismPresences && fandalismPresences.length > 0) {
+ $fandalismPresence.find('a').attr('href', 'http://www.fandalism.com/' + fandalismPresences[0].username);
+ }
+ else {
+ $fandalismPresence.hide();
+ }
+
+ var youTubePresences = profileUtils.youTubePresences(onlinePresences);
+ if (youTubePresences && youTubePresences.length > 0) {
+ $youTubePresence.find('a').attr('href', 'http://www.youtube.com/' + youTubePresences[0].username);
+ }
+ else {
+ $youTubePresence.hide();
+ }
+
+ var facebookPresences = profileUtils.facebookPresences(onlinePresences);
+ if (facebookPresences && facebookPresences.length > 0) {
+ $facebookPresence.find('a').attr('href', 'http://www.facebook.com/' + facebookPresences[0].username);
+ }
+ else {
+ $facebookPresence.hide();
+ }
+
+ var twitterPresences = profileUtils.twitterPresences(onlinePresences);
+ if (twitterPresences && twitterPresences.length > 0) {
+ $twitterPresence.find('a').attr('href', 'http://www.twitter.com/' + twitterPresences[0].username);
+ }
+ else {
+ $twitterPresence.hide();
+ }
}
+ }
+ function renderInterests() {
// current interests
var noInterests = !user.paid_sessions && !user.free_sessions && !user.cowriting && !user.virtual_band && !user.traditional_band;
if (noInterests) {
@@ -539,9 +704,16 @@
$cowritingSection.hide();
$traditionalBandSection.hide();
$virtualBandSection.hide();
+
+ if (isCurrentUser()) {
+ $btnAddInterests.show();
+ }
}
else {
+ $btnAddInterests.hide();
+ $noInterests.hide();
+
// paid sessions
if (user.paid_sessions) {
$paidGigSection.show();
@@ -610,87 +782,6 @@
$virtualBandSection.hide();
}
}
-
- if (isMusician()) {
- text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session";
- $sessionStats.html(user.session_count + text);
-
- text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording";
- $recordingStats.html(user.recording_count + text);
- } else {
- text = " Following";
- $followingStats.html(user.following_count + text);
- text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite";
- $favoriteStats.html(user.favorite_count + text);
- }
-
- renderBio();
- }
-
- /** The biography show/edit functionality */
- function renderBio() {
-
-
- function initializeBioVisibility() {
-
- $showBio.hide();
- $noBio.hide();
- $biographyEditor.hide();
-
- $bioTextArea.val(user.biography);
-
- if(user.biography) {
-
- $showBio.show();
- $biographyText.text(user.biography).show();
- }
- else {
- if(isCurrentUser()) {
- $noBio.show();
- }
- }
- }
-
- initializeBioVisibility();
-
- // $addBiographyButton.unbind('click').click(function() {
- // $biographyEditor.val(user.biography).show();
- // return false;
- // });
-
- $submitBiographyButton.unbind('click').click(function() {
- var bio = $bioTextArea.val();
- $bioTextArea.closest('div.field').removeClass('error');
- $('.error-text', $bioTextArea.closest('div.field')).remove();
- userDefer = rest.updateUser({
- biography: bio
- })
- .done(function(response) {
- user = response;
- initializeBioVisibility();
- })
- .fail(function(jqXHR) {
- if(jqXHR.status == 422) {
- var errors = JSON.parse(jqXHR.responseText)
- var biography = context.JK.format_errors("biography", errors);
- if(biography != null) {
- $bioTextArea.closest('div.field').addClass('error').end().after(biography);
- }
- else {
- app.notifyServerError(jqXHR, "Unable to update biography")
- }
- }
- else {
- app.notifyServerError(jqXHR, "Unable to update biography")
- }
- })
- return false;
- })
-
- $cancelBiographyButton.unbind('click').click(function() {
- initializeBioVisibility();
- return false;
- })
}
/****************** SOCIAL TAB *****************/
diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js
index 32b5ce542..57b51448f 100644
--- a/web/app/assets/javascripts/profile_utils.js
+++ b/web/app/assets/javascripts/profile_utils.js
@@ -232,7 +232,7 @@
profileUtils.twitterPresences = function(presences) {
var matches = $.grep(presences, function(p) {
- return s.service_type === ONLINE_PRESENCE_TYPES.TWITTER.description;
+ return p.service_type === ONLINE_PRESENCE_TYPES.TWITTER.description;
});
return matches;
diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js
index 078a41c4d..a0e275f48 100644
--- a/web/app/assets/javascripts/recordingModel.js
+++ b/web/app/assets/javascripts/recordingModel.js
@@ -132,7 +132,6 @@
$self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments});
}
});
-
});
return true;
}
diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js
index b47946641..114c12c8a 100644
--- a/web/app/assets/javascripts/session.js
+++ b/web/app/assets/javascripts/session.js
@@ -82,6 +82,7 @@
var startingRecording = false; // double-click guard
var claimedRecording = null;
var backing_track_path = null;
+ var jamTrack = null;
var playbackControls = null;
var promptLeave = false;
var rateSessionDialog = null;
@@ -93,6 +94,10 @@
var $screen = null;
var $mixModeDropdown = null;
var $templateMixerModeChange = null;
+ var $otherAudioContainer = null;
+ var $myTracksContainer = null;
+ var $liveTracksContainer = null;
+ var downloadJamTrack = null;
var $closePlaybackRecording = null;
var $openBackingTrack = null;
var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup];
@@ -106,7 +111,7 @@
window.location = '/client#/home';
}
promptLeave = true;
- $('#session-mytracks-container').empty();
+ $myTracksContainer.empty();
displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe
var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session");
@@ -247,6 +252,7 @@
$(sessionModel.recordingModel)
.on('startingRecording', function(e, data) {
displayStartingRecording();
+ lockControlsforJamTrackRecording();
})
.on('startedRecording', function(e, data) {
if(data.reason) {
@@ -292,12 +298,30 @@
{
displayStartedRecording();
displayWhoCreated(data.clientId);
+ lockControlsforJamTrackRecording();
}
})
.on('stoppingRecording', function(e, data) {
displayStoppingRecording(data);
+ unlockControlsforJamTrackRecording();
})
.on('stoppedRecording', function(e, data) {
+
+ unlockControlsforJamTrackRecording();
+ if(sessionModel.selfOpenedJamTracks()) {
+
+ var timeline = context.jamClient.GetJamTrackTimeline();
+
+ rest.addRecordingTimeline(data.recordingId, timeline)
+ .fail(function(){
+ app.notify(
+ { title: "Unable to Add JamTrack Volume Data",
+ text: "The volume of the JamTrack will not be correct in the recorded mix." },
+ null,
+ true);
+ })
+ }
+
if(data.reason) {
logger.warn("Recording Discarded: ", data);
var reason = data.reason;
@@ -461,6 +485,14 @@
playbackControls.stopMonitor();
}
backing_track_path = currentSession == null ? null : currentSession.backing_track_path;
+
+ if(jamTrack == null && (currentSession && currentSession.jam_track != null)) {
+ playbackControls.startMonitor(true);
+ }
+ else if(jamTrack && (currentSession == null || currentSession.jam_track == null)) {
+ playbackControls.stopMonitor();
+ }
+ jamTrack = currentSession == null ? null : currentSession.jam_track;
}
function sessionChanged() {
@@ -518,6 +550,14 @@
}
}
+ function resetOtherAudioContent() {
+ if ($('.session-recordings .track').length === 0 && $('.session-recordings .download-jamtrack').length === 0) {
+ $('.session-recordings .when-empty').show();
+ $('.session-recording-name-wrapper').hide();
+ $('.session-recordings .recording-controls').hide();
+ $('.session-recordings .session-recording-name').text('(No audio loaded)')
+ }
+ }
function didSelfOpenMedia() {
var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER);
@@ -530,7 +570,7 @@
}
function renderSession() {
- $('#session-mytracks-container').empty();
+ $myTracksContainer.empty();
$('.session-track').remove(); // Remove previous tracks
var $voiceChat = $('#voice-chat');
$voiceChat.hide();
@@ -545,7 +585,9 @@
if ($('.session-livetracks .track').length === 0) {
$('.session-livetracks .when-empty').show();
}
-
+ resetOtherAudioContent();
+
+ /**
if ($('.session-recordings .track').length === 0) {
$('.session-recordings .when-empty').show();
$('.session-recording-name-wrapper').hide();
@@ -558,6 +600,7 @@
$('.session-recordings .recording-controls').show();
checkShowCloseControl();
}
+ */
// Handle long labels:
$(".track-label").dotdotdot()
@@ -1047,6 +1090,7 @@
// Default trackData to participant + no Mixer state.
var trackData = {
+ type: 'backing_track',
trackId: backingTrack.id,
clientId: backingTrack.client_id,
name: 'Backing',
@@ -1101,19 +1145,14 @@
// find the track or tracks that correspond to the mixer
var correspondingTracks = []
$.each(jamTracks, function(i, jamTrack) {
- if(mixer.id.indexOf("L") == 0) {
- if(mixer.id.substring(1) == jamTrack.id) {
+ if(mixer.id == jamTrack.id) {
correspondingTracks.push(jamTrack);
}
- else {
- // this should not be possible
- alert("Invalid state: the backing track had neither persisted_track_id or persisted_client_id");
- }
- }
});
if(correspondingTracks.length == 0) {
noCorrespondingTracks = true;
+ logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
app.notify({
title: "Unable to Open JamTrack",
text: "Could not correlate server and client tracks",
@@ -1145,6 +1184,7 @@
// Default trackData to participant + no Mixer state.
var trackData = {
+ type: 'jam_track',
trackId: oneOfTheTracks.id,
clientId: oneOfTheTracks.client_id,
name: name,
@@ -1254,6 +1294,7 @@
// Default trackData to participant + no Mixer state.
var trackData = {
+ type: 'metronome',
trackId: "MS" + oneOfTheTracks.id,
clientId: oneOfTheTracks.client_id,
name: "Metronome",
@@ -1361,6 +1402,7 @@
// Default trackData to participant + no Mixer state.
var trackData = {
+ type: 'recorded_track',
trackId: oneOfTheTracks.id,
clientId: oneOfTheTracks.client_id,
name: name,
@@ -1771,15 +1813,14 @@
function _addTrack(allowDelete, trackData, mixer, oppositeMixer) {
- var parentSelector = '#session-mytracks-container';
- var $destination = $(parentSelector);
+ var $destination = $myTracksContainer;
if (trackData.clientId !== app.clientId) {
- parentSelector = '#session-livetracks-container';
- $destination = $(parentSelector);
+ $destination = $liveTracksContainer
$('.session-livetracks .when-empty').hide();
}
var template = $('#template-session-track').html();
var newTrack = $(context.JK.fillTemplate(template, trackData));
+ newTrack.data('track_data', trackData)
var audioOverlay = $('.disabled-track-overlay', newTrack);
var $trackIconMute = newTrack.find('.track-icon-mute')
$trackIconMute.muteSelector().on(EVENTS.MUTE_SELECTED, trackMuteSelected)
@@ -1789,7 +1830,7 @@
$destination.append(newTrack);
// Render VU meters and gain fader
- var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
+ var trackSelector = $destination.selector + ' .session-track[track-id="' + trackData.trackId + '"]';
var gainPercent = trackData.gainPercent || 0;
connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id);
@@ -1805,21 +1846,31 @@
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
}
-
+ // something is being shown now in the other audio area
+ function otherAudioFilled() {
+ $('.session-recordings .when-empty').hide();
+ $('.session-recording-name-wrapper').show();
+ }
function _addRecordingTrack(trackData, mixer, oppositeMixer) {
+
+ otherAudioFilled();
+
+ $('.session-recordings .recording-controls').show();
+
var parentSelector = '#session-recordedtracks-container';
var $destination = $(parentSelector);
-
+
var template = $('#template-session-track').html();
var newTrack = $(context.JK.fillTemplate(template, trackData));
- $destination.append(newTrack);
+ newTrack.data('track_data', trackData);
+ $otherAudioContainer.append(newTrack);
if(trackData.preMasteredClass) {
context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')});
}
// Render VU meters and gain fader
- var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
+ var trackSelector = $otherAudioContainer.selector + ' .session-track[track-id="' + trackData.trackId + '"]';
var gainPercent = trackData.gainPercent || 0;
var $track = connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, null);
var $trackIconMute = $track.find('.track-icon-mute')
@@ -2072,6 +2123,11 @@
return false;
}
+ if(sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') {
+ context.JK.prodBubble($control, 'jamtrack-controls-disabled', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')})
+ return false;
+ }
+
if($control.data('showHelpAboutMediaMixers')) {
if(!sessionModel.hasShownAudioMediaMixerHelp()) {
context.JK.prodBubble($control, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')})
@@ -2079,6 +2135,8 @@
}
}
+
+
$.each(mixerIds, function(i,v) {
var mixerId = v;
// behavior: if this is the user's track in personal mode, then we mute the track globally
@@ -2316,6 +2374,14 @@
$('#recording-status').text("Make a Recording");
}
+ function lockControlsforJamTrackRecording() {
+ sessionModel.lockControlsforJamTrackRecording();
+ }
+
+ function unlockControlsforJamTrackRecording() {
+ sessionModel.unlockControlsforJamTrackRecording();
+ }
+
function displayWhoCreated(clientId) {
if(app.clientId != clientId) { // don't show to creator
sessionModel.findUserBy({clientId: clientId})
@@ -2382,7 +2448,69 @@
return false;
}
- app.layout.showDialog('open-jam-track-dialog');
+ app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) {
+
+ // once the dialog is closed, see if the user has a jamtrack selected
+ if(!data.canceled && data.result.jamTrack) {
+
+ var jamTrack = data.result.jamTrack;
+
+ // hide 'other audio' placeholder
+ otherAudioFilled();
+
+ if(downloadJamTrack) {
+ // if there was one showing before somehow, destroy it.
+ logger.warn("destroying existing JamTrack")
+ downloadJamTrack.root.remove();
+ downloadJamTrack.destroy();
+ downloadJamTrack = null
+ }
+
+ downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large');
+
+ // the widget indicates when it gets to any transition; we can hide it once it reaches completion
+ $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) {
+
+ if(data.state == downloadJamTrack.states.synchronized) {
+ logger.debug("jamtrack synchronized; hide widget and show tracks")
+ downloadJamTrack.root.remove();
+ downloadJamTrack.destroy();
+ downloadJamTrack = null;
+
+ // XXX: test with this removed; it should be unnecessary
+ context.jamClient.JamTrackStopPlay();
+
+ if(jamTrack.jmep)
+ {
+ logger.debug("setting jmep data")
+ context.jamClient.JamTrackLoadJmep(jamTrack.id, jamTrack.jmep)
+ }
+ else {
+ logger.debug("no jmep data for jamtrack")
+ }
+
+ // JamTrackPlay means 'load'
+ var result = context.jamClient.JamTrackPlay(jamTrack.id);
+
+ if(!result) {
+ app.notify(
+ { title: "JamTrack Can Not Open",
+ text: "Unable to open your JamTrack. Please contact support@jamkazam.com"
+ }, null, true);
+ }
+ }
+ })
+
+ // show it on the page
+ $otherAudioContainer.append(downloadJamTrack.root)
+
+ // kick off the download JamTrack process
+ downloadJamTrack.init()
+ }
+ else {
+ logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data)
+ }
+ })
return false;
}
@@ -2497,7 +2625,7 @@
if(sessionModel.recordedTracks()) {
closeRecording();
}
- else if(sessionModel.jamTracks()) {
+ else if(sessionModel.jamTracks() || downloadJamTrack) {
closeJamTrack();
}
else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) {
@@ -2533,6 +2661,20 @@
}
function closeJamTrack() {
+
+ logger.debug("closing recording");
+
+ if(downloadJamTrack) {
+ logger.debug("closing DownloadJamTrack widget")
+ downloadJamTrack.root.remove();
+ downloadJamTrack.destroy();
+ downloadJamTrack = null;
+
+ // this is necessary because a syncing widget means no jamtracks are loaded;
+ // so removing the widget will not cause a backend media change event (and so renderSession will not be called, ultimately)
+ resetOtherAudioContent();
+ }
+
rest.closeJamTrack({id: sessionModel.id()})
.done(function() {
sessionModel.refreshCurrentSession(true);
@@ -2567,6 +2709,8 @@
}
function closeRecording() {
+ logger.debug("closing recording");
+
rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id})
.done(function(response) {
//sessionModel.refreshCurrentSession(true);
@@ -2598,10 +2742,28 @@
function onChangePlayPosition(e, data){
logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")");
+
+ if(data.jamTrackMode) {
+ context.jamClient.SessionJamTrackSeekMs(data.positionMs);
+ }
+ else {
context.jamClient.SessionTrackSeekMs(data.positionMs);
+ }
}
function startStopRecording() {
+
+ // check first if a jamtrack is loaded, and playing; if so, tell user to stop the play
+ if(sessionModel.jamTracks() && context.jamClient.isSessionTrackPlaying()) {
+ app.notify(
+ { title: "Can't Recording a Play JamTrack",
+ text: "Stop the JamTrack before trying to recording." },
+ null,
+ true);
+
+ return;
+ }
+
if(sessionModel.recordingModel.isRecording()) {
sessionModel.recordingModel.stopRecording();
}
@@ -2720,6 +2882,9 @@
$screen = $('#session-screen');
$mixModeDropdown = $screen.find('select.monitor-mode')
$templateMixerModeChange = $('#template-mixer-mode-change');
+ $otherAudioContainer = $('#session-recordedtracks-container');
+ $myTracksContainer = $('#session-mytracks-container')
+ $liveTracksContainer = $('#session-livetracks-container');
$closePlaybackRecording = $('#close-playback-recording')
$openBackingTrack = $('#open-a-backingtrack');
events();
diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js
index 7487dec2d..f1d7988ec 100644
--- a/web/app/assets/javascripts/sessionModel.js
+++ b/web/app/assets/javascripts/sessionModel.js
@@ -37,6 +37,7 @@
var previousBackingTracks = [];
var openBackingTrack = null;
var shownAudioMediaMixerHelp = false;
+ var controlsLockedForJamTrackRecording = false;
var mixerMode = MIX_MODES.PERSONAL;
@@ -113,6 +114,11 @@
}
}
+ // did I open up the current JamTrack?
+ function selfOpenedJamTracks() {
+ return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId)
+ }
+
function backingTrack() {
if(currentSession) {
// TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
@@ -152,6 +158,18 @@
return inSession;
}
+ function lockControlsforJamTrackRecording() {
+ controlsLockedForJamTrackRecording = true;
+ }
+
+ function unlockControlsforJamTrackRecording() {
+ controlsLockedForJamTrackRecording = false;
+ }
+
+ function areControlsLockedForJamTrackRecording() {
+ return controlsLockedForJamTrackRecording;
+ }
+
function onMixerModeChanged(newMixerMode)
{
mixerMode = newMixerMode;
@@ -352,6 +370,7 @@
previousBackingTracks = []
openBackingTrack = null
shownAudioMediaMixerHelp = false
+ controlsLockedForJamTrackRecording = false;
}
// you should only update currentSession with this function
@@ -830,6 +849,10 @@
this.isMasterMixMode = isMasterMixMode;
this.isPersonalMixMode = isPersonalMixMode;
this.getMixMode = getMixMode;
+ this.selfOpenedJamTracks = selfOpenedJamTracks;
+ this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording;
+ this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording;
+ this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording;
// ALERT HANDLERS
this.onBackendMixerChanged = onBackendMixerChanged;
diff --git a/web/app/assets/javascripts/subscription_utils.js.coffee b/web/app/assets/javascripts/subscription_utils.js.coffee
index afd7852ab..b9b3e7823 100644
--- a/web/app/assets/javascripts/subscription_utils.js.coffee
+++ b/web/app/assets/javascripts/subscription_utils.js.coffee
@@ -84,6 +84,7 @@ class SubscriptionUtils
# call subscribe, and use the returned object to listen for events of name context.JK.EVENTS.SUBSCRIBE_NOTIFICATION
subscribe: (type, id) =>
+ id = id.toString()
key = this.genKey(type, id)
@logger.debug("subscribing for any notifications for #{key}")
@@ -104,6 +105,7 @@ class SubscriptionUtils
# TODO: this should not send a unsubscribe message to the server it's the last listener for the specific type/id combo
unsubscribe: (type, id) =>
+ id = id.toString()
key = this.genKey(type, id)
@logger.debug("unsubscribing for any notifications for #{key}")
diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js
index 1b6c1a575..fef57ccf5 100644
--- a/web/app/assets/javascripts/utils.js
+++ b/web/app/assets/javascripts/utils.js
@@ -910,6 +910,10 @@
return null;
}
+ context.JK.makeAbsolute = function(path) {
+ return window.location.protocol + '//' + window.location.host + path;
+ }
+
context.JK.popExternalLinks = function ($parent) {
if(!$parent) $parent = $('body');
diff --git a/web/app/assets/stylesheets/client/checkout.css.scss b/web/app/assets/stylesheets/client/checkout.css.scss
index 6f9bc6a40..efe918e22 100644
--- a/web/app/assets/stylesheets/client/checkout.css.scss
+++ b/web/app/assets/stylesheets/client/checkout.css.scss
@@ -168,6 +168,40 @@
.thanks-panel {
padding: 30px;
+
+ span.notice {
+ font-style:italic;
+ font-size:12px;
+ }
+
+ br.purchase-downloads {
+ clear:both;
+ margin-bottom:20px;
+ }
+
+ .thanks-detail.purchased-jam-track {
+
+ margin-top:20px;
+
+ .purchased-jam-track-header {
+ font-size: 15px;
+ margin-bottom:10px;
+ }
+
+ ul.purchased-list {
+ float:left;
+ margin:20px 100px 0 20px;
+
+ li {
+ margin:0;
+ }
+ }
+
+ .download-jamtrack {
+ width:auto;
+ vertical-align: middle; // to make bullets mid-align when error shows
+ }
+ }
}
.order-panel {
diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css
index 8403cf75a..9e1899ce3 100644
--- a/web/app/assets/stylesheets/client/client.css
+++ b/web/app/assets/stylesheets/client/client.css
@@ -66,4 +66,5 @@
*= require jquery.Jcrop
*= require icheck/minimal/minimal
*= require users/syncViewer
+ *= require ./downloadJamTrack
*/
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss
index 1cfe42004..27eb232a0 100644
--- a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss
+++ b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss
@@ -1,5 +1,77 @@
@import "client/common";
.download-jamtrack {
+ display:inline-block;
+ width:100%;
+ .retry-button {
+ margin-top:20px;
+ }
+
+ .retry {
+ margin-top:10px;
+ }
+
+ .msg {
+ white-space: normal;
+ }
+
+ .spinner-large {
+ margin:20px auto 0;
+ text-align:center;
+ }
+
+ .small {
+ .state {
+ text-align:left;
+ }
+ font-size:inherit;
+ .msg {
+ line-height: 32px;
+ height: 32px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ .spinner-small {
+ display:inline-block;
+ vertical-align:middle;
+ }
+ }
+
+ .large {
+ .state {
+ text-align:center;
+ }
+ }
+
+ &.active {
+
+ .small {
+ margin-bottom:5px;
+
+ .msg {
+ font-weight:bold;
+ color:white;
+ display:inline;
+ }
+ .errormsg {
+ display:block;
+ font-size:14px;
+ }
+ .retry {
+ display:block;
+ margin:3px 0 0 0;
+ font-size:14px;
+ }
+ .retry-button {
+ float:right;
+ margin:5px 0 5px 20px;
+ }
+
+ .msg-holder {
+ display:block;
+ }
+ }
+
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss
index fc6f8ad17..7f38661c6 100644
--- a/web/app/assets/stylesheets/client/session.css.scss
+++ b/web/app/assets/stylesheets/client/session.css.scss
@@ -148,6 +148,17 @@
}
}
+ .session-recording-name {
+ width:60%;
+ overflow:hidden;
+ margin-top:9px;
+ margin-bottom:8px;
+ font-size:16px;
+ }
+
+ .download-jamtrack {
+ margin-top:50px;
+ }
}
diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb
index cb257be16..bd58c1f0f 100644
--- a/web/app/controllers/api_jam_tracks_controller.rb
+++ b/web/app/controllers/api_jam_tracks_controller.rb
@@ -58,14 +58,21 @@ class ApiJamTracksController < ApiController
end
def keys
- jamtrack_ids = params[:jamtracks]
+ jamtrack_holder = params[:jamtracks]
- unless jamtrack_ids.kind_of?(Array)
- render :json => {message: 'jamtracks parameter must be an array'}, :status => 200
+ unless jamtrack_holder.kind_of?(Hash)
+ render :json => {message: 'jamtracks parameter must be an hash'}, :status => 422
return
end
- @jam_tracks = JamTrackRight.list_keys(current_user, params[:jamtracks])
+ jamtrack_ids = jamtrack_holder[:tracks]
+
+ unless jamtrack_ids.kind_of?(Array)
+ render :json => {message: 'jamtracks:tracks parameter must be an array'}, :status => 422
+ return
+ end
+
+ @jam_tracks = JamTrackRight.list_keys(current_user, jamtrack_ids)
render "api_jam_tracks/list_keys", :layout => nil
end
diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb
index 5caa975a8..89923b89d 100644
--- a/web/app/controllers/api_recordings_controller.rb
+++ b/web/app/controllers/api_recordings_controller.rb
@@ -1,7 +1,7 @@
class ApiRecordingsController < ApiController
before_filter :api_signed_in_user, :except => [ :add_like ]
- before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ]
+ before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim, :add_timeline_data ]
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 ]
@@ -377,6 +377,13 @@ class ApiRecordingsController < ApiController
end
end
+ # metadata
+ def add_timeline
+ @recording.add_timeline(params[:metadata])
+
+ render :json => {}, :status => 200
+ end
+
private
def lookup_recording
diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb
index cd0aa4b55..28d16933b 100644
--- a/web/app/controllers/api_recurly_controller.rb
+++ b/web/app/controllers/api_recurly_controller.rb
@@ -57,20 +57,36 @@ class ApiRecurlyController < ApiController
def place_order
error=nil
puts "PLACING ORDER #{params.inspect}"
+ response = {jam_tracks:[]}
+
+ # 1st confirm that all specified JamTracks exist
+ jam_tracks = []
+
params[:jam_tracks].each do |jam_track_id|
jam_track = JamTrack.where("id=?", jam_track_id).first
if jam_track
- @client.place_order(current_user, jam_track)
+ jam_tracks << jam_track
else
error="JamTrack not found for '#{jam_track_id}'"
break
end
end
+ # then buy each
+ unless error
+ jam_tracks.each do |jam_track|
+ jam_track_right = @client.place_order(current_user, jam_track)
+ # build up the response object with JamTracks that were purchased.
+ # if this gets more complicated, we should switch to RABL
+ response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id}
+ end
+ end
+
+
if error
render json: { errors: {message:error}}, :status => 404
else
- render :json=>{}, :status=>200
+ render :json=>response, :status=>200
end
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb
index 4bfc90e54..ab6da676d 100644
--- a/web/app/controllers/spikes_controller.rb
+++ b/web/app/controllers/spikes_controller.rb
@@ -33,11 +33,26 @@ class SpikesController < ApplicationController
def subscription
+ #Notification.send_reload(MessageFactory::ALL_NATIVE_CLIENTS)
+
Notification.send_subscription_message('test', '1', '{"msg": "oh hai 1"}')
Notification.send_subscription_message('test', '2', '{"msg": "oh hai 2"}')
render text: 'oh hai'
end
+ def download_jam_track
+
+ jamTrack = JamTrack.find(params[:jam_track_id])
+ jamTrackRight = jamTrack.right_for_user(current_user)
+
+ gon.jamTrackId = jamTrack.id
+ gon.jamTrackRightId = jamTrackRight.id
+ gon.size = params[:size] ? params[:size] : 'large'
+ gon.switchState = params[:state]
+
+ render :layout => 'web'
+ end
+
def site_validate
render :layout => 'web'
end
@@ -45,5 +60,4 @@ class SpikesController < ApplicationController
def recording_source
render :layout => 'web'
end
-
end
diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl
index aa7de2609..91174f87b 100644
--- a/web/app/views/api_claimed_recordings/show.rabl
+++ b/web/app/views/api_claimed_recordings/show.rabl
@@ -20,8 +20,16 @@ node :mix do |claimed_recording|
end
child(:recording => :recording) {
- attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count
-
+ attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count, :jam_track_id, :jam_track_initiator_id
+
+ child(:jam_track => :jam_track) {
+ attributes :id
+
+ node :jmep do |jam_track|
+ jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil
+ end
+ }
+
child(:band => :band) {
attributes :id, :name, :location, :photo_url
}
@@ -50,6 +58,15 @@ child(:recording => :recording) {
end
}
+ child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) {
+ node do |recorded_jam_track_track|
+ {
+ id: recorded_jam_track_track.jam_track_track.id,
+ timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : []
+ }
+ end
+ }
+
child(:comments => :comments) {
attributes :comment, :created_at
diff --git a/web/app/views/api_jam_tracks/list_keys.rabl b/web/app/views/api_jam_tracks/list_keys.rabl
index 43a33b24e..f82aa5650 100644
--- a/web/app/views/api_jam_tracks/list_keys.rabl
+++ b/web/app/views/api_jam_tracks/list_keys.rabl
@@ -2,7 +2,7 @@ object @jam_tracks
node do |jam_track|
{
- id: jam_track['id'],
+ id: jam_track['id'].to_s,
private: jam_track['private_key'],
error: jam_track['private_key'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' )
}
diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl
index f9ea856db..cab264aa5 100644
--- a/web/app/views/api_jam_tracks/show.rabl
+++ b/web/app/views/api_jam_tracks/show.rabl
@@ -1,6 +1,6 @@
object @jam_track
-attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price
+attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price, :version
node :genres do |item|
[item.genre.description] # XXX: need to return single genre; not array
diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl
index 4e708a4ac..364b13031 100644
--- a/web/app/views/api_jam_tracks/show_for_client.rabl
+++ b/web/app/views/api_jam_tracks/show_for_client.rabl
@@ -1,6 +1,14 @@
object @jam_track
-attributes :id, :name, :description, :initial_play_silence, :original_artist
+attributes :id, :name, :description, :initial_play_silence, :original_artist, :version
+
+node :jmep do |jam_track|
+ jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil
+end
+
+node :jam_track_right_id do |jam_track|
+ jam_track.right_for_user(current_user).id
+end
child(:jam_track_tracks => :tracks) {
attributes :id, :part, :instrument
diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl
index cd277c3fe..7c1b0c8de 100644
--- a/web/app/views/api_music_sessions/show.rabl
+++ b/web/app/views/api_music_sessions/show.rabl
@@ -13,7 +13,7 @@ if !current_user
}
else
- attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active
+ 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, :jam_track_initiator_id
node :can_join do |session|
session.can_join?(current_user, true)
diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl
index f3c817b07..a717a6bfb 100644
--- a/web/app/views/api_recordings/show.rabl
+++ b/web/app/views/api_recordings/show.rabl
@@ -1,6 +1,6 @@
object @recording
-attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?
+attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id
node :mix do |recording|
if recording.mix
@@ -12,6 +12,13 @@ node :mix do |recording|
end
end
+child(:jam_track => :jam_track) {
+ attributes :id
+
+ node :jmep do |jam_track|
+ jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil
+ end
+}
child(:band => :band) {
attributes :id, :name, :location, :photo_url
@@ -33,6 +40,15 @@ child(:recorded_backing_tracks => :recorded_backing_tracks) {
end
}
+child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) {
+ node do |recorded_jam_track_track|
+ {
+ id: recorded_jam_track_track.jam_track_track.id,
+ timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : []
+ }
+ end
+}
+
child(:comments => :comments) {
attributes :comment, :created_at
diff --git a/web/app/views/clients/_download_jamtrack_templates.html.slim b/web/app/views/clients/_download_jamtrack_templates.html.slim
index bf5831cb4..fef7f2cd8 100644
--- a/web/app/views/clients/_download_jamtrack_templates.html.slim
+++ b/web/app/views/clients/_download_jamtrack_templates.html.slim
@@ -1,2 +1,86 @@
script type="text/template" id='template-download-jamtrack'
.download-jamtrack
+ .state
+
+script type="text/template" id="template-download-jamtrack-state-no-client"
+ .state-no-client
+ .large.hidden
+ .msg
+ | To play your JamTrack, launch the JamKazam application and open the JamTrack while in a session.
+ .small.hidden
+ .msg
+ | {{data.name}} (launch client)
+
+script type="text/template" id="template-download-jamtrack-state-synchronized"
+ .state-synchronized
+ .large.hidden
+ .msg
+ | Your JamTrack is on your system and ready to play.
+ .small.hidden
+ .msg
+ | {{data.name}} (done)
+
+script type="text/template" id="template-download-jamtrack-state-packaging"
+ .state-packaging
+ .large.hidden
+ .msg
+ | Your JamTrack is currently being created on the JamKazam server.
+ .spinner-large
+ .small.hidden
+ .msg
+ | {{data.name}} (packaging)
+ .spinner-small
+
+script type="text/template" id="template-download-jamtrack-state-downloading"
+ .state-downloading
+ .large.hidden
+ .msg
+ | Your JamTrack is currently being downloaded.
+ .spinner-large
+ .small.hidden
+ .msg
+ | {{data.name}} (downloading)
+ .spinner-small
+
+script type="text/template" id="template-download-jamtrack-state-keying"
+ .state-keying
+ .large.hidden
+ .msg
+ | Your JamTrack is being authenticated.
+ .spinner-large
+ .small.hidden
+ .msg
+ | {{data.name}} (keying)
+ .spinner-small
+
+script type="text/template" id="template-download-jamtrack-state-initial"
+ .state-initial
+ .large.hidden
+ .msg
+ | Initializing JamTrack...
+ .spinner-large
+ .small.hidden
+ .msg
+ | {{data.name}} (initializing)
+ .spinner-small
+
+script type="text/template" id="template-download-jamtrack-state-quiet"
+ .state-quiet
+ .large.hidden
+ .msg
+ .small.hidden
+ .msg
+ | {{data.name}} (pending)
+
+script type="text/template" id="template-download-jamtrack-state-errored"
+ .state-errored
+ .large.hidden
+ .msg
+ .retry
+ a.button-orange.retry-button RETRY
+ .small.hidden
+ .msg-holder
+ .msg
+ a.button-orange.retry-button RETRY
+ .errormsg
+ .retry
diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim
index 7fe11020b..60646b6ec 100644
--- a/web/app/views/clients/_help.html.slim
+++ b/web/app/views/clients/_help.html.slim
@@ -199,6 +199,9 @@ 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-jamtrack-controls-disabled"
+ | During a recording, volume and mute controls for JamTracks are disabled. So, get the session volume levels right before starting the recording.
+
script type="text/template" id="template-help-volume-media-mixers"
| Audio files only expose both master and personal mix controls, so any change here will also affect everyone in the session.
diff --git a/web/app/views/clients/_order.html.slim b/web/app/views/clients/_order.html.slim
index 241af7217..ffab91d7a 100644
--- a/web/app/views/clients/_order.html.slim
+++ b/web/app/views/clients/_order.html.slim
@@ -178,8 +178,15 @@ div layout="screen" layout-id="order" id="orderScreen" class="screen secondary"
br
.thanks-detail We'll send you an email confirming your order shortly.
br
- .thanks-detail If you purchased any JamTracks, the next time you run the JamKazam application, your JamTracks will automatically be downloaded to the app, and you will receive a notification when the download is complete.
-
+ .thanks-detail.jam-tracks-in-browser.hidden
+ | To play your purchased JamTrack, launch the JamKazam application and open the JamTrack while in a session.
+ .thanks-detail.purchased-jam-track.hidden
+ h2.purchased-jam-track-header Downloading Your Purchased JamTracks
+ span Each JamTrack will be downloaded sequentially.
+ br
+ span.notice Note that you do not have to wait for this to complete in order to use your JamTrack later.
+ br.clear
+ ul.purchased-list
@@ -276,4 +283,7 @@ script type='text/template' id='template-order-content'
span and
'
a href="http://www.jamkazam.com/corp/returns" returns policy
- span .
\ No newline at end of file
+ span .
+
+script type='text/template' id='template-purchased-jam-track'
+ li data-jam-track-id="{{data.jam_track_id}}"
\ No newline at end of file
diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb
index e95e22c45..1a41e2c86 100644
--- a/web/app/views/clients/_profile.html.erb
+++ b/web/app/views/clients/_profile.html.erb
@@ -131,37 +131,47 @@
+