diff --git a/admin/Gemfile b/admin/Gemfile
index 756225a1a..1a1a7c248 100644
--- a/admin/Gemfile
+++ b/admin/Gemfile
@@ -44,7 +44,7 @@ gem 'rails3-jquery-autocomplete'
gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable'
gem 'mime-types', '1.25'
gem 'meta_search'
-gem 'fog', "~> 1.32.0"
+gem 'fog'
gem 'unf', '0.1.3' #optional fog dependency
gem 'country-select'
gem 'aasm', '3.0.16'
diff --git a/admin/app/admin/affiliate_quarterly_totals.rb b/admin/app/admin/affiliate_quarterly_totals.rb
new file mode 100644
index 000000000..4f065c267
--- /dev/null
+++ b/admin/app/admin/affiliate_quarterly_totals.rb
@@ -0,0 +1,37 @@
+ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quarterly Payments' do
+
+ menu :label => 'Quarterly Reports', :parent => 'Affiliates'
+
+ config.sort_order = 'due_amount_in_cents DESC'
+ config.batch_actions = false
+ config.clear_action_items!
+ config.filters = true
+ config.per_page = 50
+ config.paginate = true
+
+ filter :affiliate_partner
+ filter :year
+ filter :quarter
+ filter :closed
+ filter :paid
+
+ form :partial => 'form'
+
+ index do
+
+ # default_actions # use this for all view/edit/delete links
+
+ column 'Year' do |oo| oo.year end
+ column 'Quarter' do |oo| oo.quarter end
+ column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end
+ column "Due (\u00A2)" do |oo| oo.due_amount_in_cents end
+ column 'JamTracks Sold' do |oo| oo.jamtracks_sold end
+ column 'Paid' do |oo| oo.paid end
+ column 'Closed' do |oo| oo.paid end
+
+ end
+
+ controller do
+
+ end
+end
diff --git a/admin/app/admin/affiliate_traffic_totals.rb b/admin/app/admin/affiliate_traffic_totals.rb
new file mode 100644
index 000000000..f3f24b8ab
--- /dev/null
+++ b/admin/app/admin/affiliate_traffic_totals.rb
@@ -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.display_name, 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
diff --git a/admin/app/admin/affiliate_users.rb b/admin/app/admin/affiliate_users.rb
index b21d512eb..1658d926d 100644
--- a/admin/app/admin/affiliate_users.rb
+++ b/admin/app/admin/affiliate_users.rb
@@ -2,15 +2,18 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do
menu :label => 'Referrals', :parent => 'Affiliates'
+ config.sort_order = 'created_at DESC'
config.batch_actions = false
config.clear_action_items!
- config.filters = false
+ config.filters = true
+
+ filter :affiliate_referral
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
diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb
index 0409ee36c..db7bfa53d 100644
--- a/admin/app/admin/affiliates.rb
+++ b/admin/app/admin/affiliates.rb
@@ -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
diff --git a/admin/app/admin/crash_dumps.rb b/admin/app/admin/crash_dumps.rb
index bcfcd9106..4d85ae8ae 100644
--- a/admin/app/admin/crash_dumps.rb
+++ b/admin/app/admin/crash_dumps.rb
@@ -1,28 +1,27 @@
ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do
# Note: a lame thing is it's not obvious how to make it search on email instead of user_id.
filter :timestamp
- filter :user_email, :as => :string
filter :client_id
+ filter :user_id
+
menu :parent => 'Misc'
+ config.sort_order = 'created_at DESC'
+
index do
+ column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
+ column "Client Version", :client_version
+ column "Client Type", :client_type
+ column "Download" do |post|
+ link_to 'Link', post.sign_url
+ end
column "Timestamp" do |post|
(post.timestamp || post.created_at).strftime('%b %d %Y, %H:%M')
end
- column "Client Type", :client_type
- column "Dump URL" do |post|
- link_to post.uri, post.uri
+ column "Description" do |post|
+ post.description
end
-
- column "User ID", :user_id
-
- # FIXME (?): This isn't performant (though it likely doesn't matter). Could probably do a join.
- column "User Email" do |post|
- unless post.user_id.nil?
- post.user_email
- end
- end
- column "Client ID", :client_id
actions
+
end
end
diff --git a/admin/app/admin/download_tracker.rb b/admin/app/admin/download_tracker.rb
new file mode 100644
index 000000000..6d0daa013
--- /dev/null
+++ b/admin/app/admin/download_tracker.rb
@@ -0,0 +1,35 @@
+ActiveAdmin.register JamRuby::DownloadTracker, :as => 'DownloadTrackers' do
+
+ menu :label => 'Download Trackers', :parent => 'JamTracks'
+
+ config.batch_actions = false
+ config.filters = true
+ config.per_page = 50
+
+ filter :remote_ip
+
+ index do
+ column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
+ column 'Created' do |oo| oo.created_at end
+ column 'JamTrack' do |oo| oo.jam_track end
+ column 'Paid' do |oo| oo.paid end
+ column 'Blacklisted?' do |oo| IpBlacklist.listed(oo.remote_ip) ? 'Yes' : 'No' end
+ column 'Remote IP' do |oo| oo.remote_ip end
+ column "" do |oo|
+ link_to 'Blacklist This IP', "download_trackers/#{oo.id}/blacklist_by_ip"
+ end
+ end
+
+ member_action :blacklist_by_ip, :method => :get do
+ tracker = DownloadTracker.find(params[:id])
+
+ if !IpBlacklist.listed(tracker.remote_ip)
+ ip = IpBlacklist.new
+ ip.remote_ip = tracker.remote_ip
+ ip.save!
+ end
+
+ redirect_to admin_download_trackers_path, :notice => "IP address #{tracker.remote_ip} blacklisted."
+ end
+
+end
diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb
index b8c5caa31..87d167330 100644
--- a/admin/app/admin/fake_purchaser.rb
+++ b/admin/app/admin/fake_purchaser.rb
@@ -32,6 +32,7 @@ ActiveAdmin.register_page "Fake Purchaser" do
jam_track_right.user = user
jam_track_right.jam_track = jam_track
jam_track_right.is_test_purchase = true
+ jam_track_right.version = jam_track.version
jam_track_right.save!
count = count + 1
end
diff --git a/admin/app/admin/gift_card_upload.rb b/admin/app/admin/gift_card_upload.rb
new file mode 100644
index 000000000..87e69850e
--- /dev/null
+++ b/admin/app/admin/gift_card_upload.rb
@@ -0,0 +1,41 @@
+ActiveAdmin.register_page "Giftcarduploads" do
+
+ menu :label => 'Gift Cards Upload', :parent => 'JamTracks'
+
+ page_action :upload_giftcards, :method => :post do
+ GiftCard.transaction do
+
+ puts params
+
+ file = params[:jam_ruby_gift_card][:csv]
+ array_of_arrays = CSV.read(file.tempfile.path)
+ array_of_arrays.each do |row|
+ if row.length != 1
+ raise "UKNONWN CSV FORMAT! Must be 1 column"
+ end
+
+ code = row[0]
+
+ gift_card = GiftCard.new
+ gift_card.code = code
+ gift_card.card_type = params[:jam_ruby_gift_card][:card_type]
+ gift_card.origin = file .original_filename
+ gift_card.save!
+ end
+
+ redirect_to admin_giftcarduploads_path, :notice => "Created #{array_of_arrays.length} gift cards!"
+ end
+ end
+
+ content do
+ semantic_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f|
+ f.inputs "Upload Gift Cards" do
+ f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)"
+ f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES
+ end
+ f.actions
+ end
+ end
+
+end
+
diff --git a/admin/app/admin/gift_cards.rb b/admin/app/admin/gift_cards.rb
new file mode 100644
index 000000000..8e1d4e80d
--- /dev/null
+++ b/admin/app/admin/gift_cards.rb
@@ -0,0 +1,24 @@
+ActiveAdmin.register JamRuby::GiftCard, :as => 'GiftCards' do
+
+ menu :label => 'Gift Cards', :parent => 'JamTracks'
+
+ config.batch_actions = false
+ config.filters = true
+ config.per_page = 50
+
+ scope("Redeemed Most Recently", default: true) { |scope| scope.where('user_id IS NOT NULL').order('updated_at DESC') }
+ scope("Available") { |scope| scope.where('user_id is NULL') }
+
+ filter :card_type
+ filter :origin
+ filter :code
+
+ index do
+ column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
+ column 'Code' do |oo| oo.code end
+ column 'Card Type' do |oo| oo.card_type end
+ column 'Origin' do |oo| oo.origin end
+ column 'Created' do |oo| oo.created_at end
+ end
+
+end
diff --git a/admin/app/admin/ip_blacklist.rb b/admin/app/admin/ip_blacklist.rb
new file mode 100644
index 000000000..3a8f1e14e
--- /dev/null
+++ b/admin/app/admin/ip_blacklist.rb
@@ -0,0 +1,13 @@
+ActiveAdmin.register JamRuby::IpBlacklist, :as => 'IP Blacklist' do
+
+ menu :label => 'IP Blacklist', :parent => 'Operations'
+
+ config.sort_order = 'created_at desc'
+ config.batch_actions = false
+
+ index do
+ column :remote_ip
+ column :notes
+ column :created_at
+ end
+end
\ No newline at end of file
diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb
index 534b65903..938cf0bf5 100644
--- a/admin/app/admin/jam_tracks.rb
+++ b/admin/app/admin/jam_tracks.rb
@@ -5,13 +5,16 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
config.sort_order = 'name_asc'
config.batch_actions = false
+ filter :name
+ filter :original_artist
filter :genres
filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS
scope("Default", default: true) { |scope| scope }
scope("Onboarding TODO") { |scope| scope.where('onboarding_exceptions is not null') }
scope("Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'") }
- scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') }
+ scope("TimTracks Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tim Waurick'") }
+ # scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') }
form :partial => 'form'
diff --git a/admin/app/admin/sale_line_items.rb b/admin/app/admin/sale_line_items.rb
new file mode 100644
index 000000000..b43c9cfee
--- /dev/null
+++ b/admin/app/admin/sale_line_items.rb
@@ -0,0 +1,40 @@
+ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do
+
+ menu :label => 'Line Items', :parent => 'Purchases'
+
+ config.sort_order = 'created_at DESC'
+ config.batch_actions = false
+ config.clear_action_items!
+ config.filters = true
+ config.per_page = 50
+ config.paginate = true
+
+ filter :affiliate_referral_id
+ filter :free
+
+ form :partial => 'form'
+
+ scope("Non Free", default: true) { |scope| scope.where(free: false).order('created_at desc') }
+ scope("Free") { |scope| scope.where(free: true).order('created_at desc') }
+
+ index do
+ # default_actions # use this for all view/edit/delete links
+
+ column 'Product' do |oo| oo.product end
+ column "Partner" do |oo|
+ link_to("#{oo.affiliate_referral.display_name} #{oo.affiliate_referral_fee_in_cents ? "#{oo.affiliate_referral_fee_in_cents}\u00A2" : ''}", oo.affiliate_referral.admin_url, {:title => oo.affiliate_referral.display_name}) if oo.affiliate_referral
+ end
+ column 'User' do |oo|
+ link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name})
+ end
+ column 'When' do |oo|
+ oo.created_at
+ end
+
+ end
+
+
+ controller do
+
+ end
+end
diff --git a/admin/app/admin/user_blacklist.rb b/admin/app/admin/user_blacklist.rb
new file mode 100644
index 000000000..538125600
--- /dev/null
+++ b/admin/app/admin/user_blacklist.rb
@@ -0,0 +1,13 @@
+ActiveAdmin.register JamRuby::UserBlacklist, :as => 'User Blacklist' do
+
+ menu :label => 'User Blacklist', :parent => 'Operations'
+
+ config.sort_order = 'created_at desc'
+ config.batch_actions = false
+
+ index do
+ column :user
+ column :notes
+ column :created_at
+ end
+end
\ No newline at end of file
diff --git a/admin/app/admin/user_progression.rb b/admin/app/admin/user_progression.rb
index 6ea4bb285..aa72d8014 100644
--- a/admin/app/admin/user_progression.rb
+++ b/admin/app/admin/user_progression.rb
@@ -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
diff --git a/admin/app/controllers/email_controller.rb b/admin/app/controllers/email_controller.rb
index 1e0c55182..45af1bb2e 100644
--- a/admin/app/controllers/email_controller.rb
+++ b/admin/app/controllers/email_controller.rb
@@ -15,5 +15,10 @@ class EmailController < ApplicationController
headers['Content-Type'] ||= 'text/csv'
@users = User.where(subscribe_email: true)
+
+ # if specified, return only users that have redeemed or bought a JamTrack
+ if params[:any_jam_track]
+ @users = @users.select('DISTINCT users.id, email, first_name, last_name').joins(:sales => :sale_line_items).where("sale_line_items.product_type = 'JamTrack'")
+ end
end
end
\ No newline at end of file
diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb
index b7a3b047d..78a1dce70 100644
--- a/admin/app/views/admin/affiliates/_form.html.erb
+++ b/admin/app/views/admin/affiliates/_form.html.erb
@@ -2,6 +2,8 @@
<%= f.semantic_errors *f.object.errors.keys %>
<%= f.inputs do %>
<%= f.input(:partner_name, :input_html => {:maxlength => 128}) %>
+ <%= f.input(:entity_type, :as => :select, :collection => AffiliatePartner::ENTITY_TYPES) %>
+ <%= f.input(:rate) %>
<% end %>
<%= f.actions %>
<% end %>
diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim
index 334cdc27e..ac038e5ca 100644
--- a/admin/app/views/admin/jam_tracks/_form.html.slim
+++ b/admin/app/views/admin/jam_tracks/_form.html.slim
@@ -13,6 +13,7 @@
= f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true
= f.input :genres
+ = f.input :year
= f.input :duration, hint: 'this should rarely need editing because it comes from the import process'
= f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false
= f.input :price, :required => true, :input_html => {type: 'numeric'}
diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb
index efa44b0f8..f226e68fa 100644
--- a/admin/app/views/email/dump_emailables.csv.erb
+++ b/admin/app/views/email/dump_emailables.csv.erb
@@ -1,2 +1,2 @@
<%- headers = ['email', 'name', 'unsubscribe_token'] -%>
-<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%>
\ No newline at end of file
+<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.anonymous? ? '-' : user.first_name, user.unsubscribe_token]) %><%- end -%>
\ No newline at end of file
diff --git a/admin/config/application.rb b/admin/config/application.rb
index 0e13be779..22e3df96f 100644
--- a/admin/config/application.rb
+++ b/admin/config/application.rb
@@ -110,6 +110,7 @@ module JamAdmin
config.redis_host = "localhost:6379"
+ config.email_social_alias = 'social@jamkazam.com'
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'
@@ -153,5 +154,12 @@ 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}"
+
+ config.download_tracker_day_range = 30
+ config.max_user_ip_address = 10
+ config.max_multiple_users_same_ip = 2
end
end
diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb
index b449898e8..8ae1ae4ed 100644
--- a/admin/config/environments/development.rb
+++ b/admin/config/environments/development.rb
@@ -46,4 +46,5 @@ JamAdmin::Application.configure do
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'
+ config.email_social_alias = 'social-dev@jamkazam.com'
end
diff --git a/admin/config/initializers/gift_cards.rb b/admin/config/initializers/gift_cards.rb
new file mode 100644
index 000000000..8c967ccb4
--- /dev/null
+++ b/admin/config/initializers/gift_cards.rb
@@ -0,0 +1,9 @@
+class JamRuby::GiftCard
+
+ attr_accessor :csv
+
+
+ def process_csv
+
+ end
+end
diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb
index 33efd995c..60537c467 100644
--- a/admin/config/initializers/jam_tracks.rb
+++ b/admin/config/initializers/jam_tracks.rb
@@ -2,28 +2,5 @@ class JamRuby::JamTrack
# add a custom validation
- attr_accessor :preview_generate_error
- before_save :jmep_json_generate
- validate :jmep_text_validate
-
- def jmep_text_validate
- begin
- JmepManager.execute(self.jmep_text)
- rescue ArgumentError => err
- errors.add(:jmep_text, err.to_s)
- end
- end
-
- def jmep_json_generate
- self.licensor_id = nil if self.licensor_id == ''
- self.jmep_json = nil if self.jmep_json == ''
- self.time_signature = nil if self.time_signature == ''
-
- begin
- self[:jmep_json] = JmepManager.execute(self.jmep_text)
- rescue ArgumentError => err
- #errors.add(:jmep_text, err.to_s)
- end
- end
end
diff --git a/db/Gemfile.lock b/db/Gemfile.lock
index 88080fb81..62739af59 100644
--- a/db/Gemfile.lock
+++ b/db/Gemfile.lock
@@ -16,6 +16,3 @@ PLATFORMS
DEPENDENCIES
pg_migrate (= 0.1.13)!
-
-BUNDLED WITH
- 1.10.5
diff --git a/db/manifest b/db/manifest
index 3d992ef76..00d3c8c44 100755
--- a/db/manifest
+++ b/db/manifest
@@ -298,11 +298,25 @@ musician_search.sql
enhance_band_profile.sql
alter_band_profile_rate_defaults.sql
repair_band_profile.sql
-profile_teacher.sql
jam_track_onboarding_enhancements.sql
jam_track_name_drop_unique.sql
-populate_languages.sql
-populate_subjects.sql
jam_track_searchability.sql
harry_fox_agency.sql
jam_track_slug.sql
+mixdown.sql
+aac_master.sql
+video_recording.sql
+web_playable_jamtracks.sql
+affiliate_partner_rate.sql
+track_downloads.sql
+jam_track_lang_idx.sql
+giftcard.sql
+add_description_to_crash_dumps.sql
+acappella.sql
+purchasable_gift_cards.sql
+versionable_jamtracks.sql
+session_controller.sql
+jam_tracks_bpm.sql
+profile_teacher.sql
+populate_languages.sql
+populate_subjects.sql
\ No newline at end of file
diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql
new file mode 100644
index 000000000..28f67f81d
--- /dev/null
+++ b/db/up/aac_master.sql
@@ -0,0 +1,3 @@
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint;
\ No newline at end of file
diff --git a/db/up/acappella.sql b/db/up/acappella.sql
new file mode 100644
index 000000000..cf17e052d
--- /dev/null
+++ b/db/up/acappella.sql
@@ -0,0 +1,2 @@
+INSERT INTO genres (id, description) values ('acapella', 'A Capella');
+ALTER TABLE jam_track_licensors ADD COLUMN slug VARCHAR UNIQUE;
diff --git a/db/up/add_description_to_crash_dumps.sql b/db/up/add_description_to_crash_dumps.sql
new file mode 100644
index 000000000..e08540d85
--- /dev/null
+++ b/db/up/add_description_to_crash_dumps.sql
@@ -0,0 +1 @@
+ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(20000);
\ No newline at end of file
diff --git a/db/up/affiliate_partner_rate.sql b/db/up/affiliate_partner_rate.sql
new file mode 100644
index 000000000..d36c80c81
--- /dev/null
+++ b/db/up/affiliate_partner_rate.sql
@@ -0,0 +1 @@
+ALTER TABLE affiliate_partners ADD COLUMN rate NUMERIC(8,2) DEFAULT 0.10;
\ No newline at end of file
diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql
new file mode 100644
index 000000000..96a5374b1
--- /dev/null
+++ b/db/up/crash_dumps_2.sql
@@ -0,0 +1,6 @@
+ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255);
+ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000);
+ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100);
+ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100);
+ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey;
+
diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql
new file mode 100644
index 000000000..eae33c13e
--- /dev/null
+++ b/db/up/giftcard.sql
@@ -0,0 +1,13 @@
+CREATE TABLE gift_cards (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ code VARCHAR(64) UNIQUE NOT NULL,
+ user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE,
+ card_type VARCHAR(64) NOT NULL,
+ origin VARCHAR(200),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id);
+
+ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0;
diff --git a/db/up/jam_track_lang_idx.sql b/db/up/jam_track_lang_idx.sql
new file mode 100644
index 000000000..aa5c84c26
--- /dev/null
+++ b/db/up/jam_track_lang_idx.sql
@@ -0,0 +1 @@
+CREATE INDEX ON jam_tracks(language);
diff --git a/db/up/jam_tracks_bpm.sql b/db/up/jam_tracks_bpm.sql
new file mode 100644
index 000000000..0fce5154d
--- /dev/null
+++ b/db/up/jam_tracks_bpm.sql
@@ -0,0 +1,2 @@
+ALTER TABLE jam_tracks ADD COLUMN bpm numeric(8,3);
+INSERT INTO instruments (id, description) VALUES ('percussion', 'Percussion');
\ No newline at end of file
diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql
new file mode 100644
index 000000000..24295cdae
--- /dev/null
+++ b/db/up/mixdown.sql
@@ -0,0 +1,61 @@
+CREATE TABLE jam_track_mixdowns (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ settings JSON NOT NULL,
+ name VARCHAR(1000) NOT NULL,
+ description VARCHAR(1000),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE jam_track_mixdown_packages (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE CASCADE,
+ file_type VARCHAR NOT NULL ,
+ sample_rate INTEGER NOT NULL,
+ url VARCHAR(2048),
+ md5 VARCHAR,
+ length INTEGER,
+ downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE,
+ last_step_at TIMESTAMP,
+ last_signed_at TIMESTAMP,
+ download_count INTEGER NOT NULL DEFAULT 0,
+ signed_at TIMESTAMP,
+ downloaded_at TIMESTAMP,
+ signing_queued_at TIMESTAMP,
+ error_count INTEGER NOT NULL DEFAULT 0,
+ error_reason VARCHAR,
+ error_detail VARCHAR,
+ should_retry BOOLEAN NOT NULL DEFAULT FALSE,
+ packaging_steps INTEGER,
+ current_packaging_step INTEGER,
+ private_key VARCHAR,
+ signed BOOLEAN,
+ signing_started_at TIMESTAMP,
+ first_downloaded TIMESTAMP,
+ signing BOOLEAN NOT NULL DEFAULT FALSE,
+ encrypt_type VARCHAR,
+ first_downloaded_at TIMESTAMP,
+ last_downloaded_at TIMESTAMP,
+ version VARCHAR NOT NULL DEFAULT '1',
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL;
+
+ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE;
+
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP;
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE;
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE;
+ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
+
+CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued);
+CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at);
+CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at);
+
+CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued);
+CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at);
+CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at);
diff --git a/db/up/purchasable_gift_cards.sql b/db/up/purchasable_gift_cards.sql
new file mode 100644
index 000000000..9ef8d29fa
--- /dev/null
+++ b/db/up/purchasable_gift_cards.sql
@@ -0,0 +1,24 @@
+
+
+CREATE TABLE gift_card_types (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ card_type VARCHAR(64) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5');
+INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10');
+
+CREATE TABLE gift_card_purchases (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
+ gift_card_type_id VARCHAR(64) REFERENCES gift_card_types(id) ON DELETE SET NULL,
+ recurly_adjustment_uuid VARCHAR(500),
+ recurly_adjustment_credit_uuid VARCHAR(500),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+ALTER TABLE sale_line_items ADD COLUMN gift_card_purchase_id VARCHAR(64) REFERENCES gift_card_purchases(id);
diff --git a/db/up/session_controller.sql b/db/up/session_controller.sql
new file mode 100644
index 000000000..d146481b6
--- /dev/null
+++ b/db/up/session_controller.sql
@@ -0,0 +1 @@
+ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id);
\ No newline at end of file
diff --git a/db/up/track_downloads.sql b/db/up/track_downloads.sql
new file mode 100644
index 000000000..f328a9579
--- /dev/null
+++ b/db/up/track_downloads.sql
@@ -0,0 +1,30 @@
+CREATE TABLE download_trackers (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+ remote_ip VARCHAR(400) NOT NULL,
+ jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
+ paid BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id);
+CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip);
+CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid);
+
+
+CREATE TABLE ip_blacklists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ remote_ip VARCHAR(400) UNIQUE NOT NULL,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+CREATE TABLE user_blacklists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/db/up/versionable_jamtracks.sql b/db/up/versionable_jamtracks.sql
new file mode 100644
index 000000000..9751bb940
--- /dev/null
+++ b/db/up/versionable_jamtracks.sql
@@ -0,0 +1 @@
+ALTER TABLE jam_track_rights ADD COLUMN version VARCHAR NOT NULL DEFAULT '0';
\ No newline at end of file
diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql
new file mode 100644
index 000000000..46502b104
--- /dev/null
+++ b/db/up/video_recording.sql
@@ -0,0 +1,3 @@
+ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE user_authorizations ADD refresh_token VARCHAR;
+ALTER TABLE recordings ADD external_video_id VARCHAR;
\ No newline at end of file
diff --git a/db/up/web_playable_jamtracks.sql b/db/up/web_playable_jamtracks.sql
new file mode 100644
index 000000000..b3d6e8a4e
--- /dev/null
+++ b/db/up/web_playable_jamtracks.sql
@@ -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
+);
\ No newline at end of file
diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto
index 5d5ecf100..4acccc86d 100644
--- a/pb/src/client_container.proto
+++ b/pb/src/client_container.proto
@@ -82,6 +82,10 @@ message ClientMessage {
JAM_TRACK_SIGN_COMPLETE = 260;
JAM_TRACK_SIGN_FAILED = 261;
+ // jamtracks mixdown notifications
+ MIXDOWN_SIGN_COMPLETE = 270;
+ MIXDOWN_SIGN_FAILED = 271;
+
TEST_SESSION_MESSAGE = 295;
PING_REQUEST = 300;
@@ -188,6 +192,10 @@ message ClientMessage {
optional JamTrackSignComplete jam_track_sign_complete = 260;
optional JamTrackSignFailed jam_track_sign_failed = 261;
+ // jamtrack mixdown notification
+ optional MixdownSignComplete mixdown_sign_complete = 270;
+ optional MixdownSignFailed mixdown_sign_failed = 271;
+
// Client-Session messages (to/from)
optional TestSessionMessage test_session_message = 295;
@@ -612,6 +620,15 @@ message JamTrackSignFailed {
required int32 jam_track_right_id = 1; // jam track right id
}
+message MixdownSignComplete {
+ required string mixdown_package_id = 1; // jam track mixdown package id
+}
+
+message MixdownSignFailed {
+ required string mixdown_package_id = 1; // jam track mixdown package id
+}
+
+
message SubscriptionMessage {
optional string type = 1; // the type of the subscription
optional string id = 2; // data about what to subscribe to, specifically
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 139dc579a..2502ca2a7 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -64,6 +64,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner"
require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/scheduled/tally_affiliates"
require "jam_ruby/resque/jam_tracks_builder"
+require "jam_ruby/resque/jam_track_mixdown_packager"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/resque/long_running"
@@ -105,10 +106,14 @@ 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"
require "jam_ruby/models/machine_extra"
+require "jam_ruby/models/download_tracker"
+require "jam_ruby/models/ip_blacklist"
+require "jam_ruby/models/user_blacklist"
require "jam_ruby/models/fraud_alert"
require "jam_ruby/models/fingerprint_whitelist"
require "jam_ruby/models/rsvp_request"
@@ -209,6 +214,8 @@ require "jam_ruby/models/jam_track_track"
require "jam_ruby/models/jam_track_right"
require "jam_ruby/models/jam_track_tap_in"
require "jam_ruby/models/jam_track_file"
+require "jam_ruby/models/jam_track_mixdown"
+require "jam_ruby/models/jam_track_mixdown_package"
require "jam_ruby/models/genre_jam_track"
require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"
@@ -249,6 +256,10 @@ require "jam_ruby/models/language"
require "jam_ruby/models/subject"
require "jam_ruby/models/band_search"
require "jam_ruby/import/tency_stem_mapping"
+require "jam_ruby/models/jam_track_search"
+require "jam_ruby/models/gift_card"
+require "jam_ruby/models/gift_card_purchase"
+require "jam_ruby/models/gift_card_type"
include Jampb
diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav
new file mode 100644
index 000000000..33c1aea5f
Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav differ
diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav
new file mode 100644
index 000000000..3a3fd3d68
Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav differ
diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
index f14d8e424..abe1b9564 100644
--- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
+++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
@@ -1,6 +1,6 @@
module JamRuby
# sends out a boring ale
- class AdminMailer < ActionMailer::Base
+ class AdminMailer < ActionMailer::Base
include SendGrid
@@ -20,6 +20,14 @@ module JamRuby
subject: options[:subject])
end
+ def social(options)
+ mail(to: APP_CONFIG.email_social_alias,
+ from: APP_CONFIG.email_generic_from,
+ body: options[:body],
+ content_type: "text/plain",
+ subject: options[:subject])
+ end
+
def recurly_alerts(user, options)
body = options[:body]
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
index 6f760961e..2ab1bd292 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
@@ -1,5 +1,9 @@
-<% provide(:title, 'Confirm Email') %>
+<% provide(:title, 'Welcome to JamKazam!') %>
-
Welcome to JamKazam, <%= @user.first_name %>!
+
We’re delighted you have joined our community of 20,000+ musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click here to confirm this email has reached you successfully and we will then send the orientation email.
If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.
+
+
Best Regards,
+ Team JamKazam
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
index d412a7b92..26503a494 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
@@ -1,3 +1,8 @@
-Welcome to JamKazam, <%= @user.first_name %>!
+Welcome to JamKazam!
-To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.
\ No newline at end of file
+We’re delighted you have joined our community of 20,000+ musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click <%= @signup_confirm_url %> to confirm this email has reached you successfully and we will then send the orientation email.
+
+If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
index be9aa27fb..65bf1295f 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
@@ -1,5 +1,7 @@
<% provide(:title, 'New Musicians You Should Check Out') %>
-Hi <%= @user.first_name %>,
+<% 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:
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
index 05fbbd268..ee100dbff 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
@@ -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| %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
index 2fdc11256..769b5bcda 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
@@ -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.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
index 282c6dd90..3d1e15346 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
@@ -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
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
index b72d3c133..cf4816bf1 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
@@ -1,18 +1,20 @@
<% provide(:title, 'JamKazam Session Reminder') %>
-
+<% 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,
Team JamKazam
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
index c3f0576bf..333acdb74 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
@@ -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,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
index 4fbc59ace..119d57b16 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
@@ -1,17 +1,20 @@
<% provide(:title, 'Your JamKazam session starts in 1 hour!') %>
-
+<% if !@user.anonymous? %>
+
Hi <%= @user.first_name %>,
-
+
+<% end %>
+
-
+
This is a reminder that your JamKazam session<%= @session_name %>starts in 1 hour. We hope you have fun!
-
+
-
+
Best Regards,
Team JamKazam
-
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
index 70726a9e6..d4719bd4c 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
@@ -1,4 +1,6 @@
+<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
+<% end %>
This is a reminder that your JamKazam session
<%=@session_name%>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
index 35e380618..177f38c92 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
@@ -1,63 +1,118 @@
<% provide(:title, 'Welcome to JamKazam!') %>
+<% if !@user.anonymous? %>
We're delighted that you have decided to try the JamKazam service,
- and we hope that you will enjoy using JamKazam to play
- music with others.
- Following are some resources that can help you get oriented and get the most out of JamKazam.
+
We're delighted you have decided to join the JamKazam community of musicians, and we hope
+
+ you will enjoy using JamKazam and JamTracks to play more music. Following are some
+
+ resources and some things you can do to get the most out of JamKazam.
+
Playing With JamTracks
+
+ JamTracks 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. Here are some great JamTracks resources:
+
JamTracks User Guide - A set of articles that explain how to use all the JamTracks
+
+ features.
+
Get a JamTrack Free - A web page you can visit to search our catalog of JamTracks.
+
+ When you find a song you like, click the Get It Free button, and your first one is free! If
+
+ you already redeemed a free JamTrack or purchased JamTracks, you can also access
+
+ them on this page from your web browser.
+
JamKazam Application - A web page where you can download our free Mac or Windows
+
+ app. The app lets you do a lot more with JamTracks than you can do in a browser.
+
+
+
Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over
+
+ the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio
+
+ quality. You can set up online sessions that are public or private, for you alone or for others to
+
+ join. You can find and join others’ sessions, use backing tracks and loops in sessions, make
+
+ audio and video recordings of session performances, and more. Click here for a set of tutorial
+
+ videos that show how to use these features.
+
+
+
Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how
+
+ unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and
+
+ student playing together at all. JamKazam is a terrific service for teaching and taking online
+
+ music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and
+
+ your students in getting set up and ready to go.
+
+
+
Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who
+
+ you are as a musician, as well as your musical interests. For example, what instruments do you
+
+ play? What musical genres do you like best? Are you interested in getting into a virtual/online
+
+ or a real-world band? And so on. Filling out your profile will help you connect with others with
+
+ common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then
+
+ click on the Profile tile, and click the Edit Profile button.
+
+
+
Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do
+
+ this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the
+
+ user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in
+
+ this menu, and you can then use the options to invite your friends using their email addresses,
+
+ or via Facebook, or using your Google contacts.
-
Getting Started
- There are basically three kinds of setups you can use to play on JamKazam.
-
-
Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to
- handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly,
- creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
-
Computer with External Audio Interface - You can use a Windows or Mac computer with an external audio interface that you
- already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer
- to our Minimum System Requirements
- to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our
- Getting Started Video to learn more about your options here.
-
The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster.
- It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians
- who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the
- system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only
- through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the
- JamBlaster Video to learn more about this amazing new product.
-
-
+
Get Help
-
JamKazam Features
- JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch
- to easily get up to speed on some of the things you can do with JamKazam:
-
-
+ If you run into trouble and need help, please reach out to us. We will be glad to do everything
-
Getting Help
- If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running.
- You can visit our
- Support Portal
- to find knowledge base articles and post questions that have
- not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers,
- you can visit our Community Forum
- .
-
+ we can to answer your questions and get you up and running. You can visit our Support Portal
+
+ to find knowledge base articles and post questions that have not already been answered. You
+
+ can email us at support@jamkazam.com. And if you just want to chat, share tips and war
+
+ stories, and hang out with fellow JamKazamers, you can visit our Community Forum.
+
- Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!
+
+
+ Again, welcome to JamKazam, and we hope you have a great time here!
Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
index a9f2ed06b..52dc8ab65 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
@@ -1,4 +1,4 @@
-Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
+<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %>
We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam.
diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb
index 352048dc0..12a915ce3 100644
--- a/ruby/lib/jam_ruby/connection_manager.rb
+++ b/ruby/lib/jam_ruby/connection_manager.rb
@@ -28,13 +28,13 @@ module JamRuby
##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb
def gather_friends(connection, user_id)
- friend_ids = []
- connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
- friend_results.each do |friend_result|
- friend_ids.push(friend_result['friend_id'])
- end
+ friend_ids = []
+ connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
+ friend_results.each do |friend_result|
+ friend_ids.push(friend_result['friend_id'])
end
- return friend_ids
+ end
+ return friend_ids
end
# this simulates music_session destroy callbacks with activerecord
@@ -42,7 +42,7 @@ module JamRuby
music_session = ActiveMusicSession.find_by_id(music_session_id)
music_session.before_destroy if music_session
end
-
+
# reclaim the existing connection, if ip_address is not nil then perhaps a new address as well
def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time, udp_reachable, gateway)
music_session_id = nil
@@ -65,11 +65,19 @@ module JamRuby
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
- if isp.nil? then ispid = 0 else ispid = isp.coid end
+ if isp.nil? then
+ ispid = 0
+ else
+ ispid = isp.coid
+ end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
- if block.nil? then locid = 0 else locid = block.locid end
+ if block.nil? then
+ locid = 0
+ else
+ locid = block.locid
+ end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@@ -183,11 +191,19 @@ SQL
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
- if isp.nil? then ispid = 0 else ispid = isp.coid end
+ if isp.nil? then
+ ispid = 0
+ else
+ ispid = isp.coid
+ end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
- if block.nil? then locid = 0 else locid = block.locid end
+ if block.nil? then
+ locid = 0
+ else
+ locid = block.locid
+ end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@@ -199,11 +215,11 @@ SQL
lock_connections(conn)
conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
- [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
+ [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
# we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends
conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result|
- count = result.getvalue(0, 0) .to_i
+ count = result.getvalue(0, 0).to_i
# we're passing all this stuff so that the user record might be updated as well...
blk.call(conn, count) unless blk.nil?
end
@@ -291,7 +307,7 @@ SQL
# destroy the music_session if it's empty
num_participants = nil
- conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
+ conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
[previous_music_session_id]) do |result|
num_participants = result.getvalue(0, 0).to_i
end
@@ -324,11 +340,65 @@ SQL
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
+
+ update_session_controller(previous_music_session_id)
end
end
end
+ def update_session_controller(music_session_id)
+ active_music_session = ActiveMusicSession.find(music_session_id)
+
+ if active_music_session
+ music_session = active_music_session.music_session
+ if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller)
+ # find next in line, because the current 'session controller' is not part of the session
+ next_in_line(music_session, active_music_session)
+ end
+ end
+ end
+
+ # determine who should be session controller after someone leaves
+ def next_in_line(music_session, active_music_session)
+ session_users = active_music_session.users
+
+ # check friends 1st
+ session_friends = music_session.creator.friends && session_users
+ if session_friends.length > 0
+ music_session.session_controller = session_friends[0]
+ if music_session.save
+ active_music_session.tick_track_changes
+ Notification.send_tracks_changed(active_music_session)
+ return
+ end
+ end
+
+ # check invited 2nd
+ invited = music_session.invited_musicians && session_users
+ if invited.length > 0
+ music_session.session_controller = invited[0]
+ if music_session.save
+ active_music_session.tick_track_changes
+ Notification.send_tracks_changed(active_music_session)
+ return
+ end
+ end
+
+ # go by who joined earliest
+ earliest = active_music_session.connections.order(:joined_session_at).first
+
+ if earliest
+ music_session.session_controller = earliest
+ if music_session.save
+ active_music_session.tick_track_changes
+ Notification.send_tracks_changed(active_music_session)
+ return
+ end
+ end
+ music_session.creator
+ end
+
def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil)
connection = nil
@@ -349,7 +419,10 @@ SQL
if connection.errors.any?
raise ActiveRecord::Rollback
+ else
+ update_session_controller(music_session.id)
end
+
end
connection
@@ -383,6 +456,8 @@ SQL
if result.cmd_tuples == 1
@log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}")
+ update_session_controller(music_session.id)
+
JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id)
session_checks(conn, previous_music_session_id, user_id)
blk.call() unless blk.nil?
diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb
index e05c8e00e..60dfe9f1b 100644
--- a/ruby/lib/jam_ruby/constants/notification_types.rb
+++ b/ruby/lib/jam_ruby/constants/notification_types.rb
@@ -51,4 +51,7 @@ module NotificationTypes
JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE"
JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED"
+ MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE"
+ MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED"
+
end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb
index 662fce608..e84dff7c3 100644
--- a/ruby/lib/jam_ruby/constants/validation_messages.rb
+++ b/ruby/lib/jam_ruby/constants/validation_messages.rb
@@ -19,6 +19,8 @@ module ValidationMessages
# sessions
SESSION_NOT_FOUND = "Session not found."
+ NOT_FOUND = 'not found'
+
# genres
RECORDING_GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed."
BAND_GENRE_LIMIT_EXCEEDED = "No more than 3 genres are allowed."
diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb
index 403c75330..c6a6549b3 100644
--- a/ruby/lib/jam_ruby/jam_track_importer.rb
+++ b/ruby/lib/jam_ruby/jam_track_importer.rb
@@ -12,6 +12,7 @@ module JamRuby
@@log = Logging.logger[JamTrackImporter]
attr_accessor :name
+ attr_accessor :metadata
attr_accessor :reason
attr_accessor :detail
attr_accessor :storage_format
@@ -29,10 +30,265 @@ module JamRuby
end
def finish(reason, detail)
+ @@log.info("JamTrackImporter:#{self.name} #{reason} #{detail}")
self.reason = reason
self.detail = detail
+
+ if ENV['END_ON_FAIL'] == "1" && reason != 'success' && reason != 'jam_track_exists'
+ raise "#{reason} #{detail}"
+ end
end
+ def import_click_track(jam_track)
+ # we need to download the click track, if it exists.
+ Dir.mktmpdir do |tmp_dir|
+
+ @@log.info("importing clicking track for #{jam_track.original_artist}:#{jam_track.name}")
+
+ if jam_track.click_track
+ @@log.info("already has click track: #{jam_track.original_artist}:#{jam_track.name}")
+ finish('success', 'already_has_click_track')
+ return
+ end
+
+ click_track_file = jam_track.click_track_file
+ if click_track_file.nil?
+ @@log.info("no click track for #{jam_track.original_artist}:#{jam_track.name}")
+ finish('success', 'no_click_track')
+ return
+ end
+
+ original_filename = click_track_file[:original_filename]
+
+ if original_filename.nil?
+ @@log.info("no click track s3 path for #{jam_track.original_artist}:#{jam_track.name}")
+ finish('no_original_source', 'click track is missing s3 path:' + click_track_file.id)
+ return
+ end
+
+ #wav_file = File.join(tmp_dir, File.basename(click_track_file[:original_filename]))
+ #JamTrackImporter.song_storage_manager.download(click_track_file[:original_filename], wav_file)
+
+ JamTrack.transaction do
+ click_track = jam_track.click_track
+
+ if click_track.nil?
+ click_track = JamTrackTrack.new
+ click_track.original_filename = click_track_file[:original_filename]
+ click_track.original_audio_s3_path = click_track_file[:original_filename]
+ click_track.track_type = 'Click'
+ click_track.part = 'Clicktrack'
+ click_track.instrument_id = 'computer'
+ click_track.jam_track = jam_track
+ click_track.position = 10000
+ if !click_track.save
+ @@log.error("unable to create jamtrack click track #{click_track.errors.inspect}")
+ finish("jam_track_click", "unable to create: #{click_track.errors.inspect}")
+ return false
+ end
+ end
+
+ jam_track.increment_version!
+
+ # with the click track in hand, flesh out the details
+ synchronize_audio_track(jam_track, tmp_dir, false, click_track)
+
+ finish('success', nil)
+ end
+ end
+ end
+
+ def generate_jmep(jam_track)
+ if !jam_track.blank?
+ finish('success', 'jmep already exists')
+ return
+ else
+ # we need to download the click track, if it exists.
+ Dir.mktmpdir do |tmp_dir|
+
+ master_track = jam_track.master_track
+
+ click_track = jam_track.click_track_file
+
+ if master_track.nil?
+ finish('no_master_track', nil)
+ return
+ end
+
+ master_track_file = File.join(tmp_dir, File.basename(master_track[:url_48]))
+ begin
+ JamTrackImporter.private_s3_manager.download(master_track.url_by_sample_rate(44), master_track_file)
+ rescue Exception => e
+ @@log.error("unable to download master track")
+ finish("no-download-master", master_track.url_by_sample_rate(44))
+ return
+ end
+
+ if click_track
+ click_track_file = File.join(tmp_dir, File.basename(click_track[:original_filename]))
+ JamTrackImporter.song_storage_manager.download(click_track[:original_filename], click_track_file)
+ else
+ # we'll use the master for click analysis. not ideal, but would work
+ click_track_file = master_track_file
+ end
+
+ start_time = determine_start_time(master_track_file, tmp_dir, master_track[:url])
+
+ # bpm comes from git clone http://www.pogo.org.uk/~mark/bpm-tools.git
+
+ sox="sox #{Shellwords.escape(click_track_file)} -t raw -r 44100 -e float -c 1 - | bpm"
+ cmd = "bash -c #{Shellwords.escape(sox)}"
+ @@log.debug("executing cmd #{cmd}")
+ output=`#{cmd}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ bpm = output.to_f
+
+ @@log.debug("bpm: #{bpm} start_time: #{start_time}")
+
+ metro_fin = "#{Time.at(start_time).utc.strftime("%H:%M:%S")}:#{((start_time - start_time.to_i) * 1000).round}"
+
+ jmep = ""
+ jmep << "# created via code using bpm/silence detection (bpm:#{bpm})\r\n"
+ jmep << "prelude@10.0 #number of seconds before music starts\r\n"
+ jmep << "metro_fin@#{metro_fin} bpm=#{bpm}, ticks=8, pmode=stream, name=Beep, play=mono"
+
+ @@log.info("jmep generated: #{jmep}")
+
+ jam_track.jmep_text = jmep
+ if jam_track.save
+ finish('success', nil)
+ else
+ @@log.error("jamtrack did not save. #{jam_track.errors.inspect}")
+ finish("no-save", "jamtrack did not save. #{jam_track.errors.inspect}")
+ return
+ end
+ else
+ finish("bpm-fail", "failed to run bpm: #{output}")
+ return
+ end
+ end
+ end
+ end
+
+
+
+ def determine_start_time(audio_file, tmp_dir, original_filename)
+ burp_gaps = ['0.3', '0.2', '0.1', '0.05']
+
+ out_wav = File.join(tmp_dir, 'stripped.wav')
+ total_time_command = "soxi -D \"#{audio_file}\""
+ total_time = `#{total_time_command}`.to_f
+
+ result_code = -20
+ stripped_time = total_time # default to the case where we just start the preview at the beginning
+
+ burp_gaps.each do |gap|
+ command_strip_lead_silence = "sox \"#{audio_file}\" \"#{out_wav}\" silence 1 #{gap} 1%"
+
+ @@log.debug("stripping silence: " + command_strip_lead_silence)
+
+ output = `#{command_strip_lead_silence}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ stripped_time_command = "soxi -D \"#{out_wav}\""
+ stripped_time_test = `#{stripped_time_command}`.to_f
+
+ if stripped_time_test < 1 # meaning a very short duration
+ @@log.warn("could not determine the start of non-silence. assuming beginning")
+ stripped_time = total_time # default to the case where we just start the preview at the beginning
+ else
+ stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break
+ break
+ end
+ else
+ @@log.warn("unable to determine silence for jam_track #{original_filename}, #{output}")
+ stripped_time = total_time # default to the case where we just start the preview at the beginning
+ end
+ end
+
+ preview_start_time = total_time - stripped_time
+
+ preview_start_time
+ end
+
+ def synchronize_preview_dev(jam_track)
+ jam_track.jam_track_tracks.each do |track|
+ next if track.track_type != 'Master'
+
+
+ most_recent_aac = nil
+ most_recent_ogg = nil
+ most_recent_mp3 = nil
+ public_jamkazam_s3_manager.list_files(track.preview_directory).each do |s3_preview_item|
+
+ s3_object = public_jamkazam_s3_manager.object(s3_preview_item)
+
+ if s3_preview_item.end_with?('.aac')
+ if most_recent_aac
+ if s3_object.last_modified > most_recent_aac.last_modified
+ most_recent_aac = s3_object
+ end
+ else
+ most_recent_aac = s3_object
+ end
+ end
+
+ if s3_preview_item.end_with?('.mp3')
+ if most_recent_mp3
+ if s3_object.last_modified > most_recent_mp3.last_modified
+ most_recent_mp3 = s3_object
+ end
+ else
+ most_recent_mp3 = s3_object
+ end
+ end
+
+ if s3_preview_item.end_with?('.ogg')
+ if most_recent_ogg
+ if s3_object.last_modified > most_recent_ogg.last_modified
+ most_recent_ogg = s3_object
+ end
+ else
+ most_recent_ogg = s3_object
+ end
+ end
+ end
+
+ if most_recent_aac
+ track['preview_aac_md5'] = 'md5'
+ track['preview_aac_url'] = most_recent_aac.key
+ track['preview_aac_length'] = most_recent_aac.content_length
+ end
+
+ if most_recent_mp3
+ track['preview_mp3_md5'] = 'md5'
+ track['preview_mp3_url'] = most_recent_mp3.key
+ track['preview_mp3_length'] = most_recent_mp3.content_length
+ end
+
+ if most_recent_ogg
+ track['preview_md5'] = 'md5'
+ track['preview_url'] = most_recent_ogg.key
+ track['preview_length'] = most_recent_ogg.content_length
+ end
+
+ track.save
+ end
+ end
+
+ def create_silence(tmp_dir, segment_count, duration, sample_rate, channels = 2)
+ file = File.join(tmp_dir, "#{segment_count}.wav")
+
+ # -c 2 means stereo
+ cmd("sox -n -r #{sample_rate} -c #{channels} #{file} trim 0.0 #{duration}", "silence")
+
+ file
+ end
# this method was created due to Tency-sourced data having no master track
# it goes through all audio tracks, and creates a master mix from it. (mix + normalize)
@@ -116,7 +372,6 @@ module JamRuby
end
-
temp_file = File.join(tmp_dir, "temp.wav")
output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav")
output_file = File.join(tmp_dir, output_filename)
@@ -149,7 +404,8 @@ module JamRuby
# now we need to upload the output back up
s3_target = audio_path + '/' + output_filename
@@log.debug("uploading #{output_file} to #{s3_target}")
- JamTrackImporter.song_storage_manager.upload(s3_target, output_file )
+ JamTrackImporter.song_storage_manager.upload(s3_target, output_file)
+
finish('success', nil)
end
@@ -159,6 +415,9 @@ module JamRuby
end
def dry_run(metadata, metalocation)
+ # STDIN.gets
+
+ @@log.debug("dry_run: #{metadata.inspect}")
metadata ||= {}
parsed_metalocation = parse_metalocation(metalocation)
@@ -195,7 +454,7 @@ module JamRuby
meta[:licensor] = vendor
- File.open(meta_yml, 'w') {|f| f.write meta.to_yaml }
+ File.open(meta_yml, 'w') { |f| f.write meta.to_yaml }
jamkazam_s3_manager.upload(metalocation, meta_yml)
end
@@ -206,6 +465,16 @@ module JamRuby
@storage_format == 'Tency'
end
+ def is_paris_storage?
+ assert_storage_set
+ @storage_format == 'Paris'
+ end
+
+ def is_tim_tracks_storage?
+ assert_storage_set
+ @storage_format == 'TimTracks'
+ end
+
def assert_storage_set
raise "no storage_format set" if @storage_format.nil?
end
@@ -213,7 +482,7 @@ module JamRuby
def parse_metalocation(metalocation)
# metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml
- if is_tency_storage?
+ if is_tency_storage? || is_tim_tracks_storage?
suffix = '/meta.yml'
@@ -242,16 +511,67 @@ module JamRuby
return nil
end
- last_dash = metalocation.rindex('-')
- if last_dash
- song = metalocation[(first_dash+3)...last_dash].strip
+ if is_tim_tracks_storage?
+ song = metalocation[(first_dash+3)..-1].strip
bits << song
- else
+ elsif is_tency_storage?
+ last_dash = metalocation.rindex('-')
+ if last_dash
+ song = metalocation[(first_dash+3)...last_dash].strip
+ bits << song
+ else
+ finish("invalid_metalocation", "metalocation not valid #{metalocation}")
+ return nil
+ end
+ end
+
+ bits << 'meta.yml'
+ bits
+ elsif is_paris_storage?
+ suffix = '/meta.yml'
+
+ unless metalocation.end_with? suffix
finish("invalid_metalocation", "metalocation not valid #{metalocation}")
return nil
end
+ metalocation = metalocation[0...-suffix.length]
+
+ first_path = metalocation.index('/')
+ if first_path.nil?
+ finish("invalid_metalocation", "metalocation not valid #{metalocation}")
+ return nil
+ end
+ metalocation = metalocation[(first_path + 1)..-1]
+
+ bits = ['audio']
+
+
+ last_slash = metalocation.rindex('/')
+
+ # example: S4863-Mike Oldfield-Moonlight Shadow-000bpm
+
+
+ if last_slash
+ paris_artist_song_id = metalocation[0...last_slash]
+ else
+ paris_artist_song_id = metalocation
+ end
+
+
+ bitbits = paris_artist_song_id.split('-')
+ song_id = bitbits[0].strip
+
+ artist = bitbits[1]
+ song_name = bitbits[2]
+ bpm = bitbits[-1]
+
+ bits << artist
+ bits << song_name
+
bits << 'meta.yml'
+ bits << song_id
+ bits << bpm
bits
else
bits = metalocation.split('/')
@@ -275,24 +595,14 @@ module JamRuby
end
end
- # if you change this, it will (at least without some work )break development usage of jamtracks
- def gen_plan_code(original_artist, name)
- # remove all non-alphanumeric chars from artist as well as name
- artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase
- name_code = name.gsub(/[^0-9a-z]/i, '').downcase
- "jamtrack-#{artist_code[0...20]}-#{name_code}"[0...50] # make sure it's a max of 50 long
- end
-
def dry_run_metadata(metadata, original_artist, name)
self.name = metadata["name"] || name
original_artist = metadata["original_artist"] || original_artist
- plan_code = metadata["plan_code"] || gen_plan_code(original_artist, self.name)
description = metadata["description"]
@@log.debug("#{self.name} original_artist=#{original_artist}")
- @@log.debug("#{self.name} plan_code=#{plan_code}")
true
end
@@ -309,7 +619,7 @@ module JamRuby
genres << Genre.find('holiday')
elsif genre == 'alternative'
genres << Genre.find('alternative rock')
- elsif genre == '80s'
+ elsif genre == '80s' || genre == "50's" || genre == "60's" || genre == "70's" || genre == "80's" || genre == "90's" || genre == "50/60's" || genre == "00's" || genre == "2010's"
# swallow
elsif genre == 'love'
# swallow
@@ -336,14 +646,62 @@ module JamRuby
# swallow
elsif genre == 'oriental'
genres << Genre.find('asian')
+ elsif genre == 'abba'
+ genres << Genre.find('pop')
+ elsif genre == 'movies tv show' || genre == "movies"
+ genres << Genre.find('tv & movie soundtrack')
+ elsif genre == 'ballad'
+ # swallow
+ elsif genre == "r'n'b" || genre == "pop rnb"
+ genres << Genre.find("r&b")
+ elsif genre == "rock & roll"
+ genres << Genre.find('rock')
+ elsif genre == "dance pop"
+ genres << Genre.find('dance')
+ elsif genre == "soul/motown" || genre == "soul motown"
+ genres << Genre.find('soul')
+ elsif genre == "party"
+ # swallow
+ elsif genre == "reggae/ska" || genre == "reggae ska"
+ genres << Genre.find('reggae')
+ genres << Genre.find('ska')
+ elsif genre == "pop rock" || genre == "pop/rock"
+ genres << Genre.find("rock")
+ genres << Genre.find("pop")
+ elsif genre == "singalong"
+ #swallow
+ elsif genre == "folk rock"
+ genres << Genre.find('folk')
+ genres << Genre.find('rock')
+ elsif genre == "swing" || genre == "swing/big band" || genre == "swing big band"
+ genres << Genre.find('oldies')
+ elsif genre == "rap/hip hop" || genre == "rap hip hop"
+ genres << Genre.find("rap")
+ elsif genre == "folk traditional" || genre == "folk/traditional"
+ genres << Genre.find('folk')
+ elsif genre == "elvis"
+ genres << Genre.find('rock')
+ elsif genre == "irish"
+ genres << Genre.find('celtic')
+ elsif genre == "dance/pop"
+ genres << Genre.find('dance')
+ genres << Genre.find('pop')
+ elsif genre == "the beatles"
+ genres << Genre.find("rock")
else
found = Genre.find_by_id(genre)
- genres << found if found
+ genres << found if found
end
end
end
+ # just throw them into rock/pop if not known. can fix later...
+ if genres.length == 0
+ genres << Genre.find('rock')
+ genres << Genre.find('pop')
+ end
+
genres
end
@@ -408,7 +766,7 @@ module JamRuby
prevent_concurrent_processing(metalocation)
if jam_track.new_record?
- latest_jamtrack = JamTrack.order('created_at desc').first
+ latest_jamtrack = JamTrack.order('id::int desc').first
id = latest_jamtrack.nil? ? 1 : latest_jamtrack.id.to_i + 1
if ENV['NODE_NUMBER']
@@ -418,9 +776,9 @@ module JamRuby
node_count = ENV['NODE_COUNT'].to_i
raise "NO NODE_COUNT" if node_count == 0
r = id % node_count
- id = r + id # get to the same base number if both are working at the same time
+ id = (node_count - r) + id # get to the same base number if both are working at the same time
id = id + node_number # offset by your node number
- @@log.debug("JAM TRACK ID: #{id}")
+ @@log.info("JAM TRACK ID: #{id}")
end
jam_track.id = "#{id}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that
jam_track.status = 'Staging'
@@ -431,7 +789,6 @@ module JamRuby
jam_track.year = metadata[:year]
jam_track.genres = determine_genres(metadata)
jam_track.language = determine_language(metadata)
- jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name)
jam_track.price = 1.99
jam_track.reproduction_royalty_amount = nil
jam_track.reproduction_royalty = true
@@ -444,13 +801,30 @@ module JamRuby
jam_track.alternative_license_status = false
jam_track.hfa_license_desired = true
jam_track.server_fixation_date = Time.now
- jam_track.slug = metadata['slug'] || jam_track.generate_slug
-
if is_tency_storage?
jam_track.vendor_id = metadata[:id]
- jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music')
+ jam_track.licensor = JamTrackLicensor.find_by_name!('Tency Music')
#add_licensor_metadata('Tency Music', metalocation)
+ elsif is_paris_storage?
+ raise 'no vendor id' if metadata[:id].nil?
+ jam_track.vendor_id = metadata[:id]
+ jam_track.licensor = JamTrackLicensor.find_by_name!('Paris Music')
+ jam_track.bpm = metadata[:bpm]
+ elsif is_tim_tracks_storage?
+ jam_track.vendor_id = metadata[:id]
+ jam_track.licensor = JamTrackLicensor.find_by_name!('Tim Waurick')
end
+ jam_track.slug = metadata['slug']
+ if jam_track.slug.nil?
+ jam_track.generate_slug
+ end
+ jam_track.plan_code = metadata["plan_code"]
+ if jam_track.plan_code.nil?
+ jam_track.gen_plan_code
+ end
+
+
+
else
if !options[:resync_audio]
#@@log.debug("#{self.name} skipped because it already exists in database")
@@ -511,8 +885,11 @@ module JamRuby
instrument = 'acoustic guitar'
elsif potential_instrument == 'acoutic guitar'
instrument = 'electric guitar'
- elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary'
+ elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' || potential_instrument == 'elec guitar'
instrument = 'electric guitar'
+ elsif potential_instrument == 'lead guitar'
+ instrument = 'electric guitar'
+ part = 'Lead'
elsif potential_instrument == 'keys'
instrument = 'keyboard'
elsif potential_instrument == 'vocal' || potential_instrument == 'vocals'
@@ -555,7 +932,7 @@ module JamRuby
instrument = 'computer'
part = 'Bells'
elsif potential_instrument == 'percussion'
- instrument = 'drums'
+ instrument = 'percussion'
part = 'Percussion'
elsif potential_instrument == 'fretless bass'
instrument = 'bass guitar'
@@ -583,8 +960,9 @@ module JamRuby
elsif potential_instrument == 'strings'
instrument = 'orchestra'
part = 'Strings'
- elsif potential_instrument == 'celesta'
+ elsif potential_instrument == 'celesta' || potential_instrument == 'celeste'
instrument = 'keyboard'
+ part = 'Celesta'
elsif potential_instrument == 'balalaika'
instrument = 'other'
part = 'Balalaika'
@@ -598,8 +976,11 @@ module JamRuby
instrument = 'other'
part = 'Bouzouki'
elsif potential_instrument == 'claps' || potential_instrument == 'hand claps'
- instrument = 'computer'
+ instrument = 'other'
part = 'Claps'
+ elsif potential_instrument == 'snaps' || potential_instrument == 'snap'
+ instrument = 'other'
+ part = 'Snaps'
else
found_instrument = Instrument.find_by_id(potential_instrument)
if found_instrument
@@ -631,7 +1012,7 @@ module JamRuby
part = nil
precount_num = nil
no_precount_detail = nil
- if comparable_filename == "click" || comparable_filename.include?("clicktrack")
+ if comparable_filename == "click" || comparable_filename.include?("clicktrack") || comparable_filename.include?("click track") || comparable_filename.end_with?('click') || comparable_filename.end_with?('click trac')
if filename.end_with?('.txt')
type = :clicktxt
else
@@ -650,8 +1031,7 @@ module JamRuby
precount_num = precount.to_i
end
-
- elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix")
+ elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") || (@metadata && (@metadata[:id] && comparable_filename.start_with?(@metadata[:id].downcase)))
master = true
type = :master
else
@@ -706,24 +1086,80 @@ module JamRuby
instrument = result[:instrument]
part = result[:part]
end
+ elsif is_paris_storage?
+ # example: Eternal Flame-Guide Lead Vocal.wav
+ # or with a part: Eternal Flame-Keyboard-Stab.wav
+ bits = filename_no_ext.split('-')
+ bits.collect! { |bit| bit.strip }
+
+ while true
+ instrument, part = paris_instrument_parse(bits)
+
+ if instrument.nil? && bits.length > 2
+ bits.shift
+ instrument, part = paris_instrument_parse(bits)
+ else
+ break
+ end
+ end
end
end
-
end
-
{filename: filename, master: master, instrument: instrument, part: part, type: type, precount_num: precount_num, no_precount_detail: no_precount_detail}
end
+ def paris_instrument_parse(bits)
+ instrument = nil
+ part = nil
+ possible_instrument = nil
+ possible_part = nil
+
+ if bits.length == 2
+ # second bit is instrument
+ possible_instrument = bits[1]
+ elsif bits.length == 3
+ # second bit is instrument, third bit is part
+ possible_instrument = bits[1]
+ possible_part = bits[2]
+ elsif bits.length >= 4
+ possible_instrument = bits[1]
+ possible_part = "#{bits[2]} #{bits[3]}"
+ end
+
+ # otherwise, try mapping
+ if instrument.nil? && possible_instrument
+ mapping = JamTrackImporter.paris_mapping[possible_instrument.downcase]
+ if mapping
+ instrument = mapping[:instrument].downcase
+ part = mapping[:part]
+ part = nil if part.blank?
+ end
+ end
+
+ # paris mapping didn't work; let's retry one more time with our own home-grown mapping
+ if instrument.nil?
+ result = determine_instrument(possible_instrument, possible_part)
+ instrument = result[:instrument]
+ part = result[:part]
+ end
+ return instrument, part
+ end
+
def dry_run_audio(metadata, s3_path)
all_files = fetch_important_files(s3_path)
+ masters = 0
all_files.each do |file|
# ignore click/precount
parsed_wav = parse_file(file)
if parsed_wav[:master]
@@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}")
+ masters += 1
+ if masters > 1
+ JamTrackImporter.summaries[:multiple_masters] += 1
+ end
elsif parsed_wav[:type] == :track
JamTrackImporter.summaries[:total_tracks] += 1
@@ -770,9 +1206,9 @@ module JamRuby
instrument_weight = nil
# if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end
- if track.persisted?
- instrument_weight = track.position
- else
+ #if track.persisted?
+ # instrument_weight = track.position
+ #else
if track.instrument_id == 'voice'
if track.part && track.part.start_with?('Lead')
@@ -792,7 +1228,8 @@ module JamRuby
else
instrument_weight = 170
end
-
+ elsif track.instrument_id == 'percussion'
+ instrument_weight = 175
elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass'
instrument_weight = 180
@@ -834,7 +1271,11 @@ module JamRuby
if track.track_type == 'Master'
instrument_weight = 1000
end
- end
+
+ if track.track_type == 'Click'
+ instrument_weight = 10000
+ end
+ #end
instrument_weight
@@ -895,26 +1336,70 @@ module JamRuby
end
end
- # default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track
position = 1
sorted_tracks.each do |track|
- if track.persisted?
- # persisted tracks should be sorted at the beginning of the sorted_tracks,
- # so this just keeps moving the 'position builder' up to +1 of the last persisted track
- position = track.position + 1
- else
- track.position = position
- position = position + 1
- end
-
-
+ track.position = position
+ position = position + 1
end
- sorted_tracks[sorted_tracks.length - 1].position = 1000
+ # get click/master tracks position re-set correctly
+
+ last_track = sorted_tracks[sorted_tracks.length - 1]
+ second_to_last = sorted_tracks[sorted_tracks.length - 2]
+
+ if last_track.track_type == 'Master'
+ last_track.position = 1000
+ elsif last_track.track_type == 'Click'
+ last_track.position = 10000
+ end
+
+ if second_to_last.track_type == 'Master'
+ second_to_last.position = 1000
+ elsif second_to_last.track_type == 'Click'
+ second_to_last.position = 10000
+ end
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
@@ -952,52 +1437,9 @@ module JamRuby
@@log.debug("no existing track found; creating a new one")
- track = JamTrackTrack.new
- track.original_filename = wav_file
- track.original_audio_s3_path = wav_file
-
- file = JamTrackFile.new
- file.original_filename = wav_file
- file.original_audio_s3_path = wav_file
-
- parsed_wav = parse_file(wav_file)
-
- unknowns = 0
- if parsed_wav[:master]
- track.track_type = 'Master'
- track.part = 'Master Mix'
- track.instrument_id = 'computer'
- tracks << track
- @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}")
- elsif parsed_wav[:type] == :track
-
- if !parsed_wav[:instrument] || !parsed_wav[:part]
- @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
- unknowns += 1
- else
- @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
- end
-
-
- track.instrument_id = parsed_wav[:instrument] || 'other'
- track.track_type = 'Track'
- track.part = parsed_wav[:part] || "Other #{unknowns}"
- tracks << track
- elsif parsed_wav[:type] == :clicktxt
- file.file_type = 'ClickTxt'
- addt_files << file
- elsif parsed_wav[:type] == :clickwav
- file.file_type = 'ClickWav'
- addt_files << file
- elsif parsed_wav[:type] == :precount
- file.file_type = 'Precount'
- file.precount_num = parsed_wav[:precount_num]
- addt_files << file
- else
- finish("unknown_file_type", "unknown file type #{wave_file}")
+ if !assign_instrument_parts(wav_file, tracks, addt_files)
return false
end
-
end
jam_track.jam_track_tracks.each do |jam_track_track|
@@ -1033,101 +1475,130 @@ module JamRuby
return synchronize_audio_files(jam_track, skip_audio_upload)
end
+ def reassign_instrument_parts(jam_track)
+
+ tracks = []
+ addt_files = []
+
+ jam_track.jam_track_tracks.each do |track|
+ return if !assign_instrument_parts(track.original_filename, tracks, addt_files, true)
+ end
+
+ @@log.info("sorting #{tracks.length} tracks")
+ tracks = sort_tracks(tracks)
+
+ deduplicate_parts(tracks)
+
+ changed = false
+ tracks.each do |track|
+ if track.changed?
+ changed = true
+ puts "CHANGE: #{track.changes.inspect}"
+ track.skip_inst_part_uniq = true
+ track.save!
+ end
+ end
+
+ if changed
+ # if we messed up any instrument/parts by making a dup, this will catch it
+ tracks.each do |track|
+ track.skip_inst_part_uniq = false
+ track.save!
+ end
+ end
+ end
+
+ def assign_instrument_parts(wav_file, tracks, addt_files, reassign = false)
+ if !reassign
+ track = JamTrackTrack.new
+ track.original_filename = wav_file
+ track.original_audio_s3_path = wav_file
+
+ file = JamTrackFile.new
+ file.original_filename = wav_file
+ file.original_audio_s3_path = wav_file
+ else
+ matches = JamTrackTrack.where(original_filename: wav_file)
+ if matches.count > 1
+ raise "multiple jam track tracks encountered with #{wav_file} as original_filename"
+ elsif matches.count == 0
+ raise "unable to locate jam track wit h#{wav_file} as original_filename"
+ end
+ track = matches[0]
+ track.original_audio_s3_path = wav_file
+ file = nil
+ end
+
+ parsed_wav = parse_file(wav_file)
+
+ unknowns = 0
+ if parsed_wav[:master]
+ track.track_type = 'Master'
+ track.part = 'Master Mix'
+ track.instrument_id = 'computer'
+ tracks << track
+ @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}")
+ elsif parsed_wav[:type] == :track
+
+ if !parsed_wav[:instrument] || !parsed_wav[:part]
+ @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
+ unknowns += 1
+ else
+ @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
+ end
+
+ track.instrument_id = parsed_wav[:instrument] || 'other'
+ track.track_type = 'Track'
+ track.part = parsed_wav[:part];
+ tracks << track
+ elsif parsed_wav[:type] == :clicktxt
+ file.file_type = 'ClickTxt'
+ addt_files << file
+ elsif parsed_wav[:type] == :clickwav
+ if file
+ file.file_type = 'ClickWav'
+ addt_files << file
+ end
+
+ # and also add a JamTrackTrack for this click track
+ track.track_type = 'Click'
+ track.part = 'Clicktrack'
+ track.instrument_id = 'computer'
+ track.position = 10000
+ tracks << track
+ elsif parsed_wav[:type] == :precount
+ file.file_type = 'Precount'
+ file.precount_num = parsed_wav[:precount_num]
+ addt_files << file
+ else
+ finish("unknown_file_type", "unknown file type #{wave_file}")
+ return false
+ end
+
+ return true
+ end
+
def synchronize_audio_files(jam_track, skip_audio_upload)
begin
Dir.mktmpdir do |tmp_dir|
+ # download each jam track here, and then do processing to determine:
+ # what's the longest stem
+ # and to then pad the rest of the tracks to make them all match in length
jam_track.jam_track_tracks.each do |track|
-
basename = File.basename(track.original_audio_s3_path)
- s3_dirname = File.dirname(track.original_audio_s3_path)
+ wav_file = File.join(tmp_dir, basename)
- # make a 44100 version, and a 48000 version
- ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg"
- ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg"
+ # bring the original wav file down from S3 to local file system
+ JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file)
+ track.wav_file = wav_file
+ end
- ogg_44100_s3_path = track.filename(ogg_44100_filename)
- ogg_48000_s3_path = track.filename(ogg_48000_filename)
+ same_lengthening(jam_track, tmp_dir)
- track.skip_uploader = true
-
- if skip_audio_upload
- track["url_44"] = ogg_44100_s3_path
- track["md5_44"] = 'md5'
- track["length_44"] = 1
-
- track["url_48"] = ogg_48000_s3_path
- track["md5_48"] = 'md5'
- track["length_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)
-
- # bring the original wav file down from S3 to local file system
- JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file)
-
- sample_rate = `soxi -r "#{wav_file}"`.strip
-
- ogg_44100 = File.join(tmp_dir, ogg_44100_filename)
- ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg")
-
- if sample_rate == "44100"
- `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"`
- else
- `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"`
- end
-
- if sample_rate == "48000"
- `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"`
- else
- `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"`
- end
-
- # upload the new ogg files to s3
- @@log.debug("uploading 44100 to #{ogg_44100_s3_path}")
-
- jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100)
-
- @@log.debug("uploading 48000 to #{ogg_48000_s3_path}")
-
- jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000)
-
- ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
- # and finally update the JamTrackTrack with the new info
- track["url_44"] = ogg_44100_s3_path
- track["md5_44"] = ogg_44100_digest.hexdigest
- track["length_44"] = File.new(ogg_44100).size
-
- track["url_48"] = ogg_48000_s3_path
- track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest
- track["length_48"] = File.new(ogg_48000).size
-
- synchronize_duration(jam_track, ogg_44100)
- jam_track.save!
-
- # convert entire master ogg file to mp3, and push both to public destination
- if track.track_type == 'Master'
- preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest)
-
- if !preview_succeeded
- return false
- end
- elsif track.track_type == 'Track'
- synchronize_track_preview(track, tmp_dir, ogg_44100)
- end
-
- end
-
- track.save!
+ jam_track.jam_track_tracks.each do |track|
+ synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track)
end
end
rescue Exception => e
@@ -1138,6 +1609,287 @@ module JamRuby
return true
end
+ # make all stems be the same length
+ def same_lengthening(jam_track, tmp_dir)
+ longest_duration = nil
+ jam_track.jam_track_tracks.each do |track|
+ duration_command = "soxi -D \"#{track.wav_file}\""
+ output = `#{duration_command}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ duration = output.to_f.round
+
+ track.tmp_duration = duration
+ if longest_duration.nil?
+ longest_duration = duration
+ else
+ if duration > longest_duration
+ longest_duration = duration
+ end
+ end
+ else
+ @@log.warn("unable to determine duration for jam_track track #{jam_track.name} #{jam_track_track.instrument} #{jam_track_track.part}. output #{output}")
+ end
+ end
+
+ @@log.info("duration determined to be #{longest_duration}")
+ jam_track.duration = longest_duration
+ jam_track.jam_track_tracks.each do |track|
+ if track.tmp_duration < longest_duration
+ # need to pad with silence to make all match in length
+
+ amount = longest_duration - track.tmp_duration
+
+ @@log.info("track #{track.instrument_id}:#{track.part} needs to be lengthened by #{amount}")
+
+ output = cmd("soxi -c \"#{track.wav_file}\"", "padded_silence")
+ channels = output.to_i
+
+ output = cmd("soxi -r \"#{track.wav_file}\"", "get_sample_rate")
+ sample_rate = output.to_i
+
+ create_silence(tmp_dir, "padded_silence#{track.id}", amount, sample_rate, channels)
+
+ output_file = File.join(tmp_dir, "with_padding_#{track.id}.wav")
+
+ cmd("sox \"#{track.wav_file}\" \"#{output_file}\"", "same_lengthening")
+
+ track.wav_file = output_file
+ end
+ end
+ end
+
+ def cmd(cmd, type)
+
+ @@log.debug("executing #{cmd}")
+
+ output = `#{cmd}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ output
+ else
+ @error_reason = type + "_fail"
+ @error_detail = "#{cmd}, #{output}"
+ raise "command `#{cmd}` failed. #{type}, #{output}"
+ end
+ end
+
+ def synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track)
+ basename = File.basename(track.original_audio_s3_path)
+
+ # make a 44100 version, and a 48000 version
+ ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg"
+ ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg"
+
+
+ # 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"
+
+ ogg_44100_s3_path = track.filename(ogg_44100_filename)
+ ogg_48000_s3_path = track.filename(ogg_48000_filename)
+
+ 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_44"] = ogg_44100_s3_path
+ track["md5_44"] = 'md5'
+ track["length_44"] = 1
+
+ track["url_48"] = ogg_48000_s3_path
+ track["md5_48"] = 'md5'
+ track["length_48"] = 1
+
+ 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 = track.wav_file
+
+ sample_rate = `soxi -r "#{wav_file}"`.strip
+
+ ogg_44100 = File.join(tmp_dir, ogg_44100_filename)
+ ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg")
+
+ if sample_rate == "44100"
+ `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"`
+ else
+ `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"`
+ end
+
+ if sample_rate == "48000"
+ `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"`
+ else
+ `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"`
+ end
+
+ # upload the new ogg files to s3
+ @@log.debug("uploading 44100 to #{ogg_44100_s3_path}")
+
+ jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100)
+
+ @@log.debug("uploading 48000 to #{ogg_48000_s3_path}")
+
+ jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000)
+
+ ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
+ # and finally update the JamTrackTrack with the new info
+ track["url_44"] = ogg_44100_s3_path
+ track["md5_44"] = ogg_44100_digest.hexdigest
+ track["length_44"] = File.new(ogg_44100).size
+
+ track["url_48"] = ogg_48000_s3_path
+ track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest
+ track["length_48"] = File.new(ogg_48000).size
+
+ # now create mp3 and aac files
+ 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
+
+ jam_track.save!
+
+ # convert entire master ogg file to mp3, and push both to public destination
+ if track.track_type == 'Master'
+ preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest)
+
+ if !preview_succeeded
+ return false
+ end
+ elsif track.track_type == 'Track' || track.track_type == 'Click'
+ synchronize_track_preview(track, tmp_dir, ogg_44100)
+ end
+
+ end
+
+ track.save!
+ 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
+
+ puts "track.original_audio_s3_path #{track.original_audio_s3_path}"
+ 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)
+
+ puts "mp3_48000_s3_path #{mp3_48000_s3_path}"
+ 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}`
@@ -1155,58 +1907,15 @@ module JamRuby
def synchronize_track_preview(track, tmp_dir, ogg_44100)
- out_wav = File.join(tmp_dir, 'stripped.wav')
-
- burp_gaps = ['0.3', '0.2', '0.1', '0.05']
-
- total_time_command = "soxi -D \"#{ogg_44100}\""
- total_time = `#{total_time_command}`.to_f
-
- result_code = -20
- stripped_time = total_time # default to the case where we just start the preview at the beginning
-
- burp_gaps.each do |gap|
- command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%"
-
- @@log.debug("stripping silence: " + command_strip_lead_silence)
-
- output = `#{command_strip_lead_silence}`
-
- result_code = $?.to_i
-
- if result_code == 0
- stripped_time_command = "soxi -D \"#{out_wav}\""
- stripped_time_test = `#{stripped_time_command}`.to_f
-
- if stripped_time_test < 1 # meaning a very short duration
- @@log.warn("could not determine the start of non-silencea. assuming beginning")
- stripped_time = total_time # default to the case where we just start the preview at the beginning
- else
- stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break
- break
- end
- else
- @@log.warn("unable to determine silence for jam_track #{track.original_filename}, #{output}")
- stripped_time = total_time # default to the case where we just start the preview at the beginning
- end
-
- end
-
- preview_start_time = total_time - stripped_time
+ preview_start_time = determine_start_time(ogg_44100, tmp_dir, track.original_filename)
# this is in seconds; convert to integer milliseconds
preview_start_time = (preview_start_time * 1000).to_i
- preview_start_time = nil if preview_start_time < 0
+ preview_start_time = 0 if preview_start_time < 0
track.preview_start_time = preview_start_time
- if track.preview_start_time
- @@log.debug("determined track start time to be #{track.preview_start_time}")
- else
- @@log.debug("determined track start time to be #{track.preview_start_time}")
- end
-
track.process_preview(ogg_44100, tmp_dir) if track.preview_start_time
if track.preview_generate_error
@@ -1215,6 +1924,53 @@ module JamRuby
end
+ def synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_digest)
+ begin
+ aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac')
+ convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\""
+ @@log.debug("converting to aac using: " + convert_aac_cmd)
+
+ convert_output = `#{convert_aac_cmd}`
+
+ aac_digest = ::Digest::MD5.file(aac_44100)
+
+ track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest
+
+ # upload 44100 aac to public location
+ @@log.debug("uploading aac preview to #{track.preview_filename('aac')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
+
+
+ track.skip_uploader = true
+
+ original_aac_preview_url = track["preview_aac_url"]
+
+ # and finally update the JamTrackTrack with the new info
+ track["preview_aac_url"] = track.preview_filename(aac_md5, 'aac')
+ track["preview_aac_length"] = File.new(aac_44100).size
+ track["preview_start_time"] = 0
+
+ if !track.save
+ finish("save_master_preview", track.errors.to_s)
+ return false
+ end
+
+ # if all that worked, now delete old previews, if present
+ begin
+ public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
+ rescue
+ puts "UNABLE TO CLEANUP OLD PREVIEW URL"
+ end
+ rescue Exception => e
+ finish("sync_master_preview_exception", e.to_s)
+ return false
+ end
+
+
+ return true
+
+ end
+
def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest)
begin
@@ -1226,20 +1982,33 @@ module JamRuby
mp3_digest = ::Digest::MD5.file(mp3_44100)
+ aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac')
+ convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\""
+ @@log.debug("converting to aac using: " + convert_aac_cmd)
+
+ convert_output = `#{convert_aac_cmd}`
+
+ aac_digest = ::Digest::MD5.file(aac_44100)
+
+
track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
+ track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest
- # upload 44100 ogg and mp3 to public location as well
+ # upload 44100 ogg, mp3, aac to public location as well
@@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}")
public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}")
public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
+ @@log.debug("uploading aac preview to #{track.preview_filename('aac')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
track.skip_uploader = true
original_ogg_preview_url = track["preview_url"]
original_mp3_preview_url = track["preview_mp3_url"]
+ original_aac_preview_url = track["preview_aac_url"]
# and finally update the JamTrackTrack with the new info
track["preview_url"] = track.preview_filename(ogg_md5, 'ogg')
@@ -1247,6 +2016,8 @@ module JamRuby
# and finally update the JamTrackTrack with the new info
track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3')
track["preview_mp3_length"] = File.new(mp3_44100).size
+ track["preview_aac_url"] = track.preview_filename(aac_md5, 'mp3')
+ track["preview_aac_length"] = File.new(aac_44100).size
track["preview_start_time"] = 0
if !track.save
@@ -1258,6 +2029,7 @@ module JamRuby
begin
public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"]
public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
+ public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
end
@@ -1293,6 +2065,15 @@ module JamRuby
original_artist = parsed_metalocation[1]
name = parsed_metalocation[2]
+ if is_paris_storage?
+ bpm = parsed_metalocation[-1]
+ bpm.downcase!
+ if bpm.end_with?('bpm')
+ bpm = bpm[0..-4].to_f
+ end
+ metadata[:bpm] = bpm
+ end
+
success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options)
return unless success
@@ -1331,6 +2112,8 @@ module JamRuby
attr_accessor :storage_format
attr_accessor :tency_mapping
attr_accessor :tency_metadata
+ attr_accessor :paris_mapping
+ attr_accessor :paris_metadata
attr_accessor :summaries
def report_summaries
@@ -1357,19 +2140,31 @@ module JamRuby
def song_storage_manager
if is_tency_storage?
tency_s3_manager
+ elsif is_paris_storage?
+ paris_s3_manager
+ elsif is_tim_tracks_storage?
+ tim_tracks_s3_manager
else
s3_manager
end
end
def summaries
- @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new}
+ @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new, multiple_masters: 0, total:0}
end
def tency_s3_manager
@tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
+ def paris_s3_manager
+ @paris_s3_manager ||= S3Manager.new('jamkazam-paris', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+ end
+
+ def tim_tracks_s3_manager
+ @tim_tracks_s3_manager ||= S3Manager.new('jamkazam-timtracks', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+ end
+
def s3_manager
@s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
@@ -1378,6 +2173,25 @@ module JamRuby
@private_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
+ def extract_paris_song_id(metalocation)
+
+ first_path = metalocation.index('/')
+ return nil unless first_path
+ metalocation = metalocation[(first_path + 1)..-1]
+
+ suffix = '/meta.yml'
+ metalocation = metalocation[0...-suffix.length]
+
+ first_dash = metalocation.index('-')
+ return nil if first_dash.nil?
+
+ id = metalocation[0...first_dash].strip
+
+ return nil unless id.start_with?('S') # all start with S
+ return nil if id[1..-1].to_i == 0 # and number after that
+ id
+ end
+
def extract_tency_song_id(metalocation)
# metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml
@@ -1398,15 +2212,69 @@ 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'
end
+ def is_paris_storage?
+ assert_storage_set
+ @storage_format == 'Paris'
+ end
+
+ def is_tim_tracks_storage?
+ assert_storage_set
+ @storage_format == 'TimTracks'
+ end
+
def assert_storage_set
raise "no storage_format set" if @storage_format.nil?
end
+ def iterate_tim_tracks_song_storage(&blk)
+ count = 0
+ song_storage_manager.list_directories('mapped').each do |song|
+ @@log.debug("searching through song directory '#{song}'")
+
+ metalocation = "#{song}meta.yml"
+
+ metadata = load_metalocation(metalocation)
+
+ blk.call(metadata, metalocation)
+
+ count += 1
+ #break if count > 100
+
+ end
+ end
+
+ def iterate_paris_song_storage(&blk)
+ count = 0
+ song_storage_manager.list_directories('mapped').each do |song|
+ @@log.debug("searching through song directory '#{song}'")
+
+ #next if song != 'mapped/S1555-Ashlee Simpson-L-O-V-E-96bpm/'
+
+ metalocation = "#{song}meta.yml"
+
+ metadata = load_metalocation(metalocation)
+
+ if metadata.nil?
+ # we don't do a paris song unless it has metadata
+ next
+ end
+ blk.call(metadata, metalocation)
+
+ count += 1
+ #break if count > 1000
+ end
+ end
+
def iterate_tency_song_storage(&blk)
count = 0
song_storage_manager.list_directories('mapped').each do |song|
@@ -1445,6 +2313,14 @@ module JamRuby
if is_tency_storage?
iterate_tency_song_storage do |metadata, metalocation|
blk.call(metadata, metalocation)
+ end
+ elsif is_paris_storage?
+ iterate_paris_song_storage do |metadata, metalocation|
+ blk.call(metadata, metalocation)
+ end
+ elsif is_tim_tracks_storage?
+ iterate_tim_tracks_song_storage do |metadata, metalocation|
+ blk.call(metadata, metalocation)
end
else
iterate_default_song_storage do |metadata, metalocation|
@@ -1456,6 +2332,9 @@ module JamRuby
def dry_run
iterate_song_storage do |metadata, metalocation|
jam_track_importer = JamTrackImporter.new(@storage_format)
+ jam_track_importer.metadata = metadata
+
+ JamTrackImporter.summaries[:total] += 1
jam_track_importer.dry_run(metadata, metalocation)
end
@@ -1477,6 +2356,7 @@ module JamRuby
iterate_song_storage do |metadata, metalocation|
importer = JamTrackImporter.new(@storage_format)
+ importer.metadata = metadata
song_id = JamTrackImporter.extract_tency_song_id(metalocation)
parsed_metalocation = importer.parse_metalocation(metalocation)
@@ -1496,13 +2376,13 @@ module JamRuby
CSV.open("only_in_s3.csv", "wb") do |csv|
only_in_s3.each do |song_id|
- csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ]
+ csv << [song_id, in_s3[song_id][:artist], in_s3[song_id][:song]]
end
end
CSV.open("only_in_2k_selection.csv", "wb") do |csv|
only_in_mapping.each do |song_id|
- csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ]
+ csv << [song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song]]
end
end
@@ -1515,11 +2395,12 @@ module JamRuby
break
end
end
+
def create_masters
iterate_song_storage do |metadata, metalocation|
next if metadata.nil?
jam_track_importer = JamTrackImporter.new(@storage_format)
-
+ jam_track_importer.metadata = metadata
jam_track_importer.create_master(metadata, metalocation)
end
end
@@ -1547,6 +2428,7 @@ module JamRuby
metadata = load_metalocation(metalocation)
jam_track_importer = JamTrackImporter.new
+ jam_track_importer.metadata = metadata
jam_track_importer.dry_run(metadata, metalocation)
end
@@ -1581,6 +2463,38 @@ module JamRuby
importer
end
+ # hunts for the most recent .aac, .mp3, or .ogg file
+ def synchronize_preview_dev(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ importer.synchronize_preview_dev(jam_track)
+
+ importer.finish('success', nil)
+ importer
+ end
+
+ def synchronize_jamtrack_aac_preview(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ track = jam_track.master_track
+
+ if track
+ Dir.mktmpdir do |tmp_dir|
+ ogg_44100 = File.join(tmp_dir, 'input.ogg')
+ private_s3_manager.download(track.url_by_sample_rate(44), ogg_44100)
+ ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
+ if importer.synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_44100_digest)
+ importer.finish("success", nil)
+ end
+ end
+ else
+ importer.finish('no_master_track', nil)
+ end
+ importer
+ end
+
def synchronize_jamtrack_master_preview(jam_track)
importer = JamTrackImporter.new
importer.name = jam_track.name
@@ -1603,6 +2517,97 @@ module JamRuby
importer
end
+ def synchronize_previews_dev
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_preview_dev(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "no_preview_start_time"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
+ def import_click_track(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+ importer.import_click_track(jam_track)
+
+ importer
+ end
+ def generate_jmep(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+ importer.generate_jmep(jam_track)
+
+ importer
+ end
+
+ def import_click_tracks
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ #jam_track = JamTrack.find('126')
+ importers << import_click_track(jam_track)
+ 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 generate jmep.")
+ @@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_jmeps
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << generate_jmep(jam_track)
+ 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 generate jmep.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
def synchronize_previews
importers = []
@@ -1629,6 +2634,33 @@ module JamRuby
end
end
+ def synchronize_jamtrack_aac_previews
+
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_jamtrack_aac_preview(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+
+ end
+ end
+
def synchronize_jamtrack_master_previews
importers = []
@@ -1716,6 +2748,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 = []
@@ -1745,6 +2791,70 @@ 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_paris_storage?
+ paris = JamTrackLicensor.find_by_name!('Paris Music')
+ jam_tracks = JamTrack.where(licensor_id: paris.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
@@ -1764,7 +2874,7 @@ module JamRuby
count = 0
iterate_song_storage do |metadata, metalocation|
- next if metadata.nil? && is_tency_storage?
+ next if metadata.nil? && (is_tency_storage? || is_paris_storage?)
importer = synchronize_from_meta(metalocation, options)
importers << importer
@@ -1812,7 +2922,134 @@ module JamRuby
end
end
- def genre_dump
+ def paris_genre_dump
+ load_paris_mappings
+
+ genres = {}
+ @paris_metadata.each do |id, value|
+ genre1 = value[:genre1]
+ genre2 = value[:genre2]
+ genre3 = value[:genre3]
+
+ genres[genre1.downcase.strip] = genre1.downcase.strip if genre1
+ genres[genre2.downcase.strip] = genre2.downcase.strip if genre2
+ genres[genre3.downcase.strip] = genre3.downcase.strip if genre3
+ end
+
+ all_genres = Genre.select(:id).all.map(&:id)
+
+ all_genres = Set.new(all_genres)
+ genres.each do |genre, value|
+ found = all_genres.include? genre
+
+ puts "#{genre}" unless found
+ end
+ end
+
+
+ def create_importer_from_existing(jam_track)
+ importer = JamTrackImporter.new(@storage_format)
+ importer.name = jam_track.name
+ importer.metadata = load_metalocation(jam_track.metalocation)
+ importer
+ end
+
+ def resync_instruments(licensor)
+
+ load_paris_mappings if @paris_mapping.nil?
+
+ JamTrack.where(licensor_id: licensor.id).each do |jam_track|
+
+ if @paris_metadata[jam_track.vendor_id].nil?
+ next
+ end
+ puts "RESYNCING JAMTRACK #{jam_track.id}"
+ JamTrackTrack.where(jam_track_id: jam_track.id).order(:position).each do |track|
+ puts "BEFORE TRACK #{track.instrument_id} #{track.part}"
+ end
+
+ importer = create_importer_from_existing(jam_track)
+ importer.reassign_instrument_parts(jam_track)
+
+ #puts ">>>>>>>>> HIT KEY TO CONTINUE <<<<<<<<<<"
+ #STDIN.gets
+ end
+
+ end
+
+
+ def fix_artist_song_name (licensor)
+
+ load_paris_mappings if @paris_mapping.nil?
+
+ JamTrack.where(licensor_id: licensor.id).each do |jam_track|
+
+ metadata = @paris_metadata[jam_track.vendor_id]
+
+ if metadata.nil?
+ puts "OH NO! A Paris Song that does not belong! #{jam_track.id} #{jam_track.vendor_id}"
+ next
+ end
+
+ puts "STARTING JAM_TRACK #{jam_track.id} #{jam_track.original_artist} #{jam_track.name}"
+
+ jam_track.generate_slug
+
+ if jam_track.changed?
+ puts "SLUG CHANGED! #{jam_track.changes.inspect}"
+ end
+
+ if !jam_track.save
+ puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}"
+ end
+
+ if jam_track.changed?
+ jam_track.reload
+ end
+
+ jam_track.original_artist = metadata[:original_artist]
+ jam_track.name = metadata[:name]
+ if jam_track.changed?
+ puts "ARTIST/NAME CHANGE: #{jam_track.changes.inspect}"
+ end
+
+ if !jam_track.save
+ puts "unable to save new artist/song!"
+ end
+ end
+ end
+
+ def fix_slugs(licensor)
+
+ JamTrack.where(licensor_id: licensor.id).each do |jam_track|
+
+ if jam_track.slug.end_with?('-')
+ puts "removing trailing dash"
+ jam_track.slug = jam_track.slug[0...-1]
+ if !jam_track.save
+ puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}"
+ end
+ end
+ end
+ end
+
+ def missing_masters(licensor)
+
+ count = 0
+ JamTrack.where(licensor_id: licensor.id).each do |jam_track|
+
+
+ if jam_track.master_track.nil?
+ puts "MISSING #{jam_track.metalocation}"
+ count += 1
+ end
+
+ end
+
+ puts "missing master count: #{count}"
+ end
+
+ def tency_genre_dump
load_tency_mappings
genres = {}
@@ -1841,6 +3078,65 @@ module JamRuby
end
end
+ def load_paris_mappings
+ Dir.mktmpdir do |tmp_dir|
+ mapping_file = File.join(tmp_dir, 'mapping.csv')
+ metadata_file = File.join(tmp_dir, 'metadata.csv')
+
+ # this is a developer option to skip the download and look in the CWD to grab mapping.csv and metadata.csv
+ if ENV['PARIS_ALREADY_DOWNLOADED'] == '1'
+ mapping_file = 'paris_mapping.csv'
+ metadata_file = 'paris_metadata.csv'
+ else
+ paris_s3_manager.download('mapping/mapping.csv', mapping_file)
+ paris_s3_manager.download('mapping/metadata.csv', metadata_file)
+ end
+
+ mapping_csv = CSV.read(mapping_file)
+ metadata_csv = CSV.read(metadata_file, headers: true, return_headers: false)
+
+ @paris_mapping = {}
+ @paris_metadata = {}
+ # convert both to hashes
+ mapping_csv.each do |line|
+ instrument = line[1]
+ instrument.strip! if instrument
+
+ part = line[2]
+ part.strip! if part
+ @paris_mapping[line[0].strip.downcase] = {instrument: instrument, part: part}
+ end
+
+ metadata_csv.each do |line|
+ paris_artist = line[2]
+ # Paris artist in metadata file is often all caps
+ artist = paris_artist.split(' ').collect do |item|
+ if item == 'DJ'
+ 'DJ'
+ else
+ item.titleize
+ end
+ end.join(' ')
+ @paris_metadata[line[1].strip] = {id: line[1].strip, original_artist: artist, name: line[3], genre1: line[4], genre2: line[5], genre3: line[6]}
+ end
+
+ @paris_metadata.each do |id, value|
+
+ genres = []
+
+ genre1 = value[:genre1]
+ genre2 = value[:genre2]
+ genre3 = value[:genre3]
+
+ genres << genre1.downcase.strip if genre1
+ genres << genre2.downcase.strip if genre2
+ genres << genre3.downcase.strip if genre3
+
+ value[:genres] = genres
+ end
+ end
+ end
+
def load_tency_mappings
Dir.mktmpdir do |tmp_dir|
mapping_file = File.join(tmp_dir, 'mapping.csv')
@@ -1880,11 +3176,11 @@ module JamRuby
genre4 = value[:genre4]
genre5 = value[:genre5]
- genres << genre1.downcase.strip if genre1
- genres << genre2.downcase.strip if genre2
- genres << genre3.downcase.strip if genre3
- genres << genre4.downcase.strip if genre4
- genres << genre5.downcase.strip if genre5
+ genres << genre1.downcase.strip if genre1
+ genres << genre2.downcase.strip if genre2
+ genres << genre3.downcase.strip if genre3
+ genres << genre4.downcase.strip if genre4
+ genres << genre5.downcase.strip if genre5
value[:genres] = genres
end
@@ -1912,10 +3208,31 @@ module JamRuby
end
return tency_data
+ elsif is_paris_storage?
+ load_paris_mappings if @paris_mapping.nil?
+ song_id = extract_paris_song_id(metalocation)
+
+ if song_id.nil?
+ puts "missing_song_id #{metalocation}"
+ return nil
+ end
+
+ paris_data = @paris_metadata[song_id]
+
+ if paris_data.nil?
+ @@log.warn("missing paris metadata '#{song_id}'")
+ end
+
+ return paris_data
else
begin
data = s3_manager.read_all(metalocation)
- return YAML.load(data)
+ meta = YAML.load(data)
+
+ if is_tim_tracks_storage?
+ meta[:genres] = ['acapella']
+ end
+ meta
rescue AWS::S3::Errors::NoSuchKey
return nil
end
@@ -1933,6 +3250,7 @@ module JamRuby
def sync_from_metadata(jam_track, meta, metalocation, options)
jam_track_importer = JamTrackImporter.new(@storage_format)
+ jam_track_importer.metadata = meta
JamTrack.connection.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED')
@@ -1958,7 +3276,7 @@ module JamRuby
meta = load_metalocation(metalocation)
- if meta.nil? && is_tency_storage?
+ if meta.nil? && is_paris_storage?
raise "no tency song matching this metalocation #{metalocation}"
end
jam_track_importer = nil
@@ -1982,3 +3300,4 @@ module JamRuby
end
end
end
+
diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb
index d6ae37b1e..96ccf87f8 100644
--- a/ruby/lib/jam_ruby/jam_tracks_manager.rb
+++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb
@@ -41,7 +41,7 @@ module JamRuby
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
- next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ
+ next if jam_track_track.track_type == "Master" # master mixes do not go into the JKZ
# use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata
nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate))
@@ -52,7 +52,8 @@ module JamRuby
step = bump_step(jam_track_right, step)
copy_url_to_file(track_url, track_filename)
- jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}"
+ part = jam_track_track.track_type == 'Click' ? 'ClickTrack' : jam_track_track.part
+ jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{part}")}"
end
#puts "LS + " + `ls -la '#{tmp_dir}'`
diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb
index cf86fdc9b..3398a6a3c 100644
--- a/ruby/lib/jam_ruby/lib/s3_manager.rb
+++ b/ruby/lib/jam_ruby/lib/s3_manager.rb
@@ -121,6 +121,10 @@ module JamRuby
s3_bucket.objects[filename].exists?
end
+ def object(filename)
+ s3_bucket.objects[filename]
+ end
+
def length(filename)
s3_bucket.objects[filename].content_length
end
diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb
index 6be9f7d16..02b98f3b9 100644
--- a/ruby/lib/jam_ruby/lib/subscription_message.rb
+++ b/ruby/lib/jam_ruby/lib/subscription_message.rb
@@ -14,15 +14,29 @@ module JamRuby
end
def self.mount_source_up_requested(mount)
- Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json )
+ Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json)
end
def self.mount_source_down_requested(mount)
- Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json )
+ Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json)
end
def self.jam_track_signing_job_change(jam_track_right)
- Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json )
+ Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s,
+ {signing_state: jam_track_right.signing_state,
+ current_packaging_step: jam_track_right.current_packaging_step,
+ packaging_steps: jam_track_right.packaging_steps}.to_json)
+ end
+
+ def self.mixdown_signing_job_change(jam_track_mixdown_package)
+ Notification.send_subscription_message('mixdown', jam_track_mixdown_package.id.to_s,
+ {signing_state: jam_track_mixdown_package.signing_state,
+ current_packaging_step: jam_track_mixdown_package.current_packaging_step,
+ packaging_steps: jam_track_mixdown_package.packaging_steps}.to_json)
+ end
+
+ def self.test
+ Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json)
end
end
end
diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb
index e8cf40b1b..6b7e98034 100644
--- a/ruby/lib/jam_ruby/message_factory.rb
+++ b/ruby/lib/jam_ruby/message_factory.rb
@@ -736,6 +736,30 @@ module JamRuby
)
end
+ def mixdown_sign_complete(receiver_id, mixdown_package_id)
+ signed = Jampb::MixdownSignComplete.new(
+ :mixdown_package_id => mixdown_package_id
+ )
+
+ Jampb::ClientMessage.new(
+ :type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE,
+ :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET,
+ :mixdown_sign_complete => signed
+ )
+ end
+
+ def mixdown_sign_failed(receiver_id, mixdown_package_id)
+ signed = Jampb::MixdownSignFailed.new(
+ :mixdown_package_id => mixdown_package_id
+ )
+
+ Jampb::ClientMessage.new(
+ :type => ClientMessage::Type::MIXDOWN_SIGN_FAILED,
+ :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET,
+ :mixdown_sign_failed=> signed
+ )
+ end
+
def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at)
recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new(
diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb
index 4408ffa42..e88aed9c7 100644
--- a/ruby/lib/jam_ruby/models/affiliate_partner.rb
+++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb
@@ -9,7 +9,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
- attr_accessible :partner_name, :partner_code, :partner_user_id
+ attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin
ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other }
@@ -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'
@@ -111,18 +119,16 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
end
def should_attribute_sale?(shopping_cart)
- if shopping_cart.is_jam_track?
- if created_within_affiliate_window(shopping_cart.user, Time.now)
- product_info = shopping_cart.product_info
- # subtract the total quantity from the freebie quantity, to see how much we should attribute to them
- real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
- {fee_in_cents: real_quantity * 20}
- else
- false
- end
+
+ if created_within_affiliate_window(shopping_cart.user, Time.now)
+ product_info = shopping_cart.product_info
+ # subtract the total quantity from the freebie quantity, to see how much we should attribute to them
+ real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
+ {fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round}
else
- raise 'shopping cart type not implemented yet'
+ false
end
+
end
def cumulative_earnings_in_dollars
@@ -469,4 +475,8 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
def affiliate_query_params
AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s
end
+
+ def to_s
+ display_name
+ end
end
diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb
index 24bc2b104..55c788e90 100644
--- a/ruby/lib/jam_ruby/models/anonymous_user.rb
+++ b/ruby/lib/jam_ruby/models/anonymous_user.rb
@@ -15,20 +15,47 @@ module JamRuby
ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC')
end
+
def destroy_all_shopping_carts
ShoppingCart.destroy_all(anonymous_user_id: @id)
end
+ def destroy_jam_track_shopping_carts
+ ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE)
+ end
+
def admin
false
end
def has_redeemable_jamtrack
+ raise "not a cookied anonymous user" if @cookies.nil?
+
APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack]
end
+ def gifted_jamtracks
+ 0
+ end
+
+ def free_jamtracks
+ if has_redeemable_jamtrack
+ 1
+ else
+ 0
+ end
+ end
+
+ def show_free_jamtrack?
+ ShoppingCart.user_has_redeemable_jam_track?(self)
+ end
+
def signup_hint
SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first
end
+
+ def reload
+
+ end
end
end
diff --git a/ruby/lib/jam_ruby/models/base_search.rb b/ruby/lib/jam_ruby/models/base_search.rb
index ed2feefb7..685133be8 100644
--- a/ruby/lib/jam_ruby/models/base_search.rb
+++ b/ruby/lib/jam_ruby/models/base_search.rb
@@ -102,11 +102,19 @@ module JamRuby
def self.search_target_class
end
+ def self.genre_ids
+ @@genre_ids ||= Hash[ *Genre.pluck(:id).collect { |v| [ v, v ] }.flatten ]
+ end
+
+ def self.instrument_ids
+ @@instrument_ids ||= Hash[ *Instrument.pluck(:id).collect { |v| [ v, v ] }.flatten ]
+ end
+
def _genres(rel, query_data=json)
gids = query_data[KEY_GENRES]
unless gids.blank?
- allgids = Genre.order(:id).pluck(:id)
- gids = gids.select { |gg| allgids.index(gg).present? }
+ allgids = self.class.genre_ids
+ gids = gids.select { |gg| allgids.has_key?(gg) }
unless gids.blank?
gidsql = gids.join("','")
@@ -119,8 +127,8 @@ module JamRuby
def _instruments(rel, query_data=json)
unless (instruments = query_data[KEY_INSTRUMENTS]).blank?
- instrids = Instrument.order(:id).pluck(:id)
- instruments = instruments.select { |ii| instrids.index(ii['instrument_id']).present? }
+ instrids = self.class.instrument_ids
+ instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) }
unless instruments.blank?
instsql = "SELECT player_id FROM musicians_instruments WHERE (("
diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb
index 6c4e54d84..22874a299 100644
--- a/ruby/lib/jam_ruby/models/crash_dump.rb
+++ b/ruby/lib/jam_ruby/models/crash_dump.rb
@@ -1,6 +1,8 @@
module JamRuby
class CrashDump < ActiveRecord::Base
+ include JamRuby::S3ManagerMixin
+
self.table_name = "crash_dumps"
self.primary_key = 'id'
@@ -15,7 +17,7 @@ module JamRuby
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
- self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
+ self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}"
end
def user_email
@@ -23,5 +25,8 @@ module JamRuby
self.user.email
end
+ def sign_url(expiration_time = 3600 * 24 * 7, secure=true)
+ s3_manager.sign_url(self[:ri], {:expires => expiration_time, :secure => secure})
+ end
end
end
diff --git a/ruby/lib/jam_ruby/models/download_tracker.rb b/ruby/lib/jam_ruby/models/download_tracker.rb
new file mode 100644
index 000000000..6a2bcf301
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/download_tracker.rb
@@ -0,0 +1,121 @@
+module JamRuby
+ class DownloadTracker < ActiveRecord::Base
+
+ @@log = Logging.logger[DownloadTracker]
+
+ belongs_to :user, :class_name => "JamRuby::User"
+ belongs_to :mixdown, :class_name => "JamRuby::JamTrackMixdownPackage", foreign_key: 'mixdown_id'
+ belongs_to :stem, :class_name => "JamRuby::JamTrackTrack", foreign_key: 'stem_id'
+ belongs_to :jam_track, :class_name => "JamRuby::JamTrack"
+
+ # one of mixdown or stem need to be specified. could validate this?
+ validates :user, presence:true
+ validates :remote_ip, presence: true
+ #validates :paid, presence: true
+ validates :jam_track, presence: :true
+
+ def self.create(user, remote_ip, target, owned)
+ dt = DownloadTracker.new
+ dt.user = user
+ dt.remote_ip = remote_ip
+ dt.paid = owned
+ if target.is_a?(JamTrackTrack)
+ dt.jam_track_id = target.jam_track_id
+ elsif target.is_a?(JamTrackMixdownPackage)
+ dt.jam_track_id = target.jam_track_mixdown.jam_track_id
+ end
+ if !dt.save
+ @@log.error("unable to create Download Tracker: #{dt.errors.inspect}")
+ end
+ dt
+ end
+
+ def self.check(user, remote_ip, target, owned)
+
+ return unless APP_CONFIG.guard_against_browser_fraud
+
+ create(user, remote_ip, target, owned)
+
+ # let's check the following
+ alert_freebies_snarfer(remote_ip)
+
+ alert_user_sharer(user)
+ end
+
+ # somebody who has shared account info with a large number of people
+ # high number of downloads of the same user from different IP addresses that were or were not paid for
+ # raw query created by this code:
+ # SELECT distinct(user_id), count(user_id) FROM "download_trackers" WHERE (created_at > NOW() - '30 days'::interval) GROUP BY user_id HAVING count(distinct(remote_ip)) >= 2
+ def self.check_user_sharer(max, user_id = nil)
+ query = DownloadTracker.select('distinct(user_id), count(user_id)')
+ query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
+ if !user_id.nil?
+ query = query.where('user_id = ?', user_id)
+ end
+
+ query.group(:user_id).having("count(distinct(remote_ip)) >= #{max}")
+ end
+
+
+
+
+ # somebody who has figured out how to bypass cookie based method of identity checking, and is getting lots of free JamTracks
+ # high number of downloads of different jam tracks from different users for the same IP address that weren't paid for
+ # raw query created by this code:
+ # SELECT distinct(remote_ip), count(remote_ip) FROM "download_trackers" WHERE (paid = false) AND (created_at > NOW() - '30 days'::interval) GROUP BY remote_ip HAVING count(distinct(jam_track_id)) >= 2
+ def self.check_freebie_snarfer(max, remote_ip = nil)
+
+ query = DownloadTracker.select('distinct(remote_ip), count(remote_ip)').where("paid = false")
+ query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
+ if !remote_ip.nil?
+ query = query.where('remote_ip = ?', remote_ip)
+ end
+ query.group(:remote_ip).having("count(distinct(jam_track_id)) >= #{max}")
+ end
+
+ def self.alert_user_sharer(user)
+ violation = check_user_sharer(APP_CONFIG.max_user_ip_address, user.id).first
+
+ if violation
+ body = "User has downloaded from too many IP addresses #{user.id}\n"
+ body << "Download Count: #{violation['count']}\n"
+ body << "User URL #{user.admin_url}\n"
+ body << "Add to blacklist: #{UserBlacklist.admin_url}"
+
+ AdminMailer.alerts({
+ subject:"Account IP Access Violation. USER: #{user.email}",
+ body:body
+ }).deliver
+ end
+ end
+
+ def self.alert_freebies_snarfer(remote_ip)
+
+ violation = check_freebie_snarfer(APP_CONFIG.max_multiple_users_same_ip, remote_ip).first
+
+ if violation
+ body = "IP Address: #{remote_ip}\n"
+ body << "Download Count: #{violation['count']}\n"
+ body << "Add to blacklist: #{IpBlacklist.admin_url}"
+ body << "Check Activity: #{IpBlacklist.admin_activity_url(remote_ip)}"
+
+ AdminMailer.alerts({
+ subject:"Single IP Access Violation. IP:#{remote_ip}",
+ body:body
+ }).deliver
+ end
+ end
+
+ def admin_url
+ APP_CONFIG.admin_root_url + "/admin/download_trackers/" + id
+ end
+
+ def to_s
+ if stem?
+ "stem:#{stem} #{remote_ip} #{user}"
+ else
+ "mixdown:#{mixdown} #{remote_ip} #{user}"
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb
index b6ab0bd05..3f81b8bbf 100644
--- a/ruby/lib/jam_ruby/models/genre.rb
+++ b/ruby/lib/jam_ruby/models/genre.rb
@@ -25,5 +25,13 @@ module JamRuby
def to_s
description
end
+
+ def self.jam_track_list
+ sql = "SELECT DISTINCT genre_id FROM genres_jam_tracks WHERE genre_id IS NOT NULL"
+ Genre.select("DISTINCT(genres.id), genres.*")
+ .where("genres.id IN (#{sql})")
+ .order('genres.description ASC, genres.id')
+ end
+
end
end
diff --git a/ruby/lib/jam_ruby/models/genre_jam_track.rb b/ruby/lib/jam_ruby/models/genre_jam_track.rb
index aa05e4fd8..933ef26bc 100644
--- a/ruby/lib/jam_ruby/models/genre_jam_track.rb
+++ b/ruby/lib/jam_ruby/models/genre_jam_track.rb
@@ -2,7 +2,10 @@ module JamRuby
class GenreJamTrack < ActiveRecord::Base
self.table_name = 'genres_jam_tracks'
- belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
- belongs_to :genre, class_name: 'JamRuby::Genre'
+
+ attr_accessible :jam_track_id, :genre_id
+
+ belongs_to :jam_track, class_name: 'JamRuby::JamTrack', inverse_of: :genres_jam_tracks
+ belongs_to :genre, class_name: 'JamRuby::Genre', inverse_of: :genres_jam_tracks
end
end
diff --git a/ruby/lib/jam_ruby/models/gift_card.rb b/ruby/lib/jam_ruby/models/gift_card.rb
new file mode 100644
index 000000000..0dc5f94e8
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/gift_card.rb
@@ -0,0 +1,36 @@
+# represents the gift card you hold in your hand
+module JamRuby
+ class GiftCard < ActiveRecord::Base
+
+ @@log = Logging.logger[GiftCard]
+
+ JAM_TRACKS_5 = 'jam_tracks_5'
+ JAM_TRACKS_10 = 'jam_tracks_10'
+ CARD_TYPES =
+ [
+ JAM_TRACKS_5,
+ JAM_TRACKS_10
+ ]
+
+
+ belongs_to :user, class_name: "JamRuby::User"
+
+ validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
+ validates :code, presence: true, uniqueness: true
+
+ after_save :check_gifted
+
+ def check_gifted
+ if user && user_id_changed?
+ if card_type == JAM_TRACKS_5
+ user.gifted_jamtracks += 5
+ elsif card_type == JAM_TRACKS_10
+ user.gifted_jamtracks += 10
+ else
+ raise "unknown card type #{card_type}"
+ end
+ user.save!
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/gift_card_purchase.rb b/ruby/lib/jam_ruby/models/gift_card_purchase.rb
new file mode 100644
index 000000000..0cfb00807
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/gift_card_purchase.rb
@@ -0,0 +1,17 @@
+# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard)
+module JamRuby
+ class GiftCardPurchase < ActiveRecord::Base
+
+ @@log = Logging.logger[GiftCardPurchase]
+
+ attr_accessible :user, :gift_card_type
+
+ def name
+ gift_card_type.sale_display
+ end
+
+ # who purchased the card?
+ belongs_to :user, class_name: "JamRuby::User"
+ belongs_to :gift_card_type, class_name: "JamRuby::GiftCardType"
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/gift_card_type.rb b/ruby/lib/jam_ruby/models/gift_card_type.rb
new file mode 100644
index 000000000..8294dbfed
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/gift_card_type.rb
@@ -0,0 +1,70 @@
+# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard)
+module JamRuby
+ class GiftCardType < ActiveRecord::Base
+
+ @@log = Logging.logger[GiftCardType]
+
+ PRODUCT_TYPE = 'GiftCardType'
+
+ JAM_TRACKS_5 = 'jam_tracks_5'
+ JAM_TRACKS_10 = 'jam_tracks_10'
+ CARD_TYPES =
+ [
+ JAM_TRACKS_5,
+ JAM_TRACKS_10
+ ]
+
+ validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
+
+ def self.jam_track_5
+ GiftCardType.find(JAM_TRACKS_5)
+ end
+
+ def self.jam_track_10
+ GiftCardType.find(JAM_TRACKS_10)
+ end
+
+ def name
+ sale_display
+ end
+
+ def price
+ if card_type == JAM_TRACKS_5
+ 10.00
+ elsif card_type == JAM_TRACKS_10
+ 20.00
+ else
+ raise "unknown card type #{card_type}"
+ end
+ end
+
+
+ def sale_display
+ if card_type == JAM_TRACKS_5
+ 'JamTracks Gift Card (5)'
+ elsif card_type == JAM_TRACKS_10
+ 'JamTracks Gift Card (10)'
+ else
+ raise "unknown card type #{card_type}"
+ end
+ end
+
+ def plan_code
+ if card_type == JAM_TRACKS_5
+ "jamtrack-giftcard-5"
+ elsif card_type == JAM_TRACKS_10
+ "jamtrack-giftcard-10"
+ else
+ raise "unknown card type #{card_type}"
+ end
+ end
+
+ def sales_region
+ 'Worldwide'
+ end
+
+ def to_s
+ sale_display
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb
index cc6fe40a9..3f11a01e5 100644
--- a/ruby/lib/jam_ruby/models/instrument.rb
+++ b/ruby/lib/jam_ruby/models/instrument.rb
@@ -50,6 +50,12 @@ module JamRuby
return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC')
end
+ def self.jam_track_list
+ sql = "SELECT DISTINCT instrument_id FROM jam_track_tracks WHERE instrument_id IS NOT NULL"
+ Instrument.where("instruments.id IN (#{sql})")
+ .order('instruments.description ASC')
+ end
+
def icon_name
MAP_ICON_NAME[self.id]
end
diff --git a/ruby/lib/jam_ruby/models/ip_blacklist.rb b/ruby/lib/jam_ruby/models/ip_blacklist.rb
new file mode 100644
index 000000000..a7099cba8
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/ip_blacklist.rb
@@ -0,0 +1,30 @@
+module JamRuby
+ class IpBlacklist < ActiveRecord::Base
+
+ attr_accessible :remote_ip, :notes, as: :admin
+
+ @@log = Logging.logger[IpBlacklist]
+
+ validates :remote_ip, presence:true, uniqueness:true
+
+ def self.listed(remote_ip)
+ IpBlacklist.count(:conditions => "remote_ip = '#{remote_ip}'") == 1
+ end
+
+ def self.admin_url
+ APP_CONFIG.admin_root_url + "/admin/ip_blacklists/"
+ end
+
+ def self.admin_activity_url(remote_ip)
+ APP_CONFIG.admin_root_url + "/admin/download_trackers?q[remote_ip_equals]=#{URI.escape(remote_ip)}&commit=Filter&order=id_desc"
+ end
+
+ def admin_url
+ APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + id
+ end
+
+ def to_s
+ remote_ip
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb
index 29995e9ad..eca9cd7ea 100644
--- a/ruby/lib/jam_ruby/models/jam_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track.rb
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
module JamRuby
class JamTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin
@@ -18,7 +19,7 @@ module JamRuby
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
:jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration,
- :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin
+ :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin
validates :name, presence: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
@@ -52,7 +53,7 @@ module JamRuby
belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks
- has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id"
+ has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id", inverse_of: :jam_track
has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre
has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC'
@@ -85,6 +86,10 @@ module JamRuby
after_save :sync_reproduction_royalty
after_save :sync_onboarding_exceptions
+ def increment_version!
+ self.version = version.to_i + 1
+ save!
+ end
def sync_reproduction_royalty
@@ -154,6 +159,9 @@ module JamRuby
true
end
+ def sale_display
+ "JamTrack: " + name
+ end
def duplicate_positions?
counter = {}
jam_track_tracks.each do |track|
@@ -255,12 +263,12 @@ module JamRuby
limit = options[:limit]
limit ||= 20
limit = limit.to_i
+ per_page = limit
else
limit = per_page
end
start = (page -1 )* per_page
- limit = per_page
else
limit = options[:limit]
limit ||= 20
@@ -337,9 +345,18 @@ module JamRuby
query = query.where('genre_id = ? ', options[:genre])
end
- query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank?
+ query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type = 'Track'") unless options[:instrument].blank?
query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
+ # FIXME: n+1 queries for rights and genres
+ # query = query.includes([{ jam_track_tracks: :instrument },
+ # :jam_track_tap_ins,
+ # :jam_track_rights,
+ # :genres])
+ # { genres_jam_tracks: :genre },
+ # query = query.includes([{ jam_track_tracks: :instrument },
+ # { genres_jam_tracks: :genre }])
+
count = query.total_entries
if count == 0
@@ -421,13 +438,43 @@ module JamRuby
end
end
+ def click_track_file
+ JamTrackFile.where(jam_track_id: self.id).where(file_type: 'ClickWav').first
+ end
+
+ def click_track
+ JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Click').first
+ end
+
+ def has_count_in?
+ has_count_in = false
+ if jmep_json
+ jmep = JSON.parse(jmep_json)
+
+ if jmep["Events"]
+ events = jmep["Events"]
+ metronome = nil
+ events.each do |event|
+ if event.has_key?("metronome")
+ metronome = event["metronome"]
+ break
+ end
+ end
+ if metronome
+ has_count_in = true
+ end
+ end
+ end
+
+ has_count_in
+ end
def master_track
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first
end
def stem_tracks
- JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track')
+ JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'")
end
def can_download?(user)
@@ -438,6 +485,10 @@ module JamRuby
jam_track_rights.where("user_id=?", user).first
end
+ def mixdowns_for_user(user)
+ JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id)
+ end
+
def short_plan_code
prefix = 'jamtrack-'
plan_code[prefix.length..-1]
@@ -450,7 +501,80 @@ module JamRuby
def generate_slug
self.slug = sluggarize(original_artist) + '-' + sluggarize(name)
- puts "Self.slug #{self.slug}"
+
+ if licensor && licensor.slug.present?
+ #raise "no slug on licensor #{licensor.id}" if licensor.slug.nil?
+ self.slug << "-" + licensor.slug
+ end
+ end
+
+ def gen_plan_code
+ # remove all non-alphanumeric chars from artist as well as name
+ artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase
+ name_code = name.gsub(/[^0-9a-z]/i, '').downcase
+ self.plan_code = "jamtrack-#{artist_code[0...20]}-#{name_code}"
+
+ if licensor && licensor.slug
+ raise "no slug on licensor #{licensor.id}" if licensor.slug.nil?
+ self.plan_code << "-" + licensor.slug
+ end
+
+ self.plan_code = self.plan_code[0...50] # make sure it's a max of 50 long
+
+
+ end
+
+ def to_s
+ "#{self.name} (#{self.original_artist})"
+ end
+
+ def self.latestPurchase(user_id)
+ jtx_created = JamTrackRight
+ .select('created_at')
+ .where(user_id: user_id)
+ .order('created_at DESC')
+ .limit(1)
+ jtx_created.first.created_at.to_i
+ end
+
+ attr_accessor :preview_generate_error
+
+ before_save :jmep_json_generate
+ validate :jmep_text_validate
+
+ def jmep_text_validate
+ begin
+ JmepManager.execute(self.jmep_text)
+ rescue ArgumentError => err
+ errors.add(:jmep_text, err.to_s)
+ end
+ end
+
+ def jmep_json_generate
+ self.licensor_id = nil if self.licensor_id == ''
+ self.jmep_json = nil if self.jmep_json == ''
+ self.time_signature = nil if self.time_signature == ''
+
+ begin
+ self[:jmep_json] = JmepManager.execute(self.jmep_text)
+ rescue ArgumentError => err
+ #errors.add(:jmep_text, err.to_s)
+ end
+ end
+
+ # used in mobile simulate purchase
+ def self.forsale(user)
+ sql =<= 5
+ errors.add(:jam_track, 'allowed 5 mixes')
+ end
+ end
+
+ def verify_settings
+
+ # the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do
+
+ parsed = JSON.parse(self.settings)
+ specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0
+
+ tweaked = false
+ all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track'
+
+
+ if parsed["speed"]
+ tweaked = true
+ end
+ if parsed["pitch"]
+ tweaked = true
+ end
+
+
+ if parsed["tracks"]
+ parsed["tracks"].each do |track|
+ if track["mute"]
+ tweaked = true
+ end
+ if track["vol"] && track["vol"] != 0
+ tweaked = true
+ end
+ if track["pan"] && track["pan"] != 0
+ tweaked = true
+ end
+
+ # there is at least one track with volume specified.
+ if !track["mute"] && track["vol"] != 0
+ all_quiet = false
+ end
+ end
+ end
+
+ if parsed["count-in"]
+ all_quiet = false
+ tweaked = true
+ end
+
+ if all_quiet
+ errors.add(:settings, 'are all muted')
+ end
+ if !tweaked && !parsed['full']
+ errors.add(:settings, 'have nothing specified')
+ end
+
+ if parsed["speed"] && !parsed["speed"].is_a?(Integer)
+ errors.add(:settings, 'has non-integer speed')
+ end
+
+ if parsed["pitch"] && !parsed["pitch"].is_a?(Integer)
+ errors.add(:settings, 'has non-integer pitch')
+ end
+ end
+
+ def self.create(name, description, user, jam_track, settings)
+ mixdown = JamTrackMixdown.new
+ mixdown.name = name
+ mixdown.description = description
+ mixdown.user = user
+ mixdown.jam_track = jam_track
+ mixdown.settings = settings.to_json # RAILS 4 CAN REMOVE .to_json
+ mixdown.save
+ mixdown
+ end
+
+ def will_pitch_shift?
+ self.settings["pitch"] != 0 || self.settings["speed"] != 0
+ end
+
+ def self.mixdownChecksum(user_id, jam_track_id)
+ dates = self
+ .select('created_at')
+ .where(user_id: user_id, jam_track_id: jam_track_id)
+ .order(:id)
+
+ dates = dates.map do |date|
+ date.created_at.to_i.to_s
+ end.join('')
+
+ Digest::MD5.hexdigest(dates)
+ end
+
+ end
+end
+
diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb
new file mode 100644
index 000000000..648bdfffd
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb
@@ -0,0 +1,253 @@
+module JamRuby
+
+ # describes what users have rights to which tracks
+ class JamTrackMixdownPackage < ActiveRecord::Base
+ include JamRuby::S3ManagerMixin
+
+ @@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_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC]
+
+ SAMPLE_RATE_44 = 44
+ SAMPLE_RATE_48 = 48
+ SAMPLE_RATES = [SAMPLE_RATE_44, SAMPLE_RATE_48]
+
+ ENCRYPT_TYPE_JKZ = 'jkz'
+ ENCRYPT_TYPES = [ENCRYPT_TYPE_JKZ, nil]
+
+ default_scope { order('created_at desc') }
+
+ belongs_to :jam_track_mixdown, class_name: "JamRuby::JamTrackMixdown", dependent: :destroy
+
+ validates :jam_track_mixdown, presence: true
+
+ validates :file_type, inclusion: {in: FILE_TYPES}
+ validates :sample_rate, inclusion: {in: SAMPLE_RATES}
+ validates :encrypt_type, inclusion: {in: ENCRYPT_TYPES}
+ validates_uniqueness_of :file_type, scope: [:sample_rate, :encrypt_type, :jam_track_mixdown_id]
+ validates :signing, inclusion: {in: [true, false]}
+ validates :signed, inclusion: {in: [true, false]}
+
+ validate :verify_download_count
+ before_destroy :delete_s3_files
+ after_save :after_save
+
+ MAX_JAM_TRACK_DOWNLOADS = 1000
+
+ def self.estimated_queue_time
+ jam_track_signing_count = JamTrackRight.where(queued: true).count
+ mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).first
+ total_mixdowns = mixdowns['queue_count'].to_i
+ slow_mixdowns = mixdowns['speed_pitch_count'].to_i
+ fast_mixdowns = total_mixdowns - slow_mixdowns
+
+ guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns
+
+ Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns})
+ guess
+ end
+
+ def after_save
+ # try to catch major transitions:
+
+ # if just queue time changes, start time changes, or signed time changes, send out a notice
+ if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was
+ SubscriptionMessage.mixdown_signing_job_change(self)
+ end
+ end
+
+ def self.create(mixdown, file_type, sample_rate, encrypt_type)
+
+ package = JamTrackMixdownPackage.new
+ package.speed_pitched = mixdown.will_pitch_shift?
+ package.jam_track_mixdown = mixdown
+ package.file_type = file_type
+ package.sample_rate = sample_rate
+ package.signed = false
+ package.signing = false
+ package.encrypt_type = encrypt_type
+ package.save
+ package
+ end
+
+ def verify_download_count
+ if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin
+ errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}")
+ end
+ end
+
+ def is_pitch_speed_shifted?
+ mix_settings = JSON.parse(self.settings)
+ mix_settings["speed"] || mix_settings["pitch"]
+ end
+
+ def finish_errored(error_reason, error_detail)
+ self.last_errored_at = Time.now
+ self.last_signed_at = Time.now
+ self.error_count = self.error_count + 1
+ self.error_reason = error_reason
+ self.error_detail = error_detail
+ self.should_retry = self.error_count < 5
+ self.signing = false
+ self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts
+
+ if save
+ Notification.send_mixdown_sign_failed(self)
+ else
+ raise "Error sending notification #{self.errors}"
+ end
+ end
+
+ def finish_sign(url, private_key, length, md5)
+ self.url = url
+ self.private_key = private_key
+ self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts
+ self.downloaded_since_sign = false
+ self.last_signed_at = Time.now
+ self.length = length
+ self.md5 = md5
+ self.signed = true
+ self.signing = false
+ self.error_count = 0
+ self.error_reason = nil
+ self.error_detail = nil
+ self.should_retry = false
+ save!
+ end
+
+ def store_dir
+ "jam_track_mixdowns/#{created_at.strftime('%m-%d-%Y')}/#{self.jam_track_mixdown.user_id}"
+ end
+
+ def filename
+ if encrypt_type
+ "#{id}.#{encrypt_type}"
+ else
+ "#{id}.#{file_type}"
+ end
+ end
+
+
+ # creates a short-lived URL that has access to the object.
+ # the idea is that this is used when a user who has the rights to this tries to download this JamTrack
+ # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
+ # but the url is short lived enough so that it wouldn't be easily shared
+ def sign_url(expiration_time = 120, 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
+
+
+ def enqueue
+ begin
+ self.signing_queued_at = Time.now
+ self.signing_started_at = nil
+ self.last_signed_at = nil
+ self.queued = true
+ self.save
+
+ queue_time = JamTrackMixdownPackage.estimated_queue_time
+
+ # is_pitch_speed_shifted?
+ Resque.enqueue(JamTrackMixdownPackager, self.id)
+ return queue_time
+ rescue Exception => e
+ puts "e: #{e}"
+ # implies redis is down. we don't update started_at by bailing out here
+ false
+ end
+ end
+
+ # if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
+ def enqueue_if_needed
+ state = signing_state
+ if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
+ false
+ else
+ return enqueue
+ end
+ end
+
+ def ready?
+ self.signed && self.url.present?
+ end
+
+ # returns easy to digest state field
+ # SIGNED - the package is ready to be downloaded
+ # ERROR - the package was built unsuccessfully
+ # SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung
+ # SIGNING - the package is currently signing
+ # QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed
+ # QUEUED - the package is queued to sign
+ # QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued
+ def signing_state
+ state = nil
+
+ if signed
+ state = 'SIGNED'
+ elsif signing_started_at && signing
+ # the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long.
+ if Time.now - signing_started_at > APP_CONFIG.signing_job_signing_max_time
+ state = 'SIGNING_TIMEOUT'
+ elsif Time.now - last_step_at > APP_CONFIG.mixdown_step_max_time
+ state = 'SIGNING_TIMEOUT'
+ else
+ state = 'SIGNING'
+ end
+ elsif signing_queued_at
+ if Time.now - signing_queued_at > APP_CONFIG.mixdown_job_queue_max_time
+ state = 'QUEUED_TIMEOUT'
+ else
+ state = 'QUEUED'
+ end
+ elsif error_count > 0
+ state = 'ERROR'
+ else
+ if Time.now - created_at > 60 # it should not take more than a minute to get QUIET out
+ state = 'QUIET_TIMEOUT'
+ else
+ state = 'QUIET' # needs to be poked to go build
+ end
+
+ end
+ state
+ end
+
+ def signed?
+ signed
+ end
+
+ def update_download_count(count=1)
+ self.download_count = self.download_count + count
+ self.last_downloaded_at = Time.now
+
+ if self.signed
+ self.downloaded_since_sign = true
+ end
+ end
+
+
+ def self.stats
+ stats = {}
+
+ result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count')
+
+ stats['count'] = result[0]['total'].to_i
+ stats['signing_count'] = result[0]['signing_count'].to_i
+ stats
+ end
+
+
+ def delete_s3_files
+ s3_manager.delete(self.url) if self.url && s3_manager.exists?(self.url)
+ end
+
+ end
+end
+
diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb
index 5217b620d..b400771a5 100644
--- a/ruby/lib/jam_ruby/models/jam_track_right.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_right.rb
@@ -11,7 +11,10 @@ module JamRuby
attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
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 :version, presence: true
validates :user, presence: true
validates :jam_track, presence: true
validates :is_test_purchase, inclusion: {in: [true, false]}
@@ -25,9 +28,16 @@ module JamRuby
mount_uploader :url_48, JamTrackRightUploader
mount_uploader :url_44, JamTrackRightUploader
before_destroy :delete_s3_files
+ before_create :create_private_keys
MAX_JAM_TRACK_DOWNLOADS = 1000
+ def create_private_keys
+ rsa_key = OpenSSL::PKey::RSA.new(1024)
+ key = rsa_key.to_pem()
+ self.private_key_44 = key
+ self.private_key_48 = key
+ end
def after_save
# try to catch major transitions:
@@ -58,6 +68,7 @@ module JamRuby
def finish_errored(error_reason, error_detail, sample_rate)
self.last_signed_at = Time.now
+ self.queued = false
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
@@ -77,6 +88,7 @@ module JamRuby
def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now
+ self.queued = false
if bitrate==48
self.length_48 = length
self.md5_48 = md5
@@ -99,10 +111,10 @@ 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, bitrate=48, secure=true)
- field_name = (bitrate==48) ? "url_48" : "url_44"
- s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
- end
+ def sign_url(expiration_time = 120, bitrate=48, secure=true)
+ field_name = (bitrate==48) ? "url_48" : "url_44"
+ s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
+ end
def delete_s3_files
remove_url_48!
@@ -112,7 +124,7 @@ module JamRuby
def enqueue(sample_rate=48)
begin
- JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
+ JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil, :queued => true)
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e
@@ -122,8 +134,33 @@ module JamRuby
end
end
+ def cleanup_old_package!
+ if self.jam_track.version != self.version
+ delete_s3_files
+ self[:url_48] = nil
+ self[:url_44] = nil
+ self.signing_queued_at = nil
+ self.signing_started_at_48 = nil
+ self.signing_started_at_44 = nil
+ self.last_signed_at = nil
+ self.current_packaging_step = nil
+ self.packaging_steps = nil
+ self.should_retry = false
+ self.signing_44 = false
+ self.signing_48 = false
+ self.signed_44 = false
+ self.signed_48 = false
+ self.queued = false
+ self.version = self.jam_track.version
+ self.save!
+ end
+ end
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
def enqueue_if_needed(sample_rate=48)
+
+ # delete any package that's out dated
+ cleanup_old_package!
+
state = signing_state(sample_rate)
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
@@ -137,9 +174,9 @@ module JamRuby
# @return true if signed && file exists for the sample_rate specifed:
def ready?(sample_rate=48)
if sample_rate==48
- self.signed_48 && self.url_48.present? && self.url_48.file.exists?
+ self.signed_48 && self.url_48.present? && self.url_48.file.exists? && self.version == self.jam_track.version
else
- self.signed_44 && self.url_44.present? && self.url_44.file.exists?
+ self.signed_44 && self.url_44.present? && self.url_44.file.exists? && self.version == self.jam_track.version
end
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_search.rb b/ruby/lib/jam_ruby/models/jam_track_search.rb
new file mode 100644
index 000000000..a83e7e647
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/jam_track_search.rb
@@ -0,0 +1,168 @@
+module JamRuby
+ class JamTrackSearch < BaseSearch
+
+ cattr_accessor :jschema, :search_meta
+ attr_accessor :user_counters
+
+ KEY_QUERY = 'query'
+ KEY_SEARCH_STR = 'search_str'
+ KEY_RESULT_TYPES = 'result_types'
+ KEY_SONGS = 'songs'
+ KEY_ARTISTS = 'artists'
+ KEY_RESULTS = 'results'
+ KEY_RESULT_SETS = 'result_sets'
+ KEY_PAGE_NUM = 'page_num'
+ KEY_TOTAL_COUNT = 'total_count'
+ KEY_PAGE_COUNT = 'page_count'
+ KEY_PER_PAGE = 'per_page'
+ PER_PAGE = 'development'==Rails.env ? 8 : 20
+ KEY_GENRES = 'genres'
+ KEY_INSTRUMENTS = 'instruments'
+ KEY_LANGUAGE = 'language'
+ KEY_ORIGINAL_ARTIST = 'original_artist'
+
+ def self.json_schema
+ return @@jschema ||= {
+ KEY_QUERY => {
+ KEY_SEARCH_STR => '',
+ KEY_INSTRUMENTS => [],
+ KEY_GENRES => [],
+ KEY_LANGUAGE => '',
+ KEY_ORIGINAL_ARTIST => '',
+ KEY_RESULT_TYPES => [],
+ KEY_PAGE_NUM => 1,
+ KEY_PER_PAGE => PER_PAGE,
+ },
+ KEY_RESULT_SETS => {
+ KEY_SONGS => {
+ KEY_RESULTS => [],
+ KEY_PAGE_NUM => 1,
+ KEY_TOTAL_COUNT => 0,
+ KEY_PAGE_COUNT => 0,
+ },
+ KEY_ARTISTS => {
+ KEY_RESULTS => [],
+ KEY_PAGE_NUM => 1,
+ KEY_TOTAL_COUNT => 0,
+ KEY_PAGE_COUNT => 0,
+ },
+ },
+ }
+ end
+
+ def self.search_target_class
+ JamTrack
+ end
+
+ def do_search(query)
+ rel = JamTrack.unscoped
+
+ unless (gids = query[KEY_GENRES]).blank?
+ allgids = self.class.genre_ids
+ gids = gids.select { |gg| allgids.has_key?(gg) }
+
+ unless gids.blank?
+ sqlstr = "'#{gids.join("','")}'"
+ rel = rel.joins(:genres_jam_tracks)
+ rel = rel.where("genres_jam_tracks.genre_id IN (#{sqlstr})")
+ end
+ end
+ unless (instruments = query[KEY_INSTRUMENTS]).blank?
+ instrids = self.class.instrument_ids
+ instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) }
+
+ unless instruments.blank?
+ sqlstr = "'#{instruments.join("','")}'"
+ rel = rel.joins(:jam_track_tracks)
+ rel = rel.where("jam_track_tracks.instrument_id IN (#{sqlstr})")
+ rel = rel.where("jam_track_tracks.track_type = 'Track'")
+ end
+ end
+
+ unless (artist_name = query[KEY_ORIGINAL_ARTIST]).blank?
+ rel = rel.where(original_artist: artist_name)
+ end
+
+ rel
+ end
+
+ def search_results_page(query=nil)
+ filter = {
+ KEY_QUERY => query,
+ }
+ result_types = query[KEY_RESULT_TYPES]
+ if result_types
+ has_songs, has_artists = result_types.index(KEY_SONGS), result_types.index(KEY_ARTISTS)
+ else
+ has_songs, has_artists = true, true
+ end
+ result_sets = filter[KEY_RESULT_SETS] = self.class.json_schema[KEY_RESULT_SETS].clone
+ if has_songs
+ rel = do_search(query)
+ unless (val = query[KEY_SEARCH_STR]).blank?
+ tsquery = Search.create_tsquery(val)
+ rel = rel.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) if tsquery
+ end
+ rel = rel.order(:name).includes(:genres)
+
+ pgnum = [query[KEY_PAGE_NUM].to_i, 1].max
+ rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE])
+
+ results = rel.all.collect do |jt|
+ {
+ 'id' => jt.id,
+ 'name' => jt.name,
+ 'artist' => jt.original_artist,
+ 'genre' => jt.genres.map(&:description).join(', '),
+ 'plan_code' => jt.plan_code,
+ 'year' => jt.year
+ }
+ end
+
+ result_sets[KEY_SONGS] = {
+ KEY_RESULTS => results,
+ KEY_PAGE_NUM => pgnum,
+ KEY_TOTAL_COUNT => rel.total_entries,
+ KEY_PAGE_COUNT => rel.total_pages,
+ }
+ end
+
+ if has_artists
+ rel = do_search(query)
+ counter = rel.select("DISTINCT(jam_tracks.original_artist)")
+ rel = rel.select("DISTINCT ON(jam_tracks.original_artist) jam_tracks.id, jam_tracks.original_artist")
+
+ unless (val = query[KEY_SEARCH_STR]).blank?
+ rel = rel.where("original_artist ILIKE ?","%#{val}%")
+ counter = counter.where("original_artist ILIKE ?","%#{val}%")
+ end
+ rel = rel.order(:original_artist)
+
+ pgnum = [query[KEY_PAGE_NUM].to_i, 1].max
+ rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE])
+
+ results = rel.all.collect do |jt|
+ { 'id' => jt.id, 'artist' => jt.original_artist }
+ end
+
+ artist_count = counter.count
+
+ result_sets[KEY_ARTISTS] = {
+ KEY_RESULTS => results,
+ KEY_PAGE_NUM => pgnum,
+ KEY_TOTAL_COUNT => artist_count,
+ KEY_PAGE_COUNT => (artist_count / query[KEY_PER_PAGE].to_f).ceil,
+ }
+ end
+
+ filter
+ end
+
+ def self.all_languages
+ JamTrack.select("SELECT DISTINCT(language)").order(:language).collect do |lang|
+ { description: ISO_639.find_by_code(lang), id: lang }
+ end
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb
index c0548e6fd..77938d0c3 100644
--- a/ruby/lib/jam_ruby/models/jam_track_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_track.rb
@@ -6,7 +6,7 @@ module JamRuby
include JamRuby::S3PublicManagerMixin
# there should only be one Master per JamTrack, but there can be N Track per JamTrack
- TRACK_TYPE = %w{Track Master}
+ TRACK_TYPE = %w{Track Master Click}
@@log = Logging.logger[JamTrackTrack]
@@ -19,7 +19,7 @@ module JamRuby
attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin
attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin
- attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error
+ attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error, :wav_file, :tmp_duration, :skip_inst_part_uniq
before_destroy :delete_s3_files
@@ -27,29 +27,44 @@ module JamRuby
validates :part, length: {maximum: 35}
validates :track_type, inclusion: {in: TRACK_TYPE }
validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true
- validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id]
+ validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id], unless: :skip_inst_part_uniq
# validates :jam_track, presence: true
belongs_to :instrument, class_name: "JamRuby::Instrument"
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy
+ 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
"jam_track_tracks"
end
+
+ def licensor_suffix
+ suffix = ''
+ if jam_track.licensor
+ raise "no licensor name" if jam_track.licensor.name.nil?
+ suffix = " - #{jam_track.licensor.name}"
+ end
+ suffix
+ end
+
# create name of the file
def filename(original_name)
- "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
+ "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}/#{original_name}"
end
# create name of the preview file.
# md5-'ed because we cache forever
def preview_filename(md5, ext='ogg')
original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}"
- "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
+ "#{preview_directory}/#{original_name}"
+ end
+
+ def preview_directory
+ "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}"
end
def has_preview?
@@ -58,7 +73,16 @@ module JamRuby
# generates a URL that points to a public version of the preview
def preview_public_url(media_type='ogg')
- url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url]
+ case media_type
+ when 'ogg'
+ url = self[:preview_url]
+ when 'mp3'
+ url = self[:preview_mp3_url]
+ when 'aac'
+ url = self[:preview_aac_url]
+ else
+ raise "unknown media_type #{media_type}"
+ end
if url
s3_public_manager.public_url(url,{ :secure => true})
else
@@ -66,6 +90,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")
@@ -89,6 +125,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?
@@ -157,6 +205,7 @@ module JamRuby
uuid = SecureRandom.uuid
output = File.join(tmp_dir, "#{uuid}.ogg")
output_mp3 = File.join(tmp_dir, "#{uuid}.mp3")
+ output_aac = File.join(tmp_dir, "#{uuid}.aac")
start = self.preview_start_time.to_f / 1000
stop = start + 20
@@ -176,7 +225,6 @@ module JamRuby
# now create mp3 off of ogg preview
convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\""
-
@@log.debug("converting to mp3 using: " + convert_mp3_cmd)
convert_output = `#{convert_mp3_cmd}`
@@ -187,35 +235,55 @@ module JamRuby
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute mp3 convert command #{convert_output}"
else
- ogg_digest = ::Digest::MD5.file(output)
- mp3_digest = ::Digest::MD5.file(output_mp3)
- self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
- self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
- @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
- s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
- @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
- s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
+ convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -c:a libfdk_aac -b:a 192k \"#{output_aac}\""
+ @@log.debug("converting to aac using: " + convert_aac_cmd)
- self.skip_uploader = true
+ convert_output = `#{convert_aac_cmd}`
- original_ogg_preview_url = self["preview_url"]
- original_mp3_preview_url = self["preview_mp3_url"]
+ result_code = $?.to_i
- # and finally update the JamTrackTrack with the new info
- self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
- self["preview_length"] = File.new(output).size
- # and finally update the JamTrackTrack with the new info
- self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
- self["preview_mp3_length"] = File.new(output_mp3).size
- self.save!
+ if result_code != 0
+ @@log.debug("fail #{result_code}")
+ @preview_generate_error = "unable to execute aac convert command #{convert_output}"
+ else
- # if all that worked, now delete old previews, if present
- begin
- s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
- s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
- rescue
- puts "UNABLE TO CLEANUP OLD PREVIEW URL"
+ ogg_digest = ::Digest::MD5.file(output)
+ mp3_digest = ::Digest::MD5.file(output_mp3)
+ aac_digest = ::Digest::MD5.file(output_aac)
+ self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
+ self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
+ self["preview_aac_md5"] = aac_md5 = mp3_digest.hexdigest
+
+ @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
+ s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
+ @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
+ s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
+ @@log.debug("uploading aac preview to #{self.preview_filename('aac')}")
+ s3_public_manager.upload(self.preview_filename(aac_md5, 'aac'), output_aac, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
+
+ self.skip_uploader = true
+
+ original_ogg_preview_url = self["preview_url"]
+ original_mp3_preview_url = self["preview_mp3_url"]
+ original_aac_preview_url = self["preview_aac_url"]
+
+ self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
+ self["preview_length"] = File.new(output).size
+ self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
+ self["preview_mp3_length"] = File.new(output_mp3).size
+ self["preview_aac_url"] = self.preview_filename(aac_md5, 'aac')
+ self["preview_aac_length"] = File.new(output_aac).size
+ self.save!
+
+ # if all that worked, now delete old previews, if present
+ begin
+ s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
+ s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
+ s3_public_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
+ rescue
+ puts "UNABLE TO CLEANUP OLD PREVIEW URL"
+ end
end
end
diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb
index 1f56efc08..d2998a72b 100644
--- a/ruby/lib/jam_ruby/models/music_session.rb
+++ b/ruby/lib/jam_ruby/models/music_session.rb
@@ -36,6 +36,8 @@ module JamRuby
belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id
+ belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions
+
has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all
has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id"
has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id"
@@ -116,6 +118,7 @@ module JamRuby
new_session.open_rsvps = self.open_rsvps
new_session.is_unstructured_rsvp = self.is_unstructured_rsvp
new_session.legal_terms = true
+ new_session.session_controller = self.session_controller
# copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots
RsvpSlot.find_each(:conditions => "music_session_id = '#{self.id}'") do |slot|
@@ -255,6 +258,30 @@ module JamRuby
end
end
+ def set_session_controller(current_user, user)
+
+ # only allow update of session controller by the creator or the currently marked user
+
+ should_tick = false
+
+ if current_user != creator && current_user != self.session_controller
+ return should_tick
+ end
+
+ if active_music_session
+ if user
+ if active_music_session.users.exists?(user)
+ self.session_controller = user
+ should_tick = save
+ end
+ else
+ self.session_controller = nil
+ should_tick = save
+ end
+ end
+ should_tick
+ end
+
def self.index(current_user, user_id, band_id = nil, genre = nil)
hide_private = false
if current_user.id != user_id
@@ -343,6 +370,7 @@ module JamRuby
ms.legal_terms = true
ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps]
ms.creator = user
+ ms.session_controller = user
ms.create_type = options[:create_type]
ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp]
ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone]
diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb
index 86d132025..c8b8c0834 100644
--- a/ruby/lib/jam_ruby/models/music_session_user_history.rb
+++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb
index 5b008a55f..d86d73d12 100644
--- a/ruby/lib/jam_ruby/models/notification.rb
+++ b/ruby/lib/jam_ruby/models/notification.rb
@@ -14,6 +14,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id"
belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id"
+ belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id"
validates :target_user, :presence => true
validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message?
@@ -1255,7 +1256,7 @@ module JamRuby
def send_jam_track_sign_complete(jam_track_right)
notification = Notification.new
- notification.jam_track_right_id = jam_track_right.id
+ notification.jam_track_mixdown_package = jam_track_right.id
notification.description = NotificationTypes::JAM_TRACK_SIGN_COMPLETE
notification.target_user_id = jam_track_right.user_id
notification.save!
@@ -1265,6 +1266,30 @@ module JamRuby
#@@mq_router.publish_to_all_clients(msg)
end
+ def send_mixdown_sign_failed(jam_track_mixdown_package)
+
+ notification = Notification.new
+ notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id
+ notification.description = NotificationTypes::MIXDOWN_SIGN_FAILED
+ notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id
+ notification.save!
+
+ msg = @@message_factory.mixdown_sign_failed(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id)
+ @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg)
+ end
+
+ def send_mixdown_sign_complete(jam_track_mixdown_package)
+
+ notification = Notification.new
+ notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id
+ notification.description = NotificationTypes::MIXDOWN_SIGN_COMPLETE
+ notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id
+ notification.save!
+
+ msg = @@message_factory.mixdown_sign_complete(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id)
+ @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg)
+ end
+
def send_client_update(product, version, uri, size)
msg = @@message_factory.client_update( product, version, uri, size)
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index 99f21c09e..0fbf186e8 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -205,7 +205,7 @@ module JamRuby
end
# Start recording a session.
- def self.start(music_session, owner)
+ def self.start(music_session, owner, record_video: false)
recording = nil
# Use a transaction and lock to avoid races.
music_session.with_lock do
@@ -213,6 +213,7 @@ module JamRuby
recording.music_session = music_session
recording.owner = owner
recording.band = music_session.band
+ recording.video = record_video
if recording.save
GoogleAnalyticsEvent.report_band_recording(recording.band)
@@ -700,6 +701,10 @@ module JamRuby
self.save(:validate => false)
end
+ def add_video_data(data)
+ Recording.where(id: self.id).update_all(external_video_id: data[:video_id])
+ end
+
def add_timeline(timeline)
global = timeline["global"]
raise JamArgumentError, "global must be specified" unless global
diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
index 9d0fc9bd4..006611f61 100644
--- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
+++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
@@ -92,53 +92,15 @@ module JamRuby
transaction.save!
# now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
-
-
if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
- if sale && sale.is_jam_track_sale?
- if sale.sale_line_items.length == 1
- if sale.recurly_total_in_cents == transaction.amount_in_cents
- line_item = sale.sale_line_items[0]
- jam_track = line_item.product
- jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
- if jam_track_right
- line_item.affiliate_refunded = true
- line_item.affiliate_refunded_at = Time.now
- line_item.save!
+ if sale
+ AdminMailer.recurly_alerts(transaction.user, {
+ subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice",
+ body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
+ }).deliver
- jam_track_right.destroy
-
- # associate which JamTrack we assume this is related to in this one success case
- transaction.jam_track = jam_track
- transaction.save!
-
- AdminMailer.recurly_alerts(transaction.user, {
- subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
- body: "A #{transaction.transaction_type} event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
- }).deliver
- else
- AdminMailer.recurly_alerts(transaction.user, {
- subject: "NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
- body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
- }).deliver
- end
-
- else
- AdminMailer.recurly_alerts(transaction.user, {
- subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
- body: "We received a #{transaction.transaction_type} notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
- }).deliver
- end
-
-
- else
- AdminMailer.recurly_alerts(transaction.user, {
- subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
- body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
- }).deliver
- end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales",
diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb
index 70279f8eb..cddf6b8d8 100644
--- a/ruby/lib/jam_ruby/models/sale.rb
+++ b/ruby/lib/jam_ruby/models/sale.rb
@@ -69,27 +69,12 @@ module JamRuby
}
end
- def self.preview_invoice(current_user, shopping_carts)
- line_items = {jam_tracks: []}
- shopping_carts_jam_tracks = []
- shopping_carts_subscriptions = []
- shopping_carts.each do |shopping_cart|
-
- if shopping_cart.is_jam_track?
- shopping_carts_jam_tracks << shopping_cart
- else
- # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
- shopping_carts_subscriptions << shopping_cart
- end
+ def self.ios_purchase(current_user, jam_track, receipt)
+ jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
+ jam_track_right.redeemed = false
+ jam_track_right.version = jam_track.version
end
-
- jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
- line_items[:jam_tracks] = jam_track_items if jam_track_items
-
- # TODO: process shopping_carts_subscriptions
-
- line_items
end
# place_order will create one or more sales based on the contents of shopping_carts for the current user
@@ -99,19 +84,14 @@ module JamRuby
def self.place_order(current_user, shopping_carts)
sales = []
- shopping_carts_jam_tracks = []
- shopping_carts_subscriptions = []
- shopping_carts.each do |shopping_cart|
- if shopping_cart.is_jam_track?
- shopping_carts_jam_tracks << shopping_cart
- else
- # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
- shopping_carts_subscriptions << shopping_cart
- end
+
+ if Sale.is_mixed(shopping_carts)
+ # the controller checks this too; this is just an extra-level of sanity checking
+ return sales
end
- jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
+ jam_track_sale = order_jam_tracks(current_user, shopping_carts)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
@@ -119,22 +99,52 @@ module JamRuby
sales
end
- def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
- ### XXX TODO;
- # we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
+ def self.is_only_freebie(shopping_carts)
+ free = true
+ shopping_carts.each do |cart|
+ free = cart.product_info[:free]
- # if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
- # just a pain to implement
+ if !free
+ break
+ end
+ end
+ free
end
- def self.is_only_freebie(shopping_carts_jam_tracks)
- shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free]
+ # we don't allow mixed shopping carts :/
+ def self.is_mixed(shopping_carts)
+ free = false
+ non_free = false
+ shopping_carts.each do |cart|
+ if cart.product_info[:free]
+ free = true
+ else
+ non_free = true
+ end
+ end
+ free && non_free
end
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
- def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
+ def self.order_jam_tracks(current_user, shopping_carts)
+
+ shopping_carts_jam_tracks = []
+ shopping_carts_subscriptions = []
+ shopping_carts_gift_cards = []
+
+ shopping_carts.each do |shopping_cart|
+ if shopping_cart.is_jam_track?
+ shopping_carts_jam_tracks << shopping_cart
+ elsif shopping_cart.is_gift_card?
+ shopping_carts_gift_cards << shopping_cart
+ else
+ # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
+ raise "unknown shopping cart type #{shopping_cart.cart_type}"
+ shopping_carts_subscriptions << shopping_cart
+ end
+ end
client = RecurlyClient.new
@@ -143,8 +153,8 @@ module JamRuby
sale = create_jam_track_sale(current_user)
if sale.valid?
- if is_only_freebie(shopping_carts_jam_tracks)
- sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil)
+ if is_only_freebie(shopping_carts)
+ sale.process_shopping_carts(current_user, shopping_carts, nil)
sale.recurly_subtotal_in_cents = 0
sale.recurly_tax_in_cents = 0
@@ -159,11 +169,13 @@ module JamRuby
return sale
end
- sale_line_item = sale.sale_line_items[0]
- sale_line_item.recurly_tax_in_cents = 0
- sale_line_item.recurly_total_in_cents = 0
- sale_line_item.recurly_currency = 'USD'
- sale_line_item.recurly_discount_in_cents = 0
+ sale.sale_line_items.each do |sale_line_item|
+ sale_line_item = sale.sale_line_items[0]
+ sale_line_item.recurly_tax_in_cents = 0
+ sale_line_item.recurly_total_in_cents = 0
+ sale_line_item.recurly_currency = 'USD'
+ sale_line_item.recurly_discount_in_cents = 0
+ end
sale.save
else
@@ -173,7 +185,7 @@ module JamRuby
purge_pending_adjustments(account)
- created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
+ created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
# now invoice the sale ... almost done
@@ -229,13 +241,13 @@ module JamRuby
sale
end
- def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
+ def process_shopping_carts(current_user, shopping_carts, account)
created_adjustments = []
begin
- shopping_carts_jam_tracks.each do |shopping_cart|
- process_jam_track(current_user, shopping_cart, account, created_adjustments)
+ shopping_carts.each do |shopping_cart|
+ process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
end
rescue Recurly::Error, NoMethodError => x
# rollback any adjustments created if error
@@ -251,7 +263,7 @@ module JamRuby
end
- def process_jam_track(current_user, shopping_cart, account, created_adjustments)
+ def process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
@@ -259,15 +271,20 @@ module JamRuby
shopping_cart.reload
# get the JamTrack in this shopping cart
- jam_track = shopping_cart.cart_product
+ cart_product = shopping_cart.cart_product
- if jam_track.right_for_user(current_user)
- # if the user already owns the JamTrack, we should just skip this cart item, and destroy it
- # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
- ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
- return
+ if shopping_cart.is_jam_track?
+ jam_track = cart_product
+ if jam_track.right_for_user(current_user)
+ # if the user already owns the JamTrack, we should just skip this cart item, and destroy it
+ # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
+ ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
+ return
+ end
end
+
+
if account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
@@ -300,38 +317,70 @@ module JamRuby
# if the sale line item is invalid, blow up the transaction
unless sale_line_item.valid?
- @log.error("sale item invalid! #{sale_line_item.errors.inspect}")
+ @@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
raise RecurlyClientError.new(sale_line_item.errors)
end
- # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
- jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
- jam_track_right.redeemed = shopping_cart.free?
- end
+ if shopping_cart.is_jam_track?
+ jam_track = cart_product
- # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
- if shopping_cart.free?
- User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
- current_user.has_redeemable_jamtrack = false # make sure model reflects the truth
- end
-
-
- # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
- if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
- jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
- jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
- unless jam_track_right.save
- raise RecurlyClientError.new(jam_track_right.errors)
+ # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
+ jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
+ jam_track_right.redeemed = shopping_cart.free?
+ jam_track_right.version = jam_track.version
end
+
+ # also if the purchase was a free one, then:
+ # first, mark the free has_redeemable_jamtrack field if that's still true
+ # and if still they have more free things, then redeem the giftable_jamtracks
+ if shopping_cart.free?
+ if user.has_redeemable_jamtrack
+ User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
+ current_user.has_redeemable_jamtrack = false
+ else
+ User.where(id: current_user.id).update_all(gifted_jamtracks: current_user.gifted_jamtracks - 1)
+ current_user.gifted_jamtracks = current_user.gifted_jamtracks - 1
+ end
+ end
+
+
+ # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
+ if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
+ jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
+ jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
+ unless jam_track_right.save
+ raise RecurlyClientError.new(jam_track_right.errors)
+ end
+ end
+
+ # blow up the transaction if the JamTrackRight did not get created
+ raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
+
+ elsif shopping_cart.is_gift_card?
+ gift_card_type = cart_product
+ raise "gift card is null" if gift_card_type.nil?
+ raise if current_user.nil?
+
+ shopping_cart.quantity.times do |item|
+ gift_card_purchase = GiftCardPurchase.new(
+ {
+ user: current_user,
+ gift_card_type: gift_card_type
+ })
+
+ unless gift_card_purchase.save
+ raise RecurlyClientError.new(gift_card_purchase.errors)
+ end
+ end
+
+ else
+ raise 'unknown shopping cart type: ' + shopping_cart.cart_type
end
# delete the shopping cart; it's been dealt with
shopping_cart.destroy if shopping_cart
-
- # blow up the transaction if the JamTrackRight did not get created
- raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
end
@@ -361,7 +410,7 @@ module JamRuby
def self.create_jam_track_sale(user)
sale = Sale.new
sale.user = user
- sale.sale_type = JAMTRACK_SALE
+ sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = 0
sale.save
sale
diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb
index 7d744cfe5..5cbe5ad87 100644
--- a/ruby/lib/jam_ruby/models/sale_line_item.rb
+++ b/ruby/lib/jam_ruby/models/sale_line_item.rb
@@ -4,14 +4,16 @@ module JamRuby
JAMBLASTER = 'JamBlaster'
JAMCLOUD = 'JamCloud'
JAMTRACK = 'JamTrack'
+ GIFTCARD = 'GiftCardType'
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
+ belongs_to :gift_card, class_name: 'JamRuby::GiftCard'
belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid'
- validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]}
+ validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD]}
validates :unit_price, numericality: {only_integer: false}
validates :quantity, numericality: {only_integer: true}
validates :free, numericality: {only_integer: true}
@@ -21,9 +23,19 @@ module JamRuby
validates :recurly_plan_code, presence:true
validates :sale, presence:true
+ def is_jam_track?
+ product_type == JAMTRACK
+ end
+
+ def is_gift_card?
+ product_type == GIFTCARD
+ end
+
def product
if product_type == JAMTRACK
JamTrack.find_by_id(product_id)
+ elsif product_type == GIFTCARD
+ GiftCardType.find_by_id(product_id)
else
raise 'unsupported product type'
end
diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb
index 568d94048..fb531c2bd 100644
--- a/ruby/lib/jam_ruby/models/shopping_cart.rb
+++ b/ruby/lib/jam_ruby/models/shopping_cart.rb
@@ -12,6 +12,8 @@ module JamRuby
attr_accessible :quantity, :cart_type, :product_info
+ attr_accessor :skip_mix_check
+
validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id]
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
@@ -20,12 +22,13 @@ module JamRuby
validates :cart_type, presence: true
validates :cart_class_name, presence: true
validates :marked_for_redeem, numericality: {only_integer: true}
+ validate :not_mixed
default_scope order('created_at DESC')
def product_info
product = self.cart_product
- {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region} unless product.nil?
+ {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil?
end
# multiply quantity by price
@@ -38,6 +41,31 @@ module JamRuby
(quantity - marked_for_redeem) * product.price
end
+ def not_mixed
+
+ return if @skip_mix_check
+ existing_carts = []
+ this_user = any_user()
+
+ if this_user
+ existing_carts = this_user.shopping_carts
+ end
+
+ existing_carts = existing_carts.to_a
+ existing_carts << self
+
+ if Sale.is_mixed(existing_carts)
+ if free?
+ errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.")
+ return false
+ else
+ errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.")
+ return false
+ end
+ end
+
+ false
+ end
def cart_product
self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
@@ -51,7 +79,18 @@ module JamRuby
marked_for_redeem == quantity
end
+ def any_user
+ if user
+ user
+ elsif anonymous_user_id
+ AnonymousUser.new(anonymous_user_id, nil)
+ else
+ nil
+ end
+ end
+
def self.create user, product, quantity = 1, mark_redeem = false
+
cart = ShoppingCart.new
if user.is_a?(User)
cart.user = user
@@ -72,39 +111,42 @@ module JamRuby
cart_type == JamTrack::PRODUCT_TYPE
end
+ def is_gift_card?
+ cart_type == GiftCardType::PRODUCT_TYPE
+ end
# returns an array of adjustments for the shopping cart
def create_adjustment_attributes(current_user)
- raise "not a jam track" unless is_jam_track?
+ raise "not a jam track or gift card" unless is_jam_track? || is_gift_card?
info = self.product_info
if free?
-
# create the credit, then the pseudo charge
[
{
accounting_code: PURCHASE_FREE_CREDIT,
currency: 'USD',
unit_amount_in_cents: -(info[:total_price] * 100).to_i,
- description: "JamTrack: " + info[:name] + " (Credit)",
+ description: info[:sale_display] + " (Credit)",
tax_exempt: true
},
{
accounting_code: PURCHASE_FREE,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
- description: "JamTrack: " + info[:name],
+ description: info[:sale_display],
tax_exempt: true
}
]
else
+
[
{
accounting_code: PURCHASE_NORMAL,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
- description: "JamTrack: " + info[:name],
+ description: info[:sale_display],
tax_exempt: false
}
]
@@ -113,8 +155,13 @@ module JamRuby
def self.move_to_user(user, anonymous_user, shopping_carts)
shopping_carts.each do |shopping_cart|
- mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
- cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem)
+ if shopping_cart.is_jam_track?
+ mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
+ cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem)
+ else
+ cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false)
+ end
+
end
anonymous_user.destroy_all_shopping_carts
@@ -134,28 +181,32 @@ module JamRuby
# if no shpping carts have been marked, then mark it redeemable
# should be wrapped in a TRANSACTION
def self.user_has_redeemable_jam_track?(any_user)
- mark_redeem = false
- if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack
- mark_redeem = true # start out assuming we can redeem...
+
+ if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0
+
+ free_in_cart = 0
any_user.shopping_carts.each do |shopping_cart|
# but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true
- if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0
- mark_redeem = false
- break
+ if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE
+ free_in_cart += shopping_cart.marked_for_redeem
end
end
+
+ any_user.free_jamtracks > free_in_cart
+ else
+ false
end
- mark_redeem
end
# adds a jam_track to cart, checking for promotions
- def self.add_jam_track_to_cart(any_user, jam_track)
+ def self.add_jam_track_to_cart(any_user, jam_track, clear:false)
cart = nil
ShoppingCart.transaction do
- if any_user.has_redeemable_jamtrack
- # if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart
- any_user.destroy_all_shopping_carts
+ if clear
+ # if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario'
+ any_user.destroy_jam_track_shopping_carts
+ any_user.reload
end
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
@@ -164,23 +215,66 @@ module JamRuby
cart
end
+ def self.add_item_to_cart(any_user, item)
+ cart = nil
+ ShoppingCart.transaction do
+ cart = ShoppingCart.create(any_user, item, 1, false)
+ end
+ cart
+ end
+
# deletes a jam track from the shopping cart, updating redeem flag as necessary
def self.remove_jam_track_from_cart(any_user, cart)
ShoppingCart.transaction do
cart.destroy
- # check if we should move the redemption
+
+ # so that user.shopping_carts reflects truth
+ any_user.reload
+
+ # check if we should move the redemption around automatically
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
carts = any_user.shopping_carts
- # if we find any carts on the account, mark one redeemable
+ # if we find any carts on the account that are not redeemed, mark first one redeemable
if mark_redeem && carts.length > 0
- carts[0].redeem(mark_redeem)
- carts[0].save
+ carts.each do |cart|
+ if cart.marked_for_redeem == 0
+ if cart.quantity > 1
+ raise 'unknown situation for redeemption juggling'
+ end
+ cart.redeem(mark_redeem)
+ cart.save
+ break
+ end
+ end
end
end
end
+ def self.remove_item_from_cart(any_user, cart)
+ ShoppingCart.transaction do
+ cart.destroy
+ end
+ end
+
+ # if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up
+ def self.apply_gifted_jamtracks(user)
+ jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE)
+
+ if jam_track_carts.count > user.gifted_jamtracks
+ # just whack everything in their shopping cart
+ user.destroy_all_shopping_carts
+ return
+ end
+
+ jam_track_carts.each do |cart|
+ cart.skip_mix_check = true
+ cart.marked_for_redeem = 1
+ cart.save!
+ end
+ end
+
def port(user, anonymous_user)
ShoppingCart.transaction do
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index f7d48c669..53327a746 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -40,10 +40,12 @@ module JamRuby
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection
# updating_password corresponds to a lost_password
- attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json
+ attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card
belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id'
+ has_many :controlled_sessions, :class_name=> "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id
+
# authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
@@ -149,6 +151,11 @@ module JamRuby
# events
has_many :event_sessions, :class_name => "JamRuby::EventSession"
+ # gift cards
+ has_many :gift_cards, :class_name=> "JamRuby::GiftCard"
+ has_many :gift_card_purchases, :class_name=> "JamRuby::GiftCardPurchase"
+
+
# affiliate_partner
has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user
belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count
@@ -177,11 +184,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
@@ -193,6 +202,7 @@ module JamRuby
validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false }
validates :reuse_card, :inclusion => {:in => [true, false]}
validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]}
+ validates :gifted_jamtracks, presence: true, :numericality => { :less_than_or_equal_to => 100 }
validates :subscribe_email, :inclusion => {:in => [nil, true, false]}
validates :musician, :inclusion => {:in => [true, false]}
validates :show_whats_next, :inclusion => {:in => [nil, true, false]}
@@ -213,6 +223,7 @@ module JamRuby
validate :email_case_insensitive_uniqueness
validate :update_email_case_insensitive_uniqueness, :if => :updating_email
validate :validate_mods
+ validate :presence_gift_card, :if => :expecting_gift_card
scope :musicians, where(:musician => true)
scope :fans, where(:musician => false)
@@ -232,6 +243,18 @@ module JamRuby
end
end
+ def has_any_free_jamtracks
+ has_redeemable_jamtrack || gifted_jamtracks > 0
+ end
+
+ def free_jamtracks
+ (has_redeemable_jamtrack ? 1 : 0) + gifted_jamtracks
+ end
+
+ def show_free_jamtrack?
+ ShoppingCart.user_has_redeemable_jam_track?(self)
+ end
+
def failed_qualification(reason)
self.last_failed_certified_gear_at = DateTime.now
self.last_failed_certified_gear_reason = reason
@@ -254,6 +277,12 @@ module JamRuby
end
end
+ def presence_gift_card
+ if self.gift_cards.length == 0
+ errors.add(:gift_card, ValidationMessages::NOT_FOUND)
+ end
+ end
+
def validate_current_password
# checks if the user put in their current password (used when changing your email, for instance)
errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password)
@@ -296,8 +325,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
@@ -576,9 +613,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
@@ -1018,6 +1053,7 @@ module JamRuby
reuse_card = options[:reuse_card]
signup_hint = options[:signup_hint]
affiliate_partner = options[:affiliate_partner]
+ gift_card = options[:gift_card]
user = User.new
@@ -1029,6 +1065,9 @@ module JamRuby
user.terms_of_service = terms_of_service
user.musician = musician
user.reuse_card unless reuse_card.nil?
+ user.gifted_jamtracks = 0
+ user.has_redeemable_jamtrack = true
+
# FIXME: Setting random password for social network logins. This
# is because we have validations all over the place on this.
@@ -1133,8 +1172,22 @@ module JamRuby
end
end
+ found_gift_card = nil
+
+ # if a gift card value was passed in, then try to find that gift card and apply it to user
+ if gift_card
+ user.expecting_gift_card = true
+ found_gift_card = GiftCard.where(code:gift_card).where(user_id:nil).first
+ user.gift_cards << found_gift_card if found_gift_card
+ end
+
user.save
+ if found_gift_card
+ user.reload
+ ShoppingCart.apply_gifted_jamtracks(user)
+ end
+
# if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it
# only_freebie_in_cart =
# signup_hint &&
@@ -1174,6 +1227,7 @@ module JamRuby
end
end
end
+ user.reload if user.id# gift card adding gifted_jamtracks doesn't reflect here until reload
user
end # def signup
@@ -1629,6 +1683,11 @@ module JamRuby
ShoppingCart.where("user_id=?", self).destroy_all
end
+ def destroy_jam_track_shopping_carts
+ ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE)
+ end
+
+
def unsubscribe_token
self.class.create_access_token(self)
end
@@ -1681,14 +1740,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)
diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb
index e83bb8db4..f4c1523eb 100644
--- a/ruby/lib/jam_ruby/models/user_authorization.rb
+++ b/ruby/lib/jam_ruby/models/user_authorization.rb
@@ -1,7 +1,7 @@
module JamRuby
class UserAuthorization < ActiveRecord::Base
- attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user
+ attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user, :refresh_token
self.table_name = "user_authorizations"
@@ -12,6 +12,36 @@ module JamRuby
validates_uniqueness_of :uid, scope: :provider
# token, secret, token_expiration can be missing
+ def self.refreshing_google_auth(user)
+ auth = self.where(:user_id => user.id)
+ .where(:provider => 'google_login')
+ .limit(1).first
+
+ # if we have an auth that will expire in less than 10 minutes
+ if auth && auth.refresh_token && auth.token_expiration < Time.now - 60 * 10
+
+ begin
+ oauth_client = OAuth2::Client.new(
+ Rails.application.config.google_client_id, Rails.application.config.google_secret,
+ :site => "https://accounts.google.com",
+ :token_url => "/o/oauth2/token",
+ :authorize_url => "/o/oauth2/auth")
+ access_token = OAuth2::AccessToken.from_hash(oauth_client, {:refresh_token => auth.refresh_token})
+ access_token = access_token.refresh!
+
+ auth.token = access_token.token
+ auth.token_expiration = Time.now + access_token.expires_in
+ auth.save
+ return auth
+ rescue Exception => e
+ # couldn't refresh; probably the user has revoked the app's rights
+ return nil
+ end
+ else
+ auth
+ end
+ end
+
def self.google_auth(user)
self
.where(:user_id => user.id)
diff --git a/ruby/lib/jam_ruby/models/user_blacklist.rb b/ruby/lib/jam_ruby/models/user_blacklist.rb
new file mode 100644
index 000000000..287ae5391
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_blacklist.rb
@@ -0,0 +1,27 @@
+module JamRuby
+ class UserBlacklist < ActiveRecord::Base
+
+ attr_accessible :user_id, :notes, as: :admin
+ @@log = Logging.logger[UserBlacklist]
+
+ belongs_to :user, :class_name => "JamRuby::User"
+
+ validates :user, presence:true, uniqueness: true
+
+ def self.listed(user)
+ UserBlacklist.count(:conditions => "user_id= '#{user.id}'") == 1
+ end
+
+ def self.admin_url
+ APP_CONFIG.admin_root_url + "/admin/user_blacklists/"
+ end
+
+ def admin_url
+ APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + id
+ end
+
+ def to_s
+ user
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user_event.rb b/ruby/lib/jam_ruby/models/user_event.rb
new file mode 100644
index 000000000..21446c005
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_event.rb
@@ -0,0 +1,8 @@
+module JamRuby
+ class UserEvent < ActiveRecord::Base
+
+ belongs_to :user, class_name: 'JamRuby::User'
+
+ validates :name, presence: true
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb
new file mode 100644
index 000000000..05b7a926d
--- /dev/null
+++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb
@@ -0,0 +1,771 @@
+require 'json'
+require 'resque'
+require 'resque-retry'
+require 'net/http'
+require 'digest/md5'
+
+module JamRuby
+ class JamTrackMixdownPackager
+ extend JamRuby::ResqueStats
+
+ include JamRuby::S3ManagerMixin
+
+ TAP_IN_PADDING = 2
+
+ MAX_PAN = 90
+ MIN_PAN = -90
+ KNOCK_SECONDS = 0.035
+
+ attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step
+ @queue = :jam_track_mixdown_packager
+
+ def log
+ @log || Logging.logger[JamTrackMixdownPackager]
+ end
+
+ def self.perform(mixdown_package_id, bitrate=48)
+ jam_track_builder = JamTrackMixdownPackager.new()
+ jam_track_builder.mixdown_package_id = mixdown_package_id
+ jam_track_builder.run
+ end
+
+ def compute_steps
+ @step = 0
+ number_downloads = @track_settings.length
+ number_volume_adjustments = (@track_settings.select { |track| should_alter_volume? track }).length
+
+ pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0
+ mix_steps = 1
+ package_steps = 1
+
+ number_downloads + number_volume_adjustments + pitch_shift_steps + mix_steps + package_steps
+ end
+
+ def run
+ begin
+ log.info("Mixdown job starting. mixdown_packager_id #{mixdown_package_id}")
+ begin
+ @mixdown_package = JamTrackMixdownPackage.find(mixdown_package_id)
+
+
+ # bailout check
+ if @mixdown_package.signed?
+ log.debug("package is already signed. bailing")
+ return
+ end
+
+ @mixdown = @mixdown_package.jam_track_mixdown
+ @settings = JSON.parse(@mixdown.settings)
+
+ process_jmep
+
+ track_settings
+
+ # compute the step count
+ total_steps = compute_steps
+
+ # track that it's started ( and avoid db validations )
+ signing_started_at = Time.now
+ last_step_at = Time.now
+ #JamTrackMixdownPackage.where(:id => @mixdown_package.id).update_all(:signing_started_at => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, :signing => true)
+
+ # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
+
+ @mixdown_package.current_packaging_step = 0
+ @mixdown_package.packaging_steps = total_steps
+ @mixdown_package.signing_started_at = signing_started_at
+ @mixdown_package.signing = true
+ @mixdown_package.should_retry = false
+ @mixdown_package.last_step_at = last_step_at
+ @mixdown_package.queued = false
+ @mixdown_package.save
+
+ SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
+
+ package
+
+ log.info "Signed mixdown package to #{@mixdown_package[:url]}"
+
+ rescue Exception => e
+ # record the error in the database
+ post_error(e)
+
+ #SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
+ # and let the job fail, alerting ops too
+ raise
+ end
+ end
+ end
+
+ def should_alter_volume? track
+
+ # short cut is possible if vol = 1.0 and pan = 0
+ vol = track[:vol]
+ pan = track[:pan]
+
+ vol != 1.0 || pan != 0
+ end
+
+ def process_jmep
+ @start_points = []
+ @initial_padding = 0.0
+ @tap_in_initial_silence = 0
+
+ speed = @settings['speed'] || 0
+
+ @speed_factor = 1.0 + (-speed.to_f / 100.0)
+ @inverse_speed_factor = 1 - (-speed.to_f / 100)
+
+ log.info("speed factor #{@speed_factor}")
+
+ jmep = @mixdown.jam_track.jmep_json
+ if jmep
+ jmep = JSON.parse(jmep)
+ end
+
+ if jmep.nil?
+ log.debug("no jmep")
+ return
+ end
+
+ events = jmep["Events"]
+
+ return if events.nil? || events.length == 0
+
+ metronome = nil
+ events.each do |event|
+ if event.has_key?("metronome")
+ metronome = event["metronome"]
+ break
+ end
+ end
+
+ if metronome.nil? || metronome.length == 0
+ log.debug("no metronome events for jmep", jmep)
+ return
+ end
+
+ @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" }
+
+ log.debug("found #{@start_points.length} metronome start points")
+
+ start_point = @start_points[0]
+
+ if start_point
+ start_time = parse_time(start_point["ts"])
+
+ if start_time < 2.0
+ padding = start_time - 2.0
+ @initial_padding = padding.abs
+ @initial_tap_in = start_time
+ end
+ end
+
+ if @speed_factor != 1.0
+ metronome.length.times do |count|
+
+ # we expect to find metronome start/stop grouped
+ if count % 2 == 0
+
+ start = metronome[count]
+ stop = metronome[count + 1]
+
+ if start["action"] != "start" || stop["action"] != "stop"
+ # bail out
+ log.error("found de-coupled metronome events #{start.to_json} | #{stop.to_json}")
+ next
+ end
+
+ bpm = start["bpm"].to_f
+ stop_time = parse_time(stop['ts'])
+ ticks = stop['ticks'].to_i
+
+
+ new_bpm = bpm * @inverse_speed_factor
+ new_stop_time = stop_time * @speed_factor
+ new_start_time = new_stop_time - (60.0/new_bpm * ticks)
+
+ log.info("original bpm:#{bpm} start: #{parse_time(start["ts"])} stop: #{stop_time}")
+ log.info("updated bpm:#{new_bpm} start: #{new_start_time} stop: #{new_stop_time}")
+
+ stop["ts"] = new_stop_time
+ start["ts"] = new_start_time
+ start["bpm"] = new_bpm
+ stop["bpm"] = new_bpm
+
+ @tap_in_initial_silence = (@initial_tap_in + @initial_padding) * @speed_factor
+
+ end
+
+ end
+ end
+
+ @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" }
+
+ end
+
+ # format like: "-0:00:02:820"
+ def parse_time(ts)
+
+ if ts.is_a?(Float)
+ return ts
+ end
+
+ time = 0.0
+ negative = false
+
+ if ts.start_with?('-')
+ negative = true
+ end
+
+ # parse time_format
+ bits = ts.split(':').reverse
+
+ bit_position = 0
+ bits.each do |bit|
+ if bit_position == 0
+ # milliseconds
+ milliseconds = bit.to_f
+ time += milliseconds/1000
+ elsif bit_position == 1
+ # seconds
+ time += bit.to_f
+ elsif bit_position == 2
+ # minutes
+ time += 60 * bit.to_f
+ elsif bit_position == 3
+ # hours
+ # not bothering
+ end
+
+ bit_position += 1
+ end
+
+ if negative
+ time = 0.0 - time
+ end
+
+ time
+ end
+
+ def path_to_resources
+ File.join(File.dirname(File.expand_path(__FILE__)), '../../../lib/jam_ruby/app/assets/sounds')
+ end
+
+ def knock_file
+ if long_sample_rate == 44100
+ knock = File.join(path_to_resources, 'knock44.wav')
+ else
+ knock = File.join(path_to_resources, 'knock48.wav')
+ end
+ knock
+ end
+
+ def create_silence(tmp_dir, segment_count, duration)
+ file = File.join(tmp_dir, "#{segment_count}.wav")
+
+ # -c 2 means stereo
+ cmd("sox -n -r #{long_sample_rate} -c 2 #{file} trim 0.0 #{duration}", "silence")
+
+ file
+ end
+
+ def create_tapin_track(tmp_dir)
+
+ return nil if @start_points.length == 0
+
+ segment_count = 0
+
+
+ #initial_silence = @initial_tap_in + @initial_padding
+
+ initial_silence = @tap_in_initial_silence
+
+ #log.info("tapin data: initial_tap_in: #{@initial_tap_in}, initial_padding: #{@initial_padding}, initial_silence: #{initial_silence}")
+
+ time_points = []
+ files = []
+ if initial_silence > 0
+
+ files << create_silence(tmp_dir, segment_count, initial_silence)
+
+ time_points << {type: :silence, ts: initial_silence}
+ segment_count += 1
+ end
+
+
+ time_cursor = nil
+ @start_points.each do |start_point|
+ tap_time = parse_time(start_point["ts"])
+ if !time_cursor.nil?
+ between_silence = tap_time - time_cursor
+ files << create_silence(tmp_dir, segment_count, between_silence)
+ time_points << {type: :silence, ts: between_silence}
+ end
+ time_cursor = tap_time
+ bpm = start_point["bpm"].to_f
+
+ tick_silence = 60.0/bpm - KNOCK_SECONDS
+
+ ticks = start_point["ticks"].to_i
+
+ ticks.times do |tick|
+ files << knock_file
+ files << create_silence(tmp_dir, segment_count, tick_silence)
+ time_points << {type: :knock, ts: KNOCK_SECONDS}
+ time_points << {type: :silence, ts: tick_silence}
+ time_cursor + 60.0/bpm
+ segment_count += 1
+ end
+ end
+
+ log.info("time points for tap-in: #{time_points.inspect}")
+ # do we need to pad with time? not sure
+
+ sequence_cmd = "sox "
+ files.each do |file|
+ sequence_cmd << "\"#{file}\" "
+ end
+
+ count_in = File.join(tmp_dir, "count-in.wav")
+ sequence_cmd << "\"#{count_in}\""
+
+ cmd(sequence_cmd, "count_in")
+ @count_in_file = count_in
+ count_in
+ end
+
+ # creates a list of tracks to actually mix
+ def track_settings
+ altered_tracks = @settings["tracks"] || []
+
+ @track_settings = []
+
+ #void slider2Pan(int i, float *f);
+
+ stems = @mixdown.jam_track.stem_tracks
+ @track_count = stems.length
+
+ @include_count_in = @settings["count-in"] && @start_points.length > 0 && @mixdown_package.encrypt_type.nil?
+
+ # temp
+ # @include_count_in = true
+
+ if @include_count_in
+ @track_count += 1
+ end
+
+ stems.each do |stem|
+
+ vol = 1.0
+ pan = 0
+ match = false
+ skipped = false
+ # is this stem in the altered_tracks list?
+ altered_tracks.each do |alteration|
+
+ if alteration["id"] == stem.id
+ if alteration["mute"] || alteration["vol"] == 0
+ log.debug("leaving out track because muted or 0 volume #{alteration.inspect}")
+ skipped = true
+ next
+ else
+ vol = alteration["vol"] || vol
+ pan = alteration["pan"] || pan
+ end
+ @track_settings << {stem: stem, vol: vol, pan: pan}
+ match = true
+ break
+ end
+ end
+
+ # if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged
+ if !skipped && !match
+ @track_settings << {stem: stem, vol: vol, pan: pan}
+ end
+ end
+
+ if @include_count_in
+ @track_settings << {count_in: true, vol: 1.0, pan: 0}
+ end
+
+ @track_settings
+ end
+
+ def slider_to_pan(pan)
+ # transpose MIN_PAN to MAX_PAN to
+ # 0-1.0 range
+ #assumes abs(MIN_PAN) == abs(MAX_PAN)
+ # k = f(i) = (i)/(2*MAX_PAN) + 0.5
+ # so f(MIN_PAN) = -0.5 + 0.5 = 0
+
+ k = ((pan * (1.0))/ (2.0 * MAX_PAN)) + 0.5
+ l, r = 0
+
+ if k == 0
+ l = 0.0
+ r = 1.0
+ else
+ l = Math.sqrt(k)
+ r = Math.sqrt(1-k)
+ end
+
+ [l, r]
+ end
+
+ def package
+
+ log.info("Settings: #{@settings.to_json}")
+
+ Dir.mktmpdir do |tmp_dir|
+
+ # download all files
+ @track_settings.each do |track|
+
+ if track[:count_in]
+ file = create_tapin_track(tmp_dir)
+ bump_step(@mixdown_package)
+ else
+ jam_track_track = track[:stem]
+
+ file = File.join(tmp_dir, jam_track_track.id + '.ogg')
+
+ bump_step(@mixdown_package)
+
+ # download each track needed
+ s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file)
+ end
+
+ track[:file] = file
+ end
+
+ audio_process tmp_dir
+ end
+ end
+
+ def audio_process(tmp_dir)
+ # use sox remix to apply mute, volume, pan settings
+
+
+ # step 1: apply pan and volume per track. mute and vol of 0 has already been handled, by virtue of those tracks not being present in @track_settings
+ # step 2: mix all tracks into single track, dividing by constant number of jam tracks, which is same as done by client backend
+ # step 3: apply pitch and speed (if applicable)
+ # step 4: encrypt with jkz (if applicable)
+
+ apply_vol_and_pan tmp_dir
+
+ create_silence_padding tmp_dir
+
+ mix tmp_dir
+
+ pitch_speed tmp_dir
+
+ final_packaging tmp_dir
+ end
+
+ # output is :volumed_file in each track in @track_settings
+ def apply_vol_and_pan(tmp_dir)
+ @track_settings.each do |track|
+
+ jam_track_track = track[:stem]
+ count_in = track[:count_in]
+ file = track[:file]
+
+ unless should_alter_volume? track
+ track[:volumed_file] = file
+ else
+ pan_l, pan_r = slider_to_pan(track[:pan])
+
+ vol = track[:vol]
+
+ # short
+ channel_l = pan_l * vol
+ channel_r = pan_r * vol
+
+ bump_step(@mixdown_package)
+
+ # sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0
+
+ if count_in
+ volumed_file = File.join(tmp_dir, 'count-in' + '-volumed.ogg')
+ else
+ volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg')
+ end
+
+ cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_r} 2v#{channel_l}", 'vol_pan')
+
+ track[:volumed_file] = volumed_file
+ end
+ end
+ end
+
+ def create_silence_padding(tmp_dir)
+ if @initial_padding > 0 && @include_count_in
+
+ @padding_file = File.join(tmp_dir, "initial_padding.ogg")
+
+ # -c 2 means stereo
+ cmd("sox -n -r #{long_sample_rate} -c 2 #{@padding_file} trim 0.0 #{@initial_padding}", "initial_padding")
+
+ @track_settings.each do |track|
+
+ next if track[:count_in]
+
+ input = track[:volumed_file]
+ output = input[0..-5] + '-padded.ogg'
+
+ padd_cmd = "sox '#{@padding_file}' '#{input}' '#{output}'"
+
+ cmd(padd_cmd, "pad_track_with_silence")
+ track[:volumed_file] = output
+ end
+ end
+ end
+
+ # output is @mix_file
+ def mix(tmp_dir)
+
+ bump_step(@mixdown_package)
+
+ @mix_file = File.join(tmp_dir, "mix.ogg")
+
+
+ pitch = @settings['pitch'] || 0
+ speed = @settings['speed'] || 0
+
+
+ real_count = @track_settings.count
+ real_count -= 1 if @include_count_in
+
+ # if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks
+ if real_count <= 1
+ mix_divide = 1.0/@track_count
+ cmd = "sox -v #{mix_divide} \"#{@track_settings[0][:volumed_file]}\" \"#{@mix_file}\""
+ cmd(cmd, 'volume_adjust')
+ else
+ # sox -m will divide by number of inputs by default. But we purposefully leave out tracks that are mute/no volume (to save downloading/processing time in this job)
+ # so we need to tell sox to divide by how many tracks there are as a constant, because this is how the client works today
+ #sox -m -v 1/n file1 -v 1/n file2 out
+ cmd = "sox -m"
+ mix_divide = 1.0/@track_count
+ @track_settings.each do |track|
+
+ # if pitch/shifted, we lay the tap-in after pitch/speed shift
+ # next if (pitch != 0 || speed != 0) && track[:count_in]
+ next if track[:count_in]
+
+ volumed_file = track[:volumed_file]
+ cmd << " -v #{mix_divide} \"#{volumed_file}\""
+ end
+
+
+ cmd << " \"#{@mix_file}\""
+ cmd(cmd, 'mix_adjust')
+ end
+
+
+ end
+
+ def long_sample_rate
+ sample_rate = 48000
+ if @mixdown_package.sample_rate != 48
+ sample_rate = 44100
+ end
+ sample_rate
+ end
+
+ # output is @speed_mix_file
+ def pitch_speed tmp_dir
+
+ # # usage
+ # This app will take an ogg, wav, or mp3 file (for the uploads) as its input and output an ogg file.
+ # Usage:
+ # sbsms path-to-input.ogg path-to-output.ogg TimeStrech PitchShift
+
+ # input is @mix_file, created by mix()
+ # output is @speed_mix_file
+
+ pitch = @settings['pitch'] || 0
+ speed = @settings['speed'] || 0
+
+ # if pitch and speed are 0, we do nothing here
+ if pitch == 0 && speed == 0
+ @speed_mix_file = @mix_file
+ else
+ bump_step(@mixdown_package)
+
+ @speed_mix_file = File.join(tmp_dir, "speed_mix_file.ogg")
+
+ # usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz
+
+ sample_rate = long_sample_rate
+
+ # rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/
+ sbsms_speed = speed/100.0
+ sbsms_speed = 1.0 + sbsms_speed
+
+ sbsms_pitch = pitch
+ cmd("sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift')
+ end
+
+ if @include_count_in
+ # lay the tap-ins over the recording
+ layered = File.join(tmp_dir, "layered_speed_mix.ogg")
+ cmd("sox -m '#{@count_in_file}' '#{@speed_mix_file}' '#{layered}'", "layer_tap_in")
+ @speed_mix_file = layered
+ end
+ end
+
+ def final_packaging tmp_dir
+
+ bump_step(@mixdown_package)
+
+ url = nil
+ private_key = nil
+ md5 = nil
+ length = 0
+ output = nil
+
+ if @mixdown_package.encrypt_type
+ output, private_key = encrypt_jkz tmp_dir
+ else
+ # create output file to correct output format
+ output = convert tmp_dir
+ end
+
+ # upload output to S3
+ s3_url = "#{@mixdown_package.store_dir}/#{@mixdown_package.filename}"
+ s3_manager.upload(s3_url, output)
+
+ length = File.size(output)
+ computed_md5 = Digest::MD5.new
+ File.open(output, 'rb').each { |line| computed_md5.update(line) }
+ md5 = computed_md5.to_s
+
+ @mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s)
+ end
+
+ # returns output destination, converting if necessary
+ def convert(tmp_dir)
+ # if the file already ends with the desired file type, call it a win
+ if @speed_mix_file.end_with?(@mixdown_package.file_type)
+ @speed_mix_file
+ else
+ # otherwise we need to convert from lastly created file to correct
+ output = File.join(tmp_dir, "output.#{@mixdown_package.file_type}")
+
+ cmd("#{APP_CONFIG.normalize_ogg_path} --bitrate 192 \"#{@speed_mix_file}\"", 'normalize')
+
+ 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
+
+
+ output
+ end
+ end
+
+ def encrypt_jkz(tmp_dir)
+ py_root = APP_CONFIG.jamtracks_dir
+ step = 0
+
+ private_key = nil
+ # we need to make the id of the custom mix be the name of the file (ID.ogg)
+ custom_mix_name = File.join(tmp_dir, "#{@mixdown.id}.ogg")
+ FileUtils.mv(@speed_mix_file, custom_mix_name)
+ jam_file_opts = ""
+ jam_file_opts << " -i #{Shellwords.escape("#{custom_mix_name}+mixdown")}"
+
+ sku = @mixdown_package.id
+ title = @mixdown.name
+ output = File.join(tmp_dir, "#{title.parameterize}.jkz")
+ py_file = File.join(py_root, "jkcreate.py")
+ version = @mixdown_package.version
+
+ right = @mixdown.jam_track.right_for_user(@mixdown.user)
+
+ if @mixdown_package.sample_rate == 48
+ private_key = right.private_key_48
+ else
+ private_key = right.private_key_44
+ end
+
+ unless private_key
+ @error_reason = 'no_private_key'
+ @error_detail = 'user needs to generate JamTrack for given sample rate'
+ raise @error_reason
+ end
+
+ private_key_file = File.join(tmp_dir, 'skey.pem')
+ File.open(private_key_file, 'w') { |f| f.write(private_key) }
+
+ log.debug("PRIVATE KEY")
+ log.debug(private_key)
+ log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output})"
+
+ cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}"
+
+ Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
+ pid = wait_thr.pid
+ exit_status = wait_thr.value
+ err = stderr.read(1000)
+ out = stdout.read(1000)
+ #puts "stdout: #{out}, stderr: #{err}"
+ raise ArgumentError, "Error calling python script: #{err}" if err.present?
+ raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file"))
+
+ private_key = File.read(private_key_file)
+ end
+ return output, private_key
+ end
+
+ def cmd(cmd, type)
+
+ log.debug("executing #{cmd}")
+
+ output = `#{cmd}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ output
+ else
+ @error_reason = type + "_fail"
+ @error_detail = "#{cmd}, #{output}"
+ raise "command `#{cmd}` failed."
+ end
+ end
+
+ # increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on
+ def bump_step(mixdown_package)
+ step = @step
+ last_step_at = Time.now
+ mixdown_package.current_packaging_step = step
+ mixdown_package.last_step_at = last_step_at
+ JamTrackMixdownPackage.where(:id => mixdown_package.id).update_all(last_step_at: last_step_at, current_packaging_step: step)
+ SubscriptionMessage.mixdown_signing_job_change(mixdown_package)
+
+ @step = step + 1
+ end
+
+ # set @error_reason before you raise an exception, and it will be sent back as the error reason
+ # otherwise, the error_reason will be unhandled-job-exception
+ def post_error(e)
+ begin
+ # if error_reason is null, assume this is an unhandled error
+ unless @error_reason
+ @error_reason = "unhandled-job-exception"
+ @error_detail = e.to_s
+ end
+ @mixdown_package.finish_errored(@error_reason, @error_detail)
+
+ rescue Exception => e
+ log.error "unable to post back to the database the error #{e}"
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
index 359bdc514..381bb8b83 100644
--- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
+++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
@@ -42,7 +42,7 @@ module JamRuby
signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44
signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44
last_step_at = Time.now
- JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true)
+ JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true, queued: false)
# because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
@jam_track_right.current_packaging_step = 0
@jam_track_right.packaging_steps = total_steps
@@ -50,6 +50,7 @@ module JamRuby
@jam_track_right[signing_state_symbol] = true
@jam_track_right.should_retry = false
@jam_track_right.last_step_at = Time.now
+ @jam_track_right.queued = false
SubscriptionMessage.jam_track_signing_job_change(@jam_track_right)
JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate)
diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
index 5039fc862..bf0942cd6 100644
--- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
+++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
@@ -24,6 +24,11 @@ module JamRuby
def perform
# this needs more testing
+
+ # let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough
+ JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
+ JamTrackMixdownPackage.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
+
return
#JamTrackRight.ready_to_clean.each do |jam_track_right|
# log.debug("deleting files for jam_track_right #{jam_track_right.id}")
diff --git a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb
index 6bff4192e..78a40aa6c 100644
--- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb
+++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb
@@ -31,6 +31,7 @@ module JamRuby
Stats.write('users', User.stats)
Stats.write('sessions', ActiveMusicSession.stats)
Stats.write('jam_track_rights', JamTrackRight.stats)
+ Stats.write('jam_track_mixdown_packages', JamTrackMixdownPackage.stats)
end
end
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index 49f14b592..f7763f6dd 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -19,6 +19,8 @@ FactoryGirl.define do
terms_of_service true
last_jam_audio_latency 5
reuse_card true
+ has_redeemable_jamtrack true
+ gifted_jamtracks 0
#u.association :musician_instrument, factory: :musician_instrument, user: u
@@ -733,6 +735,23 @@ FactoryGirl.define do
sequence(:phone) { |n| "phone-#{n}" }
end
+ factory :jam_track_mixdown, :class => JamRuby::JamTrackMixdown do
+ association :user, factory: :user
+ association :jam_track, factory: :jam_track
+ sequence(:name) { |n| "mixdown-#{n}"}
+ settings '{"speed":5}'
+ end
+
+ factory :jam_track_mixdown_package, :class => JamRuby::JamTrackMixdownPackage do
+ file_type JamRuby::JamTrackMixdownPackage::FILE_TYPE_OGG
+ sample_rate 48
+ signing false
+ signed false
+
+ association :jam_track_mixdown, factory: :jam_track_mixdown
+ end
+
+
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
@@ -783,6 +802,13 @@ FactoryGirl.define do
tap_in_count 3
end
+ factory :download_tracker, :class => JamRuby::DownloadTracker do
+ remote_ip '1.1.1.1'
+ paid false
+ association :user, factory: :user
+ association :jam_track, factory: :jam_track
+ end
+
factory :sale, :class => JamRuby::Sale do
order_total 0
association :user, factory:user
@@ -844,4 +870,18 @@ FactoryGirl.define do
legalese Faker::Lorem.paragraphs(6).join("\n\n")
end
+ factory :gift_card, class: 'JamRuby::GiftCard' do
+ sequence(:code) {n.to_s}
+ card_type JamRuby::GiftCardType::JAM_TRACKS_5
+ end
+
+ factory :gift_card_type, class: 'JamRuby::GiftCardType' do
+ card_type JamRuby::GiftCardType::JAM_TRACKS_5
+ end
+
+ factory :gift_card_purchase, class: 'JamRuby::GiftCardPurchase' do
+
+ association :user, factory: :user
+ end
+
end
diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb
index 822a1f527..3c23bdc73 100644
--- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb
+++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb
@@ -110,6 +110,15 @@ describe AffiliatePartner do
user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:20})
end
+ it "user with an affiliate relationship (with a custom rate) buying a jamtrack" do
+ user.affiliate_referral = partner
+ user.save!
+ partner.rate = 0.25
+ partner.save!
+ shopping_cart = ShoppingCart.create user, jam_track, 1, false
+ user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:50})
+ end
+
it "user with an affiliate relationship redeeming a jamtrack" do
user.affiliate_referral = partner
user.save!
diff --git a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb
index bf3d4da8c..003f8f173 100644
--- a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb
+++ b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb
@@ -212,6 +212,7 @@ describe 'Band Search Model' do
expect(search.results[0].id).to eq(band.id)
end
it "filters by genre" do
+ pending
band_id = band.id
filter[BandSearch::KEY_GENRES] = [band_id]
search.search_results_page(BandSearch::TO_JOIN, filter)
diff --git a/ruby/spec/jam_ruby/models/download_tracker_spec.rb b/ruby/spec/jam_ruby/models/download_tracker_spec.rb
new file mode 100644
index 000000000..f50c4c903
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/download_tracker_spec.rb
@@ -0,0 +1,162 @@
+require 'spec_helper'
+
+describe DownloadTracker do
+
+ let(:user1) {FactoryGirl.create(:user)}
+ let(:user2) {FactoryGirl.create(:user)}
+ let(:user3) {FactoryGirl.create(:user)}
+ let(:jam_track) {FactoryGirl.create(:jam_track)}
+
+ describe "check_user_sharer" do
+ describe "with max 1" do
+ it "and there is one row that is not paid for" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_user_sharer(1)
+ results.all.count.should eq 1
+ results[0]['user_id'].should eql (user1.id)
+ end
+ end
+
+ describe "with max 2" do
+
+ it "and there is one row that is not paid for" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 0
+
+ # and then add that same user at different IP, and see that something shows up
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 1
+ end
+
+ it "and there are two rows from different IP address, different jam tracks" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 1
+ results[0]['user_id'].should eql(user1.id)
+ results[0]['count'].should eql('2')
+
+ # now add a second user with one of the previous IP addresses; shouldn't matter yet
+ tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 1
+ results[0]['user_id'].should eql(user1.id)
+ results[0]['count'].should eql('2')
+ end
+
+ it "and there are two rows from same IP adresss, same jam track" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 0
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 0
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 1
+ results[0]['user_id'].should eql(user1.id)
+ results[0]['count'].should eql('3')
+ end
+
+ it "and there are two rows from same user one paid for, one not" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
+
+ results = DownloadTracker.check_user_sharer(2)
+ results.all.count.should eq 1
+ end
+ end
+ end
+
+ describe "check_freebie_snarfer" do
+
+ describe "with max 1" do
+
+ it "and there is one row that is not paid for" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(1)
+ results.all.count.should eq 1
+ end
+
+ it "and there is one row that is paid for" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(1)
+ results.all.count.should eq 0
+ end
+ end
+ describe "with max 2" do
+
+ it "and there is one row that is not paid for" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 0
+
+ # and then add a second user at same IP, and see that something shows up
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 1
+ end
+
+ it "and there are two rows from same user, different jam tracks" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 1
+ results[0]['remote_ip'].should eql('1.1.1.1')
+ results[0]['count'].should eql('2')
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '2.2.2.2')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 1
+ results[0]['remote_ip'].should eql('1.1.1.1')
+ results[0]['count'].should eql('2')
+ end
+
+ it "and there are two rows from same user, same jam track" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 0
+
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 0
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 1
+ results[0]['remote_ip'].should eql('1.1.1.1')
+ results[0]['count'].should eql('3')
+ end
+
+ it "and there are two rows from same user one paid for, one not" do
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
+ tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
+
+ results = DownloadTracker.check_freebie_snarfer(2)
+ results.all.count.should eq 0
+ end
+ end
+ end
+end
diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb
new file mode 100644
index 000000000..3a90ffcc3
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe JamTrackMixdownPackage do
+ include UsesTempFiles
+
+ it "can be created (factory girl)" do
+ package = FactoryGirl.create(:jam_track_mixdown_package)
+ end
+
+ it "can be created" do
+ mixdown= FactoryGirl.create(:jam_track_mixdown)
+
+ package = JamTrackMixdownPackage.create(mixdown, JamTrackMixdownPackage::FILE_TYPE_OGG, 48, 'jkz')
+
+ package.errors.any?.should == false
+ end
+
+
+ describe "signing_state" do
+ it "quiet" do
+ package = FactoryGirl.create(:jam_track_mixdown_package)
+ package.signing_state.should eq('QUIET')
+ end
+
+ it "signed" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, signed: true, signing_started_at: Time.now)
+ package.signing_state.should eq('SIGNED')
+ end
+
+ it "error" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, error_count: 1)
+ package.signing_state.should eq('ERROR')
+ end
+
+ it "signing" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing:true, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
+ package.signing_state.should eq('SIGNING')
+ end
+
+ it "signing timeout" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing: true, signing_started_at: Time.now - (APP_CONFIG.signing_job_signing_max_time + 1), packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
+ package.signing_state.should eq('SIGNING_TIMEOUT')
+ end
+
+ it "queued" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now)
+ package.signing_state.should eq('QUEUED')
+ end
+
+ it "signing timeout" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.mixdown_job_queue_max_time + 1))
+ package.signing_state.should eq('QUEUED_TIMEOUT')
+ end
+ end
+
+ describe "stats" do
+
+ it "empty" do
+ JamTrackMixdownPackage.stats['count'].should eq(0)
+ end
+
+ it "signing" do
+ package = FactoryGirl.create(:jam_track_mixdown_package)
+ JamTrackMixdownPackage.stats.should eq('count' => 1,
+ 'signing_count' => 0)
+
+ package.signing = true
+ package.save!
+
+ JamTrackMixdownPackage.stats.should eq('count' => 1,
+ 'signing_count' => 1)
+ end
+ end
+
+ describe "estimated_queue_time" do
+ it "succeeds with no data" do
+ JamTrackMixdownPackage.estimated_queue_time.should eq(0)
+ end
+
+ it "mixdown packages of different sorts" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, speed_pitched: true)
+ JamTrackMixdownPackage.estimated_queue_time.should eq(0)
+
+ package.queued = true
+ package.save!
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_slow_mixdown_time * 1)
+
+ package.speed_pitched = false
+ package.save!
+
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
+
+ right = FactoryGirl.create(:jam_track_right)
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
+
+ right.queued = true
+ right.save!
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1 + APP_CONFIG.estimated_jam_track_time * 1)
+ end
+
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb
new file mode 100644
index 000000000..3dd7edd8d
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe JamTrackMixdown do
+
+ let(:user) {FactoryGirl.create(:user)}
+ let(:jam_track) {FactoryGirl.create(:jam_track)}
+ let(:settings) { {speed:5} }
+
+ it "can be created (factory girl)" do
+ mixdown = FactoryGirl.create(:jam_track_mixdown)
+
+ mixdown = JamTrackMixdown.find(mixdown.id)
+ mixdown.settings.should eq('{"speed":5}')
+ end
+
+ it "can be created" do
+ mixdown = JamTrackMixdown.create('abc', 'description', user, jam_track, settings)
+ mixdown.errors.any?.should == false
+ end
+
+ it "index" do
+ query, start, count = JamTrackMixdown.index({id: jam_track}, user)
+
+ query.length.should eq(0)
+ start.should be_nil
+ count.should eq(0)
+
+ mixdown = FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track)
+
+ query, start, count = JamTrackMixdown.index({id: jam_track}, user)
+ query[0].should eq(mixdown)
+ start.should be_nil
+ count.should eq(1)
+ end
+
+ describe "settings" do
+ it "validates empty settings" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["have nothing specified"])
+ end
+
+ it "validates speed numeric" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => "5"}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer speed"])
+ end
+
+ it "validates pitch numeric" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => "5"}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer pitch"])
+ end
+
+ it "validates speed not-float" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => 5.5}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer speed"])
+ end
+
+ it "validates pitch not-float" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => 10.5}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer pitch"])
+ end
+ end
+
+
+end
+
diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
index 9119bdfd9..9e7bc225e 100644
--- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
@@ -29,6 +29,15 @@ describe JamTrackRight do
end
end
+ describe "private keys automatically created" do
+ it "created automatically" do
+ jam_track_right = FactoryGirl.create(:jam_track_right)
+ jam_track_right.private_key_44.should_not be_nil
+ jam_track_right.private_key_48.should_not be_nil
+ jam_track_right.private_key_44.should eq(jam_track_right.private_key_48)
+ end
+ end
+
describe "JKZ" do
before(:all) do
original_storage = JamTrackTrackUploader.storage = :fog
@@ -109,12 +118,14 @@ describe JamTrackRight do
end
it "valid track with rights to it by querying user" do
- jam_track_right = FactoryGirl.create(:jam_track_right, private_key_44: 'keyabc')
+ jam_track_right = FactoryGirl.create(:jam_track_right)
keys = JamTrackRight.list_keys(jam_track_right.user, [jam_track_right.jam_track.id])
keys.should have(1).items
keys[0].id.should == jam_track_right.jam_track.id
- keys[0]['private_key_44'].should eq('keyabc')
- keys[0]['private_key_48'].should be_nil
+ keys[0]['private_key_44'].should_not be_nil
+ keys[0]['private_key_48'].should_not be_nil
+ keys[0]['private_key_44'].should eq(jam_track_right.private_key_44)
+ keys[0]['private_key_48'].should eq(jam_track_right.private_key_48)
end
end
diff --git a/ruby/spec/jam_ruby/models/jam_track_search_spec.rb b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb
new file mode 100644
index 000000000..d4a3dc93d
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'JamTrack Search Model' do
+
+ let(:artist_filter) {
+ filter = JamTrackSearch.json_schema.clone
+ filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_ARTISTS]
+ filter
+ }
+ let(:song_filter) {
+ filter = JamTrackSearch.json_schema.clone
+ filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_SONGS]
+ filter
+ }
+ let(:freebird) {
+ FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'freebird')
+ }
+ let(:stairway) {
+ FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'stairway to heaven')
+ }
+
+ before :each do
+ JamTrack.delete_all
+ JamTrackTrack.delete_all
+ freebird
+ stairway
+ end
+
+ describe "Search filter" do
+
+ it "finds by artist" do
+ pending
+ filter = artist_filter.clone
+ filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist
+ filter = JamTrackSearch.new.search_results_page(filter['query'])
+ expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1)
+ end
+
+ it "paginates by artist" do
+ pending
+ JamTrackSearch::PER_PAGE.times do |nn|
+ FactoryGirl.create(:jam_track_with_tracks,
+ original_artist: freebird.original_artist + nn.to_s,
+ name: 'abc'+nn.to_s)
+ end
+ filter = artist_filter.clone
+ filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist
+ out_filter = JamTrackSearch.new.search_results_page(filter.clone['query'])
+ expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min)
+ num_page = (JamTrack.count / JamTrackSearch::PER_PAGE) + 1
+ expect(out_filter[JamTrackSearch::KEY_ARTISTS]['page_count']).to be(num_page)
+
+ filter[JamTrackSearch::KEY_ARTISTS]['page_num'] = 2
+ out_filter = JamTrackSearch.new.search_results_page(filter.clone['query'])
+ expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1)
+ end
+
+ it "finds by song" do
+ pending
+ filter = song_filter.clone
+ filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.name
+ filter = JamTrackSearch.new.search_results_page(filter.clone['query'])
+ expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(1)
+ end
+
+ it "paginates by song" do
+ pending
+ (JamTrackSearch::PER_PAGE + 2).times do |nn|
+ FactoryGirl.create(:jam_track_with_tracks,
+ original_artist: freebird.original_artist,
+ name: 'abc'+nn.to_s)
+ end
+ filter = song_filter.clone
+ filter[JamTrackSearch::KEY_SEARCH_STR] = 'abc'
+ out_filter = JamTrackSearch.new.search_results_page(filter.clone['query'])
+ expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min)
+
+ total_count = JamTrack.where("name LIKE 'abc%'").count
+ num_page = (total_count / JamTrackSearch::PER_PAGE) + (0==(total_count % JamTrackSearch::PER_PAGE) ? 0 : 1)
+ expect(out_filter[JamTrackSearch::KEY_SONGS]['page_count']).to be(num_page)
+
+ filter[JamTrackSearch::KEY_SONGS]['page_num'] = 2
+ out_filter = JamTrackSearch.new.search_results_page(filter.clone['query'])
+ expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(2)
+ end
+
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb
index 814a4cbb3..4ed8e7b28 100644
--- a/ruby/spec/jam_ruby/models/jam_track_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb
@@ -234,6 +234,17 @@ describe JamTrack do
query.size.should == 2
end
+
+ it "deals with aggregration (regression)" do
+
+ query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user)
+ count.should == 0
+
+ jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'Take a Chance On Me', original_artist: 'K.C. And The Sunshine Band')
+
+ query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user)
+ count.should == 1
+ end
end
describe "validations" do
diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb
index bfd46c1c2..af9715b38 100644
--- a/ruby/spec/jam_ruby/models/musician_search_spec.rb
+++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb
@@ -238,6 +238,7 @@ describe 'Musician Search Model' do
end
it "sorts by latency", intermittent: true do
+ pending
search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[0])
results = search.do_search
expect(results[0].id).to eq(@user1.id) # HAS FAILED HERE TOO
diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb
index ba89aee20..bce945d20 100644
--- a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb
+++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb
@@ -135,7 +135,7 @@ describe RecurlyTransactionWebHook do
RecurlyTransactionWebHook.create_from_xml(document)
- JamTrackRight.find_by_id(jam_track_right.id).should be_nil
+ JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil
end
it "deletes jam_track_right when voided" do
@@ -154,7 +154,7 @@ describe RecurlyTransactionWebHook do
RecurlyTransactionWebHook.create_from_xml(document)
- JamTrackRight.find_by_id(jam_track_right.id).should be_nil
+ JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil
end
end
diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb
index 334166734..4d0340259 100644
--- a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb
+++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb
@@ -6,6 +6,7 @@ describe SaleLineItem do
let(:user) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
+ let(:gift_card) {FactoryGirl.create(:gift_card_type, card_type: GiftCardType::JAM_TRACKS_10)}
describe "associations" do
@@ -23,7 +24,7 @@ describe SaleLineItem do
describe "state" do
- it "success" do
+ it "jam track success" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
@@ -37,5 +38,20 @@ describe SaleLineItem do
success: true
})
end
+
+ it "gift card success" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, gift_card)
+ sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
+ transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
+
+ sale_line_item.reload
+ sale_line_item.state.should eq({
+ void: false,
+ refund: false,
+ fail: false,
+ success: true
+ })
+ end
end
end
diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb
index 685d8dfbd..fc89145cb 100644
--- a/ruby/spec/jam_ruby/models/sale_spec.rb
+++ b/ruby/spec/jam_ruby/models/sale_spec.rb
@@ -5,6 +5,24 @@ describe Sale do
let(:user) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
+ let(:jam_track2) {FactoryGirl.create(:jam_track)}
+ let(:jam_track3) {FactoryGirl.create(:jam_track)}
+ let(:gift_card) {GiftCardType.jam_track_5}
+
+ def assert_free_line_item(sale_line_item, jamtrack)
+ sale_line_item.recurly_tax_in_cents.should be_nil
+ sale_line_item.recurly_total_in_cents.should be_nil
+ sale_line_item.recurly_currency.should be_nil
+ sale_line_item.recurly_discount_in_cents.should be_nil
+ sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
+ sale_line_item.unit_price.should eq(jamtrack.price)
+ sale_line_item.quantity.should eq(1)
+ sale_line_item.free.should eq(1)
+ sale_line_item.sales_tax.should be_nil
+ sale_line_item.shipping_handling.should eq(0)
+ sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
+ sale_line_item.product_id.should eq(jamtrack.id)
+ end
describe "index" do
it "empty" do
@@ -47,7 +65,11 @@ describe Sale do
let(:user) {FactoryGirl.create(:user)}
let(:jamtrack) { FactoryGirl.create(:jam_track) }
+ let(:jamtrack2) { FactoryGirl.create(:jam_track) }
+ let(:jamtrack3) { FactoryGirl.create(:jam_track) }
+ let(:jamtrack4) { FactoryGirl.create(:jam_track) }
let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i }
+ let(:gift_card_price_in_cents) { (gift_card.price * 100).to_i }
let(:client) { RecurlyClient.new }
let(:billing_info) {
info = {}
@@ -75,6 +97,77 @@ describe Sale do
end
end
+ it "for a gift card" do
+ shopping_cart = ShoppingCart.create user, gift_card, 1, false
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart])
+
+ user.reload
+ user.sales.length.should eq(1)
+
+ sales.should eq(user.sales)
+ sale = sales[0]
+ sale.recurly_invoice_id.should_not be_nil
+
+ sale.recurly_subtotal_in_cents.should eq(gift_card_price_in_cents)
+ sale.recurly_tax_in_cents.should eq(0)
+ sale.recurly_total_in_cents.should eq(gift_card_price_in_cents)
+ sale.recurly_currency.should eq('USD')
+
+ sale.order_total.should eq(gift_card.price)
+ sale.sale_line_items.length.should == 1
+ sale_line_item = sale.sale_line_items[0]
+ # validate we are storing pricing info from recurly
+ sale_line_item.recurly_tax_in_cents.should eq(0)
+ sale_line_item.recurly_total_in_cents.should eq(gift_card_price_in_cents)
+ sale_line_item.recurly_currency.should eq('USD')
+ sale_line_item.recurly_discount_in_cents.should eq(0)
+ sale_line_item.product_type.should eq(GiftCardType::PRODUCT_TYPE)
+ sale_line_item.unit_price.should eq(gift_card.price)
+ sale_line_item.quantity.should eq(1)
+ sale_line_item.free.should eq(0)
+ sale_line_item.sales_tax.should be_nil
+ sale_line_item.shipping_handling.should eq(0)
+ sale_line_item.recurly_plan_code.should eq(gift_card.plan_code)
+ sale_line_item.product_id.should eq(gift_card.id)
+ sale_line_item.recurly_subscription_uuid.should be_nil
+ sale_line_item.recurly_adjustment_uuid.should_not be_nil
+ sale_line_item.recurly_adjustment_credit_uuid.should be_nil
+ sale_line_item.recurly_adjustment_uuid.should_not be_nil
+
+ # verify subscription is in Recurly
+ recurly_account = client.get_account(user)
+ adjustments = recurly_account.adjustments
+ adjustments.should_not be_nil
+ adjustments.should have(1).items
+ purchase= adjustments[0]
+ purchase.unit_amount_in_cents.should eq((gift_card.price * 100).to_i)
+ purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL)
+ purchase.description.should eq("JamTracks Gift Card (5)")
+ purchase.state.should eq('invoiced')
+ purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
+
+ invoices = recurly_account.invoices
+ invoices.should have(1).items
+ invoice = invoices[0]
+ invoice.uuid.should eq(sale.recurly_invoice_id)
+ invoice.line_items.should have(1).items # should have single adjustment associated
+ invoice.line_items[0].should eq(purchase)
+ invoice.subtotal_in_cents.should eq((gift_card.price * 100).to_i)
+ invoice.total_in_cents.should eq((gift_card.price * 100).to_i)
+ invoice.state.should eq('collected')
+
+ # verify jam_track_rights data
+ user.gift_card_purchases.should_not be_nil
+ user.gift_card_purchases.should have(1).items
+ user.gift_card_purchases.last.gift_card_type.should eq(GiftCardType.jam_track_5)
+ user.has_redeemable_jamtrack.should be_true
+
+ sale_line_item.affiliate_referral.should be_nil
+ sale_line_item.affiliate_referral_fee_in_cents.should be_nil
+ end
+
it "for a free jam track" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
@@ -87,6 +180,7 @@ describe Sale do
sales.should eq(user.sales)
sale = sales[0]
+
sale.recurly_invoice_id.should be_nil
sale.recurly_subtotal_in_cents.should eq(0)
@@ -132,6 +226,69 @@ describe Sale do
user.has_redeemable_jamtrack.should be_false
end
+ it "for two jam tracks (1 freebie, 1 gifted), then 1 gifted/1 pay" do
+ user.gifted_jamtracks = 2
+ user.save!
+
+ shopping_cart1 = ShoppingCart.create user, jamtrack, 1, true
+ shopping_cart2 = ShoppingCart.create user, jamtrack2, 1, true
+
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart1, shopping_cart2])
+
+ user.reload
+ user.sales.length.should eq(1)
+ sale = sales[0]
+ sale.reload
+
+ sale.recurly_invoice_id.should be_nil
+
+ sale.recurly_subtotal_in_cents.should eq(0)
+ sale.recurly_tax_in_cents.should eq(0)
+ sale.recurly_total_in_cents.should eq(0)
+ sale.recurly_currency.should eq('USD')
+ sale.order_total.should eq(0)
+ sale.sale_line_items.length.should == 2
+
+ assert_free_line_item(sale.sale_line_items[0], jamtrack)
+ assert_free_line_item(sale.sale_line_items[1], jamtrack2)
+
+ # verify jam_track_rights data
+ right1 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack.id).first
+ right2 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack2.id).first
+ user.jam_track_rights.should have(2).items
+
+ right1.redeemed.should be_true
+ right2.redeemed.should be_true
+ user.has_redeemable_jamtrack.should be_false
+ user.gifted_jamtracks.should eq(1)
+
+
+
+ # OK! Now make a second purchase; this time, buy one free, one not free
+ shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true
+
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart3])
+
+ user.reload
+ user.sales.length.should eq(2)
+ sale = sales[0]
+ sale.reload
+
+ sale.recurly_invoice_id.should be_nil
+ sale.recurly_subtotal_in_cents.should eq(0)
+ sale.recurly_tax_in_cents.should eq(0)
+ sale.recurly_total_in_cents.should eq(0)
+ sale.recurly_currency.should eq('USD')
+ sale.order_total.should eq(0)
+ sale.sale_line_items.length.should == 1
+
+ assert_free_line_item(sale.sale_line_items[0], jamtrack3)
+ end
+
it "for a free jam track with an affiliate association" do
partner = FactoryGirl.create(:affiliate_partner)
user.affiliate_referral = partner
diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
index 6be02e8d4..8d3b724b1 100644
--- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
+++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
@@ -3,8 +3,15 @@ require 'spec_helper'
describe ShoppingCart do
let(:user) { FactoryGirl.create(:user) }
- let(:jam_track) {FactoryGirl.create(:jam_track) }
- let(:jam_track2) {FactoryGirl.create(:jam_track) }
+ let(:jam_track) { FactoryGirl.create(:jam_track) }
+ let(:jam_track2) { FactoryGirl.create(:jam_track) }
+ let(:jam_track3) { FactoryGirl.create(:jam_track) }
+ let(:jam_track4) { FactoryGirl.create(:jam_track) }
+ let(:jam_track5) { FactoryGirl.create(:jam_track) }
+ let(:jam_track6) { FactoryGirl.create(:jam_track) }
+ let(:jam_track7) { FactoryGirl.create(:jam_track) }
+ let(:gift_card) {FactoryGirl.create(:gift_card_type)}
+ let(:gift_card2) {FactoryGirl.create(:gift_card_type)}
before(:each) do
ShoppingCart.delete_all
@@ -13,6 +20,9 @@ describe ShoppingCart do
it "can reference a shopping cart" do
shopping_cart = ShoppingCart.create user, jam_track, 1
+ shopping_cart.errors.any?.should be_false
+ shopping_cart.valid?.should be_true
+ user.reload
ShoppingCart.count.should == 1
user.shopping_carts.count.should == 1
user.shopping_carts[0].product_info[:name].should == jam_track.name
@@ -21,18 +31,21 @@ describe ShoppingCart do
user.shopping_carts[0].quantity.should == 1
end
-
- it "maintains only one fre JamTrack in ShoppingCart" do
- cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ it "maintains only one free JamTrack in ShoppingCart" do
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true)
cart1.should_not be_nil
cart1.errors.any?.should be_false
user.reload
- cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true)
cart2.errors.any?.should be_false
user.reload
user.shopping_carts.length.should eq(1)
- cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
- cart3.errors.any?.should be_false
+ cart3 = ShoppingCart.add_item_to_cart(user, gift_card)
+ cart3.errors.any?.should be_true
+ user.reload
+ user.shopping_carts.length.should eq(1)
+ cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true)
+ cart4.errors.any?.should be_false
user.reload
user.shopping_carts.length.should eq(1)
end
@@ -48,24 +61,159 @@ describe ShoppingCart do
cart2.errors.any?.should be_true
end
+ it "a second giftcard just adds quantity" do
+
+ end
+
describe "redeemable behavior" do
it "removes redeemable item to shopping cart (maintains only one in cart)" do
user.has_redeemable_jamtrack.should be_true
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true)
+ cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+ cart1.marked_for_redeem.should eq(1)
+ user.reload
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true)
+ cart2.should_not be_nil
+ cart2.errors.any?.should be_false
+ cart2.marked_for_redeem.should eq(1)
+
+ ShoppingCart.find_by_id(cart1.id).should be nil
+
+
+ ShoppingCart.remove_jam_track_from_cart(user, cart2)
+
+ user.reload
+ user.shopping_carts.length.should eq(0)
+ ShoppingCart.find_by_id(cart2.id).should be nil
+ end
+ end
+
+ describe "multiple free jamtracks" do
+
+ before(:each) do
+ user.gifted_jamtracks = 5
+ user.save!
+ end
+
+ it "user can add and remove jamtracks without issue, until 'mixed' free/non-free is hit" do
cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+
user.reload
cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
- cart2.should_not be_nil
-
+ cart2.errors.any?.should be_false
+ user.reload
+ user.shopping_carts.length.should eq(2)
cart1.marked_for_redeem.should eq(1)
cart2.marked_for_redeem.should eq(1)
- ShoppingCart.remove_jam_track_from_cart(user, jam_track)
- user.shopping_carts.length.should eq(0)
- cart2.reload
+ cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track3)
+ cart3.errors.any?.should be_false
+ user.reload
+ user.shopping_carts.length.should eq(3)
+ cart1.marked_for_redeem.should eq(1)
cart2.marked_for_redeem.should eq(1)
+ cart3.marked_for_redeem.should eq(1)
+
+ cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track4)
+ cart4.errors.any?.should be_false
+ user.reload
+ user.shopping_carts.length.should eq(4)
+ cart1.marked_for_redeem.should eq(1)
+ cart2.marked_for_redeem.should eq(1)
+ cart3.marked_for_redeem.should eq(1)
+ cart4.marked_for_redeem.should eq(1)
+
+ cart5 = ShoppingCart.add_jam_track_to_cart(user, jam_track5)
+ cart5.errors.any?.should be_false
+ user.reload
+ user.shopping_carts.length.should eq(5)
+ cart1.marked_for_redeem.should eq(1)
+ cart2.marked_for_redeem.should eq(1)
+ cart3.marked_for_redeem.should eq(1)
+ cart4.marked_for_redeem.should eq(1)
+ cart5.marked_for_redeem.should eq(1)
+
+ cart6 = ShoppingCart.add_jam_track_to_cart(user, jam_track6)
+ cart6.errors.any?.should be_false
+ user.reload
+ user.shopping_carts.length.should eq(6)
+ cart1.marked_for_redeem.should eq(1)
+ cart2.marked_for_redeem.should eq(1)
+ cart3.marked_for_redeem.should eq(1)
+ cart4.marked_for_redeem.should eq(1)
+ cart5.marked_for_redeem.should eq(1)
+ cart6.marked_for_redeem.should eq(1)
+
+ cart7 = ShoppingCart.add_jam_track_to_cart(user, jam_track7)
+ cart7.errors.any?.should be_true
+ user.reload
+ user.shopping_carts.length.should eq(6)
+ cart1.marked_for_redeem.should eq(1)
+ cart2.marked_for_redeem.should eq(1)
+ cart3.marked_for_redeem.should eq(1)
+ cart4.marked_for_redeem.should eq(1)
+ cart5.marked_for_redeem.should eq(1)
+ cart6.marked_for_redeem.should eq(1)
+ end
+ end
+
+ describe "gift cards" do
+ it "can not add multiple of same type" do
+ cart1 = ShoppingCart.add_item_to_cart(user, gift_card)
+ cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+
+ user.reload
+ user.has_redeemable_jamtrack = true
+ user.shopping_carts.length.should eq(1)
+ user.shopping_carts[0].quantity.should eql(1)
+
+ cart2 = ShoppingCart.add_item_to_cart(user, gift_card)
+ cart2.should_not be_nil
+ # it's the same type, so it's blocked
+ cart2.errors.any?.should be_true
+ cart2.errors[:cart_id].should eq(["has already been taken"])
+ end
+ end
+
+ describe "mixed" do
+ it "non-free then free" do
+ # you shouldn't be able to add a free after a non-free
+ user.has_redeemable_jamtrack = false
+ user.save!
+
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+
+ user.has_redeemable_jamtrack = true
+ user.save!
+ user.reload
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
+ cart2.errors.any?.should be_true
+ cart2.errors[:base].should eq(["You can not add a free JamTrack to a cart with non-free items. Please clear out your cart."])
+
+ user.shopping_carts.length.should eq(1)
+ end
+
+ it "free then non-free" do
+
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+
+ user.reload
+
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
+ cart2.errors.any?.should be_true
+ cart2.errors[:base].should eq(["You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart."])
+
+ user.shopping_carts.length.should eq(1)
end
end
end
diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb
index 0fb434442..89abe6e16 100644
--- a/ruby/spec/jam_ruby/models/user_spec.rb
+++ b/ruby/spec/jam_ruby/models/user_spec.rb
@@ -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
diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb
index 93ec5dda7..7f189e159 100644
--- a/ruby/spec/spec_helper.rb
+++ b/ruby/spec/spec_helper.rb
@@ -95,13 +95,13 @@ end
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
- DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] })
+ DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] })
end
config.around(:each) do |example|
# set no_transaction: true as metadata on your test to use deletion strategy instead
if example.metadata[:no_transaction]
- DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }
+ DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }
else
DatabaseCleaner.strategy = :transaction
end
diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb
index 2717f63dc..425d40a2f 100644
--- a/ruby/spec/support/utilities.rb
+++ b/ruby/spec/support/utilities.rb
@@ -7,6 +7,10 @@ def app_config
'http://localhost:3333'
end
+ def email_social_alias
+ 'social@jamkazam.com'
+ end
+
def email_alerts_alias
'alerts@jamkazam.com'
end
@@ -179,7 +183,7 @@ def app_config
end
def signing_job_queue_max_time
- 20 # 20 seconds
+ 600 # 20 seconds
end
def one_free_jamtrack_per_user
@@ -210,6 +214,42 @@ def app_config
"AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo"
end
+ def estimated_jam_track_time
+ 40
+ end
+
+ def estimated_fast_mixdown_time
+ 30
+ end
+
+ def estimated_slow_mixdown_time
+ 80
+ end
+
+ def num_packaging_nodes
+ 2
+ end
+
+ def signing_job_signing_max_time
+ 300
+ end
+
+ def mixdown_job_queue_max_time
+ 600
+ end
+
+ def mixdown_step_max_time
+ 300
+ end
+
+ def download_tracker_day_range
+ 30
+ end
+
+ def guard_against_browser_fraud
+ true
+ end
+
private
def audiomixer_workspace_path
diff --git a/web/Gemfile b/web/Gemfile
index 96604366d..4cfc8e21f 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -41,12 +41,12 @@ gem 'eventmachine', '1.0.4'
gem 'faraday', '~>0.9.0'
gem 'amqp', '0.9.8'
gem 'logging-rails', :require => 'logging/rails'
-gem 'omniauth', '1.1.1'
-gem 'omniauth-facebook', '1.4.1'
+gem 'omniauth'
+gem 'omniauth-facebook'
gem 'omniauth-twitter'
-gem 'omniauth-google-oauth2', '0.2.1'
-gem 'google-api-client', '0.7.1'
-gem 'google-api-omniauth', '0.1.1'
+gem 'omniauth-google-oauth2'
+gem 'google-api-client' #, '0.7.1'
+#gem 'google-api-omniauth' #, '0.1.1'
gem 'signet', '0.5.0'
gem 'twitter'
gem 'fb_graph', '2.5.9'
@@ -63,6 +63,7 @@ gem 'haml-rails'
gem 'unf' #optional fog dependency
gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError)
gem 'postgres-copy'
+gem 'prawn-table'
#group :libv8 do
# gem 'libv8', "~> 3.11.8"
#end
@@ -97,7 +98,7 @@ gem 'react-rails', '~> 1.0'
source 'https://rails-assets.org' do
gem 'rails-assets-reflux'
gem 'rails-assets-classnames'
- gem 'rails-assets-react-select'
+ gem 'rails-assets-react-select', '0.6.7'
end
#group :development, :production do
diff --git a/web/README.md b/web/README.md
index 2b5096424..5fa0629f8 100644
--- a/web/README.md
+++ b/web/README.md
@@ -4,4 +4,3 @@ Jasmine Javascript Unit Tests
Open browser to localhost:3000/teaspoon
-
diff --git a/web/app/assets/images/content/icon-delete.png b/web/app/assets/images/content/icon-delete.png
new file mode 100644
index 000000000..cae694fba
Binary files /dev/null and b/web/app/assets/images/content/icon-delete.png differ
diff --git a/web/app/assets/images/content/icon-delete@2X.png b/web/app/assets/images/content/icon-delete@2X.png
new file mode 100644
index 000000000..b120befa5
Binary files /dev/null and b/web/app/assets/images/content/icon-delete@2X.png differ
diff --git a/web/app/assets/images/content/icon-edit.png b/web/app/assets/images/content/icon-edit.png
new file mode 100644
index 000000000..26ed5892f
Binary files /dev/null and b/web/app/assets/images/content/icon-edit.png differ
diff --git a/web/app/assets/images/content/icon-edit@2X.png b/web/app/assets/images/content/icon-edit@2X.png
new file mode 100644
index 000000000..7cd9052d5
Binary files /dev/null and b/web/app/assets/images/content/icon-edit@2X.png differ
diff --git a/web/app/assets/images/content/icon-mix-fail@2X.png b/web/app/assets/images/content/icon-mix-fail@2X.png
new file mode 100644
index 000000000..265022204
Binary files /dev/null and b/web/app/assets/images/content/icon-mix-fail@2X.png differ
diff --git a/web/app/assets/images/content/icon-play.png b/web/app/assets/images/content/icon-play.png
new file mode 100644
index 000000000..ecb26e78c
Binary files /dev/null and b/web/app/assets/images/content/icon-play.png differ
diff --git a/web/app/assets/images/content/icon-retry@2X.png b/web/app/assets/images/content/icon-retry@2X.png
new file mode 100644
index 000000000..584a0dca1
Binary files /dev/null and b/web/app/assets/images/content/icon-retry@2X.png differ
diff --git a/web/app/assets/images/content/icon-save@2X.png b/web/app/assets/images/content/icon-save@2X.png
new file mode 100644
index 000000000..ac80ef9c4
Binary files /dev/null and b/web/app/assets/images/content/icon-save@2X.png differ
diff --git a/web/app/assets/images/content/icon_download.png b/web/app/assets/images/content/icon_download.png
new file mode 100644
index 000000000..d56bde46c
Binary files /dev/null and b/web/app/assets/images/content/icon_download.png differ
diff --git a/web/app/assets/images/content/icon_download@2X.png b/web/app/assets/images/content/icon_download@2X.png
new file mode 100644
index 000000000..5473ff7df
Binary files /dev/null and b/web/app/assets/images/content/icon_download@2X.png differ
diff --git a/web/app/assets/images/content/icon_download@3X.png b/web/app/assets/images/content/icon_download@3X.png
new file mode 100644
index 000000000..13c2a1fd1
Binary files /dev/null and b/web/app/assets/images/content/icon_download@3X.png differ
diff --git a/web/app/assets/images/content/icon_open@2X.png b/web/app/assets/images/content/icon_open@2X.png
new file mode 100644
index 000000000..1d188a2ea
Binary files /dev/null and b/web/app/assets/images/content/icon_open@2X.png differ
diff --git a/web/app/assets/images/landing/Andy Crowley - Avatar.png b/web/app/assets/images/landing/Andy Crowley - Avatar.png
new file mode 100644
index 000000000..4a675f607
Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Avatar.png differ
diff --git a/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png
new file mode 100644
index 000000000..9dc63b79b
Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png differ
diff --git a/web/app/assets/images/landing/Andy Crowley - YouTube.png b/web/app/assets/images/landing/Andy Crowley - YouTube.png
new file mode 100644
index 000000000..0dad41bf8
Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - YouTube.png differ
diff --git a/web/app/assets/images/landing/Carl Brown - Avatar.png b/web/app/assets/images/landing/Carl Brown - Avatar.png
new file mode 100644
index 000000000..a991220a3
Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Avatar.png differ
diff --git a/web/app/assets/images/landing/Carl Brown - Speech Bubble.png b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png
new file mode 100644
index 000000000..071f8c40f
Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png differ
diff --git a/web/app/assets/images/landing/Carl Brown - YouTube.png b/web/app/assets/images/landing/Carl Brown - YouTube.png
new file mode 100644
index 000000000..289b170d3
Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - YouTube.png differ
diff --git a/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png
new file mode 100644
index 000000000..61d12e9aa
Binary files /dev/null and b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png differ
diff --git a/web/app/assets/images/landing/Julie Bonk - Avatar.png b/web/app/assets/images/landing/Julie Bonk - Avatar.png
new file mode 100644
index 000000000..0b5019588
Binary files /dev/null and b/web/app/assets/images/landing/Julie Bonk - Avatar.png differ
diff --git a/web/app/assets/images/landing/Ryan Jones - Avatar.png b/web/app/assets/images/landing/Ryan Jones - Avatar.png
new file mode 100644
index 000000000..09b908884
Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Avatar.png differ
diff --git a/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png
new file mode 100644
index 000000000..5c40b4727
Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png differ
diff --git a/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png
new file mode 100644
index 000000000..9cf7c90d9
Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 1.png b/web/app/assets/images/landing/Top 10 Image - Number 1.png
new file mode 100644
index 000000000..82e1a3847
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 1.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 10.png b/web/app/assets/images/landing/Top 10 Image - Number 10.png
new file mode 100644
index 000000000..23f1fd862
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 10.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 2.png b/web/app/assets/images/landing/Top 10 Image - Number 2.png
new file mode 100644
index 000000000..9d79989d7
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 2.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 3.png b/web/app/assets/images/landing/Top 10 Image - Number 3.png
new file mode 100644
index 000000000..7084feaf4
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 3.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 4.png b/web/app/assets/images/landing/Top 10 Image - Number 4.png
new file mode 100644
index 000000000..48f40e9be
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 4.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 5.png b/web/app/assets/images/landing/Top 10 Image - Number 5.png
new file mode 100644
index 000000000..d50812947
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 5.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 6.png b/web/app/assets/images/landing/Top 10 Image - Number 6.png
new file mode 100644
index 000000000..b76dd8555
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 6.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 7.png b/web/app/assets/images/landing/Top 10 Image - Number 7.png
new file mode 100644
index 000000000..0de272f34
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 7.png differ
diff --git a/web/app/assets/images/landing/Top 10 Image - Number 9.png b/web/app/assets/images/landing/Top 10 Image - Number 9.png
new file mode 100644
index 000000000..8fdcb8d46
Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 9.png differ
diff --git a/web/app/assets/images/landing/arrow-jamblaster-order.png b/web/app/assets/images/landing/arrow-jamblaster-order.png
new file mode 100644
index 000000000..d42844afb
Binary files /dev/null and b/web/app/assets/images/landing/arrow-jamblaster-order.png differ
diff --git a/web/app/assets/images/landing/broadcast_video.png b/web/app/assets/images/landing/broadcast_video.png
new file mode 100644
index 000000000..b3a281b60
Binary files /dev/null and b/web/app/assets/images/landing/broadcast_video.png differ
diff --git a/web/app/assets/images/landing/find_musicians.png b/web/app/assets/images/landing/find_musicians.png
new file mode 100644
index 000000000..b2160ba9d
Binary files /dev/null and b/web/app/assets/images/landing/find_musicians.png differ
diff --git a/web/app/assets/images/landing/gift_card.png b/web/app/assets/images/landing/gift_card.png
new file mode 100644
index 000000000..e652e81db
Binary files /dev/null and b/web/app/assets/images/landing/gift_card.png differ
diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_1.png b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png
new file mode 100644
index 000000000..c19bc6faf
Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png differ
diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_2.png b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png
new file mode 100644
index 000000000..0cb8cb55a
Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png differ
diff --git a/web/app/assets/images/landing/online_lessons.png b/web/app/assets/images/landing/online_lessons.png
new file mode 100644
index 000000000..7e4da04ec
Binary files /dev/null and b/web/app/assets/images/landing/online_lessons.png differ
diff --git a/web/app/assets/images/landing/phone_control_jamblaster.png b/web/app/assets/images/landing/phone_control_jamblaster.png
new file mode 100644
index 000000000..4b28201e1
Binary files /dev/null and b/web/app/assets/images/landing/phone_control_jamblaster.png differ
diff --git a/web/app/assets/images/landing/preorder_jamblaster.png b/web/app/assets/images/landing/preorder_jamblaster.png
new file mode 100644
index 000000000..932f2089e
Binary files /dev/null and b/web/app/assets/images/landing/preorder_jamblaster.png differ
diff --git a/web/app/assets/images/landing/recording_with_video.png b/web/app/assets/images/landing/recording_with_video.png
new file mode 100644
index 000000000..b07fbe180
Binary files /dev/null and b/web/app/assets/images/landing/recording_with_video.png differ
diff --git a/web/app/assets/images/landing/us_latency_diagram.png b/web/app/assets/images/landing/us_latency_diagram.png
new file mode 100644
index 000000000..6dddd6256
Binary files /dev/null and b/web/app/assets/images/landing/us_latency_diagram.png differ
diff --git a/web/app/assets/images/shared/mobile-preview-load.gif b/web/app/assets/images/shared/mobile-preview-load.gif
new file mode 100755
index 000000000..d0dee5844
Binary files /dev/null and b/web/app/assets/images/shared/mobile-preview-load.gif differ
diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png
index 8650fe322..932f2089e 100644
Binary files a/web/app/assets/images/web/button_cta_jamblaster.png and b/web/app/assets/images/web/button_cta_jamblaster.png differ
diff --git a/web/app/assets/images/web/button_cta_jamtrack.png b/web/app/assets/images/web/button_cta_jamtrack.png
index 998d89b75..d8dd6e40c 100644
Binary files a/web/app/assets/images/web/button_cta_jamtrack.png and b/web/app/assets/images/web/button_cta_jamtrack.png differ
diff --git a/web/app/assets/images/web/button_cta_platform.png b/web/app/assets/images/web/button_cta_platform.png
index dba8d499b..a2ddf74ef 100644
Binary files a/web/app/assets/images/web/button_cta_platform.png and b/web/app/assets/images/web/button_cta_platform.png differ
diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js
index ee570a513..b893ef7c7 100644
--- a/web/app/assets/javascripts/JamServer.js
+++ b/web/app/assets/javascripts/JamServer.js
@@ -23,6 +23,7 @@
var mode = null;
// heartbeat
+ var startHeartbeatTimeout = null;
var heartbeatInterval = null;
var heartbeatMS = null;
var connection_expire_time = null;
@@ -79,6 +80,7 @@
function initiateReconnect(activeElementVotes, in_error) {
var initialConnect = !!activeElementVotes;
+ console.log("activeElementVotes", activeElementVotes)
freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true));
if (in_error) {
@@ -104,6 +106,12 @@
heartbeatInterval = null;
}
+ // stop the heartbeat start delay from happening
+ if (startHeartbeatTimeout != null) {
+ clearTimeout(startHeartbeatTimeout);
+ startHeartbeatTimeout = null;
+ }
+
// stop checking for heartbeat acks
if (heartbeatAckCheckInterval != null) {
clearTimeout(heartbeatAckCheckInterval);
@@ -218,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);
@@ -236,9 +244,41 @@
heartbeatMS = payload.heartbeat_interval * 1000;
connection_expire_time = payload.connection_expire_time * 1000;
logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's');
- heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
- heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
- lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
+
+ // add some randomness to help move heartbeats apart from each other
+
+ // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time
+ var randomStartTime = connection_expire_time * (Math.random() / 2)
+
+ if (startHeartbeatTimeout) {
+ logger.warn("start heartbeat timeout is active; should be null")
+ clearTimeout(startHeartbeatTimeout)
+ }
+
+ if (heartbeatInterval != null) {
+ logger.warn("heartbeatInterval is active; should be null")
+ clearInterval(heartbeatInterval);
+ heartbeatInterval = null;
+ }
+
+ if (heartbeatAckCheckInterval != null) {
+ logger.warn("heartbeatAckCheckInterval is active; should be null")
+ clearInterval(heartbeatAckCheckInterval);
+ heartbeatAckCheckInterval = null;
+ }
+
+ startHeartbeatTimeout = setTimeout(function() {
+ if(server.connected) {
+ heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
+ heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
+ lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
+ }
+ }, randomStartTime)
+
+ logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's')
+
+
+
connectDeferred.resolve();
$self.triggerHandler(EVENTS.CONNECTION_UP)
@@ -281,7 +321,7 @@
function socketClosed(in_error) {
// tell the backend that we have logged out
- context.jamClient.OnLoggedOut();
+ if (context.jamClient) context.jamClient.OnLoggedOut();
}
@@ -295,6 +335,11 @@
logger.debug(payload.error_code + ": no longer reconnecting")
server.noReconnect = true; // stop trying to log in!!
}
+ else if (payload.error_code == 'no_reconnect') {
+ logger.debug(payload.error_code + ": no longer reconnecting")
+ server.noReconnect = true; // stop trying to log in!!
+ context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.")
+ }
}
///////////////////
@@ -384,7 +429,7 @@
}
function formatDelaySecs(secs) {
- return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + '');
+ return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + '');
}
function setCountdown($parent) {
@@ -548,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();
diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
index 08785bc50..5793d7941 100644
--- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee
+++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
@@ -19,7 +19,7 @@ context.JK.AccountJamTracks = class AccountJamTracks
@screen = $('#account-jamtracks')
beforeShow:() =>
- rest.getPurchasedJamTracks({})
+ rest.getPurchasedJamTracks({limit: 40})
.done(@populateJamTracks)
.fail(@app.ajaxError);
diff --git a/web/app/assets/javascripts/accounts_profile_avatar.js b/web/app/assets/javascripts/accounts_profile_avatar.js
index 63d1aca1f..2ab8858cd 100644
--- a/web/app/assets/javascripts/accounts_profile_avatar.js
+++ b/web/app/assets/javascripts/accounts_profile_avatar.js
@@ -40,8 +40,8 @@
var template= context.JK.fillTemplate($('#template-account-profile-avatar').html(), {
"fp_apikey" : gon.fp_apikey,
"data-fp-store-path" : createStorePath(userDetail) + createOriginalFilename(userDetail),
- "fp_policy" : filepicker_policy.policy,
- "fp_signature" : filepicker_policy.signature
+ "fp_policy" : encodeURIComponent(filepicker_policy.policy),
+ "fp_signature" : encodeURIComponent(filepicker_policy.signature)
});
$('#account-profile-avatar-content-scroller').html(template);
@@ -202,7 +202,6 @@
renderNoAvatar(avatarSpace);
}
else {
-
rest.getFilepickerPolicy({handle: fpfile.url})
.done(function(filepickerPolicy) {
avatarSpace.children().remove();
diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js
index 811277c13..2153fb9c1 100644
--- a/web/app/assets/javascripts/application.js
+++ b/web/app/assets/javascripts/application.js
@@ -61,6 +61,7 @@
//= require web/tracking
//= require webcam_viewer
//= require react-components
+//= require playbackControls
//= require_directory .
//= require_directory ./dialog
//= require_directory ./wizard
diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js
index bf5b1993b..705f9ec29 100644
--- a/web/app/assets/javascripts/backend_alerts.js
+++ b/web/app/assets/javascripts/backend_alerts.js
@@ -137,6 +137,10 @@
else if(type === ALERT_NAMES.VIDEO_WINDOW_CLOSED) {
context.VideoActions.videoWindowClosed()
}
+ else if (type === ALERT_NAMES.VST_CHANGED) {
+ console.log("VST CHANGED!")
+ context.ConfigureTracksActions.vstChanged()
+ }
else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) &&
(ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) {
// squelch these events if not in session
diff --git a/web/app/assets/javascripts/checkout_complete.js b/web/app/assets/javascripts/checkout_complete.js
index 4f1e96110..aa52214b9 100644
--- a/web/app/assets/javascripts/checkout_complete.js
+++ b/web/app/assets/javascripts/checkout_complete.js
@@ -15,6 +15,7 @@
var $templatePurchasedJamTrack = null;
var $thanksPanel = null;
var $jamTrackInBrowser = null;
+ var $giftCardPurchased = null;
var $purchasedJamTrack = null;
var $purchasedJamTrackHeader = null;
var $purchasedJamTracks = null;
@@ -75,9 +76,17 @@
else {
$thanksPanel.removeClass('hidden')
handleJamTracksPurchased(purchaseResponse.jam_tracks)
+ handleGiftCardsPurchased(purchaseResponse.gift_cards)
}
}
+
+ function handleGiftCardsPurchased(gift_cards) {
+ // were any GiftCards purchased?
+ if(gift_cards && gift_cards.length > 0) {
+ $giftCardPurchased.removeClass('hidden')
+ }
+ }
function handleJamTracksPurchased(jamTracks) {
// were any JamTracks purchased?
var jamTracksPurchased = jamTracks && jamTracks.length > 0;
@@ -194,6 +203,7 @@
$templatePurchasedJamTrack = $('#template-purchased-jam-track');
$thanksPanel = $screen.find(".thanks-panel");
$jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser");
+ $giftCardPurchased = $screen.find('.thanks-detail.gift-card')
$purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track");
$purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header");
$purchasedJamTracks = $purchasedJamTrack.find(".purchased-list")
diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js
index 168b965d6..a69c0a39c 100644
--- a/web/app/assets/javascripts/checkout_order.js
+++ b/web/app/assets/javascripts/checkout_order.js
@@ -135,15 +135,7 @@
}
}
- function displayTax(effectiveQuantity, item_tax, total_with_tax) {
- var totalTax = 0;
- var totalPrice = 0;
-
- var unitTax = item_tax * effectiveQuantity;
- totalTax += unitTax;
-
- var totalUnitPrice = total_with_tax * effectiveQuantity;
- totalPrice += totalUnitPrice;
+ function displayTax(totalTax, totalPrice) {
$screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
$screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
@@ -181,8 +173,16 @@
taxRate = 0.0825;
}
- var unitTax = 1.99 * taxRate;
- displayTax(effectiveQuantity, unitTax, 1.99 + unitTax)
+ var estimatedTax = 0;
+ var estimatedTotal = 0;
+
+ context._.each(carts, function(cart) {
+ var cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
+ estimatedTax += cart.product_info.price * cart_quantity * taxRate;
+ estimatedTotal += cart.product_info.price * cart_quantity;
+ })
+
+ displayTax(Math.round(estimatedTax*100)/100, Math.round((estimatedTotal + estimatedTax)*100)/100)
}
else {
checkoutUtils.configureRecurly()
diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js
index 9b72f4fc5..956c11886 100644
--- a/web/app/assets/javascripts/checkout_payment.js
+++ b/web/app/assets/javascripts/checkout_payment.js
@@ -95,7 +95,7 @@
$reuseExistingCardChk.iCheck(userDetail.reuse_card && userDetail.has_recurly_account ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card)
// show appropriate prompt text based on whether user has a free jamtrack
- if(user.free_jamtrack) {
+ if(user.has_redeemable_jamtrack) {
$freeJamTrackPrompt.removeClass('hidden')
}
else {
diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee
index c16e09f9b..c789ba71d 100644
--- a/web/app/assets/javascripts/checkout_utils.js.coffee
+++ b/web/app/assets/javascripts/checkout_utils.js.coffee
@@ -55,6 +55,16 @@ class CheckoutUtils
return carts[0].product_info.free
+ hasOnlyFreeItemsInShoppingCart: (carts) =>
+ if carts.length == 0
+ return false
+
+ for cart in carts
+ if !cart.product_info.free
+ return false
+
+ return true
+
configureRecurly: () =>
unless @configuredRecurly
context.recurly.configure(gon.global.recurly_public_api_key)
diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js
index ca5bdd428..dd3142e76 100644
--- a/web/app/assets/javascripts/clientUpdate.js
+++ b/web/app/assets/javascripts/clientUpdate.js
@@ -61,7 +61,10 @@
})
}
- app.layout.showDialog('client-update')
+ if(!app.layout.isDialogShowing('client-update')) {
+ app.layout.showDialog('client-update')
+ }
+
//$('#client_update').show()
//$('#client_update_overlay').show()
}
@@ -192,6 +195,11 @@
function runCheck(product, version, uri, size, currentVersion) {
+ if (app.clientUpdating) {
+ logger.debug("client is already updating; skipping")
+ return
+ }
+
if(currentVersion === undefined) {
currentVersion = context.jamClient.ClientUpdateVersion();
@@ -302,7 +310,7 @@
$(document).on(EVENTS.SESSION_ENDED, function(e, data){
if(app.clientUpdating) {
- updateClientUpdateDialog("update-start", { uri: updateUri })
+ updateClientUpdateDialog("update-start", { uri: updateUri})
}
});
diff --git a/web/app/assets/javascripts/configureTracksHelper2.js b/web/app/assets/javascripts/configureTracksHelper2.js
new file mode 100644
index 000000000..d87319a53
--- /dev/null
+++ b/web/app/assets/javascripts/configureTracksHelper2.js
@@ -0,0 +1,294 @@
+(function (context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.ConfigureTracksHelper2 = function (app) {
+ var logger = context.JK.logger;
+ var ASSIGNMENT = context.JK.ASSIGNMENT;
+ var VOICE_CHAT = context.JK.VOICE_CHAT;
+ var MAX_TRACKS = context.JK.MAX_TRACKS;
+ var MAX_OUTPUTS = context.JK.MAX_OUTPUTS;
+ var gearUtils = context.JK.GearUtils;
+
+ var $parent = null;
+ var $templateAssignablePort = null;
+ var $templateTrackTarget = null;
+ var $templateOutputTarget = null;
+ var $unassignedInputsHolder = null;
+ var $unassignedOutputsHolder = null;
+ var $tracksHolder = null;
+ var $outputChannelHolder = null;
+ var $instrumentsHolder = null;
+ var isDragging = false;
+
+
+
+ // inputChannelFilter is an optional argument that is used by the Gear Wizard.
+ // basically, if an input channel isn't in there, it's not going to be displayed
+ function loadChannels(forceInputsToUnassign, inputChannelFilter) {
+ var musicPorts = jamClient.FTUEGetChannels();
+
+ $unassignedInputsHolder.empty();
+ $unassignedOutputsHolder.empty();
+
+ var inputChannels = musicPorts.inputs;
+ var outputChannels = musicPorts.outputs;
+
+ // uncomment to add a bunch of bogus channels
+ //inputChannels = inputChannels.concat(inputChannels.concat(inputChannels.concat(inputChannels)))
+
+ context._.each(inputChannels, function (inputChannel) {
+
+ if(inputChannelFilter && !(inputChannelFilter.indexOf(inputChannel.id) > -1)) {
+ // skipping input channel because it's not in the filter
+ return;
+ }
+
+ var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, inputChannel, {direction:'in'}), { variable: 'data' }));
+
+ if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
+ unassignInputChannel($channel);
+ }
+ else if(inputChannel.assignment == ASSIGNMENT.CHAT) {
+ // well, we can't show it as unused... if there were a place to show chat inputs, we would put it there.
+ // but we don't have it, so just skip
+ logger.debug("skipping chat channel ", inputChannel)
+ return;
+ }
+ else {
+ // find the track this belongs in
+
+ var trackNumber = inputChannel.assignment - 1;
+
+ // plop down a track holder on the UI
+
+ addChannelToTrack($channel, trackNumber);
+ }
+ })
+
+ var outputAssignment = 0;
+ context._.each(outputChannels, function (outputChannel, index) {
+ var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, outputChannel, {direction:'out'}), { variable: 'data' }));
+
+
+
+ if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
+ unassignOutputChannel($channel);
+ }
+ else {
+ var $output = $outputChannelHolder.find('.output[data-num="' + outputAssignment + '"]');
+ outputAssignment++;
+
+ if($output.length == 0) {
+ context.JK.alertSupportedNeeded('Unable to find an output holder for channel: ' + outputChannel.id + ' with assignment ' + outputChannel.assignment);
+ return false;
+ }
+ addChannelToOutput($channel, $output.find('.output-target'));
+ }
+ });
+ }
+
+ // iterates through the dom and returns a pure data structure for track associations and output channels
+ function getCurrentState() {
+
+ var state = {};
+ state.tracks = [];
+ state.unassignedChannels = [];
+ state.outputs = [];
+ var $unassignedInputChannels = $unassignedInputsHolder.find('.ftue-input');
+ var $unassignedOutputChannels = $unassignedOutputsHolder.find('.ftue-input');
+ var $tracks = $tracksHolder.find('.track-target');
+ var $outputs = $outputChannelHolder.find('.output-target');
+
+ context._.each($unassignedInputChannels, function($unassignedInput) {
+ $unassignedInput = $($unassignedInput);
+ var channelId = $unassignedInput.attr('data-input-id');
+ state.unassignedChannels.push(channelId);
+ })
+
+ context._.each($unassignedOutputChannels, function($unassignedOutput) {
+ $unassignedOutput = $($unassignedOutput);
+ var channelId = $unassignedOutput.attr('data-input-id');
+ state.unassignedChannels.push(channelId);
+ })
+
+ context._.each($tracks, function($track, index) {
+ $track = $($track);
+ var $assignedChannels = $track.find('.ftue-input');
+
+ var track = {index: index, channels:[]};
+ context._.each($assignedChannels, function($assignedChannel) {
+ $assignedChannel = $($assignedChannel);
+ track.channels.push($assignedChannel.attr('data-input-id'))
+ });
+
+ // sparse array
+ if(track.channels.length > 0) {
+ state.tracks.push(track);
+ }
+ var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select');
+ track.instrument_id = $instrument.data('instrument_id');
+ })
+
+ context._.each($outputs, function($output, index) {
+ $output = $($output);
+ var $assignedChannel = $output.find('.ftue-input');
+
+ // this is overkill since there should only be 1 or 0 .ftue-inputs in a given .output
+ var outputSlot = {index: index, channels:[]};
+ context._.each($assignedChannel, function($assignedChannel) {
+ $assignedChannel = $($assignedChannel);
+ outputSlot.channels.push($assignedChannel.attr('data-input-id'))
+ });
+
+ // sparse array
+ if(outputSlot.channels.length > 0) {
+ state.outputs.push(outputSlot);
+ }
+ })
+ return state;
+ }
+
+ function validate(tracks) {
+ // there must be at least one assigned channel
+ if(tracks.tracks.length == 0) {
+ logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track.");
+ context.JK.Banner.showAlert('Must have assigned at least one input port to a track.');
+ return false;
+ }
+
+ // there must be some instruments
+ context._.each(tracks.tracks, function(track) {
+ if(!track.instrument_id) {
+ logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument.");
+ context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.');
+ return false;
+ }
+ });
+
+ // there must be exactly 2 output channels assigned
+ if(tracks.outputs.length != 2 || (tracks.outputs[0].channels.length != 1 && track.outputs[1].channels.length != 1)) {
+ logger.debug("ConfigureTracks validation error: must have assigned exactly two output ports");
+ context.JK.Banner.showAlert('Must have assigned exactly 2 output ports.');
+ return false;
+ }
+
+ return true;
+ }
+
+ function save(state) {
+
+ context._.each(state.unassignedChannels, function(unassignedChannelId) {
+ context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED);
+ });
+
+ // save input/tracks
+ context._.each(state.tracks, function(track, index) {
+
+ var trackNumber = index + 1;
+
+ context._.each(track.channels, function(channelId) {
+ context.jamClient.TrackSetAssignment(channelId, true, trackNumber);
+
+ });
+ logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id);
+ context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id);
+ });
+
+ // save outputs
+ context._.each(state.outputs, function(output, index) {
+ context._.each(output.channels, function(channelId) {
+ context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT);
+ });
+ });
+
+ var result = context.jamClient.TrackSaveAssignments();
+
+ if(!result || result.length == 0) {
+ // success
+ return true;
+ }
+ else {
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+ return false;
+ }
+ }
+
+ function loadTrackInstruments(forceInputsToUnassign) {
+ var $trackInstruments = $instrumentsHolder.find('.track-instrument');
+
+ context._.each($trackInstruments, function(trackInstrument) {
+ var $trackInstrument = $(trackInstrument);
+
+ var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1;
+
+ var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex);
+
+ var instrument = context.JK.client_to_server_instrument_map[clientInstrument];
+
+ $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument);
+ });
+ }
+
+ function trySave() {
+ var state = getCurrentState();
+
+ if(!validate(state)) {
+ return false;
+ }
+
+ var saved = save(state);
+
+ if(saved) {
+ context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS());
+ }
+
+ return saved;
+ }
+
+ function reset(forceInputsToUnassign, inputChannelFilter) {
+ loadChannels(forceInputsToUnassign, inputChannelFilter);
+ loadTrackInstruments(forceInputsToUnassign);
+ }
+
+ function unassignOutputChannel($channel) {
+ var $originallyAssignedTrack = $channel.closest('.output-target');
+ $unassignedOutputsHolder.append($channel);
+ $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
+ }
+
+ function unassignInputChannel($channel) {
+ var $originallyAssignedTrack = $channel.closest('.track-target');
+ $unassignedInputsHolder.append($channel);
+ $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
+
+ }
+
+ function addChannelToTrack($channel, $track) {
+ var $originallyAssignedTrack = $channel.closest('.track-target');
+ $track.append($channel);
+ $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length);
+ $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
+ }
+
+ function addChannelToOutput($channel, $slot) {
+ var $originallyAssignedTrack = $channel.closest('.output-target');
+ $slot.append($channel);
+ $slot.attr('output-count', $slot.find('.ftue-input:not(.ui-draggable-dragging)').length);
+ $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
+ }
+
+ function initialize(_$parent) {
+ $parent = _$parent;
+
+ }
+
+ this.initialize = initialize;
+ this.trySave = trySave;
+ this.reset = reset;
+
+ return this;
+ };
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js
index 20801cfa6..ccdee9b1f 100644
--- a/web/app/assets/javascripts/dialog/configureTrackDialog.js
+++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js
@@ -25,6 +25,7 @@
var voiceChatHelper = null;
var profiles = null;
var currentProfile = null;
+ var enableVstTimeout = null;
var configure_audio_instructions = {
@@ -50,7 +51,7 @@
function setInstructions(type) {
if (type === 'audio') {
- $instructions.html('Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.')
+ $instructions.html("Click the 'ADD LIVE TRACK' button to add more tracks. You may set up a live track for each instrumental and/or vocal part to perform in sessions. You must also set up exactly two Session Audio Output ports to deliver the stereo audio in your sessions.")
return;
var os = context.jamClient.GetOSAsString();
$instructions.html(configure_audio_instructions[os]);
@@ -91,7 +92,7 @@
}
function validateAudioSettings() {
- return configureTracksHelper.trySave();
+ return true;
}
function showVoiceChatPanel() {
@@ -103,7 +104,7 @@
$musicAudioTabSelector.click(function () {
// validate voice chat settings
if (validateVoiceChatSettings()) {
- configureTracksHelper.reset();
+ window.ConfigureTracksActions.reset(false);
voiceChatHelper.reset();
showMusicAudioPanel();
}
@@ -113,7 +114,7 @@
// validate audio settings
if (validateAudioSettings()) {
logger.debug("initializing voice chat helper")
- configureTracksHelper.reset();
+ window.ConfigureTracksActions.reset(false);
voiceChatHelper.reset();
showVoiceChatPanel();
}
@@ -133,7 +134,7 @@
//});
$btnUpdateTrackSettings.click(function() {
- if(configureTracksHelper.trySave() && voiceChatHelper.trySave()) {
+ if(voiceChatHelper.trySave()) {
app.layout.closeDialog('configure-tracks');
}
@@ -152,7 +153,7 @@
});
$certifiedAudioProfile.html(optionsHtml);
- context.JK.dropdown($certifiedAudioProfile);
+ //context.JK.dropdown($certifiedAudioProfile);
}
function deviceChanged() {
@@ -183,7 +184,7 @@
currentProfile = profile;
- configureTracksHelper.reset();
+ window.ConfigureTracksActions.reset(false);
}
function beforeShow() {
@@ -207,13 +208,45 @@
return;
}
- configureTracksHelper.reset();
+ window.ConfigureTracksActions.reset(false);
+ if(!window.SessionStore.inSession()) {
+ delayEnableVst()
+ }
+ else {
+ logger.debug("in a session, so no delayEnableVst()");
+ }
voiceChatHelper.reset();
voiceChatHelper.beforeShow();
}
+ function delayEnableVst() {
+ if (enableVstTimeout) {
+ clearTimeout(enableVstTimeout)
+ }
+ var isVstLoaded = context.jamClient.IsVstLoaded()
+ var hasVstAssignment = context.jamClient.hasVstAssignment()
+
+ if (hasVstAssignment && !isVstLoaded) {
+ enableVstTimeout = setTimeout(function() { enableVst() }, 1000)
+ }
+ }
+
+ function enableVst () {
+ enableVstTimeout = null
+
+ if (app.layout.isDialogShowing('configure-tracks')) {
+ ConfigureTracksActions.enableVst()
+ }
+ else {
+ logger.debug("no longer in configure tracks dialog; not enabling VSTs at this time")
+ }
+ }
+
function afterShow() {
sessionUtils.SessionPageEnter();
+
+ //context.ConfigureTracksActions.vstScan();
+
}
function onCancel() {
@@ -247,8 +280,8 @@
$btnAddNewGear = $dialog.find('.btn-add-new-audio-gear');
$btnUpdateTrackSettings = $dialog.find('.btn-update-settings');
- configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
- configureTracksHelper.initialize($dialog);
+ //configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
+ //configureTracksHelper.initialize($dialog);
voiceChatHelper = new context.JK.VoiceChatHelper(app);
voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191);
diff --git a/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js
new file mode 100644
index 000000000..8b6c06811
--- /dev/null
+++ b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js
@@ -0,0 +1,69 @@
+(function (context, $) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.DeleteVideoConfirmDialog = function (app) {
+ var logger = context.JK.logger;
+ var rest = context.JK.Rest();
+ var recordingId = null;
+ var $dialog = null;
+ var $deleteFromDiskChkBox = null;
+ var $deleteBtn = null;
+ var deleting = false;
+ function resetForm() {
+
+ }
+
+ function beforeShow(args) {
+
+ recordingId = args.d1;
+
+ if(!recordingId) throw "recordingId must be specified";
+
+ $dialog.data('result', null);
+ deleting = false;
+
+ }
+
+ function afterHide() {
+
+ }
+
+ function attemptDelete() {
+ if(deleting) return;
+
+ deleting = true;
+
+ rest.deleteRecordingVideoData(recordingId)
+ .done(function(){
+ $dialog.data('result', true);
+ app.layout.closeDialog('delete-video-confirm-dialog');
+ })
+ .fail(app.ajaxError)
+ }
+
+ function events() {
+ $deleteBtn.click(attemptDelete);
+ }
+
+
+ function initialize() {
+ var dialogBindings = {
+ 'beforeShow': beforeShow,
+ 'afterHide': afterHide
+ };
+
+ app.bindDialog('delete-video-confirm-dialog', dialogBindings);
+
+ $dialog = $('#deleteVideoConfirmDialog');
+ $deleteFromDiskChkBox = $dialog.find('.delete-from-disk');
+ $deleteBtn = $dialog.find('.delete-btn');
+
+ events();
+
+ context.JK.checkbox($deleteFromDiskChkBox);
+ };
+
+ this.initialize = initialize;
+ }
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/dialog/editRecordingDialog.js b/web/app/assets/javascripts/dialog/editRecordingDialog.js
index 9aac9d33a..409318c0a 100644
--- a/web/app/assets/javascripts/dialog/editRecordingDialog.js
+++ b/web/app/assets/javascripts/dialog/editRecordingDialog.js
@@ -14,7 +14,7 @@
var $isPublic = null;
var $saveBtn = null;
var $deleteBtn = null;
-
+ var $videoField = null;
var updating = false;
var deleting = false;
diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js
index 867a411f2..aa35e80e7 100644
--- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js
+++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js
@@ -50,7 +50,7 @@
$browserJamTrackBtn.click(function() {
app.layout.closeDialog('getting-started')
- window.location = '/client#/jamtrack/search'
+ window.location = '/client#/jamtrack'
return false;
})
@@ -69,9 +69,9 @@
function beforeShow() {
app.user().done(function(user) {
- var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack'
+ var jamtrackRule = user.has_redeemable_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack'
$jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule)
- if(user.free_jamtrack) {
+ if(user.has_redeemable_jamtrack) {
$jamTracksLimitedTime.removeClass('hidden')
}
})
diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js
index 8f58b0728..ed400d8c1 100644
--- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js
+++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js
@@ -103,9 +103,8 @@
sampleRate = context.jamClient.GetSampleRate()
sampleRateForFilename = sampleRate == 48 ? '48' : '44';
doSearch();
-
-
}
+
function afterHide() {
showing = false;
}
diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
index cceaaedc2..97d746144 100644
--- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
+++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
@@ -8,11 +8,29 @@
var playbackControls = null;
var recording = null; // deferred object
var $dialog = null;
+ var $saveVideoCheckbox = null
+ var $uploadToYoutube = null
function resetForm() {
// remove all display errors
$('#recording-finished-dialog form .error-text').remove()
$('#recording-finished-dialog form .error').removeClass("error")
+ if(recording.video) {
+ 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()
}
@@ -103,6 +121,20 @@
}
function afterHide() {
+ if(recording && recording.video) {
+ var name = $('#recording-finished-dialog form input[name=name]').val();
+ name = name.replace(/[^A-Za-z0-9\-\ ]/g, '');
+
+
+ var saveToDisk = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
+ var keepResult = $dialog.data('result');
+ keepResult = keepResult && keepResult.keep
+
+ logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keepResult=" + keepResult + ", saveToDisk=" + saveToDisk);
+
+ context.jamClient.VideoDecision(recording.id, name, keepResult && saveToDisk)
+ }
+
recording = null;
playbackControls.stopMonitor();
context.jamClient.ClosePreviewRecording();
@@ -141,6 +173,7 @@
window._oauth_callback = function() {
window._oauth_win.close()
+ logger.debug("closing window")
setGoogleAuthState()
}
return false;
@@ -152,6 +185,8 @@
var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked')
+ upload_to_youtube = false // don't prevent user from getting through dialog because popup now handles auth
+
if (upload_to_youtube) {
$.ajax({
type: "GET",
@@ -182,6 +217,9 @@
var save_video = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
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,
@@ -195,6 +233,11 @@
$dialog.data('result', {keep:true});
app.layout.closeDialog('recordingFinished');
context.JK.GA.trackMakeRecording();
+ 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);
+ }
+
})
.fail(function (jqXHR) {
if (jqXHR.status == 422) {
@@ -281,9 +324,9 @@
// Check for google authorization using AJAX and show/hide the
// google login button / "signed in" label as appropriate:
- $(window).on('focus', function() {
+ /**$(window).on('focus', function() {
setGoogleAuthState();
- });
+ }); */
}
function setGoogleAuthState() {
@@ -318,10 +361,24 @@
}
function initializeButtons() {
+ $saveVideoCheckbox = $('#recording-finished-dialog input[name="save_video"]')
+ $uploadToYoutube = $('#recording-finished-dialog input[name="upload_to_youtube"]')
var isPublic = $('#recording-finished-dialog input[name="is_public"]');
context.JK.checkbox(isPublic);
- context.JK.checkbox($('#recording-finished-dialog input[name="save_video"]'));
- context.JK.checkbox($('#recording-finished-dialog input[name="upload_to_youtube"]'));
+ context.JK.checkbox($saveVideoCheckbox);
+ context.JK.checkbox($uploadToYoutube);
+
+ $saveVideoCheckbox.on('ifChanged', function() {
+ var saveVideoToDisk = $saveVideoCheckbox.is(':checked')
+
+ if(saveVideoToDisk) {
+ $uploadToYoutube.iCheck('enable')
+ }
+ else {
+ $uploadToYoutube.iCheck('disable')
+ }
+
+ })
}
function initialize() {
diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js
index 960d44cfd..92e577ecf 100644
--- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js
+++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js
@@ -66,6 +66,20 @@
$('#session-settings-fan-access').val('listen-chat-band');
}
+ var $controllerSelect = $('#session-settings-master-mix-controller')
+
+ $controllerSelect.empty()
+ var sessionUsers = context.SessionStore.helper.users()
+
+ $controllerSelect.append('')
+ $.each(sessionUsers, function(userId, user) {
+ var selected = currentSession.session_controller_id == userId ? 'selected="selected"' : ''
+ $controllerSelect.append('')
+ })
+
+ var canEditController = currentSession.session_controller_id == context.JK.currentUserId || context.JK.currentUserId == currentSession.user_id
+ $controllerSelect.easyDropDown(canEditController ? 'enable' : 'disable')
+
/**
// notation files in the account screen. ugh.
$selectedFilenames.empty();
@@ -84,10 +98,12 @@
context.JK.dropdown($('#session-settings-language'));
context.JK.dropdown($('#session-settings-musician-access'));
context.JK.dropdown($('#session-settings-fan-access'));
+ context.JK.dropdown($('#session-settings-master-mix-controller'));
var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable'
$('#session-settings-musician-access').easyDropDown(easyDropDownState)
$('#session-settings-fan-access').easyDropDown(easyDropDownState)
+
}
function addNotation(notation) {
@@ -121,6 +137,7 @@
data.name = $('#session-settings-name').val();
data.description = $('#session-settings-description').val();
data.language = $('#session-settings-language').val();
+ data.session_controller = $('#session-settings-master-mix-controller').val()
// musician access
var musicianAccess = $('#session-settings-musician-access').val();
@@ -148,7 +165,13 @@
data.fan_chat = true;
}
- rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved);
+ rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved)
+ .done(function(response) {
+ context.SessionActions.updateSession.trigger(response);
+ })
+ .fail(function() {
+ app.notify({title: "Can't Update", text: "Unable to update session settings."})
+ })
return false;
}
diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee
index 8890e6b87..eb9846546 100644
--- a/web/app/assets/javascripts/download_jamtrack.js.coffee
+++ b/web/app/assets/javascripts/download_jamtrack.js.coffee
@@ -190,7 +190,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack
showDownloading: () =>
@logger.debug("showing #{@state.name}")
# while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually
- context.jamClient.JamTrackDownload(@jamTrack.id, context.JK.currentUserId,
+ context.jamClient.JamTrackDownload(@jamTrack.id, null, context.JK.currentUserId,
this.makeDownloadProgressCallback(),
this.makeDownloadSuccessCallback(),
this.makeDownloadFailureCallback())
@@ -369,8 +369,8 @@ context.JK.DownloadJamTrack = class DownloadJamTrack
@trackDetail = context.jamClient.JamTrackGetTrackDetail ("#{@jamTrack.id}-#{@sampleRateForFilename}")
if @trackDetail.version?
- @logger.error("after invalidating package, the version is still wrong!")
- throw "after invalidating package, the version is still wrong!"
+ @logger.error("after invalidating package, the version is still wrong!", @trackDetail)
+ throw "after invalidating package, the version is still wrong! #{@trackDetail.version}"
switch @trackDetail.key_state
when 'pending'
diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js
index 43a844a32..42a0f410d 100644
--- a/web/app/assets/javascripts/everywhere/everywhere.js
+++ b/web/app/assets/javascripts/everywhere/everywhere.js
@@ -185,6 +185,7 @@
context.stats.write = context.stats.writePoint;
}
+
function initializeStun(app) {
stun = new context.JK.Stun(app);
context.JK.StunInstance = stun;
diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js
index 6d573a2c8..7cbab2f28 100644
--- a/web/app/assets/javascripts/faderHelpers.js
+++ b/web/app/assets/javascripts/faderHelpers.js
@@ -23,7 +23,14 @@
var $fader = $(this);
var floaterConvert = $fader.data('floaterConverter')
var sessionModel = window.JK.CurrentSessionModel || null;
-
+
+ /**
+ if(!$fader.data('has-session-control')) {
+ var sessionControllerName = $fader.data('session-controller-name');
+ window.JK.prodBubble($fader, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $fader.closest('.top-parent'), duration:12000})
+ return false;
+ }*/
+
var mediaControlsDisabled = $fader.data('media-controls-disabled');
if(mediaControlsDisabled) {
var mediaTrackOpener = $fader.data('media-track-opener');
@@ -173,7 +180,14 @@
var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled');
var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener');
var sessionModel = window.JK.CurrentSessionModel || null;
-
+
+ /**
+ if(!$draggingFaderHandle.data('has-session-control')) {
+ var sessionControllerName = $draggingFaderHandle.data('session-controller-name');
+ window.JK.prodBubble($draggingFaderHandle, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $draggingFaderHandle.closest('.top-parent'), duration:12000})
+ return false;
+ }*/
+
if(mediaControlsDisabled) {
return false;
}
@@ -267,16 +281,33 @@
throw ("renderFader: userOptions is required");
}
var renderDefaults = {
- faderType: "vertical"
+ faderType: "vertical",
+ sessionController: null
};
var options = $.extend({}, renderDefaults, userOptions);
+ var sessionCanControl = true
+ var sessionControllerName = null
+ if(userOptions.sessionController) {
+ if(!userOptions.sessionController.can_control) {
+ sessionCanControl = false
+ sessionControllerName = userOptions.sessionController.session_controller.name
+ }
+ }
+
selector.find('div[data-control="fader"]')
.data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
.data('floaterConverter', floaterConverter)
.data('snap', userOptions.snap)
+ .data('has-session-control', sessionCanControl)
+ .data('session-controller-name', sessionControllerName)
+
+
+ if(userOptions.sessionController) {
+
+ }
selector.find('div[data-control="fader-handle"]').draggable({
drag: onFaderDrag,
@@ -289,6 +320,8 @@
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
.data('floaterConverter', floaterConverter)
.data('snap', userOptions.snap)
+ .data('has-session-control', sessionCanControl)
+ .data('session-controller-name', sessionControllerName)
// Embed any custom styles, applied to the .fader below selector
if ("style" in options) {
diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js
index 7ff48dd44..7acd44450 100644
--- a/web/app/assets/javascripts/fakeJamClient.js
+++ b/web/app/assets/javascripts/fakeJamClient.js
@@ -121,6 +121,9 @@
return 30;
}
+ function GetSampleRate() {
+ return 48;
+ }
function FTUESetVideoShareEnable(){
}
@@ -502,6 +505,9 @@
return 0;
}
+ function GetJamTrackSettings() {
+ return {tracks:[]}
+ }
function SessionGetJamTracksPlayDurationMs() {
return 60000;
}
@@ -1046,6 +1052,17 @@
function GetAutoStart() { return true; }
function SaveSettings() {}
+
+ function VSTLoad() {}
+ function VSTScan(callback) {setTimeout(eval(callback+ "()"), 1000)}
+ function hasVstHost() { return false;}
+ function getPluginList() { return {vsts:[]} }
+
+ function clearPluginList() {}
+ function listTrackAssignments() {
+ return {}
+ }
+
// Javascript Bridge seems to camel-case
// Set the instance functions:
this.AbortRecording = AbortRecording;
@@ -1211,6 +1228,7 @@
this.TrackGetChatUsesMusic = TrackGetChatUsesMusic;
this.TrackSetChatUsesMusic = TrackSetChatUsesMusic;
+ this.GetJamTrackSettings = GetJamTrackSettings;
this.JamTrackStopPlay = JamTrackStopPlay;
this.JamTrackPlay = JamTrackPlay;
this.JamTrackIsPlayable = JamTrackIsPlayable;
@@ -1275,6 +1293,7 @@
this.FTUESetSendFrameRates = FTUESetSendFrameRates;
this.GetCurrentVideoResolution = GetCurrentVideoResolution;
this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate;
+ this.GetSampleRate = GetSampleRate;
this.FTUESetVideoShareEnable = FTUESetVideoShareEnable;
this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable;
this.isSessVideoShared = isSessVideoShared;
@@ -1307,6 +1326,12 @@
this.StopNetworkTest = StopNetworkTest;
this.log = log;
this.getOperatingMode = getOperatingMode;
+ this.VSTLoad = VSTLoad;
+ this.VSTScan = VSTScan;
+ this.hasVstHost = hasVstHost;
+ this.getPluginList = getPluginList;
+ this.clearPluginList = clearPluginList;
+ this.listTrackAssignments = listTrackAssignments;
this.clientID = "devtester";
};
diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js
index cdf32c7f9..d7ce6765f 100644
--- a/web/app/assets/javascripts/feedHelper.js
+++ b/web/app/assets/javascripts/feedHelper.js
@@ -471,6 +471,43 @@
$controls.data('server-info', feed.mix) // for recordingUtils helper methods
$controls.data('view-context', 'feed')
+ // tack on video if available
+ if(feed.external_video_id) {
+
+ var $videoWrapper = $feedItem.find('.video-wrapper')
+ var $videoContainer = $feedItem.find('.video-container')
+ if(gon.isNativeClient) {
+ var $embed = $('' +
+ '' +
+ '')
+ $videoContainer.append($embed).addClass('no-embed')
+ $videoWrapper.removeClass('hidden')
+ $embed.click(function() {
+ context.JK.popExternalLink($(this).attr('href'))
+ return false;
+ })
+ }
+ else {
+ var $embed = $('')
+ $videoContainer.append($embed)
+ $videoWrapper.removeClass('hidden')
+ }
+
+ if(feed.owner_id == context.JK.currentUserId) {
+ var $deleteVideoLink = $('delete video')
+ $deleteVideoLink.click(function() {
+ app.layout.showDialog('delete-video-confirm-dialog', { d1: feed.id }).one(EVENTS.DIALOG_CLOSED, function(e, data) {
+ if(!data.canceled && data.result) {
+ $deleteVideoLink.remove();
+ $videoWrapper.slideUp(1000);
+ }
+ })
+ return false;
+ })
+ $videoContainer.append($deleteVideoLink);
+ }
+ }
+
$('.timeago', $feedItem).timeago();
context.JK.prettyPrintElements($('.duration', $feedItem));
context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem));
@@ -552,6 +589,9 @@
// put the feed item on the page
renderFeed($feedItem);
+ if(feed.external_video_id) {
+ toggleRecordingDetails.call($('.details', $feedItem).get(0))
+ }
// these routines need the item to have height to work (must be after renderFeed)
$controls.listenRecording({recordingId: feed.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'});
$controls.bind('statechange.listenRecording', stateChangeRecording);
diff --git a/web/app/assets/javascripts/feed_item_recording.js b/web/app/assets/javascripts/feed_item_recording.js
index 8679b01c5..fe37c8b12 100644
--- a/web/app/assets/javascripts/feed_item_recording.js
+++ b/web/app/assets/javascripts/feed_item_recording.js
@@ -2,6 +2,7 @@
"use strict";
+ // http://img.youtube.com/vi/x-Oas9Sc6s0/hqdefault.jpg
context.JK = context.JK || {};
context.JK.FeedItemRecording = function($parentElement, options){
diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js
index b26cf777a..0c47505ff 100644
--- a/web/app/assets/javascripts/ga.js
+++ b/web/app/assets/javascripts/ga.js
@@ -128,7 +128,8 @@
jkLike : 'jkLike',
jkFollow : 'jkFollow',
jkFavorite : 'jkFavorite',
- jkComment : 'jkComment'
+ jkComment : 'jkComment',
+ fileDownload: "DownloadFile"
};
// JamTrack categories and actions:
@@ -143,7 +144,12 @@
var jamTrackSessionLabels = {
nonSession: 'NonSession',
inSession: 'InSession'
- }
+ }
+
+ var fileDownloads = {
+ jamtrack_csv: 'JamTrackCSV',
+ jamtrack_pdf: 'JamTrackPDF'
+ }
function translatePlatformForGA(platform) {
assertOneOf(platform, context.JK.OS);
@@ -184,6 +190,11 @@
}
}
+ function trackFileDownload(type) {
+ assertOneOf(type, fileDownloads)
+ context.ga('send', 'event', categories.fileDownload, type)
+ }
+
function trackRegister(asMusician, registrationType) {
assertBoolean(asMusician);
assertOneOf(registrationType, registrationTypes);
@@ -452,6 +463,7 @@
GA.AudioTestFailReasons = audioTestFailReasons;
GA.AudioTestDataReasons = audioTestDataReasons;
GA.NetworkTestFailReasons = networkTestFailReasons;
+ GA.FileDownloadTypes = fileDownloads;
GA.trackRegister = trackRegister;
GA.trackDownload = trackDownload;
GA.trackFTUECompletion = trackFTUECompletion;
@@ -477,6 +489,7 @@
GA.trackJKSocial = trackJKSocial;
GA.virtualPageView = virtualPageView;
GA.trackTiming = trackTiming;
+ GA.trackFileDownload = trackFileDownload;
context.JK.GA = GA;
diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js
index 396e30be4..5fb037cc5 100644
--- a/web/app/assets/javascripts/globals.js
+++ b/web/app/assets/javascripts/globals.js
@@ -8,6 +8,8 @@
context.JK = context.JK || {};
var logger = context.JK.logger;
+ context.JK.MIDI_TRACK = 100
+
context.JK.OS = {
WIN32: "Win32",
OSX: "MacOSX",
@@ -53,13 +55,16 @@
METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected',
CHECKOUT_SIGNED_IN: 'checkout_signed_in',
CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in',
- PREVIEW_PLAYED: 'preview_played'
+ PREVIEW_PLAYED: 'preview_played',
+ VST_OPERATION_SELECTED: 'vst_operation_selected',
+ VST_EFFECT_SELECTED: 'vst_effect_selected'
};
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,
@@ -125,7 +130,8 @@
RECORDING_DONE :48, //the recording writer thread is done
VIDEO_WINDOW_OPENED :49, //video window opened
VIDEO_WINDOW_CLOSED :50,
- LAST_ALERT : 51
+ VST_CHANGED: 51, // VST state changed
+ LAST_ALERT : 52
}
// recreate eThresholdType enum from MixerDialog.h
context.JK.ALERT_TYPES = {
@@ -189,7 +195,8 @@
48: {"title": "", "message": ""}, // RECORDING_DONE
49: {"title": "", "message": ""}, // VIDEO_WINDOW_OPENED
50: {"title": "", "message": ""}, // VIDEO_WINDOW_CLOSED
- 51: {"title": "", "message": ""} // LAST_ALERT
+ 51: {"title": "", "message": ""}, // VST_CHANGED
+ 52: {"title": "", "message": ""} // LAST_ALERT
};
// add the alert's name to the ALERT_TYPES structure
@@ -209,6 +216,7 @@
"Bass Guitar": { "client_id": 20, "server_id": "bass guitar" },
"Computer": { "client_id": 30, "server_id": "computer" },
"Drums": { "client_id": 40, "server_id": "drums" },
+ "Percussion": { "client_id": 41, "server_id": "percussion" },
"Electric Guitar": { "client_id": 50, "server_id": "electric guitar" },
"Keyboard": { "client_id": 60, "server_id": "keyboard" },
"Piano": { "client_id": 61, "server_id": "piano" },
@@ -239,6 +247,7 @@
20: { "server_id": "bass guitar" },
30: { "server_id": "computer" },
40: { "server_id": "drums" },
+ 41: { "server_id": "percussion" },
50: { "server_id": "electric guitar" },
60: { "server_id": "keyboard" },
61: { "server_id": "piano"} ,
@@ -351,7 +360,9 @@
"PeerAudioInputMusicGroup": 13,
"PeerMediaTrackGroup": 14,
"JamTrackGroup": 15,
- "MetronomeGroup": 16
+ "MetronomeGroup": 16,
+ "MidiInputMusicGroup": 17,
+ "PeerMidiInputMusicGroup": 18
};
context.JK.ChannelGroupLookup = {
@@ -371,7 +382,9 @@
13: "PeerAudioInputMusicGroup",
14: "PeerMediaTrackGroup",
15: "JamTrackGroup",
- 16: "MetronomeGroup"
+ 16: "MetronomeGroup",
+ 17: "MidiInputMusicGroup",
+ 18: "PeerMidiInputMusicGroup"
}
context.JK.CategoryGroupIds = {
"AudioInputMusic" : "AudioInputMusic",
diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js
index 0a6774631..b348ed629 100644
--- a/web/app/assets/javascripts/helpBubbleHelper.js
+++ b/web/app/assets/javascripts/helpBubbleHelper.js
@@ -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);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index ff06650c2..18e496512 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -559,6 +559,28 @@
})
}
+ function getUserAuthorizations(options) {
+ var id = getId(options);
+
+ return $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: "/api/users/" + id + '/authorizations',
+ processData: false
+ });
+ }
+
+ function getGoogleAuth(options) {
+ var id = getId(options);
+
+ return $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: "/api/users/authorizations/google",
+ processData: false
+ });
+ }
+
function getUserDetail(options) {
var id = getId(options);
var detail = null;
@@ -570,6 +592,11 @@
processData: false
});
}
+ if(detail) {
+ detail.done(function(user) {
+ window.UserActions.loaded(user)
+ })
+ }
return detail;
}
@@ -1178,6 +1205,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",
@@ -1188,11 +1238,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',
@@ -1200,6 +1251,14 @@
data: JSON.stringify(options),
processData: false
});
+
+ deferred.done(function(user) {
+ context.JK.currentUserFreeJamTrack = user.show_free_jamtrack
+ window.UserActions.loaded(user)
+ })
+
+ return deferred;
+
}
function startRecording(options) {
@@ -1388,6 +1447,85 @@
})
}
+ function markMixdownActive(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/jamtracks/" + id + "/mixdowns/active",
+ data: JSON.stringify(options)
+ })
+ }
+
+ function createMixdown(options) {
+ return $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdowns/",
+ data: JSON.stringify(options)
+ })
+ }
+
+ function editMixdown(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdowns/" + id ,
+ data: JSON.stringify(options)
+ })
+ }
+
+ function deleteMixdown(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "DELETE",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdowns/" + id
+ })
+ }
+
+ function getMixdown(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdowns/" + id
+ })
+ }
+
+ function getMixdownPackage(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdown_packages/" + id
+ })
+ }
+
+ function enqueueMixdown(options) {
+ var id = options["id"];
+
+ return $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/mixdowns/" + id + '/enqueue' ,
+ data: JSON.stringify(options)
+ })
+ }
+
function openJamTrack(options) {
var musicSessionId = options["id"];
var jamTrackId = options["jam_track_id"];
@@ -1720,12 +1858,28 @@
}
function addJamtrackToShoppingCart(options) {
- return $.ajax({
+ var deferred = $.ajax({
type: "POST",
url: '/api/shopping_carts/add_jamtrack?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
+
+ deferred.done(function(response) {
+ window.UserActions.modify(response)
+ })
+ return deferred
+ }
+
+ function addGiftCardToShoppingCart(options) {
+ var deferred = $.ajax({
+ type: "POST",
+ url: '/api/shopping_carts/add_gift_card?' + $.param(options),
+ dataType: "json",
+ contentType: 'application/json'
+ });
+
+ return deferred
}
function getShoppingCarts() {
@@ -1739,12 +1893,17 @@
}
function removeShoppingCart(options) {
- return $.ajax({
+ var deferred = $.ajax({
type: "DELETE",
url: '/api/shopping_carts?' + $.param(options),
dataType: "json",
contentType: 'application/json'
})
+
+ deferred.done(function(response) {
+ window.UserActions.modify(response)
+ })
+ return deferred
}
function clearShoppingCart(options) {
@@ -1875,6 +2034,26 @@
});
}
+
+ function addRecordingVideoData(recordingId, data) {
+ return $.ajax({
+ type: "POST",
+ url: '/api/recordings/' + recordingId + '/video_data',
+ dataType: "json",
+ contentType: 'application/json',
+ data: JSON.stringify(data),
+ });
+ }
+
+ function deleteRecordingVideoData(recordingId) {
+ return $.ajax({
+ type: "DELETE",
+ url: '/api/recordings/' + recordingId + '/video_data',
+ dataType: "json",
+ contentType: 'application/json'
+ });
+ }
+
function createSignupHint(data) {
return $.ajax({
type: "POST",
@@ -1895,6 +2074,17 @@
});
}
+ function redeemGiftCard(data) {
+ var id = getId(data);
+ return $.ajax({
+ type: "POST",
+ url: '/api/users/' + id + '/gift_cards',
+ dataType: "json",
+ contentType: 'application/json',
+ data: JSON.stringify(data),
+ });
+ }
+
function portOverCarts() {
return $.ajax({
type: "POST",
@@ -1936,6 +2126,8 @@
this.cancelSession = cancelSession;
this.updateScheduledSession = updateScheduledSession;
this.getUserDetail = getUserDetail;
+ this.getUserAuthorizations = getUserAuthorizations;
+ this.getGoogleAuth = getGoogleAuth;
this.getUserProfile = getUserProfile;
this.getAffiliatePartnerData = getAffiliatePartnerData;
this.postAffiliatePartnerData = postAffiliatePartnerData;
@@ -2003,6 +2195,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;
@@ -2019,6 +2213,13 @@
this.claimRecording = claimRecording;
this.startPlayClaimedRecording = startPlayClaimedRecording;
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
+ this.markMixdownActive = markMixdownActive;
+ this.createMixdown = createMixdown;
+ this.editMixdown = editMixdown;
+ this.deleteMixdown = deleteMixdown;
+ this.enqueueMixdown = enqueueMixdown;
+ this.getMixdown = getMixdown;
+ this.getMixdownPackage = getMixdownPackage;
this.openJamTrack = openJamTrack
this.openBackingTrack = openBackingTrack
this.closeBackingTrack = closeBackingTrack
@@ -2069,6 +2270,7 @@
this.enqueueJamTrack = enqueueJamTrack;
this.getBackingTracks = getBackingTracks;
this.addJamtrackToShoppingCart = addJamtrackToShoppingCart;
+ this.addGiftCardToShoppingCart = addGiftCardToShoppingCart;
this.getShoppingCarts = getShoppingCarts;
this.removeShoppingCart = removeShoppingCart;
this.clearShoppingCart = clearShoppingCart;
@@ -2084,6 +2286,8 @@
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
this.addRecordingTimeline = addRecordingTimeline;
+ this.addRecordingVideoData = addRecordingVideoData;
+ this.deleteRecordingVideoData = deleteRecordingVideoData;
this.getMusicianSearchFilter = getMusicianSearchFilter;
this.postMusicianSearchFilter = postMusicianSearchFilter;
this.getBandSearchFilter = getBandSearchFilter;
@@ -2091,6 +2295,7 @@
this.playJamTrack = playJamTrack;
this.createSignupHint = createSignupHint;
this.createAlert = createAlert;
+ this.redeemGiftCard = redeemGiftCard;
this.signup = signup;
this.portOverCarts = portOverCarts;
return this;
diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee
deleted file mode 100644
index bb0c2fa2b..000000000
--- a/web/app/assets/javascripts/jam_track_screen.js.coffee
+++ /dev/null
@@ -1,484 +0,0 @@
-$ = jQuery
-context = window
-context.JK ||= {}
-
-context.JK.JamTrackScreen=class JamTrackScreen
- LIMIT = 10
- instrument_logo_map = context.JK.getInstrumentIconMap24()
-
- constructor: (@app) ->
- @EVENTS = context.JK.EVENTS
- @logger = context.JK.logger
- @screen = null
- @content = null
- @scroller = null
- @genre = null
- @artist = null
- @instrument = null
- @availability = null
- @nextPager = null
- @noMoreJamtracks = null
- @currentPage = 0
- @next = null
- @currentQuery = this.defaultQuery()
- @expanded = null
- @shownHelperBubbles = false
-
- beforeShow:(data) =>
- this.setFilterFromURL()
-
- if context.JK.currentUserId?
- @app.user().done((user) =>
- @user = user
- this.refresh()
- ).fail((arg) =>
- @logger.error("app.user.done failed: " + JSON.stringify(arg))
-
- @logger.debug(arg.statusCode);
-
- throw 'fail should not occur if user is available'
- )
- else
- this.refresh()
- unless @shownHelperBubbles
- @shownHelperBubbles = true
- @startHelperBubbles()
-
- afterShow:(data) =>
- context.JK.Tracking.jamtrackBrowseTrack(@app)
-
- beforeHide: () =>
- this.clearCtaHelpTimeout()
- this.clearBandFilterHelpTimeout()
- this.clearMasterHelpTimeout()
- this.clearResults();
-
- events:() =>
- @genre.on 'change', this.search
- @artist.on 'change', this.search
- @instrument.on 'change', this.search
- @availability.on 'change', this.search
-
- clearResults:() =>
- @currentPage = 0
- @content.empty()
- @noMoreJamtracks.hide()
- @next = null
-
- startHelperBubbles: () =>
- @showBandFilterHelpTimeout = setTimeout(@showBandFilterHelp, 3500)
-
- showBandFilterHelp: () =>
- context.JK.HelpBubbleHelper.jamtrackBrowseBand(@artist.closest('.easydropdown-wrapper'), $('body'))
-
- @showMasterHelpDueTime = new Date().getTime() + 11000 # 6000 ms for band tooltip to display, and 5 seconds of quiet time
- @scroller.on('scroll', @masterHelpScrollWatch)
- @scroller.on('scroll', @clearBubbles)
- @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime())
-
- clearBubbles: () =>
- if @helpBubble?
- @helpBubble.btOff()
- @helpBubble = null
-
- # computes when we should show the master help bubble
- masterHelpDueTime: () =>
- dueTime = @showMasterHelpDueTime - new Date().getTime()
- if dueTime <= 0
- dueTime = 2000
- dueTime
-
-
- # computes when we should show the master help bubble
- ctaHelpDueTime: () =>
- dueTime = @showCtaHelpDueTime - new Date().getTime()
- if dueTime <= 0
- dueTime = 2000
- dueTime
-
- # if the user scrolls, reset the master help due time
- masterHelpScrollWatch: () =>
- @clearMasterHelpTimeout()
- @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime() + 2000)
-
- # if the user scrolls, reset the master help due time
- ctaHelpScrollWatch: () =>
- @clearCtaHelpTimeout()
- @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime() + 2000)
-
-
- showCtaHelp: () =>
- @scroller.off('scroll', @ctaHelpScrollWatch)
- @clearCtaHelpTimeout()
-
- cutoff = @scroller.offset().top;
-
- @screen.find('.jamtrack-actions').each((i, element) =>
- $element = $(element)
-
- if ($element.offset().top >= cutoff)
- @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseCta($element, $('body'))
- return false
- else
- return true
- )
-
- showMasterHelp: () =>
- @scroller.off('scroll', @masterHelpScrollWatch)
- @clearMasterHelpTimeout()
-
- # don't show the help if the user has already clicked a preview
- unless @userPreviewed
- cutoff = @scroller.offset().top;
-
- @screen.find('.jamtrack-preview[data-track-type="Master"]').each((i, element) =>
- $element = $(element)
-
- if ($element.offset().top >= cutoff)
- @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseMasterMix($element.find('.play-button'), $('body'))
- return false
- else
- return true
- )
-
- @showCtaHelpDueTime = new Date().getTime() + 11000
- @scroller.on('scroll', @ctaHelpScrollWatch)
- @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime()) # 6000 ms for bubble show time, and 5000ms for delay
-
- previewPlayed: () =>
- @userPreviewed = true
-
- clearCtaHelpTimeout:() =>
- if @showCtaHelpTimeout?
- clearTimeout(@showCtaHelpTimeout)
- @showCtaHelpTimeout = null
-
- clearBandFilterHelpTimeout: () =>
- if @showBandFilterHelpTimeout?
- clearTimeout(@showBandFilterHelpTimeout)
- @showBandFilterHelpTimeout = null
-
- clearMasterHelpTimeout: () =>
- if @showMasterHelpTimeout?
- clearTimeout(@showMasterHelpTimeout)
- @showMasterHelpTimeout = null
-
- setFilterFromURL:() =>
- # Grab parms from URL for artist, instrument, and availability
- parms=this.getParams()
-
- if(parms.artist?)
- @artist.val(parms.artist)
- else
- @artist.val('')
- if(parms.instrument?)
- @instrument.val(parms.instrument)
- else
- @instrument.val('')
- if(parms.availability?)
- @availability.val(parms.availability)
- else
- @availability.val('')
-
- if window.history.replaceState #ie9 proofing
- window.history.replaceState({}, "", "/client#/jamtrackBrowse")
-
- getParams:() =>
- params = {}
- q = window.location.href.split("?")[1]
- if q?
- q = q.split('#')[0]
- raw_vars = q.split("&")
- for v in raw_vars
- [key, val] = v.split("=")
- params[key] = decodeURIComponent(val)
- params
-
- setFilterState: (state) =>
- if state
- @genre.easyDropDown('enable').removeAttr('disabled')
- @artist.easyDropDown('enable').removeAttr('disabled')
- @instrument.easyDropDown('enable').removeAttr('disabled')
- @availability.easyDropDown('enable').removeAttr('disabled')
- else
- @genre.easyDropDown('disable').attr('disabled', 'disabled')
- @artist.easyDropDown('disable').attr('disabled', 'disabled')
- @instrument.easyDropDown('disable').attr('disabled', 'disabled')
- @availability.easyDropDown('disable').attr('disabled', 'disabled')
-
- refresh:() =>
- this.clearResults()
- @currentQuery = this.buildQuery()
- that = this
- this.setFilterState(false)
- rest.getJamTracks(@currentQuery).done((response) =>
- that.handleJamtrackResponse(response)
- ).fail( (jqXHR) =>
- that.clearResults()
- that.noMoreJamtracks.show()
- that.app.notifyServerError jqXHR, 'Jamtrack Unavailable'
- ).always () =>
- that.setFilterState(true)
-
- search:() =>
- this.refresh()
- false
-
- defaultQuery:() =>
- query =
- per_page: LIMIT
- page: @currentPage+1
- if @next
- query.since = @next
- query
-
- buildQuery:() =>
- @currentQuery = this.defaultQuery()
- # genre filter
- # var genres = @screen.find('#jamtrack_genre').val()
- # if (genres !== undefined) {
- # @currentQuery.genre = genres
- # }
- # instrument filter
-
- instrument = @instrument.val()
- if instrument?
- @currentQuery.instrument = instrument
-
- # artist filter
- art = @artist.val()
- if art?
- @currentQuery.artist = art
-
- # availability filter
- availability = @availability.val()
- if availability?
- @currentQuery.availability = availability
- @currentQuery
-
- handleJamtrackResponse:(response) =>
- @next = response.next
- this.renderJamtracks(response)
- if response.next == null
- # if we less results than asked for, end searching
- @scroller.infinitescroll 'pause'
- if @currentPage == 0 and response.jamtracks.length == 0
- @content.append '
Loading ...')
- img: '/assets/shared/spinner.gif'
- path: (page) =>
- '/api/jamtracks?' + $.param(that.buildQuery())
-
- }, (json, opts) =>
- this.handleJamtrackResponse(json)
- @scroller.infinitescroll 'resume'
-
- playJamtrack:(e) =>
- e.preventDefault()
-
- addToCartJamtrack:(e) =>
- e.preventDefault()
- $target = $(e.target)
- params = id: $target.attr('data-jamtrack-id')
- isFree = $(e.target).is('.is_free')
-
- rest.addJamtrackToShoppingCart(params).done((response) =>
- if(isFree)
- if context.JK.currentUserId?
- context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices
- context.location = '/client#/redeemComplete'
- else
- # now make a rest call to buy it
- context.location = '/client#/redeemSignup'
-
- else
- context.location = '/client#/shoppingCart'
-
- ).fail @app.ajaxError
-
- licenseUSWhy:(e) =>
- e.preventDefault()
- @app.layout.showDialog 'jamtrack-availability-dialog'
-
- handleExpanded:(trackElement) =>
- jamTrack = trackElement.data('jamTrack')
- expanded = trackElement.data('expanded')
- expand = !expanded
- trackElement.data('expanded', expand)
-
- detailArrow = trackElement.find('.jamtrack-detail-btn')
-
- if expand
- trackElement.find('.extra').removeClass('hidden')
- detailArrow.html('hide tracks ')
- for track in jamTrack.tracks
- trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden')
- else
- trackElement.find('.extra').addClass('hidden')
- detailArrow.html('show all tracks ')
- count = 0
- for track in jamTrack.tracks
- if count < 6
- trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden')
- else
- trackElement.find("[jamtrack-track-id='#{track.id}']").addClass('hidden')
- count++
-
-
- registerEvents:(parent) =>
- #@screen.find('.jamtrack-detail-btn').on 'click', this.showJamtrackDescription
- parent.find('.play-button').on 'click', this.playJamtrack
- parent.find('.jamtrack-add-cart').on 'click', this.addToCartJamtrack
- parent.find('.license-us-why').on 'click', this.licenseUSWhy
- parent.find('.jamtrack-detail-btn').on 'click', this.toggleExpanded
- # @screen.find('.jamtrack-preview').each (index, element) =>
- # new JK.JamTrackPreview(data.app, $element, jamTrack, track, {master_shows_duration: true})
-
- rerenderJamtracks:() =>
- if @currentData?
- @clearResults()
- @renderJamtracks(@currentData)
- false
-
- computeWeight: (jam_track_track, instrument) =>
- weight = switch
- when jam_track_track.track_type == 'Master' then 0
- when jam_track_track.instrument?.id == instrument then 1 + jam_track_track.position
- else 10000 + jam_track_track.position
-
- renderJamtracks:(data) =>
- @currentData = data
- that = this
-
- for jamtrack in data.jamtracks
- jamtrackExpanded = this.expanded==jamtrack.id
- trackRow = _.clone(jamtrack)
- trackRow.track_cnt = jamtrack.tracks.length
- trackRow.tracks = []
-
- # if an instrument is selected by the user, then re-order any jam tracks with a matching instrument to the top
- instrument = @instrument.val()
- if instrument?
- jamtrack.tracks.sort((a, b) =>
- aWeight = @computeWeight(a, instrument)
- bWeight = @computeWeight(b, instrument)
- return aWeight - bWeight
- )
-
- for track in jamtrack.tracks
- trackRow.tracks.push(track)
- if track.track_type=='Master'
- track.instrument_desc = "Master"
- else
- inst = '../assets/content/icon_instrument_default24.png'
- if track.instrument?
- if track.instrument.id in instrument_logo_map
- inst = instrument_logo_map[track.instrument.id].asset
- track.instrument_desc = track.instrument.description
- track.instrument_url = inst
-
- if track.part != ''
- track.instrument_desc += ' (' + track.part + ')'
-
- free_state = if context.JK.currentUserFreeJamTrack then 'free' else 'non-free'
-
- is_free = free_state == 'free'
-
- options =
- jamtrack: trackRow
- expanded: false
- free_state: free_state,
- is_free: is_free
- @jamtrackItem = $(context._.template($('#template-jamtrack').html(), options, variable: 'data'))
- that.renderJamtrack(@jamtrackItem, jamtrack)
- that.registerEvents(@jamtrackItem)
-
-
- renderJamtrack:(jamtrackElement, jamTrack) =>
- jamtrackElement.data('jamTrack', jamTrack)
- jamtrackElement.data('expanded', true)
-
- @content.append jamtrackElement
-
- #if this.expanded==jamTrack.id
- for track in jamTrack.tracks
- trackRow = jamtrackElement.find("[jamtrack-track-id='#{track.id}']")
- previewElement = trackRow.find(".jamtrack-preview")
- preview = new JK.JamTrackPreview(@app, previewElement, jamTrack, track, {master_shows_duration: true, color:'gray'})
- $(preview).on(@EVENTS.PREVIEW_PLAYED, @previewPlayed)
-
- this.handleExpanded(jamtrackElement, false)
-
- showJamtrackDescription:(e) =>
- e.preventDefault()
- @description = $(e.target).parent('.detail-arrow').next()
- if @description.css('display') == 'none'
- @description.show()
- else
- @description.hide()
-
- toggleExpanded:(e) =>
- e.preventDefault()
- jamtrackRecord = $(e.target).parents('.jamtrack-record')
- jamTrackId = jamtrackRecord.attr("jamtrack-id")
-
- this.handleExpanded(jamtrackRecord)
-
- initialize:() =>
-
- screenBindings =
- 'beforeShow': this.beforeShow
- 'afterShow': this.afterShow
- 'beforeHide' : this.beforeHide
- @app.bindScreen 'jamtrackBrowse', screenBindings
- @screen = $('#jamtrack-find-form')
- @scroller = @screen.find('.content-body-scroller')
- @content = @screen.find('.jamtrack-content')
- @genre = @screen.find('#jamtrack_genre')
- @artist = @screen.find('#jamtrack_artist')
- @instrument = @screen.find('#jamtrack_instrument')
- @availability = @screen.find('#jamtrack_availability')
- @nextPager = @screen.find('a.btn-next-pager')
- @noMoreJamtracks = @screen.find('.end-of-jamtrack-list')
- if @screen.length == 0
- throw new Error('@screen must be specified')
- if @scroller.length == 0
- throw new Error('@scroller must be specified')
- if @content.length == 0
- throw new Error('@content must be specified')
- if @noMoreJamtracks.length == 0
- throw new Error('@noMoreJamtracks must be specified')
- #if(@genre.length == 0) throw new Error("@genre must be specified")
-
- if @artist.length == 0
- throw new Error('@artist must be specified')
- if @instrument.length == 0
- throw new Error('@instrument must be specified')
- #if @availability.length == 0
- # throw new Error('@availability must be specified')
- this.events()
-
-
diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js
index e32b0f041..efb07aa0c 100644
--- a/web/app/assets/javascripts/jamkazam.js
+++ b/web/app/assets/javascripts/jamkazam.js
@@ -133,7 +133,22 @@
app.notify({title: "Oops!", text: "What you were looking for is gone now."});
}
else if(jqXHR.status === 403) {
- logger.debug("not logged in");
+ logger.debug("not logged in or something else");
+
+ try {
+ var responseJson = JSON.parse(jqXHR.responseText)
+
+ if (responseJson['message'] == 'IP blacklisted') {
+ app.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
+ return;
+ }
+ if (responseJson['message'] == 'User blacklisted') {
+ app.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
+ return;
+ }
+ }catch(e) { }
+
+ app.notify({title: 'Not Logged In', text: 'Please log in.', icon_url: "/assets/content/icon_alert_big.png"})
}
else if (jqXHR.status === 422) {
logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText);
@@ -230,6 +245,21 @@
}
else if(jqXHR.status == 403) {
logger.debug("permission error sent from server:", jqXHR.responseText)
+
+ try {
+ var responseJson = JSON.parse(jqXHR.responseText)
+
+ if (responseJson['message'] == 'IP blacklisted') {
+ this.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
+ return;
+ }
+ if (responseJson['message'] == 'User blacklisted') {
+ this.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
+ return;
+ }
+ }catch(e) { }
+
+ // default
this.notify({title: 'Permission Error', text: 'You do not have permission to access this information', icon_url: "/assets/content/icon_alert_big.png"})
}
else {
diff --git a/web/app/assets/javascripts/jamtrack_landing.js.coffee b/web/app/assets/javascripts/jamtrack_landing.js.coffee
deleted file mode 100644
index 73cb6d3fe..000000000
--- a/web/app/assets/javascripts/jamtrack_landing.js.coffee
+++ /dev/null
@@ -1,80 +0,0 @@
-$ = jQuery
-context = window
-context.JK ||= {}
-
-context.JK.JamTrackLanding = class JamTrackLanding
- constructor: (@app) ->
- @rest = context.JK.Rest()
- @client = context.jamClient
- @logger = context.JK.logger
- @screen = null
- @noFreeJamTrack = null
- @freeJamTrack = null
- @bandList = null
- @noBandsFound = null
-
- initialize:() =>
- screenBindings =
- 'beforeShow': @beforeShow
- 'afterShow': @afterShow
-
- #@app.bindScreen('jamtrackLanding', screenBindings)
- @screen = $('#jamtrackLanding')
- @noFreeJamTrack = @screen.find('.no-free-jamtrack')
- @freeJamTrack = @screen.find('.free-jamtrack')
- @bandList = @screen.find('#band_list')
- @noBandsFound = @screen.find('#no_bands_found')
-
- beforeShow:() =>
-
- @noFreeJamTrack.addClass('hidden')
- @freeJamTrack.addClass('hidden')
-
- afterShow:() =>
-
- if context.JK.currentUserId
- @app.user().done(@onUser)
- else
- @onUser({free_jamtrack: gon.global.one_free_jamtrack_per_user})
-
- onUser:(user) =>
- if user.free_jamtrack
- @freeJamTrack.removeClass('hidden')
- else
- @noFreeJamTrack.removeClass('hidden')
-
- # Get artist names and build links
- @rest.getJamTrackArtists({group_artist: true, per_page:100})
- .done(this.buildArtistLinks)
- .fail(this.handleFailure)
-
- # Bind links to action that will open the jam_tracks list view filtered to given artist_name:
- # artist_name
- this.bindArtistLinks()
-
- buildArtistLinks:(response) =>
- # Get artist names and build links
- @logger.debug("buildArtest links response", response)
-
- artists = response.artists
- $("#band_list>li:not('#no_bands_found')").remove()
- if artists.length==0
- @noBandsFound.removeClass("hidden")
- else
- @noBandsFound.addClass("hidden")
-
- # client#/jamtrack
- for artist in artists
- artistLink = "#{artist.original_artist} (#{artist.song_count})"
- @bandList.append("
#{artistLink}
")
-
- # We don't want to do a full page load if this is clicked on here:
- bindArtistLinks:() =>
- that=this
- @bandList.on "click", "a.artist-link", (event)->
- context.location="client#/jamtrack/search"
- if window.history.replaceState # ie9 proofing
- window.history.replaceState({}, "", this.href)
- event.preventDefault()
-
- handleFailure:(error) =>
diff --git a/web/app/assets/javascripts/jquery.manageVsts.js b/web/app/assets/javascripts/jquery.manageVsts.js
new file mode 100644
index 000000000..6a7023cf7
--- /dev/null
+++ b/web/app/assets/javascripts/jquery.manageVsts.js
@@ -0,0 +1,71 @@
+(function(context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+
+
+ // creates an iconic/graphical instrument selector. useful when there is minimal real-estate
+
+ $.fn.manageVsts = function(options) {
+
+ return this.each(function(index) {
+
+ function close() {
+ $parent.btOff();
+ $parent.focus();
+ }
+
+ var $parent = $(this);
+
+ function onManageVstSelected() {
+ var $li = $(this);
+ var vstOperation = $li.attr('data-manage-vst-option');
+
+ close();
+ $parent.triggerHandler(context.JK.EVENTS.VST_OPERATION_SELECTED, {vstOperation: vstOperation});
+ return false;
+ };
+
+ // if the user goes into the bubble, remove
+ function waitForBubbleHover($bubble) {
+ $bubble.hoverIntent({
+ over: function() {
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ },
+ out: function() {
+ $parent.btOff();
+ }});
+ }
+
+ var timeout = null;
+
+ context.JK.hoverBubble($parent, $('#template-manage-vsts').html(), {
+ trigger:'none',
+ cssClass: 'manage-vsts-popup',
+ spikeGirth:0,
+ spikeLength:0,
+ width:250,
+ closeWhenOthersOpen: true,
+ offsetParent: $parent.closest('.dialog'),
+ positions:['bottom'],
+ preShow: function() {
+ },
+ postShow:function(container) {
+ $(container).find('li').click(onManageVstSelected)
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ waitForBubbleHover($(container))
+ timeout = setTimeout(function() {$parent.btOff()}, 3000)
+ }
+ });
+ });
+ }
+
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/jquery.trackEffects.js b/web/app/assets/javascripts/jquery.trackEffects.js
new file mode 100644
index 000000000..b0a15c74e
--- /dev/null
+++ b/web/app/assets/javascripts/jquery.trackEffects.js
@@ -0,0 +1,75 @@
+(function(context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+
+
+ // creates an iconic/graphical instrument selector. useful when there is minimal real-estate
+
+ $.fn.trackEffects = function(options) {
+
+ return this.each(function(index) {
+
+ function close() {
+ $parent.btOff();
+ $parent.focus();
+ }
+
+ var $parent = $(this);
+
+ function onOptionSelected() {
+ var $li = $(this);
+ var vstOperation = $li.attr('data-manage-vst-option');
+
+ close();
+ $parent.triggerHandler(context.JK.EVENTS.VST_EFFECT_SELECTED, {vstOperation: vstOperation});
+ return false;
+ };
+
+ // if the user goes into the bubble, remove
+ function waitForBubbleHover($bubble) {
+ $bubble.hoverIntent({
+ over: function() {
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ },
+ out: function() {
+ $parent.btOff();
+ }});
+ }
+
+ var timeout = null;
+
+ context.JK.hoverBubble($parent, $('#template-vst-effects').html(), {
+ trigger:'none',
+ cssClass: 'vst-effects-popup',
+ spikeGirth:0,
+ spikeLength:0,
+ width:220,
+ closeWhenOthersOpen: true,
+ offsetParent: $parent.closest('.screen'),
+ positions:['bottom'],
+ preShow: function() {
+
+ },
+ postShow:function(container) {
+ if (options && options['postShow']) {
+ options['postShow']($(container))
+ }
+ $(container).find('li').click(onOptionSelected)
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ waitForBubbleHover($(container))
+ timeout = setTimeout(function() {$parent.btOff()}, 3000)
+ }
+ });
+ });
+ }
+
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js
index fd39f6b9f..df9772d01 100644
--- a/web/app/assets/javascripts/layout.js
+++ b/web/app/assets/javascripts/layout.js
@@ -618,6 +618,7 @@
// if string passed, see if dialog is showing (even if buried) of a given name
function isDialogShowing() {
if(arguments.length == 1) {
+ console.log("what?", arguments[0])
// user passed in dialog id
var dialogId = arguments[0];
context._.each(openDialogs, function(dialog) {
diff --git a/web/app/assets/javascripts/minimal/minimal.js b/web/app/assets/javascripts/minimal/minimal.js
index 70bfd5f0b..0c216c297 100644
--- a/web/app/assets/javascripts/minimal/minimal.js
+++ b/web/app/assets/javascripts/minimal/minimal.js
@@ -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
\ No newline at end of file
+//= 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()
\ No newline at end of file
diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js
deleted file mode 100644
index 23360c626..000000000
--- a/web/app/assets/javascripts/order.js
+++ /dev/null
@@ -1,670 +0,0 @@
-(function(context,$) {
-
- "use strict";
- context.JK = context.JK || {};
- context.JK.OrderScreen = function(app) {
-
- var EVENTS = context.JK.EVENTS;
- var logger = context.JK.logger;
-
- var $screen = null;
- var $templateOrderContent = null;
- var $templatePurchasedJamTrack = null;
- var $navigation = null;
- var $billingInfo = null;
- var $shippingInfo = null;
- var $paymentMethod = null;
- var $shippingAddress = null;
- var $shippingAsBilling = null;
- var $paymentInfoPanel = null;
- var $orderPanel = null;
- var $thanksPanel = null;
- var $jamTrackInBrowser = null;
- var $purchasedJamTrack = null;
- var $purchasedJamTrackHeader = null;
- var $purchasedJamTracks = null;
- var $orderContent = null;
- var userDetail = null;
- var step = null;
- var billing_info = null;
- var shipping_info = null;
- var shipping_as_billing = null;
- var downloadJamTracks = [];
- var purchasedJamTracks = null;
- var purchasedJamTrackIterator = 0;
-
- function beforeShow() {
- beforeShowPaymentInfo();
- resetJamTrackDownloadInfo();
- }
-
- function beforeShowPaymentInfo() {
- step = 2;
- renderNavigation();
- renderAccountInfo();
- $("#order_error").addClass("hidden")
- }
-
- function resetJamTrackDownloadInfo() {
- $purchasedJamTrack.addClass('hidden');
- $purchasedJamTracks.children().remove()
- $jamTrackInBrowser.hide('hidden');
- }
-
- function renderAccountInfo() {
- rest.getUserDetail()
- .done(populateAccountInfo)
- .error(app.ajaxError);
- }
-
- function populateAccountInfo(user) {
- userDetail = user;
-
- if (userDetail.has_recurly_account) {
- rest.getBillingInfo()
- .done(function(response) {
- $billingInfo.find("#billing-first-name").val(response.first_name);
- $billingInfo.find("#billing-last-name").val(response.last_name);
- $billingInfo.find("#billing-address1").val(response.address1);
- $billingInfo.find("#billing-address2").val(response.address2);
- $billingInfo.find("#billing-city").val(response.city);
- $billingInfo.find("#billing-state").val(response.state);
- $billingInfo.find("#billing-zip").val(response.zip);
- $billingInfo.find("#billing-country").val(response.country);
-
- $shippingAddress.find("#shipping-first-name").val(response.first_name);
- $shippingAddress.find("#shipping-last-name").val(response.last_name);
- $shippingAddress.find("#shipping-address1").val(response.address1);
- $shippingAddress.find("#shipping-address2").val(response.address2);
- $shippingAddress.find("#shipping-city").val(response.city);
- $shippingAddress.find("#shipping-state").val(response.state);
- $shippingAddress.find("#shipping-zip").val(response.zip);
- $shippingAddress.find("#shipping-country").val(response.country);
- })
- .error(app.ajaxError);
- }
- else {
- $billingInfo.find("#billing-first-name").val(userDetail.first_name);
- $billingInfo.find("#billing-last-name").val(userDetail.last_name);
- $billingInfo.find("#billing-city").val(userDetail.city);
- $billingInfo.find("#billing-state").val(userDetail.state);
- $billingInfo.find("#billing-country").val(userDetail.country);
-
- $shippingAddress.find("#shipping-first-name").val(userDetail.first_name);
- $shippingAddress.find("#shipping-last-name").val(userDetail.last_name);
- $shippingAddress.find("#shipping-city").val(userDetail.city);
- $shippingAddress.find("#shipping-state").val(userDetail.state);
- $shippingAddress.find("#shipping-country").val(userDetail.country);
- }
- }
-
- function afterShow(data) {
- // XXX : style-test code
- // moveToThanks({jam_tracks: [{id: 14, jam_track_right_id: 11, name: 'Back in Black'}, {id: 15, jam_track_right_id: 11, name: 'In Bloom'}, {id: 16, jam_track_right_id: 11, name: 'Love Bird Supreme'}]});
- }
-
- function beforeHide() {
- if(downloadJamTracks) {
- context._.each(downloadJamTracks, function(downloadJamTrack) {
- downloadJamTrack.destroy();
- downloadJamTrack.root.remove();
- })
-
- downloadJamTracks = [];
- }
- purchasedJamTracks = null;
- purchasedJamTrackIterator = 0;
- }
-
- // TODO: Refactor: this function is long and fraught with many return points.
- function next(e) {
- e.preventDefault();
- $("#order_error").addClass("hidden")
-
- // validation
- var billing_first_name = $billingInfo.find("#billing-first-name").val();
- var billing_last_name = $billingInfo.find("#billing-last-name").val();
- var billing_address1 = $billingInfo.find("#billing-address1").val();
- var billing_address2 = $billingInfo.find("#billing-address2").val();
- var billing_city = $billingInfo.find("#billing-city").val();
- var billing_state = $billingInfo.find("#billing-state").val();
- var billing_zip = $billingInfo.find("#billing-zip").val();
- var billing_country = $billingInfo.find("#billing-country").val();
-
- if (!billing_first_name) {
- $billingInfo.find('#divBillingFirstName .error-text').remove();
- $billingInfo.find('#divBillingFirstName').addClass("error");
- $billingInfo.find('#billing-first-name').after("
“If you want to use an audio plugin, click the manage audio plugins link above, and then click the scan for new or updated plugins link in the menu.
`
+
+ for input in @state.configureTracks.musicPorts.inputs
+
+ include = false
+ # we need to see that this input is unassigned, or one of the two selected
+ for unassignedInputs in @state.configureTracks.trackAssignments.inputs.unassigned
+ if unassignedInputs.id == input.id
+ include = true
+ break
+
+ if !include
+ # not see if it's the currently edited track
+ for currentInput in @state.configureTracks.editingTrack
+ if currentInput.id == input.id
+ include = true
+
+ if include
+ item = ``
+ inputOneOptions.push(item)
+ inputTwoOptions.push(item)
+
+
+ for plugin in @state.configureTracks.vstPluginList.vsts
+ if plugin.category == 'NONE'
+ vsts.push(``)
+ else if plugin.isInstrument == false
+ vsts.push(``)
+
+ if @state.configureTracks.editingTrack.vst?
+ vstAssignedThisTrack = true
+ selectedVst = @state.configureTracks.editingTrack.vst.file
+
+ vstSettingBtnClasses = classNames({'button-orange': vstAssignedThisTrack, 'button-grey': !vstAssignedThisTrack})
+ `
+
+
Audio Input Ports
+
Select one or two inputs ports to assign to this track. Note that if you assign a single input port, the app will automatically duplicate this port into a stereo track.
- To play with your JamTracks, open a JamTrack while in a session in the JamKazam app. Or visit the JamTracks section of your account.
-
+ The fastest way to start playing with your JamTracks is to open them below and use our custom mix feature to play them back in your browser. To access the full set of JamTrack features, install our free app. To learn more about all you can do with JamTracks, check out our JamTracks help docs.
+
`
+ playJamTracks = []
+
+ for jamTrack in @state.purchasedJamTracks
+ playJamTracks.push `
`
+
+ 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 `
+ Or download the entire JamTracks catalog or to easily browse everything we have.
+
@@ -60,15 +99,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:
-
Solo just the part you want to play in order to hear and learn it
-
Mute just the part you want to play and play along with the rest
+
Choose your favorites from our catalog of 3,700+ songs
+
Listen to just the single part you want to play to learn it
+
Mute the part you want to play, and play along with the rest of the band
Slow down playback to practice without changing the pitch
Change the song key by raising or lowering pitch in half steps
-
Make audio recordings and share them via Facebook or URL
-
Make video recordings and share them via YouTube
-
And even go online to play with others live & in sync
+
Save your custom mixes for easy access, and export them to use anywhere
+
Apply VST & AU audio plugin effects to your live performance
+
Use MIDI with VST & AU instruments for keys, electronic drums, more
+
Make audio recordings and share via Facebook or URL
+
Make video recordings and share via YouTube
+
Play online live and in sync with others from different locations
+
JamTracks work in standard browser, with more features in Win/Mac apps
@@ -80,6 +124,55 @@ MIX_MODES = context.JK.MIX_MODES
componentDidMount: () ->
$root = $(@getDOMNode())
+ doPurchasedSearch: (searchQuery) ->
+ if !searchQuery?
+ #searchQuery = $.cookie(@cookieName)
+
+ # and parse that cookie if defined
+ if searchQuery
+ try
+ searchQuery = JSON.parse(searchQuery)
+ catch e
+ searchQuery = {searchType: 'user-input', searchData: ''}
+ logger.error("unable to parse search query: " + e)
+
+
+ # if still no query (after checking cookie and what was specified in function, then default to anything
+ if !searchQuery?
+ searchQuery = {searchType: 'user-input', searchData: ''}
+
+ query = {page:1, per_page:20}
+
+ if searchQuery && searchQuery.searchData && searchQuery.searchData.length > 0 && searchQuery.searchType && searchQuery.searchType.length > 0
+
+ if searchQuery.searchType == 'user-input'
+ query.search = searchQuery.searchData
+ else if searchQuery.searchType == 'artist-select'
+ query.artist_search = searchQuery.searchData
+ else if searchQuery.searchType == 'song-select'
+ query.song_search = searchQuery.searchData
+
+ rest.getPurchasedJamTracks(query)
+ .done((purchasedJamTracks) =>
+ if @redeemedFlow
+ setTimeout((() => @preparePlayJamTrackProd()), 200)
+ @redeemedFlow = false
+ @setState({purchasedJamTracks: purchasedJamTracks.jamtracks})
+ )
+ .fail((jqXHR, textStatus, errorMessage) =>
+ @app.ajaxError(jqXHR, textStatus, errorMessage)
+ )
+
+ searchMyJamTracks: (searchType, searchData) ->
+ searchQuery = {searchType: searchType, searchData: searchData}
+ #$.cookie(@cookieName, JSON.stringify(searchQuery))
+ @doPurchasedSearch(searchQuery)
+
+ searchMyJamTracksByString: (e) ->
+ e.preventDefault()
+
+ @doPurchasedSearch(searchType:'user-input',searchData: window.JamTrackSearchInput)
+
search: (searchType, searchData) ->
context.JamTrackActions.requestSearch(searchType, searchData)
@@ -96,12 +189,27 @@ 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
- @onUser({free_jamtrack: context.JK.currentUserFreeJamTrack})
+ @processUrl()
beforeShow: () ->
@setState({user: null})
@@ -109,6 +217,9 @@ MIX_MODES = context.JK.MIX_MODES
onUser:(user) ->
@setState({user: user})
+ if context.JK.currentUserId?
+ @doPurchasedSearch()
+
# Get artist names and build links
#@rest.getJamTrackArtists({group_artist: true, per_page:100})
#.done(this.buildArtistLinks)
@@ -118,6 +229,81 @@ 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) ->
+
+ if context.jamClient.IsNativeClient()
+ e.preventDefault()
+ tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient)
+ data = {}
+ data.client_id = @app.clientId
+ #data.description = $('#description').val()
+ data.description = "Jam Track Session"
+ data.as_musician = true
+ data.legal_terms = true
+ data.intellectual_property = true
+ data.approval_required = false
+ data.musician_access = false
+ data.fan_access = false
+ data.fan_chat = false
+ console.log("jamTrack", jamTrack)
+ data.genre = $.map(jamTrack.genres, (genre) -> genre.id)
+ data.genres = $.map(jamTrack.genres, (genre)-> genre.id)
+
+ data.genre = ['rock'] if data.genre.length == 0
+ data.genres = ['rock'] if data.genres.length == 0
+ # data.genres = context.JK.GenreSelectorHelper.getSelectedGenres('#create-session-genre')
+ # data.musician_access = if $('#musician-access option:selected').val() == 'true' then true else false
+ # data.approval_required = if $('input[name=\'musician-access-option\']:checked').val() == 'true' then true else false
+ # data.fan_access = if $('#fan-access option:selected').val() == 'true' then true else false
+ # data.fan_chat = if $('input[name=\'fan-chat-option\']:checked').val() == 'true' then true else false
+ # if $('#band-list option:selected').val() != ''
+ # data.band = $('#band-list option:selected').val()
+ data.audio_latency = context.jamClient.FTUEGetExpectedLatency().latency
+ data.tracks = tracks
+
+ rest.legacyCreateSession(data).done((response) =>
+ newSessionId = response.id
+ context.JK.SessionUtils.setAutoOpenJamTrack(jamTrack) # so that the session screen will pick this up
+ context.location = '/client#/session/' + newSessionId
+ # Re-loading the session settings will cause the form to reset with the right stuff in it.
+ # This is an extra xhr call, but it keeps things to a single codepath
+ #loadSessionSettings()
+ context.JK.GA.trackSessionCount data.musician_access, data.fan_access, 0
+ context.JK.GA.trackSessionMusicians context.JK.GA.SessionCreationTypes.create
+ ).fail (jqXHR) =>
+ handled = false
+ if jqXHR.status = 422
+ response = JSON.parse(jqXHR.responseText)
+ if response['errors'] and response['errors']['tracks'] and response['errors']['tracks'][0] == 'Please select at least one track'
+ @app.notifyAlert 'No Inputs Configured', $('You will need to reconfigure your audio device.')
+ handled = true
+ if !handled
+ @app.notifyServerError jqXHR, 'Unable to Create Session'
+
+ else
+ if true # /iPhone|iPad|iPod|android/i.test(navigator.userAgent)
+ # popup window
+ JamTrackPlayerActions.opened(jamTrack)
+
+ else
+ # popup window
+ JamTrackPlayerActions.open(jamTrack)
+ e.preventDefault()
+
+
+
onAppInit: (@app) ->
@rest = context.JK.Rest()
diff --git a/web/app/assets/javascripts/react-components/JamTrackPdfLink.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackPdfLink.js.jsx.coffee
new file mode 100644
index 000000000..c29e4311a
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamTrackPdfLink.js.jsx.coffee
@@ -0,0 +1,15 @@
+context = window
+rest = context.JK.Rest()
+
+
+@JamTrackPdfLink = React.createClass({
+
+ render: () ->
+ `as a PDF file`
+
+ pdfClick: (e) ->
+ window.JK.GA.trackFileDownload(window.JK.GA.FileDownloadTypes.jamtrack_pdf);
+
+ window.JK.popExternalLink($(e.target).attr('href'))
+ e.preventDefault()
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee
index 04b8fcb6b..0e251bbea 100644
--- a/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee
@@ -4,7 +4,7 @@ MIX_MODES = context.JK.MIX_MODES
@JamTrackSearchScreen = React.createClass({
- mixins: [Reflux.listenTo(@AppStore,"onAppInit")]
+ mixins: [Reflux.listenTo(@AppStore,"onAppInit"), Reflux.listenTo(@UserStore,"onUserChanged")]
LIMIT: 10
instrument_logo_map: context.JK.getInstrumentIconMap24()
@@ -35,19 +35,22 @@ MIX_MODES = context.JK.MIX_MODES
)
###
for track in jamtrack.tracks
- trackRow.tracks.push(track)
- if track.track_type=='Master'
- track.instrument_desc = "Master"
- else
- inst = '../assets/content/icon_instrument_default24.png'
- if track.instrument?
- if track.instrument.id in @instrument_logo_map
- inst = @instrument_logo_map[track.instrument.id].asset
- track.instrument_desc = track.instrument.description
- track.instrument_url = inst
- if track.part != ''
- track.instrument_desc += ' (' + track.part + ')'
+ if track.track_type == 'Master' || track.track_type == 'Track'
+ trackRow.tracks.push(track)
+
+ if track.track_type == 'Master'
+ track.instrument_desc = "Master"
+ else if track.track_type == 'Track'
+ inst = '../assets/content/icon_instrument_default24.png'
+ if track.instrument?
+ if track.instrument.id in @instrument_logo_map
+ inst = @instrument_logo_map[track.instrument.id].asset
+ track.instrument_desc = track.instrument.description
+ track.instrument_url = inst
+
+ if track.part != ''
+ track.instrument_desc += ' (' + track.part + ')'
trackRow.free_state = if @state.is_free then 'free' else 'non-free'
@@ -91,10 +94,10 @@ MIX_MODES = context.JK.MIX_MODES
`
actionBtn = null
- if jamtrack.is_free
- actionBtn = ` GET IT FREE!`
- else if jamtrack.purchased
+ if jamtrack.purchased
actionBtn = `PURCHASED`
+ else if jamtrack.is_free
+ actionBtn = ` GET IT FREE!`
else if jamtrack.added_cart
actionBtn = `ALREADY IN CART`
else
@@ -209,6 +212,7 @@ MIX_MODES = context.JK.MIX_MODES
+
Download JamTracks catalog or
{artistSection}
@@ -219,14 +223,13 @@ MIX_MODES = context.JK.MIX_MODES
clearResults:() ->
- @setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null, is_free: context.JK.currentUserFreeJamTrack, first_search: true})
+ @setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null, is_free: @user.show_free_jamtrack, first_search: true})
getInitialState: () ->
{search: '', type: 'user-input', artists:[], jamtracks:[], show_all_artists: false, currentPage: 0, next: null, searching: false, first_search: true, count: 0, is_free: context.JK.currentUserFreeJamTrack}
onSelectChange: (val) ->
- #@logger.debug("CHANGE #{val}")
return false unless val?
@@ -435,18 +438,39 @@ MIX_MODES = context.JK.MIX_MODES
isFree = $(e.target).is('.is_free')
@rest.addJamtrackToShoppingCart(params).done((response) =>
- if(isFree)
- if context.JK.currentUserId?
- context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices
- context.location = '/client#/redeemComplete'
+ if context.JK.currentUserId?
+ if isFree
+ if @user.has_redeemable_jamtrack
+ # this is the 1st jamtrack; let's user the user to completion
+ context.location = '/client#/redeemComplete'
+ else
+ # this is must be a user's gifted jamtrack, to treat them normally in that they'll go to the shopping cart
+ #context.location = '/client#/shoppingCart'
+ context.location = '/client#/redeemComplete'
else
- # now make a rest call to buy it
- context.location = '/client#/redeemSignup'
-
+ # this user has nothing free; so send them to shopping cart
+ context.location = '/client#/shoppingCart'
else
- context.location = '/client#/shoppingCart'
+ if isFree
+ # user not logged in; make them signup
+ context.location = '/client#/redeemSignup'
+ else
+ # this user has nothing free; so send them to shopping cart
+ context.location = '/client#/shoppingCart'
- ).fail(() => @app.ajaxError)
+
+ ).fail(((jqxhr) =>
+
+ handled = false
+ if jqxhr.status == 422
+ body = JSON.parse(jqxhr.responseText)
+ if body.errors && body.errors.base
+ handled = true
+ context.JK.Banner.showAlert("You can not have a mix of free and non-free items in your shopping cart.
If you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.")
+ if !handled
+ @app.ajaxError(arguments[0], arguments[1], arguments[2])
+
+ ))
licenseUSWhy:(e) ->
e.preventDefault()
@@ -510,6 +534,9 @@ MIX_MODES = context.JK.MIX_MODES
if search?
performSearch = true
@search(search.searchType, search.searchData)
+ else
+ if !@state.first_search
+ @search(@state.type, window.JamTrackSearchInput)
if performSearch
if window.history.replaceState #ie9 proofing
@@ -517,11 +544,6 @@ MIX_MODES = context.JK.MIX_MODES
beforeShow: () ->
- @setState({is_free: context.JK.currentUserFreeJamTrack})
- if !@state.first_search
- @search(@state.type, window.JamTrackSearchInput)
-
-
onAppInit: (@app) ->
@@ -530,10 +552,14 @@ MIX_MODES = context.JK.MIX_MODES
@rest = context.JK.Rest()
@logger = context.JK.logger
-
screenBindings =
'beforeShow': @beforeShow
'afterShow': @afterShow
@app.bindScreen('jamtrack/search', screenBindings)
+
+ onUserChanged: (userState) ->
+ @user = userState?.user
+ @setState({is_free: @user?.show_free_jamtrack})
+
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/ManageVstsDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/ManageVstsDialog.js.jsx.coffee
new file mode 100644
index 000000000..508fb203c
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/ManageVstsDialog.js.jsx.coffee
@@ -0,0 +1,88 @@
+context = window
+ConfigureTracksStore = @ConfigureTracksStore
+ConfigureTracksActions = @ConfigureTracksActions
+@ManageVstsDialog = React.createClass({
+
+ mixins: [Reflux.listenTo(@ConfigureTracksStore,"onConfigureTracksChanged"), Reflux.listenTo(@AppStore, "onAppInit")]
+
+ onConfigureTracksChanged:(configureTracks) ->
+ @setState({configureTracks: configureTracks})
+
+ onAppInit: (@app) ->
+
+ getInitialState: () ->
+ {configureTracks: null}
+
+ render: () ->
+
+ if @state.configureTracks?
+ action = 'CLOSE'
+
+ paths = []
+ for path in @state.configureTracks.scanPaths.paths
+ if path.type == "VST"
+ for vstPath in path.paths
+
+ paths.push(`
+ If a scan is not finding the VST or AU plugin you want to use, it’s likely that we aren’t scanning the location where the plugin is installed. Click the ADD SCAN FOLDER button below, and navigate to the folder where the plugin is installed to add that location to the scan list.
+
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.
`
- else if @props.mediaSummary.backingTrackOpen
+ header = `
{mediaType}: {mediaName}
`
+ else if @state.media.mediaSummary.jamTrackOpen || @state.jamTrackState.jamTrack?
+ if @state.media.mediaSummary.isOpener || @state.jamTrackState.jamTrack?
+ # if you opened the JamTrack, then you get all the good info
+ 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
+
+
+ if selectedMixdown?
+ jamTrackTypeHeader = 'Custom Mix'
+
+ disabled = true
+ if selectedMixdown.client_state?
+ switch selectedMixdown.client_state
+ when 'cant_open'
+ customMixName = `
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.
+ 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.
+
+
+
`
+
+ getInitialState: () ->
+ {processing:false}
+
+ componentDidMount:() ->
+ $root = $(this.getDOMNode())
+
+# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (card_type, e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ loggedIn = context.JK.currentUserId?
+
+ rest.addGiftCardToShoppingCart({id: card_type}).done((response) =>
+
+ if loggedIn
+ @setState({done: true})
+ context.location = '/client#/shoppingCart'
+ else
+ @setState({done: true})
+ context.location = '/client#/shoppingCart'
+
+ ).fail((jqXHR, textStatus, errorMessage) =>
+ if jqXHR.status == 422
+ errors = JSON.parse(jqXHR.responseText)
+ cart_errors = errors?.errors?.cart_id
+ context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
+ @setState({processing:false})
+ )
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee
new file mode 100644
index 000000000..605822869
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee
@@ -0,0 +1,177 @@
+context = window
+
+@JamTrackLandingBottomPage = React.createClass({
+
+ render: () ->
+ `
+
+
What Makes JamTracks Awesome?
+
+
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.
+
+
+
+
Leading Musicians & Teachers Love JamTracks
+
+
+
+
+
Andy Crowley of AndyGuitar
+
+
+
+
+
+
Ryan Jones of PianoKeyz
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
Watch A JamTracks Overview Video
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1
Huge, High Quality Multi-Track Catalog
+
+ 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.
+
+
+
+
+
+
+
2
Solo, Mute, Pan or Set Level on Any Part
+
+ 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.
+
+
+
+
+
+
+
3
Make Custom Mixes
+
+ 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.
+
+
+
+
+
+
+
4
Slow Down For Practice
+
+ 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.
+
+
+
+
+
+
+
5
Change Pitch/Key
+
+ 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).
+
+
+
+
+
+
+
6
Apply VST & AU Audio Plug-In Effects
+
+ The free JamKazam app lets you easily apply VST & 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.
+
+
+
+
+
+
+
7
Use MIDI Instruments
+
+ 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 & 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.
+
+
+
+
+
+
+
8
Make & Share Recordings
+
+
+
+
+
+
watch this sample video made by one of our users
+
+
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!
+
+
+
+
+
+
+
9
Play Live In Sync With Others Over the Internet
+
+
+
+
+
+
watch this sample video made by three of our users
+
+
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.
+
+
+
+
+
+
+
10
JamTracks Work With All Your Stuff
+
+ 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.
+
+
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..0150f24e9
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee
@@ -0,0 +1,191 @@
+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 = ``
+ if !hasFree
+ loggedInPriceAdvisory = `
$1.99
`
+ else
+ if !hasFree
+ loggedOutPriceAdvisory = `
$1.99
`
+
+ 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 = `
+
Register for a free account to get this JamTrack free. We will not share your email. See our privacy policy.
by {this.props.jam_track.original_artist.toUpperCase()}
+
+
+
+
+
+
+ Preview JamTrack
+
+
+
Click the play buttons below to preview the master mix and 20-second samples of all the isolated tracks.
+
+
+
+ {loggedInCtaButton}
+ {loggedInPriceAdvisory}
+
+
+ {register}
+
+
+
+
+ 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.
+
Play Music Live in Sync from Different Locations over the Internet
+
+
+ The ability to play live together from different locations enables many new possibilities including:
+
+
+
Rehearse without needing to pack gear, travel, and find rehearsal space
+
+
Co-write new music interactively as if you're sitting in the same room rather than just sharing
+ files, which limits both creativity and speed
+
+
Join open jam sessions any time to play live with others, make new connections, learn, and just have
+ fun playing more music
+
+
+
+ Latency issues have historically prevented musicians from playing together over the Internet, but the
+ JamBlaster and JamKazam's patent pending software innovations have brought this dream to life.
+
+
+
+ To demonstrate the kind of live distributed performances that are possible with the JamBlaster, we flew
+ the members of the band Big Cat to Austin TX, Atlanta GA, Chicago IL, and Brooklyn NY, and had them play
+ together using a JamBlaster at each location. They played from the homes of friends in those cities
+ using normal consumer Internet connections. We used a headphone splitter to record the audio that
+ Malford (singer) heard in real-time while singing, so you can hear exactly what he heard while
+ performing. Watch the video below to see and hear it!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Make Pro Quality Audio Recordings
+
+ The JamBlaster delivers professional quality audio recording, and enables a number of very handy recording scenarios.
+
+
+
Record Yourself
+
+ Plug your instruments and/or microphones into the JamBlaster, and you are ready to record. The JamBlaster automatically records both a master mix and the individual fully isolated stems/tracks from each audio input port, and these audio recordings are saved to the JamBlaster, so they don't use up precious storage space on your smartphone, tablet, or computer. When your recording is complete, the audio files are automatically synched and stored in the JamKazam cloud, so that you can access these audio files from any device, anytime, anywhere, and you can easily share them with others if desired. You can also grab the audio files to use in your favorite DAW (e.g. ProTools, Ableton, GarageBand, etc.) for further editing.
+
+
+ In addition to audio, you can easily make recordings that combine and sync audio from the JamBlaster with video from your smartphone or tablet or from a webcam on your computer to create great videos, and the JamKazam app can upload these videos automatically to YouTube for you.
+
+
+
+
+
Record an Online JamKazam Session
+
+ You can also easily record JamKazam online multiplayer sessions - for example a band rehearsal. When you do this, the recording feature captures the master mix of the combined performance, and also captures the fully isolated audio stems for each musician in the session. When the recording is complete, the high quality stems from all musicians are automatically uploaded to the JamKazam cloud, which then automatically downloads all stems to all musicians who played on the recording. And again in this case, you can easily record video + audio of the full multiplayer session, and upload this to YouTube for easy sharing. Here is an example of a recorded JamKazam session:
+
+
+
+
+
+
+
+
Record a Remote Musician Laying Down a Track
+
+ Producers and composers can use the JamBlaster to open an audio file of a piece of music (work in progress) and play this audio file to a remote musician, who plays along with this audio file to lay down a new track to add to it. When complete, the remote musician's high quality stem audio file(s) are automatically uploaded and accessible to the producer or composer.
+
+
Works With Your Favorite DAW Too
+
+ You can even use the JamBlaster like a traditional audio interface by connecting the JamBlaster with a USB cable to your computer, and you can then record through the JamBlaster straight into your favorite recording app, like ProTools, Ableton, GarageBand, etc.
+
+
+
+
+
+
+
+
Learn And Play Along With 4,000+ Of Your Favorite Songs
Listen to just the single part you want to play to learn it
+
Mute the part you want to play, and play along with the rest of the band
+
Slow down playback to practice without changing the pitch
+
Change the song key by raising or lowering pitch in half steps
+
Save your custom mixes for easy access, and export them to use anywhere
+
Make audio recordings and share via Facebook or URL
+
Make video recordings and share via YouTube
+
Play JamTracks with other musicians online live and in sync
+
+
Here's an example of a YouTube video recording made with a JamTrack by a JamKazam user:
+
+
+
+
+
+
Your first JamTrack is free, and after that JamTracks are just $1.99 each.
+
+
+
+
+
+
+
Teach Or Take Online Music Lessons
+
There are compelling reasons that many music teachers and students want to teach and take music lessons online. Unfortunately Skype, Google Hangouts, and similar apps are horrible for music lessons. Latency is far too high for teacher and student to play together, and audio quality is very poor. Plus these apps don't offer features important to lessons, such as using backing audio tracks in sessions, making quality video/audio recordings of lesson demonstrations by the teacher or performances by the student, and so on.
+
The JamBlaster delivers the ideal platform for online music lessons, with great audio quality, the lowest possible latency, plug-and-play ease of use, and access to all of the JamKazam features for live music performance. Teachers can reach and effectively teach students across great distances, massively increasing the size of the lesson market opportunity for the teacher. Students can connect with the best teacher for their specific needs, rather than settling for the teacher who lives within a 30-minute drive. And both teacher and student can avoid the wasted time and expense of traveling to and from lessons.
+
+
+ Online Music Lesson
+
+
JamKazam is also currently working to develop an online music lesson marketplace, through which we can connect students to teachers. We plan to launch this marketplace in late Q1 2016.
+
+
+
+
+
+
+
Broadcast Live Performances Through YouTube
+
With the JamBlaster, you can broadcast your musical performances live to family, friends, and fans through YouTube, without understanding codecs, without buying streaming software to integrate with YouTube, and so on. Just click a button to broadcast your session, and the JamBlaster takes care of the rest. It combines the pro quality audio of your performance with video from your smartphone, tablet, or computer webcam, and streams this combined video through YouTube Live. You can even schedule a performance and distribute and promote a URL for the live stream in advance of the event, and you can use multiple webcams to cut between different shots/angles during the live broadcast.
+
+
+ YouTube Live Broadcast
+
+
+
+
+
+
+
+
Why Did We Design and Build the JamBlaster?
+
Speed
+
When we initially built the free JamKazam service to let musicians play together live over the Internet, we started by having musicians use the Mac and Windows computers and audio interfaces they already own. We've signed up 20,000+ musicians along the way. We've analyzed data from more than 100,000 online sessions. And we've collected audio processing latency data on thousands of combinations of computers and interfaces, as well as 10 million Internet latency measurements between unique pairs of locations and ISPs. We've learned a lot from all this data.
+
Typically you need to keep total one way latency down to 30 to 35 milliseconds or less in an online session, or the session will get too sloppy and fall apart. We found that the average audio processing latency of industry standard gear is 14 milliseconds (full round trip including analog-to-digital and digital-to-analog conversions). So just processing the audio eats up half of your total latency budget!
+
We designed the JamBlaster from the ground up to be the fastest audio processing device possible, and we have the JamBlaster running at 2.8 milliseconds of latency full round trip - a massive latency savings. Every one millisecond saved on audio processing is worth about 100 miles of range on the Internet backbone. The JamBlaster also reduces something called audio processing jitter, which delivers additional latency savings. The result is that the JamBlaster saves audio latency equivalent to about 1,500 miles of distance compared to today's standard computers and interfaces.
+
Looking at it another way, using JamKazam with standard computers and interfaces, a musician in the U.S. can play effectively with about 10% of the other musicians in the U.S. With the JamBlaster, that same musician can now play with about 35% of the other musicians in the U.S.
+
+
Ease of Use
+
We also found that many users struggled to get their computers and interfaces working properly. Sometimes the operating system was incompatible. Sometimes interface drivers had problems, or the user couldn't figure out how to configure the driver for low latency. And so on and on. We heard from many musicians that they did not love or understand technology very well, and they wanted something that "just worked". The JamBlaster just works. It contains all the hardware, software, drivers, etc. that are needed. You don't have to install any apps or drivers or configure things. You just plug in your instruments and/or microphones and connect it to your network.
+
Post PC Ready
+
Finally, we found that many musicians either have ancient computers, or for younger musicians, no computer at all - often just a smartphone or tablet. The JamBlaster is both a computer and an audio interface in one device, so you don't need a computer to use it. You can control it with your iOS or Android smartphone, or with a standard browser running on a tablet or a Windows, Mac, or Linux computer.
+
+
+
+
+
+
+
How Does the JamBlaster Work?
+
It's important to understand that the JamBlaster is both a computer and an audio interface in one highly optimized device. You simply connect your instruments or microphones into the two input ports, connect the JamBlaster to your network, and plug headphones in to hear the music.
+
To control the JamBlaster, you can run a companion app either on an iPhone or Android smartphone, or you can use the interface at www.jamkazam.com in a standard browser on a tablet or a Windows, Mac, or Linux computer. The companion app or website talks to the JamBlaster over WiFi to tell it what to do. There are no physical connections needed, so you don't have to worry about compatibility problems with your current (or future) mobile devices and computers.
+
+
+
+
+
+
+
+
How Does Latency Work For Online Music Performance?
+
Latency exists in all music performance environments. The speed of sound is so slow (sound travels 1 foot per millisecond) that two musicians sitting 10 feet away from each other will experience 10 milliseconds of delay, or latency, from the time one plays an instrument until the other hears it. The key to enabling online live music performance is to keep latency so low that it doesn't interfere with the ability to play in sync.
+
The Internet backbone is fiber, and data can move across fiber at approximately the speed of light. This is the key to enabling distributed live music performance. With the JamBlaster cutting audio processing latency down so close to zero, all that's left to deal with is Internet latency.
+
In the video above where the band Big Cat played together from Austin, Atlanta, Chicago, and Brooklyn, the latency in that online session was equivalent to having the musicians sit about 25 feet away from each other in a room.
+
Here's a video that explains a little bit more about latency:
+
+
+
+
+
+
+
+
+
+
+
+
What Internet Service Do You Need To Play Online?
+
When you get a JamBlaster, you have a device that you know will "just work". The other thing you need is broadband Internet service. You don't need anything special or unusual. Most home broadband Internet connections today work fine. Here's what to watch for.
+
We've found that both cable broadband and fiber broadband Internet services work well, while low-end DSL service sometimes doesn't work well, and satellite/wireless Internet doesn't work due to high latency.
+
To play in groups of 4 musicians (audio only), you'll want to have Internet service rated at 1Mbps of uplink/upload bandwidth. To play in groups of 4 musicians (video + audio), you'll want to have Internet service rated at 2.5Mbps of uplink/upload bandwidth. If you're not sure what your upload bandwidth is, we'd recommend you go to www.speedtest.net and run the test there.
+
And finally, to play in online, real-time sessions, we recommend avoiding WiFi connections. For the best performance, connect the JamBlaster to your router using an Ethernet cable. If you want to play in a room far away from the room where your home router is located, you can buy a 100-foot Ethernet cable on Amazon for about $10, and then just run it from your router to another room in your house, and then coil it back up and store it when you're done. It's cheap and easy. For recording, live YouTube broadcasts, and JamTracks, WiFi is fine.
+
+
+
+
+
+
+
What Else Do You Need To Use With The JamBlaster?
+
If you sing or play an acoustic instrument like a violin, trumpet, or piano, you'll need to use a microphone to capture the audio from your instrument/voice. If you have an electronic instrument like an electric guitar, bass, or keyboard, you can plug a 1/4" TS or XLR connector into either of the two input ports. The JamBlaster can automatically duplicate mono inputs into stereo signals, so you can use just one port per instrument, and still have stereo audio on each instrument/vocal.
+
If you are a multi-instrumentalist and need more than 2 input ports, we recommend using a simple analog mixer like a Behringer Xenyx 1002B. You can plug all your gear into the mixer, and then connect the audio outputs from the mixer into the inputs on the JamBlaster. Analog mixers don't add any latency.
+
Other than that, you'll need a device that presents the user interface to set up or join sessions, record, and so on. This can either be a smartphone - specifically an iPhone (v8 or later) or Android phone (v4.4 or later). Or you can run a standard browser on a tablet or on a Windows, Mac, or Linux computer. Video services are also provided by your smartphone, tablet, or a computer webcam.
+
+
+
+
+
+
+
The JamBlaster Plugs Into The JamKazam Platform And Community
+
JamKazam has already signed up 20,000+ musicians who play in thousands of online sessions per month using their computers and audio interfaces. The JamBlaster interoperates seamlessly with other musicians who are running Mac and Windows PC setups, so you can jump right in and start playing with other musicians in the community using your JamBlaster from day one.
+
+
+
+
+
+
+
+
JamBlaster Specifications
+
Following are tech specs on the JamBlaster:
+
+
JamBlaster Computing Resources
+ The JamBlaster includes a quad-core 1.2GHz ARM processor, 1GB memory, 8GB storage, and GigE Ethernet network connectivity. It runs a Linux operating system with an optimized real-time kernel, and the entire system has been designed, architected, and optimized end-to-end to deliver blazing audio processing speed.
+
+
JamBlaster Audio Quality
+ The JamBlaster uses premium audio components throughout to deliver superb audio quality on par with audio interfaces that were designed only for recording. The JamBlaster delivers up to 24-bit, 96kHz premium audio. Frequency response for mic/line/instrument inputs is 20Hz – 20kHz +/-0.2dB. THD+N is -107dB for mic inputs and <-100 dB for line and instrument inputs.
+
+
Input Ports
+ The JamBlaster features two Neutrik combo ports on the front, so you can plug in either TS 1/4" or XLR style connectors. Each input port can be individually set to accept line level (lo Z) or instrument level (hi Z) inputs, and each input port can be individually set to supply 48V phantom power if needed.
+
+
Headphone Port
+ The headphone port accepts a standard 1/4" TRS connector from your headphones.
+
+
Chat Mic
+ The horizontal cutout labeled "MIC" on the front of the JamBlaster is a built-in chat mic you can use to talk with other musicians while in sessions.
+
+
Ethernet Port
+ The Ethernet port on the rear of the JamBlaster is a GigE Ethernet port, and the JamBlaster can be connected to your home router using an Ethernet cable (not included) for Internet connectivity. We strongly recommend a hardwired Ethernet connection for the best performance in online distributed sessions. However, if you want to use WiFi for things like making recordings, live broadcasting through YouTube, or playing along with JamTracks, that is totally fine, and we'd recommend a cheap Ethernet-to-WiFi adapter to WiFi-enable the JamBlaster if desired for greater mobility.
+
+
USB Ports
+ The JamBlaster includes 8GB of onboard storage, and the Linux operating system and JamKazam application that run on the JamBlaster use less than 1GB of this storage. For musicians who want more storage capacity for recordings, we plan to support a short list of "certified" USB memory sticks that you can buy and plug into a USB port to expand the JamBlaster's storage capacity.
+
+
Reset Button
+ This button is available in case troubleshooting is needed on the JamBlaster. For example, it can be used to hard reset the box to factory defaults.
+
+
Power Supply
+ The JamBlaster's power supply (included with the JamBlaster) is rated for 100-240VAC 50/60Hz, so it works with both North America's standard 120V/60Hz and Europe's standard 220V/50Hz power infrastructure. Please note that the physical power supply plug is a standard US plug type, so if you live outside the US, you will need to use a plug adapter (not included), but not a power converter.
+
+
Compatibility with Recording Software
+ The JamBlaster may be connected to Windows and Mac computers using a USB cable (not included), and will act as a "normal" audio interface when used in this configuration, so you can use it with your favorite recording software (e.g. ProTools, Ableton, Cubase, Reaper, GarageBand, etc.).
+
+
Controls
+ The JamBlaster is controlled using a companion app that can run on iOS (v8 or later) or Android (v4.4 or later) smartphones. Or it may be controlled using a web interface that runs in a standard browser on tablets or Windows, Mac, or Linux computers.
+
+
+
+
+
+
+
+
+
The JamBlaster Plugs Into The JamKazam Platform And Community
+
JamKazam has already signed up 20,000+ musicians who play in thousands of online sessions per month using their computers and audio interfaces. The JamBlaster interoperates seamlessly with other musicians who are running Mac and Windows PC setups, so you can jump right in and start playing with other musicians in the community using your JamBlaster from day one.
+
+
+
+
+
+
+
+
Ready to Order Your JamBlaster?
+
The JamBlaster had a great run on Kickstarter, as we hit 347% of our Kickstarter revenue goal, but the Kickstarter campaign is over now. We don't have enough cash to have JamBlaster inventory sitting around waiting for orders, so we are using pre-orders to fund our next JamBlaster production run. This is very similar to the way Kickstarter works.
+
Simply stated, you can pre-order a JamBlaster now by clicking the "Pre-Order a JamBlaster" button below and following the on-screen instructions. You will not be charged until we have accumulated enough orders to move forward with our next manufacturing batch. We expect this will be in Q2 2016. When we have enough orders, we will charge everyone together at one time, and we expect we'll be able to build and ship JamBlasters about 30 days after we have charged customers for this next batch.
+
So if you want a JamBlaster, we'd advise you go ahead and place your order now to reserve your place in the next batch.
+
We are currently able to ship to customers in the United States, Canada, the European Union (EU), and Australia. If you have any other questions about the JamBlaster, please email us at support@jamkazam.com.
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee
new file mode 100644
index 000000000..8931b36b8
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee
@@ -0,0 +1,60 @@
+context = window
+rest = context.JK.Rest()
+
+@ProductJamBlasterPage = React.createClass({
+
+ render: () ->
+
+ `
+
+
+
JAMBLASTER
+
by JamKazam
+
+
+
+
See What You Can Do With The JamBlaster
+
+
+
+
+
+
+
+
+
+
+
+
+ With your smartphone and a JamBlaster, you can:
+
+
+
+
Play live in sync with other musicians from different locations over the Internet – great for rehearsals without travel or space, co-writing, or joining open jams for fun
+
Make pro quality audio (and optionally video) recordings of yourself and others – both master mix and fully isolated stems
+
Learn and play along with 4,000+ of your favorite songs – with the ability to solo or mute any part, slow down playback for practice, change pitch/key, and more
+
Teach or take online music lessons that really work – unlike Skype and Google Hangouts, which suffer from very high latency and poor audio quality
+
Broadcast live video performances with pro quality audio through YouTube to family, friends, and fans – either yourself or your band playing in one location, or your online distributed JamKazam sessions