VRFS-3459 merging develop

This commit is contained in:
Jonathan Kolyer 2015-10-19 19:58:41 +00:00
commit 7a2dd6ea0b
122 changed files with 4098 additions and 327 deletions

View File

@ -0,0 +1,36 @@
ActiveAdmin.register JamRuby::AffiliateTrafficTotal, :as => 'Affiliate Daily Stats' do
menu :label => 'Daily Stats', :parent => 'Affiliates'
config.sort_order = 'referral_user_count DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = true
config.per_page = 50
config.paginate = true
filter :affiliate_partner
filter :day
filter :signups
filter :visits
form :partial => 'form'
scope("Active", default: true) { |scope| scope.where('visits != 0 or signups != 0').order('day desc') }
index do
# default_actions # use this for all view/edit/delete links
column 'Day' do |oo| oo.day end
column 'Partner' do |oo| link_to(oo.affiliate_partner, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end
column 'Signups' do |oo| oo.signups end
column 'Visits' do |oo| oo.visits end
end
controller do
end
end

View File

@ -7,10 +7,10 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do
config.filters = false
index do
column 'User' do |oo| link_to(oo.name, "http://www.jamkazam.com/client#/profile/#{oo.id}", {:title => oo.name}) end
column 'User' do |oo| link_to(oo.name, oo.admin_url, {:title => oo.name}) end
column 'Email' do |oo| oo.email end
column 'Created' do |oo| oo.created_at end
column 'Partner' do |oo| oo.affiliate_referral.partner_name end
column 'Partner' do |oo| oo.affiliate_referral.display_name end
end
controller do

View File

@ -6,10 +6,12 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do
config.batch_actions = false
# config.clear_action_items!
config.filters = false
config.per_page = 50
config.paginate = true
form :partial => 'form'
scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') }
scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') }
scope("Unpaid") { |partner| partner.unpaid }
index do

View File

@ -9,7 +9,7 @@ ActiveAdmin.register JamRuby::User, :as => 'User Progression' do
config.filters = false
index do
column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.first_name} #{user.last_name} (#{user.email})"}) end
column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.name} (#{user.email})"}) end
column :updated_at do |uu| uu.updated_at.strftime(PROGRESSION_DATE) end
column :created_at do |uu| uu.created_at.strftime(PROGRESSION_DATE) end
column :city

View File

@ -1,2 +1,2 @@
<%- headers = ['email', 'name', 'unsubscribe_token'] -%>
<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%>
<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.anonymous? ? '-' : user.first_name, user.unsubscribe_token]) %><%- end -%>

View File

@ -154,5 +154,8 @@ module JamAdmin
config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep"))
config.email_dump_code = 'rcAUyC3TZCbgGx4YQpznBRbNnQMXW5iKTzf9NSBfzMLsnw9dRQ'
config.admin_port = ENV['ADMIN_PORT'] || 3333
config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}"
end
end

View File

@ -306,4 +306,5 @@ jam_track_slug.sql
mixdown.sql
aac_master.sql
video_recording.sql
web_playable_jamtracks.sql
jam_track_lang_idx.sql

View File

@ -0,0 +1,16 @@
ALTER TABLE users ADD COLUMN first_opened_jamtrack_web_player TIMESTAMP;
ALTER TABLE jam_track_rights ADD COLUMN last_stem_id VARCHAR(64) REFERENCES jam_track_tracks(id) ON DELETE SET NULL;
ALTER TABLE jam_track_tracks ADD COLUMN url_mp3_48 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN md5_mp3_48 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN length_mp3_48 BIGINT;
ALTER TABLE jam_track_tracks ADD COLUMN url_aac_48 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN md5_aac_48 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN length_aac_48 BIGINT;
CREATE TABLE user_events (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
name VARCHAR(100) NOT NULL,
detail JSON,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -106,6 +106,7 @@ require "jam_ruby/models/max_mind_release"
require "jam_ruby/models/genre_player"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
require "jam_ruby/models/user_event"
require "jam_ruby/models/anonymous_user"
require "jam_ruby/models/signup_hint"
require "jam_ruby/models/machine_fingerprint"

View File

@ -1,5 +1,5 @@
<% provide(:title, 'Confirm Email') %>
<p>Welcome to JamKazam, <%= @user.first_name %>!</p>
<p>Welcome to JamKazam<%= @user.anonymous? ? '!' : ", #{@user.first_name}!" %></p>
<p>To confirm this email address, please go to the <a style="color: #ffcc00;" href="<%= @signup_confirm_url %>">signup confirmation page</a>.</p>

View File

@ -1,3 +1,3 @@
Welcome to JamKazam, <%= @user.first_name %>!
Welcome to JamKazam<%= @user.anonymous? ? '!' : ", #{@user.first_name}!" %>
To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.

View File

@ -1,5 +1,7 @@
<% provide(:title, 'New Musicians You Should Check Out') %>
Hi <%= @user.first_name %>,
<% if !@user.anonymous? %>
<p>Hi <%= @user.first_name %>,</p>
<% end %>
<p>The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:
</p>

View File

@ -1,7 +1,8 @@
New Musicians You Should Check Out
<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
<% end %>
The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:
<% @new_musicians.each do |user| %>

View File

@ -1,7 +1,8 @@
<% provide(:title, @title) %>
<p>Hello <%= @user.first_name %> --
</p>
<% if !@user.anonymous? %>
<p>Hi <%= @user.first_name %>,</p>
<% end %>
<p>The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.</p>

View File

@ -1,7 +1,8 @@
<% provide(:title, @title) %>
Hello <%= @user.first_name %> --
<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
<% end %>
The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.
GENRE | NAME | DESCRIPTION | LATENCY

View File

@ -1,18 +1,20 @@
<% provide(:title, 'JamKazam Session Reminder') %>
<div>
<% if !@user.anonymous? %>
<p>
Hi <%= @user.first_name %>,
</div>
</p>
<% end %>
<br/>
<div>
<p>
<span>This is a reminder that your JamKazam session</span>
<a href='<%=@session_url%>'><%= @session_name %></a>
<span>is scheduled for tomorrow. We hope you have fun!</span>
</div>
</p>
<br/>
<div>
<p>
Best Regards,
<br/>
Team JamKazam
</div>
</p>

View File

@ -1,5 +1,6 @@
<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
<% end %>
This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun!
Best Regards,

View File

@ -1,17 +1,20 @@
<% provide(:title, 'Your JamKazam session starts in 1 hour!') %>
<div>
<% if !@user.anonymous? %>
<p>
Hi <%= @user.first_name %>,
</div>
</p>
<% end %>
<br/>
<div>
<p>
<span>This is a reminder that your JamKazam session</span>
<a href='<%=@session_url%>'><%= @session_name %></a>
<span>starts in 1 hour. We hope you have fun!</span>
</div>
</p>
<br/>
<div>
<p>
Best Regards,
<br/>
Team JamKazam
</div>
</p>

View File

@ -1,4 +1,6 @@
<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
<% end %>
This is a reminder that your JamKazam session
<%=@session_name%>

View File

@ -981,6 +981,44 @@ module JamRuby
sorted_tracks
end
# this will put original_audio_s3_path on each jam_track_track
def associate_tracks_with_original_stems(jam_track, s3_path)
attempt_to_match_existing_tracks = true
# find all wav files in the JamTracks s3 bucket
wav_files = fetch_important_files(s3_path)
tracks = []
wav_files.each do |wav_file|
if attempt_to_match_existing_tracks
# try to find a matching track from the JamTrack based on the name of the 44.1 path
basename = File.basename(wav_file)
ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg"
found_track = nil
jam_track.jam_track_tracks.each do |jam_track_track|
if jam_track_track["url_44"] && jam_track_track["url_44"].end_with?(ogg_44100_filename)
# found a match!
found_track = jam_track_track
break
end
end
if found_track
@@log.debug("found a existing track to reuse")
found_track.original_audio_s3_path = wav_file
tracks << found_track
next
end
end
end
tracks
end
def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload)
attempt_to_match_existing_tracks = true
@ -1104,6 +1142,7 @@ module JamRuby
begin
Dir.mktmpdir do |tmp_dir|
generate_mp3_aac_stem(jam_track, tmp_dir, skip_audio_upload)
jam_track.jam_track_tracks.each do |track|
basename = File.basename(track.original_audio_s3_path)
@ -1204,6 +1243,87 @@ module JamRuby
return true
end
def generate_mp3_aac_stem(jam_track, tmp_dir, skip_audio_upload)
jam_track.jam_track_tracks.each do |track|
if track.original_audio_s3_path.nil?
@@log.error("jam_track #{jam_track.name} has empty stem. stem: #{track.id}")
next
end
basename = File.basename(track.original_audio_s3_path)
s3_dirname = File.dirname(track.original_audio_s3_path)
# make a 44100 version, and a 48000 version
mp3_48000_filename = File.basename(basename, ".wav") + "-48000.mp3"
aac_48000_filename = File.basename(basename, ".wav") + "-48000.aac"
mp3_48000_s3_path = track.filename(mp3_48000_filename)
aac_48000_s3_path = track.filename(aac_48000_filename)
track.skip_uploader = true
if skip_audio_upload
track["url_mp3_48"] = mp3_48000_filename
track["md5_mp3_48"] = 'md5'
track["length_mp3_48"] = 1
track["url_aac_48"] = aac_48000_filename
track["md5_aac_48"] = 'md5'
track["length_aac_48"] = 1
# we can't fake the preview as easily because we don't know the MD5 of the current item
#track["preview_md5"] = 'md5'
#track["preview_mp3_md5"] = 'md5'
#track["preview_url"] = track.preview_filename('md5', 'ogg')
#track["preview_length"] = 1
#track["preview_mp3_url"] = track.preview_filename('md5', 'mp3')
#track["preview_mp3_length"] = 1
#track["preview_start_time"] = 0
else
wav_file = File.join(tmp_dir, basename)
# the wave file might already be on the system...
# don't bother with the same track twice
next if track["url_mp3_48"] && track["url_aac_48"]
# bring the original wav file down from S3 to local file system
JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) unless File.exists?(wav_file)
mp3_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.mp3")
aac_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.aac")
`ffmpeg -i "#{wav_file}" -ar 48000 -ab 192k "#{mp3_48000}"`
`ffmpeg -i "#{wav_file}" -c:a libfdk_aac -b:a 192k "#{aac_48000}"`
# upload the new ogg files to s3
@@log.debug("uploading mp3 48000 to #{mp3_48000_s3_path}")
jamkazam_s3_manager.upload(mp3_48000_s3_path, mp3_48000)
@@log.debug("uploading aac 48000 to #{aac_48000_s3_path}")
jamkazam_s3_manager.upload(aac_48000_s3_path, aac_48000)
mp3_48000_digest = ::Digest::MD5.file(mp3_48000)
# and finally update the JamTrackTrack with the new info
track["url_mp3_48"] = mp3_48000_s3_path
track["md5_mp3_48"] = mp3_48000_digest.hexdigest
track["length_mp3_48"] = File.new(mp3_48000).size
track["url_aac_48"] = aac_48000_s3_path
track["md5_aac_48"] = ::Digest::MD5.file(aac_48000).hexdigest
track["length_aac_48"] = File.new(aac_48000).size
track.save
end
end
end
def synchronize_duration(jam_track, ogg_44100)
duration_command = "soxi -D \"#{ogg_44100}\""
output = `#{duration_command}`
@ -1527,6 +1647,11 @@ module JamRuby
id
end
def is_default_storage?
assert_storage_set
@storage_format == 'default'
end
def is_tency_storage?
assert_storage_set
@storage_format == 'Tency'
@ -1929,6 +2054,20 @@ module JamRuby
importer
end
def generate_mp3_aac_stem(jam_track)
importer = JamTrackImporter.new
importer.name = jam_track.name
Dir.mktmpdir do |tmp_dir|
audio_path = jam_track.metalocation[0...-"/meta.yml".length]
importer.associate_tracks_with_original_stems(jam_track, audio_path)
importer.generate_mp3_aac_stem(jam_track, tmp_dir, false)
end
importer
end
def download_masters
importers = []
@ -1958,6 +2097,67 @@ module JamRuby
filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '')
end
def generate_mp3_aac_stems(format)
importers = []
jam_tracks = []
tency = JamTrackLicensor.find_by_name('Tency Music')
@@log.info("processing storage #{@storage_format}")
if is_tency_storage?
tency = JamTrackLicensor.find_by_name!('Tency Music')
jam_tracks = JamTrack.where(licensor_id: tency.id)
elsif is_default_storage?
# XXX IF WE ADD ANOTHER STORAGE, UPDATE THE WHERE TO EXCLUDE IT AS WELL
jam_tracks = JamTrack.where('licensor_id is null OR licensor_id != ?', tency.id )
else
raise 'unknown storage format!'
end
jam_tracks.each do |jam_track|
if ENV['NODE_COUNT']
node_count = ENV['NODE_COUNT'].to_i
node_number = ENV['NODE_NUMBER'].to_i
raise "NO NODE_COUNT" if node_count == 0
jam_track_id = jam_track.id.to_i
jam_track_id = jam_track_id + node_number
if jam_track_id == 0
@@log.warn("skipping #{jam_track_id} because non-numeric ID")
next
elsif jam_track_id % node_count == 0
@@log.warn("starting JamTrack #{jam_track.id} (#{jam_track_id})")
importers << generate_mp3_aac_stem(jam_track)
else
@@log.warn("skipping #{jam_track_id}")
next
end
else
importers << generate_mp3_aac_stem(jam_track)
end
end
@@log.info("SUMMARY")
@@log.info("-------")
importers.each do |importer|
if importer
if importer.reason == "success"
@@log.info("#{importer.name} #{importer.reason}")
else
@@log.error("#{importer.name} failed to download.")
@@log.error("#{importer.name} reason=#{importer.reason}")
@@log.error("#{importer.name} detail=#{importer.detail}")
end
else
@@log.error("NULL IMPORTER")
end
end
end
def generate_slugs
JamTrack.all.each do |jam_track|
jam_track.generate_slug

View File

@ -50,6 +50,14 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
record.entity_type ||= ENTITY_TYPES.first
end
def display_name
partner_name || (partner_user ? partner_user.name : 'abandoned')
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/affiliates/" + id
end
# used by admin
def self.create_with_params(params={})
raise 'not supported'

View File

@ -16,7 +16,7 @@ module JamRuby
validates :jam_track, presence: true
validates :settings, presence: true
validates_uniqueness_of :name, scope: :user_id
validates_uniqueness_of :name, scope: [:user_id, :jam_track_id]
validate :verify_settings
validate :verify_max_mixdowns

View File

@ -7,9 +7,10 @@ module JamRuby
@@log = Logging.logger[JamTrackMixdownPackage]
# these are used as extensions for the files stored in s3
FILE_TYPE_MP3 = 'mp3'
FILE_TYPE_OGG = 'ogg'
FILE_TYPE_AAC = 'aac'
FILE_TYPES = [FILE_TYPE_OGG, FILE_TYPE_AAC]
FILE_TYPES = [FILE_TYPE_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC]
SAMPLE_RATE_44 = 44
SAMPLE_RATE_48 = 48
@ -135,8 +136,11 @@ module JamRuby
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120)
s3_manager.sign_url(self['url'], {:expires => expiration_time, :secure => true})
def sign_url(expiration_time = 120, content_type = nil, response_content_disposition = nil)
options = {:expires => expiration_time, :secure => true}
options[:response_content_type] = content_type if content_type
options[:response_content_disposition] = response_content_disposition if response_content_disposition
s3_manager.sign_url(self['url'], options)
end

View File

@ -12,6 +12,7 @@ module JamRuby
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right
belongs_to :last_stem, class_name: 'JamRuby::JamTrackTrack', foreign_key: 'last_stem_id', inverse_of: :jam_track_right
validates :user, presence: true
validates :jam_track, presence: true

View File

@ -34,6 +34,7 @@ module JamRuby
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
has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_stem_id', inverse_of: :last_stem
# create storage directory that will house this jam_track, as well as
def store_dir
@ -79,6 +80,18 @@ module JamRuby
end
end
def display_name
if track_type == 'Master'
'Master Mix'
else
display_part = ''
if part
display_part = "-(#{part})"
end
"#{instrument.description}#{display_part}"
end
end
def manually_uploaded_filename(mounted_as)
if track_type == 'Master'
filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg")
@ -102,6 +115,18 @@ module JamRuby
def sign_url(expiration_time = 120, sample_rate=48)
s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true})
end
def web_download_sign_url(expiration_time = 120, type='mp3', content_type = nil, response_content_disposition = nil)
options = {:expires => expiration_time, :secure => true}
options[:response_content_type] = content_type if content_type
options[:response_content_disposition] = response_content_disposition if response_content_disposition
url_field = self['url_' + type + '_48']
url_field = self['url_48'] if type == 'ogg' # ogg has different column format in database
s3_manager.sign_url(url_field, options)
end
def can_download?(user)
# I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download?

View File

@ -21,6 +21,10 @@ module JamRuby
.first
end
def name
user.name
end
def music_session
@msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id)
end

View File

@ -176,11 +176,13 @@ module JamRuby
has_one :musician_search, :class_name => 'JamRuby::MusicianSearch'
has_one :band_search, :class_name => 'JamRuby::BandSearch'
before_save :default_anonymous_names
before_save :create_remember_token, :if => :should_validate_password?
before_save :stringify_avatar_info , :if => :updating_avatar
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :first_name, length: {maximum: 50}, no_profanity: true
validates :last_name, length: {maximum: 50}, no_profanity: true
validates :last_name, length: {maximum: 50}, no_profanity: true
validates :biography, length: {maximum: 4000}, no_profanity: true
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
@ -295,8 +297,16 @@ module JamRuby
online?
end
def anonymous?
first_name == 'Anonymous' && last_name == 'Anonymous'
end
def name
"#{first_name} #{last_name}"
if anonymous?
'Anonymous'
else
"#{first_name} #{last_name}"
end
end
def location
@ -575,9 +585,7 @@ module JamRuby
def to_s
return email unless email.nil?
if !first_name.nil? && !last_name.nil?
return first_name + ' ' + last_name
end
return name unless name.nil?
id
end
@ -1680,14 +1688,17 @@ module JamRuby
else
false
end
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
def default_anonymous_names
self.first_name = 'Anonymous' if self.first_name.nil?
self.last_name = 'Anonymous' if self.last_name.nil?
end
def stringify_avatar_info
# fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR,
# so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object)

View File

@ -0,0 +1,8 @@
module JamRuby
class UserEvent < ActiveRecord::Base
belongs_to :user, class_name: 'JamRuby::User'
validates :name, presence: true
end
end

View File

@ -352,9 +352,15 @@ module JamRuby
# otherwise we need to convert from lastly created file to correct
output = File.join(tmp_dir, "output.#{@mixdown_package.file_type}")
raise 'unknown file_type' if @mixdown_package.file_type != JamTrackMixdownPackage::FILE_TYPE_AAC
if @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_AAC
cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac')
elsif @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_MP3
cmd("ffmpeg -i \"#{@speed_mix_file}\" -ab 192k \"#{output}\"", 'convert_mp3')
else
raise 'unknown file_type'
end
cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac')
output
end
end

View File

@ -101,12 +101,12 @@ describe User do
describe "when first name is not present" do
before { @user.first_name = " " }
it { should_not be_valid }
it { should be_valid }
end
describe "when last name is not present" do
before { @user.last_name = " " }
it { should_not be_valid }
it { should be_valid }
end
describe "when email is not present" do

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -226,7 +226,7 @@
app.clientId = payload.client_id;
if (isClientMode()) {
if (isClientMode() && context.jamClient) {
// tell the backend that we have logged in
context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION
$.cookie('client_id', payload.client_id);
@ -321,7 +321,7 @@
function socketClosed(in_error) {
// tell the backend that we have logged out
context.jamClient.OnLoggedOut();
if (context.jamClient) context.jamClient.OnLoggedOut();
}
@ -593,7 +593,10 @@
clientType = context.JK.clientType();
}
if(!mode) {
mode = context.jamClient.getOperatingMode ? context.jamClient.getOperatingMode() : 'client';
mode = 'client'
if (context.jamClient && context.jamClient.getOperatingMode) {
mode = context.jamClient.getOperatingMode()
}
}
connectDeferred = new $.Deferred();

View File

@ -61,6 +61,7 @@
//= require web/tracking
//= require webcam_viewer
//= require react-components
//= require playbackControls
//= require_directory .
//= require_directory ./dialog
//= require_directory ./wizard

View File

@ -15,12 +15,21 @@
// remove all display errors
$('#recording-finished-dialog form .error-text').remove()
$('#recording-finished-dialog form .error').removeClass("error")
console.log("save video?", recording)
if(recording.video) {
$dialog.find('.save-video').show()
if(recording.owner.id == context.JK.currentUserId) {
// only the owner of the video gets to see video options
$dialog.find('.save-video').show()
$dialog.find('.upload-to-youtube').show()
}
else {
$dialog.find('.save-video').hide()
$dialog.find('.upload-to-youtube').hide()
}
}
else {
$dialog.find('.save-video').hide()
$dialog.find('.upload-to-youtube').hide()
}
removeGoogleLoginErrors()
}
@ -209,6 +218,8 @@
var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked')
var recording_id = recording.id
var recording_video = recording.video
rest.claimRecording({
id: recording.id,
name: name,
@ -222,7 +233,7 @@
$dialog.data('result', {keep:true});
app.layout.closeDialog('recordingFinished');
context.JK.GA.trackMakeRecording();
if(save_video && upload_to_youtube) {
if(recording_video && save_video && upload_to_youtube) {
// you have to have elected to save video to have upload to youtube have
context.VideoUploaderActions.showUploader(recording_id);
}

View File

@ -185,6 +185,7 @@
context.stats.write = context.stats.writePoint;
}
function initializeStun(app) {
stun = new context.JK.Stun(app);
context.JK.StunInstance = stun;

View File

@ -59,7 +59,8 @@
context.JK.PLAYBACK_MONITOR_MODE = {
MEDIA_FILE: 'MEDIA_FILE',
JAMTRACK: 'JAMTRACK',
METRONOME: 'METRONOME'
METRONOME: 'METRONOME',
BROWSER_MEDIA: 'BROWSER_MEDIA'
}
context.JK.ALERT_NAMES = {
NO_EVENT : 0,

View File

@ -131,4 +131,8 @@
return context.JK.prodBubble($element, 'jamtrack-browse-cta', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent}))
}
helpBubble.jamtrackWebPlay = function($element, $offsetParent) {
return context.JK.prodBubble($element, 'jamtrack-web-play', {}, bigHelpOptions({positions:['bottom'], offsetParent: $offsetParent}))
}
})(window, jQuery);

View File

@ -540,6 +540,11 @@
processData: false
});
}
if(detail) {
detail.done(function(user) {
window.UserActions.loaded(user)
})
}
return detail;
}
@ -1134,6 +1139,29 @@
});
}
function userOpenedJamTrackWebPlayer(options) {
var id = getId(options);
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/users/progression/opened_jamtrack_web_player",
processData: false
});
}
function postUserEvent(options) {
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/users/event/record",
data: JSON.stringify(options),
processData: false
});
}
function signout() {
return $.ajax({
type: "DELETE",
@ -1144,11 +1172,12 @@
}
function updateUser(options) {
options = options || {};
var id = getId(options);
delete options['id'];
return $.ajax({
var deferred = $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
@ -1156,6 +1185,13 @@
data: JSON.stringify(options),
processData: false
});
deferred.done(function(user) {
window.UserActions.loaded(user)
})
return deferred;
}
function startRecording(options) {
@ -2058,6 +2094,8 @@
this.userDownloadedClient = userDownloadedClient;
this.userCertifiedGear = userCertifiedGear;
this.userSocialPromoted = userSocialPromoted;
this.userOpenedJamTrackWebPlayer = userOpenedJamTrackWebPlayer;
this.postUserEvent = postUserEvent;
this.createJoinRequest = createJoinRequest;
this.updateJoinRequest = updateJoinRequest;
this.updateUser = updateUser;

View File

@ -9,17 +9,39 @@
//= require jquery.icheck
//= require jquery.easydropdown
//= require jquery.metronomePlaybackMode
//= require jquery.cookie
//= require influxdb-latest
//= require howler.core.js
//= require classnames
//= require reflux
//= require AAC_underscore
//= require AAA_Log
//= require globals
//= require AAB_message_factory
//= require jam_rest
//= require ga
//= require layout
//= require jamkazam
//= require utils
//= require playbackControls
//= require subscription_utils
//= require webcam_viewer
//= require JamServer
//= require react
//= require react_ujs
//= require react-init
//= require react-components
//= require react-components
//= require playbackControls
function initializeInfluxDB() {
window.stats = new InfluxDB({
"host" : gon.global.influxdb_host,
"port" : gon.global.influxdb_port,
"username" : gon.global.influxdb_username,
"password" : gon.global.influxdb_password,
"database" : gon.global.influxdb_database
});
window.stats.write = window.stats.writePoint;
}
initializeInfluxDB()

View File

@ -2,8 +2,25 @@
* Playback widget (play, pause , etc)
*/
(function (context, $) {
// this check ensures we attempt to listen if this component is created in a popup
var reactContext = window.opener ? window.opener : window
// make sure this is actually us opening the window, not someone else (by checking for MixerStore)
if (window.opener) {
try {
m = window.opener.MixerStore
}
catch (e) {
reactContext = window
}
}
var BrowserMediaStore = reactContext.BrowserMediaStore
"use strict";
var PlaybackMode = {
@ -75,7 +92,6 @@
}
else {
$self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode});
}
if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) {
@ -241,6 +257,9 @@
else if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) {
$parentElement.addClass('metronome-mode');
}
else if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.BROWSER_MEDIA) {
$parentElement.addClass('mediafile-mode');
}
else {
throw "unknown playbackMonitorMode: " + playbackMonitorMode;
}
@ -294,14 +313,19 @@
var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs();
var duration = context.jamClient.SessionGetJamTracksPlayDurationMs();
var durationMs = duration.media_len;
var isPlaying = context.jamClient.isSessionTrackPlaying();
}
else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.BROWSER_MEDIA) {
var positionMs = BrowserMediaStore.onGetPlayPosition() || 0
var durationMs = BrowserMediaStore.onGetPlayDuration() || 0;
var isPlaying = BrowserMediaStore.playing;
}
else {
var positionMs = context.jamClient.SessionCurrrentPlayPosMs();
var durationMs = context.jamClient.SessionGetTracksPlayDurationMs();
var isPlaying = context.jamClient.isSessionTrackPlaying();
}
var isPlaying = context.jamClient.isSessionTrackPlaying();
executeMonitor(positionMs, durationMs, isPlaying)
}
}

View File

@ -4,6 +4,7 @@
//= require_directory ./react-components/helpers
//= require_directory ./react-components/actions
//= require ./react-components/stores/AppStore
//= require ./react-components/stores/BrowserMediaStore
//= require ./react-components/stores/RecordingStore
//= require ./react-components/stores/VideoStore
//= require ./react-components/stores/SessionStore
@ -11,11 +12,13 @@
//= require ./react-components/stores/JamTrackStore
//= require ./react-components/stores/SessionNotificationStore
//= require ./react-components/stores/MediaPlaybackStore
//= require ./react-components/stores/BrowserMediaPlaybackStore
//= require ./react-components/stores/SessionMyTracksStore
//= require ./react-components/stores/SessionOtherTracksStore
//= require ./react-components/stores/SessionMediaTracksStore
//= require ./react-components/stores/PlatformStore
//= require ./react-components/stores/VideoUploaderStore
//= require ./react-components/stores/JamTrackPlayerStore
//= require_directory ./react-components/stores
//= require_directory ./react-components/mixins
//= require_directory ./react-components

View File

@ -0,0 +1,132 @@
context = window
PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE
EVENTS = context.JK.EVENTS
logger = context.JK.logger
mixins = []
# this check ensures we attempt to listen if this component is created in a popup
reactContext = if window.opener? then window.opener else window
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
if window.opener?
try
m = window.opener.MixerStore
catch e
reactContext = window
# temporarily..
# reactContext = window
MediaPlaybackActions = reactContext.MediaPlaybackActions
BrowserMediaStore = reactContext.BrowserMediaStore
BrowserMediaPlaybackStore = reactContext.BrowserMediaPlaybackStore
BrowserMediaPlaybackActions = reactContext.BrowserMediaPlaybackActions
mixins.push(Reflux.listenTo(BrowserMediaPlaybackStore,"onMediaStateChanged"))
@BrowserMediaControls = React.createClass({
mixins: mixins
tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ]
onMediaStateChanged: (changes) ->
if changes.playbackStateChanged
if @state.controls?
if changes.playbackState == 'play_start'
@state.controls.onPlayStartEvent()
else if changes.playbackState == 'play_stop'
@state.controls.onPlayStopEvent()
else if changes.playbackState == 'play_pause'
@state.controls.onPlayPauseEvent();
if changes.positionUpdateChanged
if @state.controls?
@state.controls.executeMonitor(changes.positionMs, changes.durationMs, changes.isPlaying)
if changes.currentTimeChanged
@setState({time: changes.time})
monitorControls: (controls) ->
controls.startMonitor(PLAYBACK_MONITOR_MODE.BROWSER_MEDIA)
render: () ->
tempo_options = []
for tempo in @tempos
tempo_options.push(`<option key={tempo} value={tempo}>{tempo}</option>`)
`<div className="media-controls has-mix">
<div className="jam-track-get-ready">
<div className="spinner-small"></div>
<span>Get Ready!</span>
</div>
<div className="play-buttons">
<a className="play-button" href="#">
<img src="/assets/content/icon_playbutton.png" width="20" height="20" className="playbutton" />
<img src="/assets/content/icon_pausebutton.png" width="20" height="20" className="pausebutton" />
</a>
<a className="stop-button" href="#">
<img src="/assets/content/icon_stopbutton.png" width="20" height="20" className="stopbutton" />
</a>
</div>
<div className="metronome-playback-options">
<span id="metronome-playback-select"></span>
</div>
<div className="metronome-options">
<div className="metronome-selects">
<div class="metronome-field">
<select className="metronome-select metro-sound" title="Metronome Sound" name="metronome_sound">
<option value="Beep">Knock</option>
<option value="Click">Tap</option>
<option value="Snare">Snare</option>
<option value="Kick">Kick</option>
</select>
<label htmlFor="metronome_sound">Sound:</label>
</div>
<div class="metronome-field">
<select className="metronome-select metro-tempo" title="Metronome Tempo" name="metronome_tempo">
{tempo_options}
</select>
<label htmlFor="metronome_tempo">Tempo:</label>
</div>
</div>
</div>
<div className="recording-time start-time">{this.state.time}</div>
<div className="recording-playback">
<div className="recording-slider"><img src="/assets/content/slider_playcontrols.png" height="16" width="5" /></div>
</div>
<div className="recording-time duration-time">0:00</div>
<div className="recording-current">0:00</div>
<div className="playback-mode-buttons icheckbuttons">
<input type="radio" name="playback-mode" defaultChecked="checked" value="preview-to-all" className="preview-to-all" /><label htmlFor="playback-mode-preview-all" className="radio-text">Preview to All</label>
<input type="radio" name="playback-mode" value="preview-to-me" className="preview-to-me" /><label htmlFor="playback-mode-preview-me" className="radio-text">Preview Only to Me</label>
</div>
</div>`
getInitialState: () ->
{controls: null, time: '0:00'}
componentDidUpdate: (prevProps, prevState) ->
componentDidMount: () ->
$root = jQuery(this.getDOMNode())
controls = context.JK.PlaybackControls($root, {mediaActions: BrowserMediaPlaybackActions})
controls.setDisabled(@props.disabled)
@monitorControls(controls)
@setState({controls:controls})
componentWillUpdate: (nextProps) ->
@state.controls.setDisabled(nextProps.disabled) if @state.controls?
})

View File

@ -1,13 +1,17 @@
context = window
MIX_MODES = context.JK.MIX_MODES
rest = context.JK.Rest()
@JamTrackLandingScreen = React.createClass({
mixins: [Reflux.listenTo(@AppStore,"onAppInit")]
mixins: [Reflux.listenTo(@AppStore,"onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
getInitialState: () ->
{user: null}
{user: null, purchasedJamTracks: []}
onUserChanged: (userState) ->
@onUser(userState.user) if userState.user
render: () ->
@ -22,10 +26,31 @@ MIX_MODES = context.JK.MIX_MODES
else
howTo = `<div className="no-free-jamtrack">
<span>
To play with your JamTracks, open a JamTrack while in a session in the JamKazam app. Or <a href="/client#/account/jamtracks">visit the JamTracks section of your account.</a>
</span>
The fastest way to start playing with your JamTracks is to open them below and use our <a href="https://jamkazam.desk.com/customer/en/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch" onClick={this.customMixHelpClicked}>custom mix feature</a> to play them back in your browser. To access the full set of JamTrack features, <a href="/downloads" rel="external" onClick={this.downloadsClicked}>install our free app</a>. To learn more about all you can do with JamTracks, check out our <a href="https://jamkazam.desk.com/customer/en/portal/articles/2124663-playing-with-jamtracks" onClick={this.jamTrackHelpClicked}>JamTracks help docs</a>.
</span>
</div>`
playJamTracks = []
for jamTrack in @state.purchasedJamTracks
playJamTracks.push `<tr className="play-jamtrack"><td><a onClick={this.onPlayJamTrack.bind(this, jamTrack)}>{jamTrack.name}</a> by {jamTrack.original_artist}</td></tr>`
if @state.purchasedJamTracks.length < 5
# fill out the table with empty rows
for x in [@state.purchasedJamTracks.length...(6 - @state.purchasedJamTracks.length )] by 1
playJamTracks.push `<tr><td>&nbsp;</td></tr>`
playableJamTracks =
`<div className="purchased-jam-tracks">
<table className="jamtable" cellspacing="0" cellpadding="0" border="0">
<thead>
<tr><th className="jamtrack-name" align="left">JAMTRACKS</th></tr>
</thead>
<tbody>
{playJamTracks}
</tbody>
</table>
</div>`
`<div className="content-body-scroller">
<div className="list-columns">
@ -33,6 +58,7 @@ MIX_MODES = context.JK.MIX_MODES
<h2>my jamtracks</h2>
<div className="howto">
{howTo}
{playableJamTracks}
</div>
<h2 className="browse-jamtracks">search jamtracks</h2>
<div className="search-area">
@ -60,15 +86,20 @@ MIX_MODES = context.JK.MIX_MODES
JamTracks are the best way to play along with your favorite music! Unlike traditional backing tracks, JamTracks are professionally mastered, complete multitrack recordings, with fully isolated tracks for each part of the master mix. Used with the free JamKazam app & Internet service, you can:
</div>
<ul>
<li>Solo just the part you want to play in order to hear and learn it</li>
<li>Mute just the part you want to play and play along with the rest</li>
<li>Choose your favorites from our catalog of 3,700+ songs</li>
<li>Listen to just the single part you want to play to learn it</li>
<li>Mute the part you want to play, and play along with the rest of the band</li>
<li>Slow down playback to practice without changing the pitch</li>
<li>Change the song key by raising or lowering pitch in half steps</li>
<li>Make audio recordings and share them via Facebook or URL</li>
<li>Make video recordings and share them via YouTube</li>
<li>And even go online to play with others live &amp; in sync</li>
<li>Save your custom mixes for easy access, and export them to use anywhere</li>
<li>Apply VST &amp; AU audio plugin effects to your live performance</li>
<li>Use MIDI with VST &amp; AU instruments for keys, electronic drums, more</li>
<li>Make audio recordings and share via Facebook or URL</li>
<li>Make video recordings and share via YouTube</li>
<li>Play online live and in sync with others from different locations</li>
<li>JamTracks work in standard browser, with more features in Win/Mac apps</li>
</ul>
<a className="video-thumbnail" href="https://www.youtube.com/watch?v=askHvcCoNfw" rel="external">
<a className="video-thumbnail" href="https://www.youtube.com/watch?v=07zJC7C2ICA" rel="external">
<img className="play" src="/assets/content/icon_youtube_play.png" />
</a>
</div>
@ -96,19 +127,50 @@ MIX_MODES = context.JK.MIX_MODES
instrument = $root.find('select.instrument-list').val()
context.JamTrackActions.requestFilter(genre, instrument)
processUrl: () ->
if $.QueryString['redeemed_flow']?
@redeemedFlow = true
if @redeemedFlow
if window.history.replaceState #ie9 proofing
window.history.replaceState({}, "", "/client#/jamtrack")
preparePlayJamTrackProd: () ->
setTimeout((() =>
$element = $(this.getDOMNode()).find('.purchased-jam-tracks .play-jamtrack a').first()
$offsetParent = $element.closest('.screen')
context.JK.HelpBubbleHelper.jamtrackWebPlay($element, $offsetParent)
), 1500)
afterShow: (data) ->
if context.JK.currentUserId
@app.user().done(@onUser)
else
@processUrl()
if !context.JK.currentUserId
@onUser({free_jamtrack: context.JK.currentUserFreeJamTrack})
beforeShow: () ->
@setState({user: null})
onUser:(user) ->
@setState({user: user})
rest.getPurchasedJamTracks({page:1, per_page:20})
.done((purchasedJamTracks) =>
if @redeemedFlow
@preparePlayJamTrackProd()
@redeemedFlow = false
@setState({purchasedJamTracks: purchasedJamTracks.jamtracks})
)
.fail((jqXHR, textStatus, errorMessage) =>
@app.ajaxError(jqXHR, textStatus, errorMessage);
)
# Get artist names and build links
#@rest.getJamTrackArtists({group_artist: true, per_page:100})
#.done(this.buildArtistLinks)
@ -118,6 +180,24 @@ MIX_MODES = context.JK.MIX_MODES
# artist_name
#@bindArtistLinks()
customMixHelpClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink($(e.target).attr('href'))
downloadsClicked:(e) ->
e.preventDefault()
context.JK.popExternalLink($(e.target).attr('href'))
jamTrackHelpClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink($(e.target).attr('href'))
onPlayJamTrack: (jamTrack, e) ->
e.preventDefault()
# popup window
JamTrackPlayerActions.open(jamTrack)
onAppInit: (@app) ->
@rest = context.JK.Rest()

View File

@ -0,0 +1,750 @@
context = window
logger = context.JK.logger
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest = context.JK.Rest()
mixins = []
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
# this check ensures we attempt to listen if this component is created in a popup
reactContext = if window.opener? then window.opener else window
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
if window.opener?
try
m = window.opener.MixerStore
catch e
reactContext = window
# temporarily..
# reactContext = window
AppActions = reactContext.AppActions
JamTrackPlayerActions = reactContext.JamTrackPlayerActions
JamTrackPlayerStore = reactContext.JamTrackPlayerStore
mixins.push(Reflux.listenTo(JamTrackPlayerStore, 'onJamTrackPlayerStoreChanged'))
@PopupJamTrackPlayer = React.createClass({
mixins: mixins
onJamTrackPlayerStoreChanged: (changes) ->
#logger.debug("PopupMediaControls: jamtrack changed", changes)
@setState({jamTrackState: changes})
showMetronome: (e) ->
e.preventDefault()
SessionActions.showNativeMetronomeGui()
getInitialState: () ->
state = {}
state.jamTrackState = JamTrackPlayerStore.getState()
state.showCustomMixes = true
return state
close: () ->
window.close()
help: (e) ->
e.preventDefault()
AppActions.openExternalUrl($(e.target).attr('href'))
render: () ->
closeLinkText = null
header = null
extraControls = null
jamTrack = @state.jamTrackState?.jamTrack
mediaType = "JamTrack"
mediaName = jamTrack?.name
closeLinkText = 'CLOSE JAMTRACK'
helpLink = 'https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch'
selectedMixdown = jamTrack?.activeMixdown
selectedStem = jamTrack?.activeStem
if selectedMixdown?
jamTrackTypeHeader = 'Custom Mix'
disabled = true
if selectedMixdown.client_state?
switch selectedMixdown.client_state
when 'cant_open'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying_timeout'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'download_fail'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'downloading'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'ready'
customMixName = `<h5>{selectedMixdown.name}</h5>`
disabled = false
else
if selectedMixdown.myPackage
customMixName = `<h5>Creating mixdown... <img src="/assets/shared/spinner.gif" /></h5>`
else
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
else if selectedStem?
if selectedStem.instrument
instrumentId = selectedStem.instrument.id
instrumentDescription = selectedStem.instrument.description
part = "(#{selectedStem.part})" if selectedStem.part? && selectedStem.part != instrumentDescription
part = '' unless part
jamTrackTypeHeader = 'Track'
trackName = "#{instrumentDescription} #{part}"
disabled = true
if selectedStem.client_state?
switch selectedStem.client_state
when 'downloading'
customMixName = `<h5>Loading {trackName}... <img src="/assets/shared/spinner.gif" /></h5>`
when 'ready'
customMixName = `<h5>{trackName}</h5>`
disabled = false
else
customMixName = `<h5>{trackName}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
else
if jamTrack?.client_state == 'downloading'
downloader = `<img src="/assets/shared/spinner.gif" />`
jamTrackTypeHeader = `<span>Full JamTrack {downloader}</span>`
header = `
<div className="header">
<h3>{mediaType}: {mediaName}</h3>
<h4>{jamTrackTypeHeader}</h4>
{customMixName}
</div>`
myMixes = null
if @state.showMyMixes
myMixdowns = []
boundPlayClick = this.jamTrackPlay.bind(this, jamTrack);
boundDownloadClick = this.jamTrackDownload.bind(this, jamTrack);
active = jamTrack.last_mixdown_id == null && jamTrack.last_stem_id == null
myMixdowns.push `
<div key="full-track" className={classNames({'full-track': true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
Full JamTrack
</div>
<div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>
<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={boundDownloadClick}/>
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} />
</div>
</div>`
for mixdown in jamTrack.mixdowns
boundPlayClick = this.mixdownPlay.bind(this, mixdown);
boundEditClick = this.mixdownEdit.bind(this, mixdown);
boundSaveClick = this.mixdownSave.bind(this, mixdown);
boundDeleteClick = this.mixdownDelete.bind(this, mixdown);
boundErrorClick = this.mixdownError.bind(this, mixdown);
boundEditKeydown = this.onEditKeydown.bind(this, mixdown);
boundDownloadNotReadyClick = this.downloadNotReady.bind(this, mixdown)
boundDownloadReadyClick = this.downloadMixdownReady.bind(this, mixdown)
mixdown_package = mixdown.myPackage
active = mixdown.id == jamTrack.last_mixdown_id
editing = mixdown.id == @state.editingMixdownId
# if there is a package, check it's state; otherwise let the user enqueue it
if mixdown_package
switch mixdown_package.signing_state
when 'QUIET_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'QUIET'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNING'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'SIGNING_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNED'
action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>`
when 'ERROR'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
else
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
if editing
mixdownName = `<input className="edit-name" type="text" defaultValue={mixdown.name} onKeyDown={boundEditKeydown} />`
editIcon = `<img src="/assets/content/icon-save@2X.png" className="mixdown-edit" onClick={boundSaveClick}/>`
else
mixdownName = mixdown.name
editIcon = `<img src="/assets/content/icon-edit@2X.png" className="mixdown-edit" onClick={boundEditClick}/>`
# create hidden objects to deal with alginment issues using a table
if !editIcon
editIcon = `<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />`
download = `<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={boundDownloadReadyClick} />`
myMixdowns.push `
<div key={mixdown.id} className={classNames({'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
{mixdownName}
</div>
<div className="mixdown-actions">
{action}
{download}
{editIcon}
<img src ="/assets/content/icon-delete@2X.png" className="mixdown-delete" onClick={boundDeleteClick} />
</div>
</div>`
active = jamTrack.last_stem_id?
trackOptions = []
for track in jamTrack.tracks
if track.track_type == 'Track'
if track.instrument
instrumentId = track.instrument.id
instrumentDescription = track.instrument.description
if track.part? && track.part != instrumentDescription
part = "(#{track.part})"
else
part = ''
trackOptions.push(`<option key={track.id} value={track.id}>{instrumentDescription} {part}</option>`)
boundStemActivateClick = this.activateStem.bind(this)
boundStemPlayClick = this.downloadStem.bind(this)
boundStemChange = this.stemChanged.bind(this)
action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundStemActivateClick}/>`
download = `<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={boundStemPlayClick} />`
myMixdowns.push `
<div key={track.id} className={classNames({'stem-track' : true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
<select className="active-stem-select" name="active-stem-select" onChange={boundStemChange} defaultValue={jamTrack.last_stem_id}>
<option key="null" value="">or select a track</option>
{trackOptions}
</select>
</div>
<div className="mixdown-actions">
{action}
{download}
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} />
</div>
</div>`
myMixes = `<div key="my-mixes" className="my-mixes">{myMixdowns}</div>`
mixControls = null
if @state.showCustomMixes
tracks = []
if jamTrack?
for track in jamTrack.tracks
if track.track_type == 'Track'
if track.instrument
instrumentId = track.instrument.id
instrumentDescription = track.instrument.description
if track.part? && track.part != instrumentDescription
part = "(#{track.part})"
else
part = ''
tracks.push(`
<tr className="stem">
<td><img src={context.JK.getInstrumentIcon24(instrumentId)} className="instrument-icon" /> {instrumentDescription} {part}</td>
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id={track.id} /></td>
</tr>`)
stems = `<div key="stems" className="stems">
<table>
<thead>
<col align="left" /><col align="right"/>
<tr><th>TRACKS</th><th className="mute">mute</th></tr>
</thead>
<tbody>
{tracks}
</tbody>
</table>
</div>`
nameClassData = {field: true}
if @state.createMixdownErrors?
errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown})
mixControls = `
<div key="create-mix" className="create-mix">
<p>Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.</p>
{stems}
<div className="field">
<label>TEMPO</label>
<select name="mix-speed">
<option value="">No change</option>
<option value="separator-1">------------</option>
<option value="-5">Slower by 5%</option>
<option value="-10">Slower by 10%</option>
<option value="-15">Slower by 15%</option>
<option value="-20">Slower by 20%</option>
<option value="-25">Slower by 25%</option>
<option value="-30">Slower by 30%</option>
<option value="-35">Slower by 35%</option>
<option value="-40">Slower by 40%</option>
<option value="-45">Slower by 45%</option>
<option value="-50">Slower by 50%</option>
<option value="-60">Slower by 60%</option>
<option value="-70">Slower by 70%</option>
<option value="-80">Slower by 80%</option>
<option value="separator-2">------------</option>
<option value="5">Faster by 5%</option>
<option value="10">Faster by 10%</option>
<option value="15">Faster by 15%</option>
<option value="20">Faster by 20%</option>
<option value="30">Faster by 30%</option>
<option value="40">Faster by 40%</option>
<option value="50">Faster by 50%</option>
</select>
</div>
<div className="field">
<label>PITCH</label>
<select name="mix-pitch">
<option value="">No change</option>
<option value="separator-1">---------------</option>
<option value="-1">Down 1 Semitone</option>
<option value="-2">Down 2 Semitones</option>
<option value="-3">Down 3 Semitones</option>
<option value="-4">Down 4 Semitones</option>
<option value="-5">Down 5 Semitones</option>
<option value="-6">Down 6 Semitones</option>
<option value="-7">Down 7 Semitones</option>
<option value="-8">Down 8 Semitones</option>
<option value="-9">Down 9 Semitones</option>
<option value="-10">Down 10 Semitones</option>
<option value="-11">Down 11 Semitones</option>
<option value="-12">Down 12 Semitones</option>
<option value="separator-2">---------------</option>
<option value="1">Up 1 Semitone</option>
<option value="2">Up 2 Semitones</option>
<option value="3">Up 3 Semitones</option>
<option value="4">Up 4 Semitones</option>
<option value="5">Up 5 Semitones</option>
<option value="6">Up 6 Semitones</option>
<option value="7">Up 7 Semitones</option>
<option value="8">Up 8 Semitones</option>
<option value="9">Up 9 Semitones</option>
<option value="10">Up 10 Semitones</option>
<option value="11">Up 11 Semitones</option>
<option value="12">Up 12 Semitones</option>
</select>
</div>
<div className={classNames(nameClassData)}>
<label>MIX NAME</label>
<input type="text" name="mix-name"/>
</div>
<div className="field">
<a className={createMixClasses} onClick={this.createMix}>CREATE MIX</a>
{errorHtml}
</div>
<div className="clearall"/>
</div>`
if @state.showMyMixes
showMyMixesText = `<a onClick={this.toggleMyMixes}>hide my mixes <div className="details-arrow arrow-up" /></a>`
else
showMyMixesText = `<a onClick={this.toggleMyMixes}>show my mixes <div className="details-arrow arrow-down" /></a>`
if @state.showCustomMixes
showMixControlsText = `<a onClick={this.toggleCustomMixes}>hide mix controls <div className="details-arrow arrow-up" /></a>`
else
showMixControlsText = `<a onClick={this.toggleCustomMixes}>show mix controls <div className="details-arrow arrow-down" /></a>`
extraControls = `
<div className="extra-controls">
<h4>My Mixes {showMyMixesText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={true}>
{myMixes}
</ReactCSSTransitionGroup>
<h4 className="custom-mix-header">Create Custom Mix {showMixControlsText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={false}>
{mixControls}
</ReactCSSTransitionGroup>
</div>`
if helpLink?
helpButton = `<a className="help-link button-grey" href={helpLink} onClick={this.help}>HELP</a>`
`<div className="media-controls-popup">
{header}
<BrowserMediaControls disabled={this.disableLoading}/>
{extraControls}
<div className="actions">
{helpButton}
<a className="close-link button-orange" onClick={this.close}>{closeLinkText}</a>
</div>
</div>`
windowUnloaded: () ->
JamTrackPlayerActions.windowUnloaded() unless window.DontAutoCloseMedia
toggleMyMixes: (e) ->
e.preventDefault()
@setState({showMyMixes: !@state.showMyMixes})
toggleCustomMixes: (e) ->
e.preventDefault()
@setState({showCustomMixes: !@state.showCustomMixes})
downloadNotReady: (mixdown, e) ->
alert("This mix is not yet ready to download.")
e.preventDefault()
downloadMixdownReady: (mixdown, e) ->
e.preventDefault()
if mixdown.myPackage?.signing_state == 'SIGNED'
iframe = document.createElement("iframe")
iframe.src = window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1"
iframe.style.display = "none"
document.body.appendChild(iframe);
else
alert("The mix is not yet ready to download")
activateStem: (e) ->
e.preventDefault()
$select = $(this.getDOMNode()).find('.active-stem-select')
selectedTrackId = $select.val()
if !selectedTrackId? || selectedTrackId == ''
alert("You must pick a track from the dropdown in order to play it.")
else
@setState({editingMixdownId: null})
e.preventDefault()
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
# make this package the active one
JamTrackPlayerActions.openStem(selectedTrackId)
downloadStem: (e) ->
e.preventDefault()
$select = $(this.getDOMNode()).find('.active-stem-select')
selectedTrackId = $select.val()
if !selectedTrackId? || selectedTrackId == ''
alert("You must select a track in order to download.")
else
e.preventDefault()
iframe = document.createElement("iframe")
iframe.src = window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@state.jamTrackState.jamTrack.id}/stems/#{selectedTrackId}/download.mp3?file_type=mp3&download=1"
iframe.style.display = "none"
document.body.appendChild(iframe);
stemChanged: () ->
mixdownPlay: (mixdown, e) ->
@setState({editingMixdownId: null})
e.preventDefault()
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
# make this package the active one
JamTrackPlayerActions.openMixdown(mixdown)
jamTrackPlay: (jamtrack, e) ->
e.preventDefault()
# user wants to select the full track
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
JamTrackPlayerActions.activateNoMixdown(jamtrack)
jamTrackDownload: (jamTrack, e) ->
e.preventDefault()
iframe = document.createElement("iframe")
iframe.src = window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1"
iframe.style.display = "none"
document.body.appendChild(iframe);
onEditKeydown: (mixdown, e) ->
logger.debug("on edit keydown", e)
if e.keyCode == 13 # enter
@mixdownSave(mixdown, e)
else if e.keyCode == 27 # esc
@setState({editingMixdownId: null})
mixdownEdit: (mixdown) ->
@setState({editingMixdownId: mixdown.id})
mixdownSave: (mixdown, e) ->
e.preventDefault()
$input = $(this.getDOMNode()).find('input.edit-name')
newValue = $input.val()
logger.debug("editing mixdown name to be: " + newValue)
JamTrackPlayerActions.editMixdown({id: mixdown.id, name: newValue})
@setState({editingMixdownId: null})
mixdownDelete: (mixdown) ->
if @state.editingMixdownId?
@setState({editingMixdownId:null})
return
if confirm("Delete this custom mix?")
JamTrackPlayerActions.deleteMixdown(mixdown)
mixdownError: (mixdown) ->
myPackage = mixdown.myPackage
if myPackage?
switch myPackage.signing_state
when 'QUIET_TIMEOUT'
action = 'Custom mix never got created. Retry?'
when 'QUEUED_TIMEOUT'
action = 'Custom mix was never built. Retry?'
when 'SIGNING_TIMEOUT'
action = 'Custom mix took took long to build. Retry?'
when 'ERROR'
action = 'Custom mix failed to build. Retry?'
else
action = 'Custom mix never got created. Retry?'
return unless action?
if confirm(action)
JamTrackPlayerActions.enqueueMixdown(mixdown, @enqueueDone)
enqueueDone: (enqueued) ->
@promptEstimate(enqueued)
promptEstimate: (enqueued) ->
time = enqueued.queue_time
if time == 0
alert("Your custom mix will take about 1 minute to be created.")
else
guess = Math.ceil(time / 60.0)
if guess == 1
msg = '1 minute'
else
msg = "#{guess} minutes"
alert("Your custom mix will take about #{msg} to be created.")
createMix: (e) ->
e.preventDefault()
return if @state.creatingMix
$root = $(@getDOMNode())
name = $root.find('input[name="mix-name"]').val()
speed = $root.find('select[name="mix-speed"]').val()
pitch = $root.find('select[name="mix-pitch"]').val()
if name == null || name == ''
@setState({createMixdownErrors: {errors: {'Mix Name': ["can't be blank"]}}})
return
# sanitize junk out of speed/pitch
if speed == '' || speed.indexOf('separator') > -1
speed = undefined
else
speed = parseInt(speed)
if pitch == '' || pitch.indexOf('separator') > -1
pitch = undefined
else
pitch = parseInt(pitch)
# get mute state of all tracks
$mutes = $(@getDOMNode()).find('.stems .stem .stem-mute')
tracks = []
$mutes.each((i, mute) =>
$mute = $(mute)
stemId = $mute.attr('data-stem-id')
muted = $mute.is(':checked')
tracks.push({id: stemId, mute: muted})
)
mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch, tracks:tracks}}
JamTrackPlayerActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail)
@setState({creatingMixdown: true, createMixdownErrors: null})
createMixdownDone: (created) ->
logger.debug("created (within PopupMediaControls)", created)
@setState({creatingMixdown: false, showCustomMixes: true, showMyMixes: true})
@promptEstimate(created)
createMixdownFail: (jqXHR) ->
logger.debug("create mixdown fail (within PopupMediaControls)", jqXHR.status)
@setState({creatingMixdown: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
logger.warn("failed to create mixdown", response, jqXHR.responseText)
@setState({createMixdownErrors: response})
fetchJamTrackInfo: () ->
rest.getJamTrack({plan_code: gon.jamtrack_id})
.done((response) =>
JamTrackPlayerActions.open(response, false);
@fetchUserInfo()
)
.fail((jqXHR) =>
alert("Unable to fetch JamTrack information. Try logging in.")
)
fetchUserInfo: () ->
rest.getUserDetail()
.done((response) =>
rest.postUserEvent({name: 'jamtrack_web_player_open'})
context.stats.write('web.jamtrack_web_player.open', {
value: 1,
user_id: context.JK.currentUserId,
user_name: context.JK.currentUserName
})
if !response.first_opened_jamtrack_web_player
setTimeout((() =>
logger.debug("first time user")
rest.userOpenedJamTrackWebPlayer()
$root = $(@getDOMNode())
#context.JK.prodBubble($root.find('.create-mix-btn'), 'first-time-jamtrack-web-player', {}, {positions:['left'], offsetParent: $root})
), 1500)
)
componentDidMount: () ->
$(window).unload(@windowUnloaded)
@root = jQuery(this.getDOMNode())
$loop = @root.find('input[name="loop"]')
context.JK.checkbox($loop)
$loop.on('ifChecked', () =>
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, true)
)
$loop.on('ifUnchecked', () =>
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, false)
)
@resizeWindow()
# this is necessary due to whatever the client's rendering behavior is.
setTimeout(@resizeWindow, 300)
@fetchJamTrackInfo()
componentDidUpdate: () ->
@resizeWindow()
setTimeout(@resizeWindow, 1000)
resizeWindow: () =>
$container = $('#minimal-container')
width = $container.width()
height = $container.height()
# there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container)
#mysteryTopMargin = 20
mysteryTopMargin = 0
# deal with chrome in real browsers
offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin
# handle native client chrome that the above outer-inner doesn't catch
#if navigator.userAgent.indexOf('JamKazam') > -1
#offset += 25
window.resizeTo(450, height + offset)
computeDisableLoading: (state) ->
@disableLoading = false
return unless nextState?
selectedMixdown = state?.jamTrackState?.jamTrack?.activeMixdown
mixdownDownloading = false
if selectedMixdown?
switch selectedMixdown.client_state
when 'downloading'
mixdownDownloading = true
selectedStem = state?.jamTrackState?.jamTrack?.activeStem
stemDownloading = false
if selectedStem?
switch selectedStem.client_state
when 'downloading'
stemDownloading = true
@disableLoading = state?.jamTrackState?.jamTrack?.client_state == 'downloading' || mixdownDownloading || stemDownloading
componentWillMount: () ->
@computeDisableLoading(@state)
componentWillUpdate: (nextProps, nextState) ->
@computeDisableLoading(nextState)
})

View File

@ -252,7 +252,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
if !selectedMixdown?
mixControls = `
<div key="create-mix" className="create-mix">
<p>Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button. Please note that changing the tempo or pitch of the JamTrack may take a long time, and won't be ready right away.</p>
<p>Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.</p>
<div className="field">
<label>Change Tempo:</label>
<select name="mix-speed">

View File

@ -0,0 +1,11 @@
context = window
@BrowserMediaActions = Reflux.createActions({
load: {}
play: {}
stop: {}
pause: {}
seek: {}
setVolume: {}
getPlayPosition: {}
})

View File

@ -0,0 +1,11 @@
context = window
@BrowserMediaPlaybackActions = Reflux.createActions({
playbackStateChange: {}
positionUpdate:{}
mediaStartPlay: {}
mediaStopPlay: {}
mediaPausePlay: {}
mediaChangePosition: {}
currentTimeChanged: {}
})

View File

@ -0,0 +1,18 @@
context = window
@JamTrackPlayerActions = Reflux.createActions({
open: {}
createMixdown: {}
editMixdown: {}
deleteMixdown: {}
openMixdown: {}
activateNoMixdown: {}
closeMixdown: {}
enqueueMixdown: {}
downloadMixdown: {}
refreshMixdown: {}
openStem: {}
windowUnloaded: {}
})

View File

@ -0,0 +1,6 @@
context = window
@UserActions = Reflux.createActions({
loaded: {}
})

View File

@ -0,0 +1,177 @@
context = window
@JamTrackLandingBottomPage = React.createClass({
render: () ->
`<div className="top-container">
<div className="row awesome">
<h2 className="awesome">What Makes JamTracks Awesome?</h2>
<p>JamTracks by JamKazam deliver an unparalleled combination of multitrack pro audio and whiz bang technology -
all with an eye toward the things that really matter to musicians who love to play. Below are the top 10 great
things about JamTracks.
</p>
<div className="testimonials">
<h3>Leading Musicians &amp; Teachers Love JamTracks</h3>
<div className="testimonial">
<img src="/assets/landing/Andy Crowley - Speech Bubble.png" className="testimonial-speech-bubble" />
<img src="/assets/landing/Andy Crowley - Avatar.png" className="testimonial-avatar" />
<a rel="external" href="https://www.youtube.com/user/andycrowley"><img src="/assets/landing/Andy Crowley - YouTube.png" className="testimonial-youtube" /></a>
<h4><strong>Andy Crowley</strong> of AndyGuitar</h4>
</div>
<div className="testimonial">
<img src="/assets/landing/Ryan Jones - Speech Bubble.png" className="testimonial-speech-bubble" />
<img src="/assets/landing/Ryan Jones - Avatar.png" className="testimonial-avatar" />
<a rel="external" href="https://www.youtube.com/user/gotitans999"><img src="/assets/landing/Ryan Jones - PianoKeyz - YouTube.png" className="testimonial-youtube" /></a>
<h4><strong>Ryan Jones</strong> of PianoKeyz</h4>
</div>
<div className="testimonial">
<img src="/assets/landing/Carl Brown - Speech Bubble.png" className="testimonial-speech-bubble" />
<img src="/assets/landing/Carl Brown - Avatar.png" className="testimonial-avatar" />
<a rel="external" href="https://www.youtube.com/channel/UCvnfBBzEizi1T5unOXNCxdQ"><img src="/assets/landing/Carl Brown - YouTube.png" className="testimonial-youtube" /></a>
<h4><strong>Carl Brown</strong> of GuitarLessions365</h4>
</div>
<div className="jamtrack-overview-video">
<h3>Watch A JamTracks Overview Video</h3>
<div className="video-wrapper left">
<div className="video-container">
<iframe src="//www.youtube.com/embed/07zJC7C2ICA" frameborder="0" allowfullscreen="allowfullscreen" />
</div>
</div>
</div>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">1</div>Huge, High Quality Multi-Track Catalog</h3>
<p><img src="/assets/landing/Top 10 Image - Number 1.png" className="awesome-image" width="405" height="212"/>
JamKazam offers a catalog of 3,700+ songs. Each song is reviewed for quality, and every recording
is a complete multi-track, with fully isolated tracks for each part of the music - e.g. lead vocal,
backing vocals, lead guitar, rhythm guitar, keys, bass, drums, etc. This gives you complete creative control
over every aspect of the music and how you want to use it for learning, practice, recording, and other creative endeavors.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">2</div>Solo, Mute, Pan or Set Level on Any Part</h3>
<p><img src="/assets/landing/Top 10 Image - Number 2.png" className="awesome-image left" width="150" height="191" />
When learning to play a part, it's incredibly valuable to be able to hear just one part in isolation.
Once you've learned your part, you can turn around and mute just that one part, and then play along with the rest of the band.
Or if you prefer, you can turn that part down low but keep it around as a subtle hint. Or pan the recorded track into
your left ear while your live performance is panned into your right ear. Whatever you like! You are in control.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">3</div>Make Custom Mixes</h3>
<p><img src="/assets/landing/Top 10 Image - Number 3.png" className="awesome-image" width="224" height="276" />
When you've customized the JamTrack mix, you can easily save your custom mixes to use them again later without
having to recreate them. Your custom mixes are saved to the JamKazam cloud, so you can access them from almost
any Internet-connected device. If you want to use your mixes outside the JamKazam app, you can also export custom mixes
as a simple .mp3, .wav, or .ogg audio file to use anywhere.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">4</div>Slow Down For Practice</h3>
<p><img src="/assets/landing/Top 10 Image - Number 4.png" className="awesome-image left" width="220" height="203" />
You can easily slow down playback of your JamTrack by a specific % without changing pitch, so that the song still
sounds "right", just slower. This is great for building your technique on tougher sections while gradually increasing tempo.
You can also make JamTracks play faster if you want to hit the jets.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">5</div>Change Pitch/Key</h3>
<p><img src="/assets/landing/Top 10 Image - Number 5.png" className="awesome-image" width="228" height="200" />
If you're a singer and you need to bring the song down into your vocal range, or if you're an instrumentalist
and want to change the piece to a different key, the JamKazam app lets you change the pitch of any JamTrack up or down
by a specified number of semitones (half steps).
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">6</div>Apply VST &amp; AU Audio Plug-In Effects</h3>
<p><img src="/assets/landing/Top 10 Image - Number 6.png" className="awesome-image left" width="350" height="240" />
The free JamKazam app lets you easily apply VST &amp; AU plugin effects to your live performance, mixed together
seamlessly with JamTrack playback. For example, guitarists can apply popular amp sims like AmpliTube to get
just the right guitar tone to match the song, and vocalists can apply effects like reverb, pitch correction, etc.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">7</div>Use MIDI Instruments</h3>
<p><img src="/assets/landing/Top 10 Image - Number 7.png" className="awesome-image" width="320" height="257" />
The free JamKazam app also lets you use MIDI instruments, and mix and record this instrumental audio with JamTracks.
For example, keys players can use MIDI keyboard controllers with VST &amp; AU plugins to generate traditional piano sounds,
Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits can use their favorite
plugins to power their percussive audio.
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">8</div>Make &amp; Share Recordings</h3>
<p>
<div className="video-wrapper left">
<div className="video-container">
<iframe src="//www.youtube.com/embed/ysptXwFYDhQ" frameborder="0" allowfullscreen="allowfullscreen" />
</div>
<div className="cta-text">watch this sample video made by one of our users</div>
</div>
<p>Use the JamKazam app to make either audio-only or video + audio recordings. The app captures video from built-in
or external webcams and combines this video with the mixed audio from the JamTrack and your live performance
into a single integrated video, and will even upload the video to YouTube for you!</p>
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">9</div>Play Live In Sync With Others Over the Internet</h3>
<p>
<div className="video-wrapper">
<div className="video-container">
<iframe src="//www.youtube.com/embed/MASQJnlUBAM" frameborder="0" allowfullscreen="allowfullscreen" />
</div>
<div className="cta-text">watch this sample video made by three of our users</div>
</div>
<p>Perhaps the most mind-blowing thing you can do with JamKazam is that you can play live in sync with others
from different locations over the Internet using the free JamKazam app and Internet service. And you can play
with your JamTracks in these sessions, with others playing different parts.</p>
<div className="clearall" />
</p>
</div>
</div>
<div className="row awesome-thing">
<div className="awesome-item">
<h3> <div className="awesome-number">10</div>JamTracks Work With All Your Stuff</h3>
<p><img src="/assets/landing/Top 10 Image - Number 10.png" className="awesome-image" width="160" height="326" />
You can use your JamTracks with any device that can run a standard web browser for playback of the JamTracks.
If you want to mix your live performance with the JamTrack for recordings, and to access other advanced
features, you'll need to use a JamKazam app. Our app is currently available for Mac and Windows computers,
and we will have iOS and Android apps coming very soon as well. So you can access your JamTracks on just about any device.
<div className="clearall" />
</p>
</div>
</div>
</div>`
})

View File

@ -0,0 +1,192 @@
context = window
rest = context.JK.Rest()
@JamTrackLandingPage = React.createClass({
render: () ->
hasFree = context.JK.currentUserFreeJamTrack
loggedIn = context.JK.currentUserId?
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if hasFree
ctaButtonText = 'GET IT FREE!'
else
ctaButtonText = 'Add To Cart'
if loggedIn
loggedInCtaButton = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>`
if !hasFree
loggedInPriceAdvisory = `<div className="price-advisory">$1.99</div>`
else
if !hasFree
loggedOutPriceAdvisory = `<div className="price-advisory">$1.99</div>`
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors, {email: 'Email', password: 'Password', 'terms_of_service' : 'The terms of service'})
register = `<div className="register-area">
<p>Register for a free account to get this JamTrack free. We will not share your email. See our <a className="privacy-policy" onClick={this.privacyPolicy}>privacy policy</a>.</p>
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="jamtrack-signup-form">
<label>Email: </label><input type="text" name="email" />
<label>Password: </label><input type="password" name="password" />
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms" /><label className="terms-help">I have read and agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>
{loggedOutPriceAdvisory}
</form>
<a className="browse-all" href="/client?search=#/jamtrack/search">or browse our catalog of 3,700+ songs</a>
</div>`
`<div className="top-container">
<div className="full-row name-and-artist">
<div>
<img className="app-preview" width="340" height="178" src="/assets/landing/JK_FBAd_Guitar_with_Keys.png" alt="screenshot of app"/>
<h1 className="jam-track-name">{this.props.jam_track.name.toUpperCase()}</h1>
<h2 className="original-artist">by {this.props.jam_track.original_artist.toUpperCase()}</h2>
<div className="clearall"/>
</div>
<div className="preview-and-action-box">
<img src="/assets/landing/jamtrack_landing_arrow_1.png" className="arrow1" />
<img src="/assets/landing/jamtrack_landing_arrow_2.png" className="arrow2" />
<div className="preview-jamtrack-header">
Preview JamTrack
</div>
<div className={classNames({'preview-area': true, 'logged-in' : loggedIn})}>
<p>Click the play buttons below to preview the master mix and 20-second samples of all the isolated tracks.</p>
<div className="tracks previews">
</div>
{loggedInCtaButton}
{loggedInPriceAdvisory}
</div>
{register}
</div>
</div>
<div className="row summary-text">
<p className="top-summary">
JamTracks by JamKazam are the best way to play along with your favorite songs. Far better and different than traditional
backing tracks, our JamTracks are complete multi-track professional recordings, with fully isolated tracks for each part of the music.
And our free app and Internet service are packed with features that give you unmatched creative freedom to learn, practice, record, play with others, and share your performances.
</p>
</div>
</div>`
getInitialState: () ->
{loginErrors: null, processing:false}
privacyPolicy: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/privacy')
termsClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/terms')
componentDidMount:() ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
console.log("$checkbox", $checkbox)
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
isFree = context.JK.currentUserFreeJamTrack
rest.addJamtrackToShoppingCart({id: @props.jam_track.id}).done((response) =>
if isFree
if loggedIn
context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices
@setState({done: true})
context.location = '/client?redeemed_flow=1#/jamtrack'
else
@createUser()
else
if loggedIn
@setState({done: true})
context.location = '/client#/shoppingCart'
else
@createUser(true)
).fail((jqXHR, textStatus, errorMessage) =>
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
cart_errors = errors?.errors?.cart_id
if cart_errors?.length == 1 && cart_errors[0] == 'has already been taken'
if loggedIn
@setState({done: true})
context.location = '/client#/shoppingCart'
else
@createUser(true)
else
context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
@setState({processing:false})
)
createUser: (redirectToShoppingCart) ->
$form = $('.jamtrack-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({email: email, password: password, first_name: null, last_name: null, terms:terms})
.done((response) =>
if redirectToShoppingCart
@setState({done: true})
context.location = '/client#/shoppingCart'
return
rest.placeOrder()
.done((response) =>
this.setState({done: true})
context.JK.Tracking.redeemCompleteTrack()
window.location = '/client?redeemed_flow=1#/jamtrack'
)
.fail((jqXHR) =>
logger.error("unable to place an older after creating the user")
window.reload()
)
).fail((jqXHR) =>
@setState({processing:false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
@setState({processing:true})
})

View File

@ -0,0 +1,84 @@
$ = jQuery
context = window
logger = context.JK.logger
PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE
RecordingActions = @RecordingActions
BrowserMediaActions = @BrowserMediaActions
@BrowserMediaPlaybackStore = Reflux.createStore(
{
listenables: @BrowserMediaPlaybackActions
playbackStateChanged: false
positionUpdateChanged: false
currentTimeChanged: false
playbackState: null
positionMs: 0
durationMs: 0
isRecording: false
sessionHelper: null
init: () ->
this.listenTo(context.BrowserMediaStore, this.onBrowserMediaChange);
onBrowserMediaChange: (changes) ->
@onPositionUpdate(PLAYBACK_MONITOR_MODE.BROWSER_MEDIA)
onCurrentTimeChanged: (time) ->
@time = time
@currentTimeChanged = true
@issueChange()
onMediaStartPlay: (data) ->
BrowserMediaActions.play()
onMediaStopPlay: (data) ->
if !data.endReached
BrowserMediaActions.stop()
onMediaPausePlay: (data) ->
# if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording
if !data.endReached
BrowserMediaActions.pause()
onMediaChangePosition: (data) ->
seek = data.positionMs;
BrowserMediaActions.seek(seek);
issueChange: () ->
@state =
playbackState: @playbackState
playbackStateChanged: @playbackStateChanged
positionUpdateChanged: @positionUpdateChanged
currentTimeChanged: @currentTimeChanged
positionMs: @positionMs
durationMs: @durationMs
isPlaying: @isPlaying
time: @time
this.trigger(@state)
@playbackStateChanged = false
@positionUpdateChanged = false
@currentTimeChanged = false
onPlaybackStateChange: (text) ->
@playbackState = text
@playbackStateChanged = true
@issueChange()
onPositionUpdate: (playbackMode) ->
if playbackMode == PLAYBACK_MONITOR_MODE.BROWSER_MEDIA
@positionMs = BrowserMediaStore.onGetPlayPosition() || 0
@durationMs = BrowserMediaStore.onGetPlayDuration() || 0;
@isPlaying = BrowserMediaStore.playing;
else
raise 'Only BROWSER_MEDIA is supported'
@positionUpdateChanged = true
@issueChange()
}
)

View File

@ -0,0 +1,159 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
BrowserMediaActions = @BrowserMediaActions
@BrowserMediaStore = Reflux.createStore(
{
listenables: BrowserMediaActions
audio: null
loaded: false
loading: false
playing: false
paused: false
id: null
media_type: null
playbackState:(position, time) ->
state = {}
if @playing
state.playbackState = 'play_start'
else if @paused
state.playbackState = 'play_pause'
else
state.playbackState = 'play_stop'
state.playbackStateChanged = !@state? || @state.isPlaying != @playing || @state.paused != @paused
state.positionUpdateChanged = !@state? || @state.positionMs != position
state.currentTimeChanged = !@state? || @state.time != time
state
changed: () ->
position = @onGetPlayPosition()
time = context.JK.prettyPrintSeconds(parseInt(position / 1000))
playbackState = @playbackState(position, time)
# XXX: how to deal with duration? no mention in Howler API
target = {id: @id, isPlaying: @playing, loaded: @loaded, paused: @paused, positionMs: position, time: time, durationMs: @onGetPlayDuration()}
$.extend(true, target, playbackState)
@state = target
@trigger(@state)
onLoad: (id, urls, media_type) ->
if @audio
@audio.stop()
@audio.unload()
@loaded = false
@loading = false
@playing = false
@paused = false
@id = id
@media_type = media_type
@loading = true
@playing = false
@paused = false
console.log("URLS", urls)
@audio = new Howl({
src: urls,
autoplay: false,
loop: false,
volume: 1,
preload: true,
onend: @onAudioEnded,
onload: @onAudioLoaded,
onpause: @onAudioPause,
onplay: @onAudioPlay
})
@changed()
onPlay: () ->
if @audio
@playing = true
@paused = false
@audio.play()
try
if !@audio.recorded_play
rest.postUserEvent({name: @media_type + '_play'}) if @media_type?
context.stats.write('web.' + @media_type + '.play', {
value: 1,
user_id: context.JK.currentUserId,
user_name: context.JK.currentUserName
}) if @media_type
@audio.recorded_play = true
catch e
logger.warn("BrowserMediaStore: unable to post user event")
onPause: () ->
if @audio
@playing = false
@paused = true
@audio.pause()
onStop: () ->
if @audio
@playing = false
@paused = false
@audio.pause()
@audio.seek(0)
onSeek: (pos) ->
if @audio
console.log("seek time", pos)
@audio.seek(pos / 1000)
onGetPlayPosition: () ->
if @audio
try
position = @audio.seek()
if position == @audio
return 0
position * 1000
catch e
return 0
else
0
onGetPlayDuration: () ->
if @audio
# XXX : how to determine duration?
try
duration = @audio.duration()
return Math.round(duration) * 1000
catch e
return 0
else
0
onAudioEnded: () ->
logger.debug("onAudioEnded", this, arguments)
@playing = false
@changed()
onAudioLoaded: () ->
logger.debug("onAudioLoaded")
@loaded = true
@changed()
onAudioPause: () ->
logger.debug("onAudioPause")
@changed()
onAudioPlay: () ->
logger.debug("onAudioPlay")
@playing = true
@paused = false
@changed()
}
)

View File

@ -0,0 +1,454 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
JamTrackPlayerActions = @JamTrackPlayerActions
BrowserMediaStore = @BrowserMediaStore
BrowserMediaActions = @BrowserMediaActions
@JamTrackPlayerStore = Reflux.createStore(
{
listenables: JamTrackPlayerActions
jamTrack: null
previous: null
requestedSearch: null
requestedFilter: null
subscriptions: {}
enqueuedMixdowns: {}
childWindow: null
watchedMixdowns: {}
init: ->
# Register with the app store to get @app
this.listenTo(context.AppStore, this.onAppInit)
this.listenTo(context.BrowserMediaStore, this.onBrowserMediaChanged)
onAppInit: (app) ->
@app = app
onBrowserMediaChanged: (browserMediaState) ->
@browserMediaState = browserMediaState
@changed()
getState: () ->
@state
onOpen: (jamTrack, popup=true) ->
if jamTrack.id == @jamTrack?.id && @childWindow?
logger.info("JamTrackPlayerStore: request to open already-opened JamTrack; ignoring")
return
@enqueuedMixdowns = {}
@jamTrack = jamTrack
sampleRate = 48
@sampleRate = if sampleRate == 48 then 48 else 44
if popup
if @childWindow?
@childWindow.close()
logger.debug("opening JamTrackPlayer window")
@childWindow = window.open("/popups/jamtrack-player/" + @jamTrack.id, 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=580,width=450')
@changed()
onWindowUnloaded: () ->
BrowserMediaActions.stop()
@childWindow = null
pickMyPackage: () ->
return unless @jamTrack?
for mixdown in @jamTrack.mixdowns
myPackage = null
for mixdown_package in mixdown.packages
if mixdown_package.file_type == 'mp3' && mixdown_package.encrypt_type == null && mixdown_package.sample_rate == @sampleRate
myPackage = mixdown_package
break
mixdown.myPackage = myPackage
subscriptionKey: (mixdown_package) ->
"mixdown-#{mixdown_package.id}"
subscribe: (mixdown_package) ->
key = @subscriptionKey(mixdown_package)
if !@watchedMixdowns[key]?
# we need to register
context.JK.SubscriptionUtils.subscribe('mixdown', mixdown_package.id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onMixdownSubscriptionEvent)
@watchedMixdowns[key] = {type:'mixdown', id: mixdown_package.id}
unsubscribe: (mixdown_package) ->
key = @subscriptionKey(mixdown_package)
if @watchedMixdowns[key]?
context.JK.SubscriptionUtils.unsubscribe('mixdown', mixdown_package.id)
delete @watchedMixdowns[key]
manageWatchedMixdowns: () ->
if @jamTrack?
for mixdown in @jamTrack.mixdowns
if mixdown.myPackage
if mixdown.myPackage.signing_state == 'SIGNED'
@unsubscribe(mixdown.myPackage)
else
@subscribe(mixdown.myPackage)
else
for key, subscription of @watchedMixdowns
logger.debug("unsubscribing bulk", key, subscription)
context.JK.SubscriptionUtils.unsubscribe(subscription.type, subscription.id)
# we cleared them all out; clear out storage
@watchedMixdowns = {}
onMixdownSubscriptionEvent: (e, data) ->
logger.debug("JamTrackStore: subscription notification received: type:" + data.type, data)
return unless @jamTrack?
mixdown_package_id = data.id
for mixdown in @jamTrack.mixdowns
for mixdown_package in mixdown.packages
if mixdown_package.id == mixdown_package_id
mixdown_package.signing_state = data.body.signing_state
mixdown_package.packaging_steps = data.body.packaging_steps
mixdown_package.current_packaging_step = data.body.current_packaging_step
logger.debug("updated package with subscription notification event")
if mixdown_package.signing_state == 'SIGNING_TIMEOUT' || mixdown_package.signing_state == 'QUEUED_TIMEOUT' || mixdown_package.signing_state == 'QUIET_TIMEOUT' || mixdown_package.signing_state == 'ERROR'
@reportError(mixdown)
@changed()
break
# this drives the state engine required to get a Mixdown from 'available on the server' to
manageMixdownSynchronization: () ->
if @jamTrack
@jamTrack.activeMixdown = null
@jamTrack.activeStem = null
# let's see if we have a mixdown active?
#if !@jamTrack?.last_mixdown_id?
# logger.debug("JamTrackStore: no mixdown active")
for mixdown in @jamTrack.mixdowns
if mixdown.id == @jamTrack.last_mixdown_id
@jamTrack.activeMixdown = mixdown
#logger.debug("JamTrackStore: mixdown active:", mixdown)
break
for stem in @jamTrack.tracks
if stem.id == @jamTrack.last_stem_id
@jamTrack.activeStem = stem
break
# let's check and see if we've asked the BrowserMediaStore to load this particular file or not
if @jamTrack?.activeStem
if @browserMediaState?.id != @jamTrack.activeStem.id
BrowserMediaActions.load(@jamTrack.activeStem.id, [window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@jamTrack.id}/stems/#{@jamTrack.activeStem.id}/download.mp3?file_type=mp3"], 'jamtrack_web_player')
@jamTrack.activeStem.client_state = 'downloading'
else
if @browserMediaState.loaded
@jamTrack.activeStem.client_state = 'ready'
else
@jamTrack.activeStem.client_state = 'downloading'
else if @jamTrack?.activeMixdown
# if we don't have this on the server yet, don't engage the rest of this logic...
return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
activePackage = @jamTrack.activeMixdown.myPackage
if activePackage?
if @browserMediaState?.id != activePackage.id
BrowserMediaActions.load(activePackage.id, [window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{@jamTrack.activeMixdown.id}/download.mp3?file_type=mp3&sample_rate=48"], 'jamtrack_web_player')
@jamTrack.activeMixdown.client_state = 'downloading'
else
if @browserMediaState.loaded
@jamTrack.activeMixdown.client_state = 'ready'
else
@jamTrack.activeMixdown.client_state = 'downloading'
else if @jamTrack?
masterTrack = null
for jamTrackTrack in @jamTrack.tracks
if jamTrackTrack.track_type == 'Master'
masterTrack = jamTrackTrack
break
if @browserMediaState?.id != @jamTrack.id
BrowserMediaActions.load(@jamTrack.id, [masterTrack.preview_mp3_url], 'jamtrack_web_player')
@jamTrack.client_state = 'downloading'
else
if @browserMediaState.loaded
@jamTrack.client_state = 'ready'
else
@jamTrack.client_state = 'downloading'
else
logger.error("unknown condition when processing manageMixdownSynchronization")
changed: () ->
@pickMyPackage()
@manageWatchedMixdowns()
@manageMixdownSynchronization()
@state = {
jamTrack: @jamTrack,
opened: @previous == null && @jamTrack != null,
closed: @previous != null && @jamTrack == null,
fullTrackActivated: @previousMixdown != null && @jamTrack?.activeMixdown == null}
@previous = @jamTrack
@previousMixdown = @jamTrack?.activeMixdown
this.trigger(@state)
onCreateMixdown: (mixdown, done, fail) ->
#volumeSettings = context.jamClient.GetJamTrackSettings();
#track_settings = []
#for track in volumeSettings.tracks
# track_settings.push({id: track.id, pan: track.pan, vol: track.vol_l, mute: track.mute})
#mixdown.settings.tracks = track_settings
logger.debug("creating mixdown", mixdown)
rest.createMixdown(mixdown)
.done((created) =>
@addMixdown(created)
logger.debug("created mixdown", created)
@onEnqueueMixdown({id: created.id}, done, fail)
)
.fail((jqxhr) =>
fail(jqxhr)
)
onEditMixdown: (mixdown) ->
logger.debug("editing mixdown", mixdown)
rest.editMixdown(mixdown)
.done((updatedMixdown) =>
logger.debug("edited mixdown")
@updateMixdown(updatedMixdown)
).fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Custom Mix', text: 'The server was unable to edit this mix.'})
)
onDeleteMixdown: (mixdown) ->
logger.debug("deleting mixdown", mixdown)
rest.deleteMixdown(mixdown)
.done(() =>
logger.debug("deleted mixdown")
@deleteMixdown(mixdown)
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Deleted Custom Mix', text: 'The server was unable to delete this mix.'})
)
stopPlaying: () ->
alert("stop playing")
onOpenMixdown: (mixdown) ->
logger.debug("opening mixdown", mixdown)
# check if it's already available in the backend or not
rest.markMixdownActive({id: @jamTrack.id, mixdown_id: mixdown.id})
.done((edited) =>
logger.debug("marked mixdown as active")
@jamTrack = edited
BrowserMediaActions.stop()
@changed()
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
)
onOpenStem: (stem_id) ->
logger.debug("opening stem", stem_id)
# check if it's already available in the backend or not
rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null, stem_id: stem_id})
.done((edited) =>
logger.debug("marked stem as active")
@jamTrack = edited
BrowserMediaActions.stop()
@changed()
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
)
onActivateNoMixdown: (jamTrack) ->
logger.debug("activating no mixdown")
rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null})
.done((edited) =>
logger.debug("marked JamTrack as active")
@jamTrack = edited
@changed()
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
)
onCloseMixdown: (mixdown) ->
logger.debug("closing mixdown", mixdown)
onEnqueueMixdown: (mixdown, done, fail) ->
logger.debug("enqueuing mixdown", mixdown)
package_settings = {file_type: 'mp3', encrypt_type: null, sample_rate: @sampleRate}
package_settings.id = mixdown.id
rest.enqueueMixdown(package_settings)
.done((enqueued) =>
@enqueuedMixdowns[mixdown.id] = {}
logger.debug("enqueued mixdown package", package_settings)
@addOrUpdatePackage(enqueued)
done(enqueued) if done
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Create Custom Mix', text: 'Click the error icon to retry.'})
fail(jqxhr) if fail?
)
onDownloadMixdown: (mixdown) ->
logger.debug("download mixdown", mixdown)
onRefreshMixdown: (mixdown) ->
logger.debug("refresh mixdown", mixdown)
addMixdown: (mixdown) ->
if @jamTrack?
logger.debug("adding mixdown to JamTrackStore", mixdown)
@jamTrack.mixdowns.splice(0, 0, mixdown)
@changed()
else
logger.warn("no jamtrack to add mixdown to in JamTrackStore", mixdown)
deleteMixdown: (mixdown) ->
if @jamTrack?
logger.debug("deleting mixdown from JamTrackStore", mixdown)
index = null
for matchMixdown, i in @jamTrack.mixdowns
if mixdown.id == matchMixdown.id
index = i
if index?
@jamTrack.mixdowns.splice(index, 1)
if @jamTrack.activeMixdown?.id == mixdown.id
@onActivateNoMixdown(@jamTrack)
@changed()
else
logger.warn("unable to find mixdown to delete in JamTrackStore", mixdown)
else
logger.warn("no jamtrack to delete mixdown for in JamTrackStore", mixdown)
updateMixdown: (mixdown) ->
if @jamTrack?
logger.debug("editing mixdown from JamTrackStore", mixdown)
index = null
for matchMixdown, i in @jamTrack.mixdowns
if mixdown.id == matchMixdown.id
index = i
if index?
@jamTrack.mixdowns[index] = mixdown
@changed()
else
logger.warn("unable to find mixdown to edit in JamTrackStore", mixdown)
else
logger.warn("no jamtrack to edit mixdown for in JamTrackStore", mixdown)
addOrUpdatePackage: (mixdown_package) ->
if @jamTrack?
added = false
index = null
for mixdown in @jamTrack.mixdowns
existing = false
if mixdown_package.jam_track_mixdown_id == mixdown.id
for possiblePackage, i in mixdown.packages
if possiblePackage.id == mixdown_package.id
existing = true
index = i
break
if existing
mixdown.packages[index] = mixdown_package
logger.debug("replacing mixdown package in JamTrackStore", mixdown_package)
else
mixdown.packages.splice(0, 0, mixdown_package)
logger.debug("adding mixdown package in JamTrackStore")
added = true
@changed()
break
if !added
logger.debug("couldn't find the mixdown associated with package in JamTrackStore", mixdown_package)
else
logger.warn("no mixdown to add package to in JamTrackStore", mixdown_package)
reportError: (mixdown) ->
enqueued = @enqueuedMixdowns[mixdown?.id]
# don't double-report
if !enqueued? || enqueued.marked
return
enqueued.marked = true
data = {
value: 1,
user_id: context.JK.currentUserId,
user_name: context.JK.currentUserName,
result: "signing state: #{mixdown.myPackage?.signing_state}, client state: #{mixdown.client_state}",
mixdown: mixdown.id,
package: mixdown.myPackage?.id
detail: mixdown.myPackage?.error_reason
}
rest.createAlert("Mixdown Sync failed for #{context.JK.currentUserName}", data)
context.stats.write('web.mixdown.error', data)
}
)

View File

@ -0,0 +1,18 @@
$ = jQuery
context = window
logger = context.JK.logger
@UserStore = Reflux.createStore(
{
user: null
listenables: @UserActions
onLoaded:(user) ->
@user = user
@changed()
changed:() ->
@trigger({user: @user})
}
)

View File

@ -35,6 +35,9 @@
}
function afterShow(data) {
context.JK.Tracking.redeemCompleteTrack()
$noPurchasesPrompt.addClass('hidden')
$purchasedJamTracks.empty()
$thanksPanel.addClass("hidden")
@ -68,6 +71,7 @@
rest.placeOrder()
.done(function(purchaseResponse) {
context.JK.currentUserFreeJamTrack = false // make sure the user sees no more free notices without having to do a full page refresh
rest.updateUser()
checkoutUtils.setLastPurchase(purchaseResponse)
jamTrackUtils.checkShoppingCart()

View File

@ -110,7 +110,7 @@
function getItemHtml(item) {
var replacements = {
id: item.id,
name: item.first_name + " " + item.last_name,
name: item.name,
image: item.photo_url,
subtext: item.location
};

View File

@ -81,7 +81,13 @@
}
function updateHeader() {
$('#user').html(userMe.name);
if (userMe.first_name =='Anonymous' && userMe.last_name == 'Anonymous') {
$('#user').html(userMe.email);
}
else {
$('#user').html(userMe.name);
}
showAvatar();
}

View File

@ -799,7 +799,7 @@
// * Unix
context.JK.GetOSAsString = function() {
if(!os) {
if(context.jamClient.IsNativeClient()) {
if(context.jamClient && context.jamClient.IsNativeClient()) {
os = context.jamClient.GetOSAsString();
}
else {
@ -868,6 +868,25 @@
}
}
context.JK.getFullFirstError = function(fieldName, errors, field_mapper) {
if (errors == null) return null;
if (errors[fieldName] && errors[fieldName].length > 0) {
var displayField = fieldName
if (field_mapper) {
displayField = field_mapper[fieldName]
if (!displayField) {
// in case mapper has no data for this field
displayField = fieldName;
}
}
return displayField + ' ' + errors[fieldName][0]
}
else {
return null;
}
}
/**
* Returns a ul with an li per field name.
* @param fieldName the name of the field

View File

@ -3,7 +3,7 @@
"use strict";
context.JK = context.JK || {};
context.JK.IndividualJamTrack = function (app) {
context.JK.IndividualJamTrack = function (app, dontPoke) {
var rest = context.JK.Rest();
var logger = context.JK.logger;
@ -47,7 +47,7 @@
new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: true, color:'black', master_adds_line_break: false, preload_master:true})
if(track.track_type =='Master') {
if(track.track_type =='Master' && !dontPoke) {
context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.one_by_two .watch-video'), $page.find('.checkout'));
}
})

View File

@ -74,6 +74,9 @@ class Tracking
@logger.debug("existing new recorded")
context.JK.GA.virtualPageView('/client#/redeemSignup/new-user')
redeemCompleteTrack: () ->
context.JK.GA.virtualPageView('/client#/redeemed-successful')
context.JK.Tracking = new Tracking()

View File

@ -52,8 +52,17 @@ body.jam, body.web, .dialog{
font-size:20px;
color: #ed3618;
p {color:#ed3618}
&.jamtrack-web-play {
p {
font-size:20px;
}
}
}
.help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks {
font-size:12px;

View File

@ -29,6 +29,31 @@
display:inline-block;
}
.purchased-jam-tracks {
height:167px;
overflow:auto;
margin-top:20px;
}
.jamtable {
a {
text-decoration: underline !important;
color:#fc0 !important;
}
th {
font-size:14px;
padding:3px 10px;
}
td {
padding:4px 15px;
font-size:14px;
}
tbody {
}
}
.search-area {
.easydropdown-wrapper{
max-width:350px;

View File

@ -1,81 +1,444 @@
body.web.landing_jamtrack.individual_jamtrack {
@import "client/common";
.landing-content {
.header {
text-align:center;
body.web.individual_jamtrack {
$copy-color-on-dark: #b9b9b9;
$copy-color-on-white: #575757;
$cta-color: #e03d04;
$chunkyBorderWidth: 6px;
.full-row {
width: 100%;
}
.row {
width: 663px;
}
.name-and-artist {
padding-top: 60px;
}
.summary-text {
p {
color: $copy-color-on-dark;
clear: both;
font-size: 20px;
line-height: 175%;
}
}
.row.awesome {
h2 {
color: $copy-color-on-white;
font-size: 30px;
margin-bottom: 20px;
}
p {
width:100%;
color: $copy-color-on-white;
font-size: 18px;
line-height: 175%;
}
h1 {
font-size:24px;
padding: 40px 0;
}
.awesome-thing {
color: $copy-color-on-white;
margin-bottom: 20px;
.awesome-item {
padding: 24px;
background-color: #dedede;
border-radius: 12px;
@include border_box_sizing;
width: 663px;
}
.video-wrapper {
float: right;
margin-left: 10px;
margin-bottom:80px;
.cta-text {
margin-left: 10px;
text-align:center;
margin-top:20px;
}
&.left {
float: left;
margin-left: 0;
margin-right: 10px;
.cta-text {
margin-left: 0;
margin-right: 10px;
}
}
}
.video-container {
width: 420px;
padding-bottom: 53.33%;
}
.awesome-image {
float: right;
margin-left: 10px;
&.left {
float: left;
margin-left: 0;
margin-right: 10px;
}
}
.awesome-number {
height: 44px;
width: 44px;
display: inline-block;
background-color: $cta-color;
color: white;
font-size: 24px;
font-family: Arial, Helvetica, sans-serif;
font-weight: 400;
text-align: center;
padding-top: 8px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
border-width: 2px;
border-color: white;
border-style: solid;
margin-right: 10px;
@include border_box_sizing;
}
h3 {
font-size: 24px;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 150%;
}
}
p {
font-size: 16px;
}
.cta-row {
padding: 40px 0;
background-color: #262626;
h2 {
font-size:18px;
color:white;
text-decoration:underline;
margin-bottom:8px;
font-size: 30px;
margin-bottom: 10px;
}
ul {
width: 100%;
margin:5px 0 0;
p {
color: $copy-color-on-dark;
}
li {
margin-bottom:4px;
}
.white-bar {
p {
color: $copy-color-on-white;
}
.row .column {
padding:0 30px;
.top-container {
padding-bottom: 20px;
}
}
.red-bar {
font-size: 30px;
font-weight: bold;
color: white;
background-color: #e03c02;
padding: 40px 0;
text-align: center;
}
img.app-preview {
width: 340px;
float: left;
margin-left: -15px; // because image has black on the left, which you can't see on back background
margin-right: 20px;
margin-bottom: 30px;
}
h1.jam-track-name {
color: white;
font-size: 30px !important;
padding-top: 0;
margin-left: 35px;
}
h2.original-artist {
margin-top: 10px;
color: #8d8d8d;
font-size: 24px;
margin-left: 35px;
}
.preview-text {
color: #fefefe;
}
.cta-button {
background-color: $cta-color;
&.processing {
background-color:gray;
}
}
.cta-text {
color: $cta-color !important;
}
.privacy-policy {
color: #ffcf00;
}
.browse-all {
color: #ffb800;
}
p {
line-height: 150%;
width: 100% !important;
}
.bottom-banner {
background-color: #e03c02;
}
.arrow1 {
position:absolute;
left: -371px;
top: -80px;
}
.arrow2 {
position:absolute;
left:-230px;
top:373px;
}
.testimonials {
background-color:white;
position:absolute;
width:350px;
right:55px;
top:287px;
@include border_box_sizing;
z-index:1;
.jamtrack-overview-video {
h3 {
font-size:21px;
width:400px;
}
.video-wrapper {
.video-container {
width: 400px;
padding-bottom: 53.33%;
}
}
}
h3 {
color:$copy-color-on-white;
text-align:center;
margin-bottom:20px;
font-size:24px;
line-height:125%;
}
h4 {
position:absolute;
float:none;
top: 161px;
left: 173px;
font-size:14px;
color:$copy-color-on-white;
strong {
font-weight:bold;
}
white-space:nowrap;
}
.row:nth-of-type(2) {
margin-top:30px;
.testimonial {
position:relative;
min-height:250px;
width:350px;
margin-bottom:40px;
}
.row .column:nth-of-type(1) {
width: 50%;
left:0;
height:100%;
.testimonial-speech-bubble {
width:350px;
}
.row .column:nth-of-type(2) {
width: 50%;
left:50%;
.testimonial-avatar {
width:60px;
position:relative;
border-width:0 0 0 1px;
border-color:white;
border-style:solid;
top:5px;
left:92px;
}
.testimonial-youtube {
width:143px;
position:relative;
left:112px;
top:0;
}
}
.watch-video {
position:relative;
top:40px;
text-align:left;
display:block;
font-size:16px;
left:20%;
width:300px;
}
.preview-and-action-box {
background-color: black;
position: absolute;
right: 55px;
width: 330px;
top: 200px;
@include border_box_sizing;
border-width: $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
z-index: 1;
.previews {
margin-top:10px;
}
.jamtrack-reasons {
margin: 10px 0 0 20px;
}
.preview-jamtrack-header {
background-color: $cta-color;
color: white;
font-size: 24px;
text-align: center;
padding: 20px 0;
border-width: 0 0 $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
}
.white-bordered-button {
margin-top: 20px;
}
.preview-area {
.browse-jamtracks-wrapper {
text-align:center;
width:90%;
}
padding: 10px;
.prompt {
margin-top:10px;
border-width: 0 0 $chunkyBorderWidth;
border-style: solid;
border-color: $copy-color-on-dark;
&.logged-in {
border-width:0;
.tracks {
height:258px;
}
}
.tracks {
padding:10px 0 20px;
margin-top:10px;
overflow-y: scroll;
height: 160px;
margin-bottom:10px;
}
.cta-button {
font-size: 24px;
color: white;
background-color: $cta-color;
text-align: center;
padding: 10px;
display: block;
width: 100%;
border: 1px outset buttonface;
font-family: Raleway, Arial, Helvetica, sans-serif;
}
}
.terms-help {
float:left;
margin-top:2px;
font-size:12px;
}
.register-area {
padding: 10px;
input {
background-color: $copy-color-on-dark;
color: black;
font-size: 16px;
&[name="terms"] {
width:auto;
line-height:24px;
vertical-align:middle;
}
}
.icheckbox_minimal {
float: left;
top: 6px;
margin-left:64px;
}
.cta-button {
font-size: 24px;
color: white;
background-color: $cta-color;
text-align: center;
padding: 10px;
display: block;
width: 100%;
border: 1px outset buttonface;
font-family: Raleway, Arial, Helvetica, sans-serif;
}
.browse-all {
text-decoration: underline;
text-align: center;
display: block;
}
.privacy-policy {
text-decoration: underline;
}
form {
margin: 0 0 10px;
}
.errors {
font-size:12px;
height:20px;
margin:0;
visibility: hidden;
text-align: center;
color: red;
font-weight: bold;
&.active {
visibility: visible;
}
}
label {
display: inline-block;
height: 36px;
vertical-align: middle;
line-height: 36px;
margin-bottom: 15px;
@include border-box_sizing;
&.terms-help {
width:205px;
height:28px;
line-height:14px;
float:right;
}
}
input {
width: 206px;
height: 36px;
float: right;
margin-bottom: 15px;
@include border-box_sizing;
}
}
p {
line-height: 150%;
color: white;
font-size: 14px;
}
}
.jam-track-preview-holder {
@ -87,7 +450,7 @@ body.web.landing_jamtrack.individual_jamtrack {
}
&[data-track-type="Track"] {
width: 50%;
width: 100%;
}
}
@ -107,41 +470,10 @@ body.web.landing_jamtrack.individual_jamtrack {
background-color:black;
}
}
.cta-holder {
&.instrument-selection {
a {
font-size: 18px;
text-decoration:underline;
}
.browse-instrument {
margin-top:20px;
text-decoration:underline;
}
}
margin-top:30px;
.price-advisory {
font-size:14px;
text-align:center;
.checkout {
display:inline-block;
position:relative;
}
.browse-band {
display:inline-block;
position:relative;
margin-top:20px;
}
.browse-all {
display:inline-block;
position:relative;
margin-top:20px;
}
.value-indicator {
position:absolute;
left: 301px;
top: 19px;
width:80px;
}
color:white;
margin-top:10px;
}
}

View File

@ -0,0 +1,147 @@
body.web.landing_jamtrack.individual_jamtrack_v2 {
.landing-content {
.header {
text-align:center;
}
p {
width:100%;
}
h1 {
font-size:24px;
}
h2 {
font-size:18px;
color:white;
text-decoration:underline;
margin-bottom:8px;
}
ul {
width: 100%;
margin:5px 0 0;
}
li {
margin-bottom:4px;
}
.row .column {
padding:0 30px;
position:absolute;
float:none;
}
.row:nth-of-type(2) {
margin-top:30px;
position:relative;
min-height:250px;
}
.row .column:nth-of-type(1) {
width: 50%;
left:0;
height:100%;
}
.row .column:nth-of-type(2) {
width: 50%;
left:50%;
position:relative;
border-width:0 0 0 1px;
border-color:white;
border-style:solid;
}
}
.watch-video {
position:relative;
top:40px;
text-align:left;
display:block;
font-size:16px;
left:20%;
width:300px;
}
.previews {
margin-top:10px;
}
.jamtrack-reasons {
margin: 10px 0 0 20px;
}
.white-bordered-button {
margin-top: 20px;
}
.browse-jamtracks-wrapper {
text-align:center;
width:90%;
}
.prompt {
margin-top:10px;
}
.jam-track-preview-holder {
margin-bottom: 7px;
float:left;
&[data-track-type="Master"] {
width: 100%;
}
&[data-track-type="Track"] {
width: 50%;
}
}
.jam-track-preview {
font-size:12px;
.instrument-part {
width:175px
}
.instrument-name,.part {
display:none;
}
.loading-text {
right:-1px;
background-color:black;
}
}
.cta-holder {
&.instrument-selection {
a {
font-size: 18px;
text-decoration:underline;
}
.browse-instrument {
margin-top:20px;
text-decoration:underline;
}
}
margin-top:30px;
text-align:center;
.checkout {
display:inline-block;
position:relative;
}
.browse-band {
display:inline-block;
position:relative;
margin-top:20px;
}
.browse-all {
display:inline-block;
position:relative;
margin-top:20px;
}
.value-indicator {
position:absolute;
left: 301px;
top: 19px;
width:80px;
}
}
}

View File

@ -86,12 +86,29 @@ body.web.landing_page {
}
}
.white-bar {
z-index:-1; // to let sidebar be interactable
background-color:white;
}
.landing-tag-play-learn-earn {
color:#929292;
font-size: 18px;
position: absolute;
top: 52px;
right: 0;
}
&.landing_jamtrack, &.landing_product {
.landing-tag {
left:50%;
text-align:center;
}
p, ul, li {
font-size:14px;
line-height:125%;

View File

@ -0,0 +1,320 @@
@import "client/common";
body.jamtrack-player-popup.popup {
text-align:center;
background-color: #242323;
#minimal-container {
padding-bottom:0;
}
.media-controls-popup {
padding:15px 15px 3px 15px;
}
.field {
margin-top:20px;
}
.icheckbox_minimal {
float:left;
}
label {
float: left;
margin-left: 5px;
margin-top:2px;
}
h3 {
text-align:left;
margin-bottom:5px;
}
.actions {
position:relative;
margin-top:20px;
font-size:11px;
margin-bottom:10px;
}
.help-link {
position:absolute;
left:-6px;
top:0;
}
.close-link {
}
.header {
padding-bottom:20px;
h3 {
text-align:center;
font-weight:bold;
}
h4 {
margin-top:15px;
font-size:12px;
font-weight:normal;
span {
vertical-align:middle;
}
img {
vertical-align:middle;
margin-left:5px;
height:16px;
}
}
h5 {
font-size:12px;
font-weight:normal;
span {
vertical-align:middle;
}
img {
vertical-align:middle;
margin-left:5px;
height:16px;
}
}
}
.extra-controls {
margin-top:20px;
h4 {
text-align:left;
font-size:18px;
a {
font-size:11px;
position:absolute;
right:20px;
}
&.custom-mix-header {
margin-top:20px;
}
}
.my-mixes {
margin-top:5px;
max-height:170px;
border-width:1px;
border-bottom-color:#676767;
border-top-color:#676767;
border-left-color:#171717;
border-right-color:#171717;
border-style:solid;
overflow:auto;
@include border_box_sizing;
}
.mixdown-display {
display:table;
font-size:12px;
color:$ColorTextTypical;
width:100%;
border-width:1px 0;
border-top-color:#343434;
border-bottom-color:#282828;
border-style:solid;
background-color:#2c2c2c;
@include border_box_sizing;
border-spacing:7px;
text-align: left;
&.active {
background-color:#44423f;
}
}
.active-stem-select {
max-width:206px;
}
.stems {
height:147px;
overflow:auto;
margin:20px 0;
table {
width:100%;
}
th {
color:white;
font-size:14px;
line-height:20px;
vertical-align:middle;
}
td {
font-size:12px;
height:24px;
line-height:28px;
vertical-align:middle;
input {
width:auto;
}
img{
vertical-align:middle;
margin-right:10px;
}
}
td.mute {
padding-right:25px;
}
.mute {
text-align:right;
padding-right:20px;
}
}
.stems-holder {
height:250px;
overflow:auto;
}
.stem {
}
.mixdown-name {
line-height:125%;
width:210px;
text-align:left;
display: table-cell;
vertical-align: middle;
}
.mixdown-actions {
display: table-cell;
vertical-align: middle;
margin-left:10px;
width:100px;
white-space:nowrap;
min-width:100px;
}
.mixdown-stateful {
display:inline-block;
vertical-align:middle;
width:24px;
height:24px;
cursor:pointer;
}
.mixdown-download {
width:24px;
height:24px;
cursor:pointer;
&.not-ready {
visibility:hidden;
}
}
.mixdown-play {
width:24px;
height:24px;
margin-right:10px;
cursor:pointer;
}
.mixdown-edit {
margin-left:10px;
width:24px;
height:24px;
cursor:pointer;
}
.mixdown-delete {
margin-left:10px;
width:24px;
height:24px;
cursor:pointer;
}
.create-mix {
margin-top:5px;
border-color:$ColorTextTypical;
border-style: solid;
border-width:1px 0;
padding: 7px 0 20px;
&.not-active {
padding:7px 0 7px;
}
p {
line-height:125%;
color:$ColorTextTypical;
text-align:left;
font-size:12px;
}
.field {
display:block;
height:25px;
margin-top:15px;
}
ul.error-text {
float:right;
display:block !important;
color: red;
margin-top: 5px;
}
a.create-mix-btn {
margin-top:15px;
float:right;
margin-right: 2px;
margin-top: 3px;
}
label {
display:inline;
float:left;
}
select, input {
width:170px;
float:right;
@include border_box_sizing;
background-color:$ColorTextBoxBackground;
}
}
}
.arrow-down {
float:none;
margin-left:5px;
margin-top:0;
margin-right:0;
border-top: 4px solid #fc0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
display:inline-block;
padding-top:1px;
}
.arrow-up {
float:none;
margin-right:0;
margin-left:5px;
margin-bottom:2px;
border-bottom: 4px solid #fc0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
display:inline-block;
padding-top:1px;
}
}

View File

@ -1,5 +1,7 @@
/**
*= require web/Raleway
*= require client/jamServer
*= require dialogs/banner
*= require client/ie
*= require client/jamkazam
*= require client/screen_common
@ -12,4 +14,8 @@
*= require_directory .
*= require client/metronomePlaybackModeSelect
*= require_directory ../client/react-components
*/
*/
.no-websocket-connection.active {
display:none !important;
}

View File

@ -33,6 +33,11 @@ body.web {
}
}
.after-black-bar-border {
border-top:1px solid #444;
margin: 0 6%;
box-sizing: border-box;
}
#profile.userinfo {
margin-top:30px;
}

View File

@ -83,7 +83,14 @@ class ApiJamTrackMixdownsController < ApiController
@package.last_downloaded_at = now
@package.first_downloaded_at = now if @package.first_downloaded_at.nil?
@package.save!
redirect_to @package.sign_url(120)
if params[:download]
redirect_to @package.sign_url(120, 'application/octet-stream', "attachment; filename=#{@package.jam_track_mixdown.name}.#{params[:file_type]}")
else
redirect_to @package.sign_url(120)
end
else
@package.enqueue_if_needed
render :json => { :message => "not available, digitally signing Jam Track Mixdown offline." }, :status => 202

View File

@ -3,7 +3,7 @@ class ApiJamTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right, :mark_active]
before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right, :mark_active, :download_stem]
respond_to :json
@ -30,8 +30,16 @@ class ApiJamTracksController < ApiController
def mark_active
mixdown_id = params[:mixdown_id]
stem_id = params[:stem_id]
# only one should be active at a time, so we enforce that on the server here. if you specify one, the other will get nulled automatically
if mixdown_id
stem_id = nil
else
mixdown_id = nil
end
@jam_track_right.last_mixdown_id = mixdown_id
@jam_track_right.last_stem_id = stem_id
@jam_track_right.save
if @jam_track_right.errors.any?
@ -96,6 +104,25 @@ class ApiJamTracksController < ApiController
render "api_jam_tracks/purchased", :layout => nil
end
def download_stem
if @jam_track_right.valid?
if params[:stem_id] == 'master'
jam_track_track = @jam_track_right.jam_track.master_track
else
jam_track_track = JamTrackTrack.find(params[:stem_id])
end
if params[:download]
redirect_to jam_track_track.web_download_sign_url(120, params[:file_type], 'application/octet-stream', "attachment; filename=#{@jam_track_right.jam_track.name + '-' + jam_track_track.display_name}.mp3")
else
redirect_to jam_track_track.web_download_sign_url(120, params[:file_type])
end
else
render :json => { :message => "download limit surpassed", :errors=>@jam_track_right.errors }, :status => 403
end
end
def download
if @jam_track_right.valid?

View File

@ -396,8 +396,9 @@ class ApiRecordingsController < ApiController
body << "User: " + current_user.admin_url + "\n\n"
body << "Recording Landing: #{recording_detail_url(@recording.id)}\n"
AdminMailer.alerts({
subject:"Video Uploaded by #{current_user.name}",
private_public = @recording.is_public ? 'Public' : 'Private'
AdminMailer.social({
subject:"#{private_public } Video Uploaded by #{current_user.name}",
body:body
}).deliver

View File

@ -2,7 +2,7 @@ require 'sanitize'
class
ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth]
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth, :user_event]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :authorizations,
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
@ -683,6 +683,21 @@ ApiUsersController < ApiController
render :json => {}, :status => 200
end
def opened_jamtrack_web_player
User.where(id: current_user.id).update_all(first_opened_jamtrack_web_player: Time.now)
render :json => {}, :status => 200
end
def user_event
event = UserEvent.new
event.name = params[:name]
event.detail = params[:detail].to_json if params[:detail]
event.user_id = current_user.id if current_user
event.save
render :json => {}, :status => 200
end
# creates display-ready session data for sharing
def share_session
provider = params[:provider]

View File

@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base
end
def track_affiliate_visits
if params[:affiliate] && params[:utm_medium] == 'affiliate' && params[:utm_source] == 'affiliate'
if params[:affiliate]
visit_cookie = cookies[:affiliate_visitor]
AffiliateReferralVisit.track(affiliate_id: params[:affiliate], visited: visit_cookie, remote_ip: request.remote_ip, visited_url: request.fullpath, referral_url: request.referer, current_user: current_user)

View File

@ -68,7 +68,65 @@ class LandingsController < ApplicationController
render 'watch_overview_tight', layout: 'web'
end
def individual_jamtrack
@no_landing_tag = true
@landing_tag_play_learn_earn = true
@show_after_black_bar_border = true
instrument_id = nil
instrument_name = nil
instrument_count = 0
if params[:instrument]
instrument = params[:instrument].downcase.sub('-', ' ')
instrument = Instrument.find_by_id(instrument)
instrument_id = instrument.id if instrument
instrument_name = instrument.description
query, next_ptr, instrument_count = JamTrack.index({instrument: instrument_id}, current_user)
end
@jam_track = JamTrack.find_by_slug(params[:plan_code])
@jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code]) unless @jam_track
if @jam_track.nil?
redirect_to '/client/#jamtrack'
return
end
band_jam_track_count = @jam_track.band_jam_track_count
jam_track_count = JamTrack.count
@title = individual_jamtrack_title(false, params[:generic], @jam_track)
@description = individual_jamtrack_desc(false, params[:generic], @jam_track)
@page_data = {jam_track: @jam_track, all_track_count: jam_track_count, band_track_count: band_jam_track_count, band: false, generic: params[:generic], instrument: instrument_name, instrument_id: instrument_id, instrument_count: instrument_count}
gon.jam_track_plan_code = @jam_track.plan_code if @jam_track
gon.generic = params[:generic]
gon.instrument_id = instrument_id
render 'individual_jamtrack', layout: 'web'
end
def individual_jamtrack_band
@no_landing_tag = true
@landing_tag_play_learn_earn = true
@show_after_black_bar_border = true
@jam_track = JamTrack.find_by_slug(params[:plan_code])
@jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code]) unless @jam_track
if @jam_track.nil?
redirect_to '/client/#jamtrack'
return
end
band_jam_track_count = @jam_track.band_jam_track_count
jam_track_count = JamTrack.count
@title = individual_jamtrack_title(true, params[:generic], @jam_track)
@description = individual_jamtrack_desc(true, params[:generic], @jam_track)
@page_data = {jam_track: @jam_track, all_track_count: jam_track_count, band_track_count: band_jam_track_count, band: true, generic: params[:generic]}
gon.jam_track_plan_code = @jam_track.plan_code if @jam_track
gon.generic = params[:generic]
render 'individual_jamtrack', layout: 'web'
end
def individual_jamtrack_v2
@no_landing_tag = true
instrument_id = nil
instrument_name = nil
@ -90,11 +148,11 @@ class LandingsController < ApplicationController
gon.jam_track_plan_code = @jam_track.plan_code if @jam_track
gon.generic = params[:generic]
gon.instrument_id = instrument_id
render 'individual_jamtrack', layout: 'web'
render 'individual_jamtrack_v2', layout: 'web'
end
def individual_jamtrack_band
def individual_jamtrack_band_v2
@no_landing_tag = true
@jam_track = JamTrack.find_by_slug(params[:plan_code])
@jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code]) unless @jam_track
@ -105,7 +163,7 @@ class LandingsController < ApplicationController
@page_data = {jam_track: @jam_track, all_track_count: jam_track_count, band_track_count: band_jam_track_count, band: true, generic: params[:generic]}
gon.jam_track_plan_code = @jam_track.plan_code if @jam_track
gon.generic = params[:generic]
render 'individual_jamtrack', layout: 'web'
render 'individual_jamtrack_v2', layout: 'web'
end
def individual_jamtrack_v1

View File

@ -28,4 +28,11 @@ class PopupsController < ApplicationController
gon.recording_id = @recording_id
render :layout => "minimal"
end
def jamtrack_player
@jamtrack_id = params[:jam_track_id]
@websocket = true
gon.jamtrack_id = @jamtrack_id
render :layout => "minimal"
end
end

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