diff --git a/admin/Gemfile b/admin/Gemfile index b5e4ca236..2e1799c5d 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -41,7 +41,7 @@ gem 'uuidtools', '2.1.2' gem 'jquery-rails' # , '2.3.0' # pinned because jquery-ui-rails was split from jquery-rails, but activeadmin doesn't support this gem yet gem 'jquery-ui-rails', '4.2.1' gem 'rails3-jquery-autocomplete' -gem 'activeadmin', '0.6.2' +gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable' gem 'mime-types', '1.25' gem 'meta_search' gem 'fog', "~> 1.18.0" @@ -49,7 +49,7 @@ gem 'unf', '0.1.3' #optional fog dependency gem 'country-select' gem 'aasm', '3.0.16' gem 'postgres-copy', '0.6.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'bugsnag' gem 'gon' gem 'cocoon' @@ -74,6 +74,7 @@ gem 'sanitize' gem 'slim' gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' +gem 'recurly' group :libv8 do gem 'libv8', "~> 3.11.8" diff --git a/admin/app/admin/cohorts.rb b/admin/app/admin/cohorts.rb new file mode 100644 index 000000000..c39ec8048 --- /dev/null +++ b/admin/app/admin/cohorts.rb @@ -0,0 +1,71 @@ +ActiveAdmin.register Cohort, :as => 'Cohorts' do + + menu :label => 'Cohorts All-time', :parent => 'Reports' + + config.sort_order = 'group_start_desc' + config.batch_actions = false + config.clear_action_items! + config.filters = false + config.per_page = 50 + + controller do + + def scoped_collection + objs = super + Cohort.alltime_cohorts! + objs.where(all_time: true).order('group_start DESC') + end + + end + + index :title => "All-Time Cohorts" do + column 'Cohort' do |cc| + div(class: :cohort_col) { cc.group_start_str } + end + + column Cohort::ALLTIME_LABELS[:registered_users] do |cc| + div(class: :cohort_col) { cc.data_val(:registered_users) } + end + + column Cohort::ALLTIME_LABELS[:first_downloaded_client_at] do |cc| + div(class: :cohort_col) { cc.data_val(:first_downloaded_client_at) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:first_downloaded_client_at, true) } end + + column Cohort::ALLTIME_LABELS[:first_certified_gear_at] do |cc| + div(class: :cohort_col) { cc.data_val(:first_certified_gear_at) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:first_certified_gear_at, true) } end + + column Cohort::ALLTIME_LABELS[:music_sessions_user_history] do |cc| + div(class: :cohort_col) { cc.data_val(:music_sessions_user_history) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:music_sessions_user_history, true) } end + + column Cohort::ALLTIME_LABELS[:jam_tracks_played] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_tracks_played) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_tracks_played, true) } end + + column Cohort::ALLTIME_LABELS[:jam_track_rights] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_track_rights) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_track_rights, true) } end + + column Cohort::ALLTIME_LABELS[:recorded_tracks] do |cc| + div(class: :cohort_col) { cc.data_val(:recorded_tracks) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:recorded_tracks, true) } end + + column Cohort::ALLTIME_LABELS[:friendships] do |cc| + div(class: :cohort_col) { cc.data_val(:friendships) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:friendships, true) } end + + column Cohort::ALLTIME_LABELS[:invited_users] do |cc| + div(class: :cohort_col) { cc.data_val(:invited_users) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:invited_users, true) } end + end + +end diff --git a/admin/app/admin/cohorts_monthly.rb b/admin/app/admin/cohorts_monthly.rb new file mode 100644 index 000000000..5c24179c7 --- /dev/null +++ b/admin/app/admin/cohorts_monthly.rb @@ -0,0 +1,96 @@ +ActiveAdmin.register Cohort, :as => 'Cohorts Monthly' do + + menu :label => 'Cohorts Monthly', :parent => 'Reports' + + config.sort_order = 'group_start_desc' + config.batch_actions = false + config.clear_action_items! + config.per_page = 50 + + filter(:monthly_start, as: :select, collection: Cohort.monthly_starts) + + controller do + def scoped_collection + args = params[:q] || {} + Cohort.monthly_cohorts!(Time.parse(args[:monthly_start_eq])) if ! args[:monthly_start_eq].nil? + super.where(all_time: false).order('group_start DESC') + end + end + + index :title => proc { "Monthly Cohorts #{params[:q] ? '('+Time.parse(params[:q][:monthly_start_eq]).strftime('%Y-%m')+')' : ''}" } do + + column 'Cohort' do |cc| + div(class: :cohort_col) { cc.group_start_str } + end + + column Cohort::MONTHLY_LABELS[:registered_users] do |cc| + div(class: :cohort_col) { cc.data_val(:registered_users) } + end + + column Cohort::MONTHLY_LABELS[:first_downloaded_client_at] do |cc| + div(class: :cohort_col) { cc.data_val(:first_downloaded_client_at) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:first_downloaded_client_at, true) } end + + column Cohort::MONTHLY_LABELS[:first_certified_gear_at] do |cc| + div(class: :cohort_col) { cc.data_val(:first_certified_gear_at) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:first_certified_gear_at, true) } end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_1] do |cc| + div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_1) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_1, true) } end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_2_5] do |cc| + div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_2_5) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_2_5, true) } end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_6_] do |cc| + div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_6_) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:music_sessions_user_history_6_, true) } end + + column Cohort::MONTHLY_LABELS[:jam_tracks_played_1] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_tracks_played_1) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_tracks_played_1, true) } end + + column Cohort::MONTHLY_LABELS[:jam_tracks_played_2_5] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_tracks_played_2_5) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_tracks_played_2_5, true) } end + + column Cohort::MONTHLY_LABELS[:jam_tracks_played_6_] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_tracks_played_6_) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_tracks_played_6_, true) } end + + column Cohort::MONTHLY_LABELS[:jam_track_rights_redeemed] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_track_rights_redeemed) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_track_rights_redeemed, true) } end + + column Cohort::MONTHLY_LABELS[:jam_track_rights] do |cc| + div(class: :cohort_col) { cc.data_val(:jam_track_rights) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:jam_track_rights, true) } end + + column Cohort::MONTHLY_LABELS[:recorded_tracks] do |cc| + div(class: :cohort_col) { cc.data_val(:recorded_tracks) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:recorded_tracks, true) } end + + column Cohort::MONTHLY_LABELS[:friendships] do |cc| + div(class: :cohort_col) { cc.data_val(:friendships) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:friendships, true) } end + + column Cohort::MONTHLY_LABELS[:invited_users] do |cc| + div(class: :cohort_col) { cc.data_val(:invited_users) } + end + column '%' do |cc| div(class: :cohort_col) { cc.data_val(:invited_users, true) } end + end + +end diff --git a/admin/app/admin/crash_dumps.rb b/admin/app/admin/crash_dumps.rb index 759f3e656..bcfcd9106 100644 --- a/admin/app/admin/crash_dumps.rb +++ b/admin/app/admin/crash_dumps.rb @@ -3,7 +3,7 @@ ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do filter :timestamp filter :user_email, :as => :string filter :client_id - menu :parent => 'Debug' + menu :parent => 'Misc' index do column "Timestamp" do |post| diff --git a/admin/app/admin/email_batch.rb b/admin/app/admin/email_batch.rb index e31ec5cce..0ceecec4a 100644 --- a/admin/app/admin/email_batch.rb +++ b/admin/app/admin/email_batch.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do - menu :label => 'Batch Emails', :parent => 'Email' + menu :label => 'Batch Emails', :parent => 'Misc' config.sort_order = 'updated_at DESC' config.batch_actions = false diff --git a/admin/app/admin/email_daily_scheduled_session.rb b/admin/app/admin/email_daily_scheduled_session.rb index 94bdfa16c..32e997bb6 100644 --- a/admin/app/admin/email_daily_scheduled_session.rb +++ b/admin/app/admin/email_daily_scheduled_session.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::EmailBatchScheduledSessions, :as => 'Daily Sessions' do - menu :label => 'Daily Sessions', :parent => 'Email' + menu :label => 'Daily Sessions', :parent => 'Misc' config.sort_order = 'updated_at DESC' config.filters = false diff --git a/admin/app/admin/event.rb b/admin/app/admin/event.rb index 186592d30..48981a4d7 100644 --- a/admin/app/admin/event.rb +++ b/admin/app/admin/event.rb @@ -1,3 +1,3 @@ ActiveAdmin.register JamRuby::Event, :as => 'Event' do - menu :parent => 'Events' + menu :parent => 'Misc' end diff --git a/admin/app/admin/event_session.rb b/admin/app/admin/event_session.rb index df540716d..560f7351f 100644 --- a/admin/app/admin/event_session.rb +++ b/admin/app/admin/event_session.rb @@ -1,3 +1,3 @@ ActiveAdmin.register JamRuby::EventSession, :as => 'Event Session' do - menu :parent => 'Events' + menu :parent => 'Misc' end diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb new file mode 100644 index 000000000..75be6c653 --- /dev/null +++ b/admin/app/admin/fake_purchaser.rb @@ -0,0 +1,54 @@ +ActiveAdmin.register_page "Fake Purchaser" do + menu :parent => 'Misc' + + + page_action :bulk_jamtrack_purchase, :method => :post do + + puts params.inspect + + user_field = params[:jam_ruby_jam_track_right][:user] + + if user_field.blank? + redirect_to admin_fake_purchaser_path, :notice => "user not specified" + return + end + + bits = user_field.strip.split(' ') + + user = User.find_by_email(bits[0]) + if user.nil? + redirect_to admin_fake_purchaser_path, :notice =>"no user with email #{bits[0]}" + return + end + + if !user.admin + redirect_to admin_fake_purchaser_path, :notice =>"user is not admin" + return + end + + count = 0 + JamTrack.all.each do |jam_track| + unless jam_track.right_for_user(user) + + jam_track_right=JamTrackRight.new + jam_track_right.user = user + jam_track_right.jam_track = jam_track + jam_track_right.is_test_purchase = true + jam_track_right.save! + count = count + 1 + end + end + + redirect_to admin_fake_purchaser_path, :notice => "Bought #{count} jamtracks for #{user.email}" + end + + content do + + semantic_form_for JamTrackRight.new, :url => admin_fake_purchaser_bulk_jamtrack_purchase_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Admin User to Fake JamTrack Purchases" do + f.input :user, :as => :autocomplete, :url => autocomplete_user_email_admin_users_path, :input_html => { :id_element => "#jam_trak_right_user_id" }, hint: 'All JamTracks in the system will be \'bought\' for this user. No Recurly interaction occurs with this feature.' + end + f.actions + end + end +end diff --git a/admin/app/admin/isp_scoring_data.rb b/admin/app/admin/isp_scoring_data.rb index 02756ffc0..ad93fed9a 100644 --- a/admin/app/admin/isp_scoring_data.rb +++ b/admin/app/admin/isp_scoring_data.rb @@ -2,6 +2,6 @@ ActiveAdmin.register JamRuby::IspScoreBatch, :as => 'Isp Score Data' do config.sort_order = 'created_at_desc' - menu :parent => 'Debug' + menu :parent => 'Misc' end diff --git a/admin/app/admin/jam_ruby_artifact_updates.rb b/admin/app/admin/jam_ruby_artifact_updates.rb index 62185042c..01c0379e1 100644 --- a/admin/app/admin/jam_ruby_artifact_updates.rb +++ b/admin/app/admin/jam_ruby_artifact_updates.rb @@ -1,5 +1,5 @@ ActiveAdmin.register JamRuby::ArtifactUpdate, :as => 'Artifacts' do - menu :label => 'Artifacts' + menu :label => 'Artifacts', :parent => 'Operations' config.sort_order = 'product,environment' #config.batch_actions = false diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 3e8c1dd6b..0fe3c29d9 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -80,6 +80,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do User.select("email, first_name, last_name, id").where(["email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", "%#{parameters[:term]}%", "%#{parameters[:term]}%", "%#{parameters[:term]}%"]) end + def create @jam_ruby_user = JamRuby::User.new(params[:jam_ruby_user]) @jam_ruby_user.administratively_created = true diff --git a/admin/app/admin/jam_track_right.rb b/admin/app/admin/jam_track_right.rb new file mode 100644 index 000000000..48da8231a --- /dev/null +++ b/admin/app/admin/jam_track_right.rb @@ -0,0 +1,87 @@ +require 'jam_ruby/recurly_client' +ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do + + menu :label => 'Purchased JamTracks', :parent => 'JamTracks' + + config.sort_order = 'updated_at DESC' + config.batch_actions = false + + #form :partial => 'form' + + index do + default_actions + + column "Order" do |right| + link_to("Place", order_admin_jam_track_right_path(right)) + " | " + + link_to("Refund", refund_admin_jam_track_right_path(right)) + end + + column "Last Name" do |right| + right.user.last_name + end + column "First Name" do |right| + right.user.first_name + end + column "Jam Track" do |right| + link_to(right.jam_track.name, admin_jam_track_right_path(right.jam_track)) + # right.jam_track + end + column "Plan Code" do |right| + + right.jam_track.plan_code + end + + + end + + form do |f| + f.inputs 'New Jam Track Right' do + f.input :jam_track, :required=>true, collection: JamTrack.all, include_blank: false + f.input :user, :required=>true, collection: User.all, include_blank: false + end + f.actions + end + + member_action :order, :method => :get do + right = JamTrackRight.where("id=?",params[:id]).first + user = right.user + jam_track = right.jam_track + client = RecurlyClient.new + billing_info = { + first_name: user.first_name, + last_name: user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: user.city, + state: user.state, + country: user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' + } + + begin + client.find_or_create_account(user, billing_info) + client.place_order(user, jam_track, nil, nil) + rescue RecurlyClientError=>x + redirect_to admin_jam_track_rights_path, notice: "Could not order #{jam_track} for #{user.to_s}: #{x.errors.inspect}" + else + redirect_to admin_jam_track_rights_path, notice: "Placed order of #{jam_track} for #{user.to_s}." + end + end + + member_action :refund, :method => :get do + right = JamTrackRight.where("id=?",params[:id]).first + client = RecurlyClient.new + + begin + client.refund_user_subscription(right.user, right.jam_track) + rescue RecurlyClientError=>x + redirect_to admin_jam_track_rights_path, notice: "Could not issue refund on #{right.jam_track} for #{right.user.to_s}: #{x.errors.inspect}" + else + redirect_to admin_jam_track_rights_path, notice: "Issued full refund on #{right.jam_track} for #{right.user.to_s}" + end + 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 8e62bbc85..d51e92f31 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -22,29 +22,19 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do links end - - column :id - column :name - column :description - column :initial_play_silence - column :time_signature - column :status - column :recording_type column :original_artist - column :songwriter - column :publisher + column :name + column :onboarding_flags do |jam_track| jam_track.onboard_warnings end + column :status + column :master_track do |jam_track| jam_track.master_track.nil? ? 'None' : (link_to "Download", jam_track.master_track.url_by_sample_rate(44)) end column :licensor - column :pro column :genre - column :sales_region column :price column :reproduction_royalty column :public_performance_royalty column :reproduction_royalty_amount column :licensor_royalty_amount - column :pro_royalty_amount - column :url - column :created_at + column :id column :jam_track_tracks do |jam_track| table_for jam_track.jam_track_tracks.order('position ASC') do diff --git a/admin/app/admin/promo_buzz.rb b/admin/app/admin/promo_buzz.rb index b3fc90514..98c76429c 100644 --- a/admin/app/admin/promo_buzz.rb +++ b/admin/app/admin/promo_buzz.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::PromoBuzz, :as => 'Buzz' do - menu :label => 'Buzz', :parent => 'Home Page' + menu :label => 'Promo Buzz', :parent => 'Misc' config.sort_order = 'position ASC aasm_state DESC updated_at DESC' config.batch_actions = false diff --git a/admin/app/admin/promo_latest.rb b/admin/app/admin/promo_latest.rb index ec0c593ba..9177bdda9 100644 --- a/admin/app/admin/promo_latest.rb +++ b/admin/app/admin/promo_latest.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::PromoLatest, :as => 'Latest' do - menu :label => 'Latest', :parent => 'Home Page' + menu :label => 'Promo Latest', :parent => 'Misc' config.batch_actions = false config.sort_order = '' diff --git a/admin/app/admin/recurly_health.rb b/admin/app/admin/recurly_health.rb new file mode 100644 index 000000000..aebd21250 --- /dev/null +++ b/admin/app/admin/recurly_health.rb @@ -0,0 +1,12 @@ +ActiveAdmin.register_page "Recurly Health" do + menu :parent => 'Misc' + + content :title => "Recurly Transaction Totals" do + table_for Sale.check_integrity_of_jam_track_sales do + column "Total", :total + column "Successes", :succeeded + column "Voids", :voided + end + end + +end \ No newline at end of file diff --git a/admin/app/admin/score_export.rb b/admin/app/admin/score_export.rb index 1387cc17d..076928140 100644 --- a/admin/app/admin/score_export.rb +++ b/admin/app/admin/score_export.rb @@ -1,5 +1,6 @@ -ActiveAdmin.register_page "Download CSV" do - menu :parent => 'Score' +=begin +ActiveAdmin.register_page "Download Score CSV" do + menu :parent => 'Misc' page_action :create_csv, :method => :post do @@ -95,4 +96,5 @@ ActiveAdmin.register_page "Download CSV" do #end end -end \ No newline at end of file +end +=end diff --git a/admin/app/admin/score_history.rb b/admin/app/admin/score_history.rb index 85ed34393..7786b4c29 100644 --- a/admin/app/admin/score_history.rb +++ b/admin/app/admin/score_history.rb @@ -1,3 +1,4 @@ +=begin ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do menu :parent => 'Score' @@ -80,3 +81,4 @@ ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do column "To Client", :to_client_id end end +=end diff --git a/admin/app/admin/scoring_load.rb b/admin/app/admin/scoring_load.rb index f6f3e3b2c..3afcc7219 100644 --- a/admin/app/admin/scoring_load.rb +++ b/admin/app/admin/scoring_load.rb @@ -1,5 +1,5 @@ ActiveAdmin.register_page "Current Scoring Load" do - menu :parent => 'Score' + menu :parent => 'Misc' content :title => "Current Scoring Load" do table_for GetWork.summary do diff --git a/admin/app/assets/stylesheets/active_admin.css.scss b/admin/app/assets/stylesheets/active_admin.css.scss index 48aa7bb98..b71df4545 100644 --- a/admin/app/assets/stylesheets/active_admin.css.scss +++ b/admin/app/assets/stylesheets/active_admin.css.scss @@ -31,3 +31,7 @@ // .active_admin applies to any Active Admin namespace // .admin_namespace applies to the admin namespace (eg: /admin) // .other_namespace applies to a custom namespace named other (eg: /other) + +.cohort_col { + text-align: center; +} diff --git a/admin/app/controllers/checks_controller.rb b/admin/app/controllers/checks_controller.rb new file mode 100644 index 000000000..52330c2aa --- /dev/null +++ b/admin/app/controllers/checks_controller.rb @@ -0,0 +1,19 @@ +class ChecksController < ApplicationController + + #respond_to :json + + # create or update a client_artifact row + def check_latency_tester + + latency_tester_name = params[:name] + + exists = Connection.where(client_type: Connection::TYPE_LATENCY_TESTER).where(client_id: latency_tester_name).first + + if exists + render :text => "", :status => :ok + else + render :text => "", :status => 404 + end + end + +end \ No newline at end of file diff --git a/admin/app/models/cohort.rb b/admin/app/models/cohort.rb new file mode 100644 index 000000000..2b72a3936 --- /dev/null +++ b/admin/app/models/cohort.rb @@ -0,0 +1,363 @@ +require 'date' + +class Cohort < ActiveRecord::Base + + EARLIEST_DATE = Time.parse('2014-03-01') + TOTAL_COHORT_DATE = Time.at(0) + + ALLTIME_LABELS = { + registered_users: 'Registered Users', + first_downloaded_client_at: 'DL app', + first_certified_gear_at: 'Certified Gear', + music_sessions_user_history: 'Played Online', + jam_tracks_played: 'Played JT', + jam_track_rights: 'Purchased JT', + recorded_tracks: 'Made Recording', + friendships: 'Friended', + invited_users: 'Invite Others', + } + + MONTHLY_LABELS = { + registered_users: 'Registered Users', + first_downloaded_client_at: 'DL app', + first_certified_gear_at: 'Certified Gear', + music_sessions_user_history_1: 'Played Online 1', + music_sessions_user_history_2_5: 'Played Online 2-5', + music_sessions_user_history_6_: 'Played Online 6+', + jam_tracks_played_1: 'Played JT 1', + jam_tracks_played_2_5: 'Played JT 2-5', + jam_tracks_played_6_: 'Played JT 6+', + jam_track_rights_redeemed: 'Redeemed JT', + jam_track_rights: 'Purchased JT', + recorded_tracks: 'Made Recording', + friendships: 'Friended', + invited_users: 'Invite Others', + } + + attr_accessible :all_time, :monthly_start + serialize :data_set, JSON + + before_create do + self.data_set ||= {} + end + + def self.date_tuples(from,to) + prec = from.size + start = Date.new(*from) + finish = Date.new(*to) + + filter_on = [:day,:mon].first(3-prec) + filter = ->(d) { filter_on.all? {|attr| d.send(attr) == 1 } } + + (start..finish) + .select(&filter) + .map { |d| [d.year,d.mon,d.day].first(prec) } + end + + def self.earliest_cohort + starting = User.where(admin: false).order(:created_at).first.created_at + starting = EARLIEST_DATE if starting.nil? || starting < EARLIEST_DATE + starting # this is necessary to always return not null + end + + def self.cohort_group_ranges(starting=nil, ending=nil) + starting ||= self.earliest_cohort + ending ||= Time.now + dates = self.date_tuples([starting.year, starting.month], [ending.year, ending.month]) + ranges = [] + dates.each_with_index do |d1, idx| + d2 = dates[idx+1] || [Time.now.next_month.year,Time.now.next_month.month] + rr = Time.parse("#{d1[0]}-#{d1[1]}-1")..(Time.parse("#{d2[0]}-#{d2[1]}-1") - 1.second) + ranges << rr + end + ranges + end + + def self.generate_monthly_cohorts(monthly_start, monthly_end) + Cohort.delete_all(['all_time = ?',false]) + self.cohort_group_ranges.collect do |range| + next if range.first > monthly_end + cc = Cohort.new + cc.group_start = range.first + cc.group_end = range.last + cc.monthly_start = monthly_start + cc.monthly_end = monthly_end + cc.all_time = false + cc.save! + cc + end + end + + def self.generate_all_time_cohorts + Cohort.delete_all("all_time = 't'") + self.cohort_group_ranges.collect do |range| + unless cc = Cohort.where(group_start: range.first).where(all_time: true).limit(1).first + cc = Cohort.new + cc.group_start = range.first + cc.group_end = range.last + cc.all_time = true + cc.save! + end + cc + end + end + + def _put_data_set(key, count, num_user) + self.data_set[key.to_s] = count + xx = (count.to_f / num_user.to_f) + self.data_set["#{key}%"] = 100.0 * xx.round(2) + end + + def self.cohort_users(cohort) + User.where(created_at: cohort.group_start..cohort.group_end) + end + + def _played_online_subquery(constraint) + where = if constraint.is_a?(Range) + "played.cnt >= #{constraint.first} AND played.cnt <= #{constraint.last}" + else + "played.cnt #{constraint}" + end + start_date = all_time ? self.group_start : self.monthly_start + end_date = all_time ? self.group_end : self.monthly_end + sql =<= '#{start_date}' AND + msuh1.created_at <= '#{end_date}' AND + EXTRACT(EPOCH FROM (msuh1.session_removed_at - msuh1.created_at)) >= 900 AND + (SELECT COUNT(*) FROM music_sessions_user_history msuh2 + WHERE msuh1.music_session_id = msuh2.music_session_id + ) > 1 + GROUP BY user_id + ) played +WHERE #{where} +SQL + end + + def _played_jamtrack_subquery(constraint) + where = if constraint.is_a?(Range) + "played.cnt >= #{constraint.first} AND played.cnt <= #{constraint.last}" + else + "played.cnt #{constraint}" + end + start_date = all_time ? self.group_start : self.monthly_start + end_date = all_time ? self.group_end : self.monthly_end + sql =<= '#{start_date}' AND + pp.created_at <= '#{end_date}' AND + pp.playable_type = 'JamRuby::JamTrack' /* VRFS-2916 jam_tracks.id is varchar: ADD */ + GROUP BY player_id + ) played +WHERE #{where} +SQL + end + + def _subquery(assoc_key, num_user) + assoc = User.reflections[assoc_key] + return 0 unless assoc + start_date = all_time ? self.group_start : self.monthly_start + end_date = all_time ? self.group_end : self.monthly_end + sql =<= '#{start_date}' AND + tt.created_at <= '#{end_date}' +SQL + if block_given? + yield_sql = yield(sql) + sql = yield_sql unless yield_sql.blank? + end + self.class.cohort_users(self).where("users.id IN (#{sql})").count + end + + def _monthly! + unless 0 < num_user = self.class.cohort_users(self).count + self.update_attribute(:data_set, {}) + return + end + + self.data_set['registered_users'] = num_user + num_user = num_user.to_f + + qq = self.class.cohort_users(self) + .where(first_downloaded_client_at: self.monthly_start..self.monthly_end) + _put_data_set(:first_downloaded_client_at, qq.count, num_user) + + qq = self.class.cohort_users(self) + .where(first_certified_gear_at: self.monthly_start..self.monthly_end) + _put_data_set(:first_certified_gear_at, qq.count, num_user) + + count = _subquery(assoc_key = :invited_users, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :recorded_tracks, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql| + subsql += " AND tt.is_test_purchase = 'f' AND tt.redeemed = 'f' " + end + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql| + subsql += " AND tt.is_test_purchase = 'f' AND tt.redeemed = 't' " + end + _put_data_set(:jam_track_rights_redeemed, count, num_user) + + count = _subquery(assoc_key = :friendships, num_user) + _put_data_set(assoc_key, count, num_user) + + sql = _played_online_subquery(' = 1 ') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_1, count, num_user) + + sql = _played_online_subquery(2..5) + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_2_5, count, num_user) + + sql = _played_online_subquery(' >= 6') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_6_, count, num_user) + + sql = _played_jamtrack_subquery(' = 1 ') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:jam_tracks_played_1, count, num_user) + + sql = _played_jamtrack_subquery(2..5) + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:jam_tracks_played_2_5, count, num_user) + + sql = _played_jamtrack_subquery(' >= 6') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:jam_tracks_played_6_, count, num_user) + + self.save! + end + + def _all_time! + unless 0 < num_user = self.class.cohort_users(self).count + self.update_attribute(:data_set, {}) + return + end + + self.data_set['registered_users'] = num_user + num_user = num_user.to_f + + count = self.class.cohort_users(self) + .where(['first_downloaded_client_at IS NOT NULL']) + .count + _put_data_set('first_downloaded_client_at', count, num_user) + + count = self.class.cohort_users(self) + .where(['first_certified_gear_at IS NOT NULL']) + .count + _put_data_set('first_certified_gear_at', count, num_user) + + count = _subquery(assoc_key = :invited_users, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :recorded_tracks, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :friendships, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql| + subsql += " AND tt.is_test_purchase = 'f'" + end + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_tracks_played, num_user) do |subsql| + # VRFS-2916 jam_tracks.id is varchar: REMOVE + # subsql += " AND tt.jam_track_id IS NOT NULL " + # VRFS-2916 jam_tracks.id is varchar: ADD + subsql += " AND tt.playable_type = 'JamRuby::JamTrack' " + end + _put_data_set(assoc_key, count, num_user) + + sql = _played_online_subquery(' >= 1') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history, count, num_user) + + self.save! + end + + def populate! + self.all_time ? _all_time! : _monthly! + end + + def calculate_totals(cohorts) + self.group_start = self.group_end = TOTAL_COHORT_DATE + labels = all_time ? Cohort::ALLTIME_LABELS : Cohort::MONTHLY_LABELS + + self.data_set = labels.inject({}) do |hh, (kk,vv)| + hh[kk.to_s] = hh["#{kk}%"] = 0 + hh + end + + labels = labels.keys.map(&:to_s) + cohorts.each do |cc| + labels.each do |key| + self.data_set[key] += cc.data_set[key].to_i + end + end + + user_total = self.data_set['registered_users'].to_f + labels.delete('registered_users') + labels.each do |key| + xx = (self.data_set[key].to_f / user_total) + self.data_set["#{key}%"] = 100.0 * xx.round(4) + end + + self.save! + cohorts << self + end + + def self.alltime_cohorts! + cohorts = Cohort.generate_all_time_cohorts.each do |cc| + cc._all_time! + end + Cohort.new(all_time: true).calculate_totals(cohorts) + end + + def self.monthly_cohorts!(monthly_start, monthly_end=nil) + monthly_end ||= monthly_start + 1.month - 1.second + cohorts = self.generate_monthly_cohorts(monthly_start, monthly_end).compact.each do |cc| + cc._monthly! + end + Cohort.new(all_time: false, monthly_start: monthly_start).calculate_totals(cohorts) + end + + def group_start_str + is_total_cohort? ? 'Total' : self.group_start.strftime('%Y-%m') + end + + def group_end_str + self.group_end.strftime('%Y-%m-%d') + end + + def data_val(col, percent=false) + if percent + val = self.data_set["#{col}%"] + val ? "#{'%0.f' % val}%" : '' + else + self.data_set[col.to_s] + end + end + + def self.monthly_starts + self.cohort_group_ranges.collect do |rr| + rr.first.to_s + end + end + + def is_total_cohort? + self.group_start == TOTAL_COHORT_DATE + end + +end + diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index 16e53837d..51341d812 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -2,37 +2,36 @@ = f.semantic_errors *f.object.errors.keys = f.inputs name: 'JamTrack fields' do = f.input :name, :input_html => { :rows=>1, :maxlength=>200 } - b style='margin-left:10px' - i - | JamTrack should only be made available (to end users) if all its sub-component are in place: - = f.input :available, as: :boolean = f.input :description, :input_html => { :rows=>5, :maxlength=>1000 } - = f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' - = f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: false - = f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false + = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly' + //= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' + = f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: true + = f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false, hint: 'Only set to Production when end users should be able to purchase this JamTrack' = f.input :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE, include_blank: false - = f.input :original_artist, :input_html => { :rows=>2, :maxlength=>200 } - = f.input :songwriter, :input_html => { :rows=>5, :maxlength=>1000 } - = f.input :publisher, :input_html => { :rows=>5, :maxlength=>1000 } - = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: false - = f.input :pro, collection: JamRuby::JamTrack::PRO, include_blank: false + = f.input :original_artist, :input_html => { :rows=>1, :maxlength=>1000 } + = f.input :songwriter, :input_html => { :rows=>1, :maxlength=>1000 } + = f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 } + = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true = f.input :genre, collection: JamRuby::Genre.all, include_blank: false + = f.input :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'} + = f.input :price, :required => true, :input_html => {type: 'numeric'} + = f.input :pro_ascap, :label => 'ASCAP royalties due?' + = f.input :pro_bmi, :label => 'BMI royalties due?' + = f.input :pro_sesac, :label => 'SESAC royalties due?' = f.input :reproduction_royalty, :label => 'Reproduction Royalty' = f.input :public_performance_royalty, :label => 'Public Performance Royalty' = f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'} = f.input :licensor_royalty_amount, :required=>true, :input_html=>{type:'numeric'} - = f.input :pro_royalty_amount, :required=>true, :input_html=>{type:'numeric'} - = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true - = f.input :url, :as => :file, :label => 'Audio File' + + //= f.input :url, :as => :file, :label => 'Audio File' + = f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 }, :hint => 'Tap-Ins & Lead Silence. Examples: https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=39289025#JamKazamMeta-EventProcessor(JMEP)-CommonExamples' + = f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly => true }, :hint => 'Readonly field. This is shown here just so you can see what your JMEP got converted to readily' + = f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the media in the JamTrack. Changing JMEP does not count as a version change; changing anything about a track (audio, instrument, part) does.' = f.semantic_fields_for :jam_track_tracks do |track| = render 'jam_track_track_fields', f: track - = f.semantic_fields_for :jam_track_tap_ins do |tap_in| - = render 'jam_track_tap_in_fields', f: tap_in .links = link_to_add_association 'Add Track', f, :jam_track_tracks, class: 'button', style: 'margin:20px;padding:10px 20px' - = link_to_add_association 'Add Tap In', f, :jam_track_tap_ins, class: 'button', style: 'margin:20px;padding:10px 20px' = f.actions \ No newline at end of file diff --git a/admin/app/views/admin/jam_tracks/_jam_track_right_fields.html.slim b/admin/app/views/admin/jam_tracks/_jam_track_right_fields.html.slim new file mode 100644 index 000000000..9bf130e56 --- /dev/null +++ b/admin/app/views/admin/jam_tracks/_jam_track_right_fields.html.slim @@ -0,0 +1,5 @@ += f.inputs name: 'Jam Track Right fields' do + + ol.nested-fields + = f.input :jam_track, :required=>true, collection: JamTrack.all, include_blank: false + = f.input :user, :required=>true, collection: User.all, include_blank: false diff --git a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim index 0503f4b54..4439c9550 100644 --- a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim +++ b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim @@ -1,21 +1,35 @@ = f.inputs name: 'Track fields' do ol.nested-fields - = f.input :track_type, :as => :select, collection: JamRuby::JamTrackTrack::TRACK_TYPE, include_blank: false + = f.input :track_type, :as => :select, collection: ['Track', 'Master'], include_blank: false = f.input :instrument, collection: Instrument.all, include_blank: false = f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' } - = f.input :position + - if !f.object.nil? && f.object.track_type != 'Master' + = f.input :preview_start_time_raw, :label => 'Preview Start Time', :hint => 'MM:SS:MLS', :as => :string + - unless f.object.nil? || f.object[:preview_url].nil? + .current_file_holder style='margin-bottom:10px' + a href=f.object.preview_public_url('ogg') style='padding:0 0 0 20px' + | Download Preview (ogg) + a href=f.object.preview_public_url('mp3') style='padding:0 0 0 20px' + | Download Preview (mp3) + // temporarily disable - if f.object.new_record? - p style='margin-left:10px' - i - | before you can upload, you must select 'Update JamTrack' + //p style='margin-left:10px' + //i + // | before you can upload, you must select 'Update JamTrack' - else - = f.input :url, :as => :file, :label => 'Track file' - - unless f.object.nil? || f.object[:url].nil? + // = f.input :url_48, :as => :file, :label => 'Track file (48kHz)' + - unless f.object.nil? || f.object[:url_48].nil? .current_file_holder style='margin-bottom:10px' a href=f.object.sign_url(3600) style='padding:0 0 0 20px' - | Download + | #{File.basename(f.object[:url_48])} + + // = f.input :url_44, :as => :file, :label => 'Track file (44kHz)' + - unless f.object.nil? || f.object[:url_44].nil? + .current_file_holder style='margin-bottom:10px' + a href=f.object.sign_url(3600, 44) style='padding:0 0 0 20px' + | #{File.basename(f.object[:url_44])} = link_to_remove_association "Delete Track", f, class: 'button', style: 'margin-left:10px' \ No newline at end of file diff --git a/admin/config/application.rb b/admin/config/application.rb index 684f30d7e..102a03176 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -147,5 +147,8 @@ module JamAdmin config.influxdb_hosts = ["localhost"] config.influxdb_port = 8086 config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development'] + + config.jamtracks_dir = ENV['JAMTRACKS_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jamtracks")) + config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep")) end end diff --git a/admin/config/initializers/jam_track_tracks.rb b/admin/config/initializers/jam_track_tracks.rb new file mode 100644 index 000000000..b05126d52 --- /dev/null +++ b/admin/config/initializers/jam_track_tracks.rb @@ -0,0 +1,138 @@ +class JamRuby::JamTrackTrack + + # add a custom validation + + attr_accessor :preview_generate_error + + validate :preview + + def preview + if preview_generate_error + errors.add(:preview_start_time, preview_generate_error) + end + end + + + # this is used by active admin/jam-admin + def preview_start_time_raw + if self.preview_start_time.nil? || self.preview_start_time.nil? + '' + else + seconds = self.preview_start_time.to_f/1000 + time = Time.at(seconds) + time.strftime("%M:%S:#{(self.preview_start_time % 1000).to_s.rjust(3, '0')}") + end + end + + # this is used by active admin/jam-admin + def preview_start_time_raw=(new_value) + + value = nil + if new_value == nil || new_value == '' + value = nil + else + if new_value && new_value.kind_of?(String) && new_value.include?(':') + bits = new_value.split(':') + if bits.length != 3 + raise "format of preview start time must be MM:SS:MLS" + end + + value = (bits[0].to_i * 60000) + (bits[1].to_i * 1000) + (bits[2].to_i) + + else + raise "format of preview start time must be MM:SS:MLS" + end + end + + if !value.nil? && value != self.preview_start_time + self.preview_start_time = value + generate_preview + else + self.preview_start_time = value + end + end + + def generate_preview + + begin + Dir.mktmpdir do |tmp_dir| + + input = File.join(tmp_dir, 'in.ogg') + output = File.join(tmp_dir, 'out.ogg') + output_mp3 = File.join(tmp_dir, 'out.mp3') + + start = self.preview_start_time.to_f / 1000 + stop = start + 20 + + raise 'no track' unless self["url_44"] + + s3_manager.download(self.url_by_sample_rate(44), input) + + command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" + + @@log.debug("trimming using: " + command) + + sox_output = `#{command}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute cut command #{sox_output}" + else + # now create mp3 off of ogg preview + + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" + + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" + else + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + # and finally update the JamTrackTrack with the new info + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + + end + end + end + rescue Exception => e + @@log.error("error in sox command #{e.to_s}") + @preview_generate_error = e.to_s + end + + end + +end diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb new file mode 100644 index 000000000..cd02eccee --- /dev/null +++ b/admin/config/initializers/jam_tracks.rb @@ -0,0 +1,30 @@ +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.genre_id = nil if self.genre_id == '' + self.licensor_id = nil if self.licensor_id == '' + self.jmep_json = nil if self.jmep_json == '' + self.time_signature = nil if self.time_signature == '' + + 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/admin/config/routes.rb b/admin/config/routes.rb index 05aed06bc..bae57c044 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -32,6 +32,7 @@ JamAdmin::Application.routes.draw do match '/api/artifacts' => 'artifacts#update_artifacts', :via => :post match '/api/mix/:id/enqueue' => 'admin/mixes#mix_again', :via => :post + match '/api/checks/latency_tester' => 'checks#check_latency_tester', :via => :get mount Resque::Server.new, :at => "/resque" diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index 08f668ff3..d6888a6bc 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -11,6 +11,7 @@ FactoryGirl.define do state "NC" country "US" terms_of_service true + reuse_card true factory :admin do @@ -40,6 +41,7 @@ FactoryGirl.define do scoring_timeout Time.now sequence(:channel_id) { |n| "Channel#{n}"} association :user, factory: :user + metronome_open false end factory :artifact_update, :class => JamRuby::ArtifactUpdate do @@ -223,7 +225,6 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } - bpm 100.1 time_signature '4/4' status 'Production' recording_type 'Cover' diff --git a/build b/build index 6d408c180..db8ab33a4 100755 --- a/build +++ b/build @@ -66,7 +66,7 @@ DEB_SERVER=http://localhost:9010/apt-`uname -p` GEM_SERVER=http://localhost:9000/gems # if still going, then push all debs up - if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* ]]; then + if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* || "$GIT_BRANCH" == *feature* || "$GIT_BRANCH" == *hotfix* ]]; then echo "" echo "PUSHING DB ARTIFACTS" diff --git a/db/manifest b/db/manifest index fdfec768a..5a4689017 100755 --- a/db/manifest +++ b/db/manifest @@ -246,4 +246,34 @@ text_message_migration.sql user_model_about_changes.sql performance_samples.sql user_presences.sql -discard_scores_optimized.sql \ No newline at end of file +discard_scores_optimized.sql +backing_tracks.sql +metronome.sql +recorded_backing_tracks.sql +recorded_backing_tracks_add_filename.sql +user_syncs_include_backing_tracks.sql +remove_bpm_from_jamtracks.sql +widen_user_authorization_token.sql +jam_track_version.sql +recorded_jam_track_tracks.sql +jam_track_jmep_data.sql +add_jam_track_bitrates.sql +jam_track_importer.sql +jam_track_pro_licensing_update.sql +jam_track_redeemed.sql +connection_metronome.sql +preview_jam_track_tracks.sql +cohorts.sql +jam_track_right_admin_purchase.sql +jam_track_playable_plays.sql +shopping_cart_anonymous.sql +user_reuse_card_and_reedem.sql +jam_track_id_to_varchar.sql +drop_position_unique_jam_track.sql +recording_client_metadata.sql +preview_support_mp3.sql +jam_track_duration.sql +sales.sql +show_whats_next_count.sql +recurly_adjustments.sql +signup_hints.sql \ No newline at end of file diff --git a/db/up/add_jam_track_bitrates.sql b/db/up/add_jam_track_bitrates.sql new file mode 100644 index 000000000..aadfd2ead --- /dev/null +++ b/db/up/add_jam_track_bitrates.sql @@ -0,0 +1,14 @@ +ALTER TABLE jam_track_tracks RENAME COLUMN url TO url_48; +ALTER TABLE jam_track_tracks RENAME COLUMN md5 TO md5_48; +ALTER TABLE jam_track_tracks RENAME COLUMN length TO length_48; +ALTER TABLE jam_track_tracks ADD COLUMN url_44 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN md5_44 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN length_44 BIGINT; + +ALTER TABLE jam_track_rights RENAME COLUMN url TO url_48; +ALTER TABLE jam_track_rights RENAME COLUMN md5 TO md5_48; +ALTER TABLE jam_track_rights RENAME COLUMN length TO length_48; +ALTER TABLE jam_track_rights ADD COLUMN url_44 VARCHAR; +ALTER TABLE jam_track_rights ADD COLUMN md5_44 VARCHAR; +ALTER TABLE jam_track_rights ADD COLUMN length_44 BIGINT; + diff --git a/db/up/backing_tracks.sql b/db/up/backing_tracks.sql new file mode 100644 index 000000000..328b36820 --- /dev/null +++ b/db/up/backing_tracks.sql @@ -0,0 +1,2 @@ +ALTER TABLE active_music_sessions ADD COLUMN backing_track_path VARCHAR(1024); +ALTER TABLE active_music_sessions ADD COLUMN backing_track_initiator_id VARCHAR(64); diff --git a/db/up/cohorts.sql b/db/up/cohorts.sql new file mode 100644 index 000000000..3a7cbbb5b --- /dev/null +++ b/db/up/cohorts.sql @@ -0,0 +1,19 @@ +CREATE TABLE cohorts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + + data_set JSON NOT NULL DEFAULT '{}', + + group_start TIMESTAMP NOT NULL, + group_end TIMESTAMP NOT NULL, + + all_time BOOLEAN NOT NULL DEFAULT FALSE, + monthly_start TIMESTAMP, + monthly_end TIMESTAMP, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX index_group_date ON cohorts USING btree (group_start); + +CREATE INDEX msuh_music_session_idx ON music_sessions_user_history USING btree(music_session_id); diff --git a/db/up/connection_metronome.sql b/db/up/connection_metronome.sql new file mode 100644 index 000000000..78ed3219e --- /dev/null +++ b/db/up/connection_metronome.sql @@ -0,0 +1 @@ +ALTER TABLE connections ADD COLUMN metronome_open BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/drop_position_unique_jam_track.sql b/db/up/drop_position_unique_jam_track.sql new file mode 100644 index 000000000..6055702d4 --- /dev/null +++ b/db/up/drop_position_unique_jam_track.sql @@ -0,0 +1 @@ +DROP INDEX jam_track_tracks_position_uniqkey; \ No newline at end of file diff --git a/db/up/jam_track_duration.sql b/db/up/jam_track_duration.sql new file mode 100644 index 000000000..a4dbee409 --- /dev/null +++ b/db/up/jam_track_duration.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks ADD COLUMN duration INTEGER; \ No newline at end of file diff --git a/db/up/jam_track_id_to_varchar.sql b/db/up/jam_track_id_to_varchar.sql new file mode 100644 index 000000000..9a2c0cdfd --- /dev/null +++ b/db/up/jam_track_id_to_varchar.sql @@ -0,0 +1,25 @@ +-- change jam_tracks PRIMARY KEY to VARCHAR(64) + +-- first, drop all constraints and change the types +ALTER TABLE jam_track_tracks DROP CONSTRAINT jam_track_tracks_jam_track_id_fkey; +ALTER TABLE jam_track_tracks ALTER COLUMN jam_track_id TYPE VARCHAR(64); +ALTER TABLE jam_track_tap_ins DROP CONSTRAINT jam_track_tap_ins_jam_track_id_fkey; +ALTER TABLE jam_track_tap_ins ALTER COLUMN jam_track_id TYPE VARCHAR(64); +ALTER TABLE jam_track_rights DROP CONSTRAINT jam_track_rights_jam_track_id_fkey; +ALTER TABLE jam_track_rights ALTER COLUMN jam_track_id TYPE VARCHAR(64); +ALTER TABLE active_music_sessions ALTER COLUMN jam_track_id TYPE VARCHAR(64); +ALTER TABLE recordings DROP CONSTRAINT recordings_jam_track_id_fkey; +ALTER TABLE recordings ALTER COLUMN jam_track_id TYPE VARCHAR(64); +ALTER TABLE playable_plays DROP COLUMN jam_track_id; + + +-- then drop the jamtrack sequence, change it's type, and then set default to UUID +-- DROP SEQUENCE jam_tracks_next_seq; +ALTER TABLE jam_tracks ALTER COLUMN id TYPE VARCHAR(64); +ALTER TABLE jam_tracks ALTER COLUMN id SET DEFAULT uuid_generate_v4(); + +-- add back in all the constraints on the fk tables +ALTER TABLE jam_track_tracks ADD CONSTRAINT jam_track_tracks_jam_track_id_fkey FOREIGN KEY (jam_track_id) REFERENCES jam_tracks(id) ON DELETE CASCADE; +ALTER TABLE jam_track_tap_ins ADD CONSTRAINT jam_track_tap_ins_jam_track_id_fkey FOREIGN KEY (jam_track_id) REFERENCES jam_tracks(id) ON DELETE CASCADE; +ALTER TABLE jam_track_rights ADD CONSTRAINT jam_track_rights_jam_track_id_fkey FOREIGN KEY (jam_track_id) REFERENCES jam_tracks(id); +ALTER TABLE recordings ADD CONSTRAINT recordings_jam_track_id_fkey FOREIGN KEY (jam_track_id) REFERENCES jam_tracks(id); diff --git a/db/up/jam_track_importer.sql b/db/up/jam_track_importer.sql new file mode 100644 index 000000000..161656560 --- /dev/null +++ b/db/up/jam_track_importer.sql @@ -0,0 +1,5 @@ +ALTER TABLE jam_tracks DROP COLUMN available; +ALTER TABLE jam_tracks DROP COLUMN initial_play_silence; +ALTER TABLE jam_tracks ADD COLUMN metalocation VARCHAR UNIQUE; +ALTER TABLE jam_tracks ADD CONSTRAINT plan_code_unique UNIQUE (plan_code); +ALTER TABLE jam_track_rights ADD COLUMN recurly_subscription_uuid VARCHAR; \ No newline at end of file diff --git a/db/up/jam_track_jmep_data.sql b/db/up/jam_track_jmep_data.sql new file mode 100644 index 000000000..46c6e67b5 --- /dev/null +++ b/db/up/jam_track_jmep_data.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_tracks ADD COLUMN jmep_text VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN jmep_json JSON; \ No newline at end of file diff --git a/db/up/jam_track_playable_plays.sql b/db/up/jam_track_playable_plays.sql new file mode 100644 index 000000000..dfa95d07d --- /dev/null +++ b/db/up/jam_track_playable_plays.sql @@ -0,0 +1,6 @@ +ALTER TABLE playable_plays ADD COLUMN jam_track_id bigint; +ALTER TABLE playable_plays ALTER COLUMN playable_id DROP NOT NULL; +ALTER TABLE playable_plays ALTER COLUMN playable_type DROP NOT NULL; + + + diff --git a/db/up/jam_track_pro_licensing_update.sql b/db/up/jam_track_pro_licensing_update.sql new file mode 100644 index 000000000..006483f44 --- /dev/null +++ b/db/up/jam_track_pro_licensing_update.sql @@ -0,0 +1,12 @@ +ALTER TABLE jam_tracks ADD COLUMN pro_ascap BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE jam_tracks ADD COLUMN pro_bmi BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE jam_tracks ADD COLUMN pro_sesac BOOLEAN DEFAULT FALSE NOT NULL; +UPDATE jam_tracks SET pro_ascap = TRUE WHERE pro = 'ASCAP'; +UPDATE jam_tracks SET pro_bmi = TRUE WHERE pro = 'BMI'; +UPDATE jam_tracks SET pro_sesac = TRUE WHERE pro = 'SESAC'; +ALTER TABLE jam_tracks DROP COLUMN pro; +ALTER TABLE jam_tracks DROP Column pro_royalty_amount; +ALTER TABLE jam_tracks ADD COLUMN preview_start_time INTEGER; +ALTER TABLE jam_tracks RENAME COLUMN url TO preview_url; +ALTER TABLE jam_tracks RENAME COLUMN md5 TO preview_md5; +ALTER TABLE jam_tracks RENAME COLUMN length TO preview_length; diff --git a/db/up/jam_track_redeemed.sql b/db/up/jam_track_redeemed.sql new file mode 100644 index 000000000..8a09cd56f --- /dev/null +++ b/db/up/jam_track_redeemed.sql @@ -0,0 +1 @@ +ALTER TABLE jam_track_rights ADD COLUMN redeemed BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/jam_track_right_admin_purchase.sql b/db/up/jam_track_right_admin_purchase.sql new file mode 100644 index 000000000..aec1d5cfa --- /dev/null +++ b/db/up/jam_track_right_admin_purchase.sql @@ -0,0 +1 @@ +ALTER TABLE jam_track_rights ADD COLUMN is_test_purchase BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/db/up/jam_track_version.sql b/db/up/jam_track_version.sql new file mode 100644 index 000000000..7c1efff86 --- /dev/null +++ b/db/up/jam_track_version.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks ADD COLUMN version VARCHAR NOT NULL DEFAULT 0; diff --git a/db/up/metronome.sql b/db/up/metronome.sql new file mode 100644 index 000000000..25d18dc57 --- /dev/null +++ b/db/up/metronome.sql @@ -0,0 +1,2 @@ +ALTER TABLE active_music_sessions ADD COLUMN metronome_active BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE active_music_sessions ADD COLUMN metronome_initiator_id VARCHAR(64); \ No newline at end of file diff --git a/db/up/preview_jam_track_tracks.sql b/db/up/preview_jam_track_tracks.sql new file mode 100644 index 000000000..a8c9ae224 --- /dev/null +++ b/db/up/preview_jam_track_tracks.sql @@ -0,0 +1,9 @@ +ALTER TABLE jam_tracks DROP COLUMN preview_url; +ALTER TABLE jam_tracks DROP COLUMN preview_md5; +ALTER TABLE jam_tracks DROP COLUMN preview_length; +ALTER TABLE jam_tracks DROP COLUMN preview_start_time; + +ALTER TABLE jam_track_tracks ADD COLUMN preview_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_length BIGINT; +ALTER TABLE jam_track_tracks ADD COLUMN preview_start_time INTEGER; \ No newline at end of file diff --git a/db/up/preview_support_mp3.sql b/db/up/preview_support_mp3.sql new file mode 100644 index 000000000..90dd1c642 --- /dev/null +++ b/db/up/preview_support_mp3.sql @@ -0,0 +1,4 @@ +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT; +UPDATE jam_track_tracks SET preview_url = NULL where track_type = 'Master'; \ No newline at end of file diff --git a/db/up/recorded_backing_tracks.sql b/db/up/recorded_backing_tracks.sql new file mode 100644 index 000000000..6657dee1a --- /dev/null +++ b/db/up/recorded_backing_tracks.sql @@ -0,0 +1,38 @@ +CREATE UNLOGGED TABLE backing_tracks ( + id VARCHAR(64) NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + filename VARCHAR(1024) NOT NULL, + + connection_id VARCHAR(64) NOT NULL REFERENCES connections(id) ON DELETE CASCADE, + client_track_id VARCHAR(64) NOT NULL, + client_resource_id VARCHAR(100), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE recorded_backing_tracks ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + backing_track_id VARCHAR(64), + recording_id VARCHAR(64) NOT NULL, + + client_track_id VARCHAR(64) NOT NULL, + is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE, + next_part_to_upload INTEGER NOT NULL DEFAULT 0, + upload_id CHARACTER VARYING(1024), + part_failures INTEGER NOT NULL DEFAULT 0, + discard BOOLEAN, + download_count INTEGER NOT NULL DEFAULT 0, + md5 CHARACTER VARYING(100), + length BIGINT, + client_id VARCHAR(64) NOT NULL, + file_offset BIGINT, + + url VARCHAR(1024) NOT NULL, + fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE, + upload_failures INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE recorded_backing_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); diff --git a/db/up/recorded_backing_tracks_add_filename.sql b/db/up/recorded_backing_tracks_add_filename.sql new file mode 100644 index 000000000..e686700b7 --- /dev/null +++ b/db/up/recorded_backing_tracks_add_filename.sql @@ -0,0 +1,2 @@ +ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL; +ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/db/up/recorded_jam_track_tracks.sql b/db/up/recorded_jam_track_tracks.sql new file mode 100644 index 000000000..08a485643 --- /dev/null +++ b/db/up/recorded_jam_track_tracks.sql @@ -0,0 +1,15 @@ +ALTER TABLE recordings ADD COLUMN jam_track_id BIGINT REFERENCES jam_tracks(id); +ALTER TABLE recordings ADD COLUMN jam_track_initiator_id VARCHAR(64) REFERENCES users(id); + +CREATE TABLE recorded_jam_track_tracks ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jam_track_track_id VARCHAR(64) REFERENCES jam_track_tracks(id) NOT NULL, + recording_id VARCHAR(64) REFERENCES recordings(id) NOT NULL, + discard BOOLEAN, + timeline JSON, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE recorded_jam_track_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); diff --git a/db/up/recording_client_metadata.sql b/db/up/recording_client_metadata.sql new file mode 100644 index 000000000..f800f234a --- /dev/null +++ b/db/up/recording_client_metadata.sql @@ -0,0 +1 @@ +ALTER TABLE recordings ADD COLUMN timeline JSON; \ No newline at end of file diff --git a/db/up/recurly_adjustments.sql b/db/up/recurly_adjustments.sql new file mode 100644 index 000000000..dc31d21e9 --- /dev/null +++ b/db/up/recurly_adjustments.sql @@ -0,0 +1,24 @@ +ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_uuid VARCHAR(500); +ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500); +ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_uuid VARCHAR(500); +ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500); +ALTER TABLE sales ADD COLUMN recurly_invoice_id VARCHAR(500) UNIQUE; +ALTER TABLE sales ADD COLUMN recurly_invoice_number INTEGER; + +ALTER TABLE sales ADD COLUMN recurly_subtotal_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_tax_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_total_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_currency VARCHAR; + +ALTER TABLE sale_line_items ADD COLUMN recurly_tax_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN recurly_total_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN recurly_currency VARCHAR; +ALTER TABLE sale_line_items ADD COLUMN recurly_discount_in_cents INTEGER; + +ALTER TABLE sales ADD COLUMN sale_type VARCHAR NOT NULL DEFAULT 'jamtrack'; + +ALTER TABLE recurly_transaction_web_hooks ALTER COLUMN subscription_id DROP NOT NULL; + +CREATE INDEX recurly_transaction_web_hooks_invoice_id_ndx ON recurly_transaction_web_hooks(invoice_id); + +ALTER TABLE jam_track_rights DROP COLUMN recurly_subscription_uuid; \ No newline at end of file diff --git a/db/up/remove_bpm_from_jamtracks.sql b/db/up/remove_bpm_from_jamtracks.sql new file mode 100644 index 000000000..681c3b8b8 --- /dev/null +++ b/db/up/remove_bpm_from_jamtracks.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks DROP COLUMN bpm; \ No newline at end of file diff --git a/db/up/sales.sql b/db/up/sales.sql new file mode 100644 index 000000000..fc8f118a6 --- /dev/null +++ b/db/up/sales.sql @@ -0,0 +1,49 @@ +CREATE TABLE sales ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + order_total DECIMAL NOT NULL DEFAULT 0, + shipping_info JSON, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE sale_line_items ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + product_type VARCHAR NOT NULL, + product_id VARCHAR(64), + unit_price DECIMAL NOT NULL, + quantity INTEGER NOT NULL, + free INTEGER NOT NULL, + sales_tax DECIMAL, + shipping_handling DECIMAL NOT NULL, + recurly_plan_code VARCHAR NOT NULL, + recurly_subscription_uuid VARCHAR, + sale_id VARCHAR(64) NOT NULL REFERENCES sales(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE recurly_transaction_web_hooks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + recurly_transaction_id VARCHAR NOT NULL, + transaction_type VARCHAR NOT NULL, + subscription_id VARCHAR NOT NULL, + action VARCHAR NOT NULL, + status VARCHAR NOT NULL, + amount_in_cents INT, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + invoice_id VARCHAR, + invoice_number_prefix VARCHAR, + invoice_number INTEGER, + message VARCHAR, + reference VARCHAR, + transaction_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + + +CREATE UNIQUE INDEX sale_line_items_recurly_subscription_uuid_ndx ON sale_line_items(recurly_subscription_uuid); +CREATE INDEX recurly_transaction_web_hooks_subscription_id_ndx ON recurly_transaction_web_hooks(subscription_id); +CREATE UNIQUE INDEX jam_track_rights_recurly_subscription_uuid_ndx ON jam_track_rights(recurly_subscription_uuid); + diff --git a/db/up/shopping_cart_anonymous.sql b/db/up/shopping_cart_anonymous.sql new file mode 100644 index 000000000..e22cfeecd --- /dev/null +++ b/db/up/shopping_cart_anonymous.sql @@ -0,0 +1,2 @@ +ALTER TABLE shopping_carts ALTER COLUMN user_id DROP NOT NULL; +ALTER TABLE shopping_carts ADD COLUMN anonymous_user_id VARCHAR(1000); \ No newline at end of file diff --git a/db/up/show_whats_next_count.sql b/db/up/show_whats_next_count.sql new file mode 100644 index 000000000..82e50d469 --- /dev/null +++ b/db/up/show_whats_next_count.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN show_whats_next_count INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/db/up/signup_hints.sql b/db/up/signup_hints.sql new file mode 100644 index 000000000..9a079e216 --- /dev/null +++ b/db/up/signup_hints.sql @@ -0,0 +1,12 @@ +CREATE TABLE signup_hints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + anonymous_user_id VARCHAR(64) UNIQUE, + redirect_location VARCHAR, + want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE users ADD COLUMN want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/user_reuse_card_and_reedem.sql b/db/up/user_reuse_card_and_reedem.sql new file mode 100644 index 000000000..2f2b811ea --- /dev/null +++ b/db/up/user_reuse_card_and_reedem.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN reuse_card BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE users ADD COLUMN has_redeemable_jamtrack BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE shopping_carts ADD COLUMN marked_for_redeem INTEGER DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/db/up/user_syncs_include_backing_tracks.sql b/db/up/user_syncs_include_backing_tracks.sql new file mode 100644 index 000000000..165617abf --- /dev/null +++ b/db/up/user_syncs_include_backing_tracks.sql @@ -0,0 +1,47 @@ + +DROP VIEW user_syncs; + +CREATE VIEW user_syncs AS + SELECT DISTINCT b.id AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + b.id AS unified_id, + a.user_id AS user_id, + b.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id + UNION ALL + SELECT CAST(NULL AS BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + a.id AS recorded_backing_track_id, + a.id AS unified_id, + a.user_id AS user_id, + a.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + mixes.id AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + mixes.id AS unified_id, + claimed_recordings.user_id AS user_id, + NULL as fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + quick_mixes.id AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + quick_mixes.id AS unified_id, + quick_mixes.user_id, + quick_mixes.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE; diff --git a/db/up/widen_user_authorization_token.sql b/db/up/widen_user_authorization_token.sql new file mode 100644 index 000000000..070f67b83 --- /dev/null +++ b/db/up/widen_user_authorization_token.sql @@ -0,0 +1 @@ +alter table user_authorizations ALTER COLUMN token TYPE character varying(2000); \ No newline at end of file diff --git a/monitor/spec/production_spec.rb b/monitor/spec/production_spec.rb index 99e296464..01cf90fa1 100755 --- a/monitor/spec/production_spec.rb +++ b/monitor/spec/production_spec.rb @@ -50,6 +50,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe end it "is possible for #{user3} to sign in and not get disconnected within 30 seconds" do + pending "continual failures - need to debug - try using Selenium instead of PhantomJS" as_monitor(user3) do sign_in_poltergeist(user3) repeat_for(30.seconds) do diff --git a/monitor/spec/support/utilities.rb b/monitor/spec/support/utilities.rb index be0e32722..694d9e604 100755 --- a/monitor/spec/support/utilities.rb +++ b/monitor/spec/support/utilities.rb @@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[]) users.each do |user| in_client(user) do users.reject {|u| u==user}.each do |other| - find('div.track-label', text: other.name) + find('div.track-label > span', text: other.name) #puts user.name + " is able to see " + other.name + "\'s track" end end diff --git a/ruby/Gemfile b/ruby/Gemfile index f38b2de02..8ef539d9a 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -28,7 +28,7 @@ gem 'amqp', '1.0.2' gem 'will_paginate' gem 'actionmailer', '3.2.13' gem 'sendgrid', '1.2.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'carrierwave', '0.9.0' gem 'aasm', '3.0.16' gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError) @@ -49,6 +49,7 @@ gem 'iso-639' gem 'rubyzip' gem 'sanitize' gem 'influxdb', '0.1.8' +gem 'recurly' group :test do gem 'simplecov', '~> 0.7.1' diff --git a/ruby/README.md b/ruby/README.md index 302798539..4f85c80a6 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -7,5 +7,3 @@ Create development database 'jam_ruby' Once you've created your database, migrate it: `bundle exec jam_ruby up` - - diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 91c2063ac..de12d03e1 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -30,6 +30,7 @@ require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/conflict_error" require "jam_ruby/lib/app_config" require "jam_ruby/lib/s3_manager_mixin" +require "jam_ruby/lib/s3_public_manager_mixin" require "jam_ruby/lib/module_overrides" require "jam_ruby/lib/s3_util" require "jam_ruby/lib/s3_manager" @@ -62,11 +63,13 @@ require "jam_ruby/resque/scheduled/stats_maker" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/mq_router" +require "jam_ruby/recurly_client" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" require "jam_ruby/version" require "jam_ruby/environment" require "jam_ruby/init" +require "jam_ruby/app/mailers/admin_mailer" require "jam_ruby/app/mailers/user_mailer" require "jam_ruby/app/mailers/invited_user_mailer" require "jam_ruby/app/mailers/corp_mailer" @@ -87,6 +90,7 @@ require "jam_ruby/lib/stats.rb" require "jam_ruby/amqp/amqp_connection_manager" require "jam_ruby/database" require "jam_ruby/message_factory" +require "jam_ruby/models/backing_track" require "jam_ruby/models/feedback" require "jam_ruby/models/feedback_observer" #require "jam_ruby/models/max_mind_geo" @@ -95,6 +99,8 @@ 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/anonymous_user" +require "jam_ruby/models/signup_hint" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" @@ -132,8 +138,12 @@ require "jam_ruby/models/search" require "jam_ruby/models/recording" require "jam_ruby/models/recording_comment" require "jam_ruby/models/recording_liker" +require "jam_ruby/models/recorded_backing_track" +require "jam_ruby/models/recorded_backing_track_observer" require "jam_ruby/models/recorded_track" require "jam_ruby/models/recorded_track_observer" +require "jam_ruby/models/recorded_video" +require "jam_ruby/models/recorded_jam_track_track" require "jam_ruby/models/quick_mix" require "jam_ruby/models/quick_mix_observer" require "jam_ruby/models/share_token" @@ -197,9 +207,13 @@ require "jam_ruby/models/score_history" require "jam_ruby/models/jam_company" require "jam_ruby/models/user_sync" require "jam_ruby/models/video_source" -require "jam_ruby/models/recorded_video" require "jam_ruby/models/text_message" +require "jam_ruby/models/sale" +require "jam_ruby/models/sale_line_item" +require "jam_ruby/models/recurly_transaction_web_hook" require "jam_ruby/jam_tracks_manager" +require "jam_ruby/jam_track_importer" +require "jam_ruby/jmep_manager" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb new file mode 100644 index 000000000..c4addb5cc --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -0,0 +1,22 @@ +module JamRuby + # sends out a boring ale + class AdminMailer < ActionMailer::Base + include SendGrid + + + DEFAULT_SENDER = "JamKazam " + + default :from => DEFAULT_SENDER + + sendgrid_category :use_subject_lines + #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) + sendgrid_unique_args :env => Environment.mode + + def alerts(options) + mail(to: APP_CONFIG.email_alerts_alias, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + end +end diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb index 4a4d8cc4f..be480ffb2 100644 --- a/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb @@ -11,7 +11,7 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. def extension_white_list - %w(ogg) + %w(ogg wav) end def store_dir @@ -23,6 +23,8 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base end def filename - "#{model.store_dir}/#{model.filename}" if model.id + if model.id && !model.skip_uploader + model.manually_uploaded_filename(mounted_as) + end end end diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb index 98e555e3d..9ec9a97ec 100644 --- a/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb @@ -11,7 +11,7 @@ class JamTrackUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. def extension_white_list - %w(jkz) + %w(ogg) end def store_dir @@ -23,6 +23,6 @@ class JamTrackUploader < CarrierWave::Uploader::Base end def filename - "#{model.store_dir}/#{model.filename}" if model.id + "#{model.preview_filename}" if model.id && model.uploading_preview end end 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 439aefe3a..35e380618 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,45 +1,64 @@ <% provide(:title, 'Welcome to JamKazam!') %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --

+

We're delighted that you have decided to try the JamKazam service, - and we hope that you will enjoy using JamKazam to play music with others. - Following are links to some resources that can help to get you up and running quickly. + and we hope that you will enjoy using JamKazam to play + music with others. + Following are some resources that can help you get oriented and get the most out of JamKazam. +

+ + +

+ +

Getting Started
+ 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.
  • +
+

+ +

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:
+

+

+ +

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 + .

-Getting Started Video
-We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running: -https://www.youtube.com/watch?v=DBo--aj_P1w + Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!

-

-Other Great Tutorial Videos
-There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here: -https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles -

- -

-Knowledge Base Articles
-You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here: -https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles -

- -

-JamKazam Support Portal
-If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here: -https://jamkazam.desk.com/ -

- -

-JamKazam Community Forum
-And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here: -http://forums.jamkazam.com/ -

- -

-Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon! -

- -  -- Team JamKazam +

Best Regards,
+ Team JamKazam

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

This email was sent to you because you have an account at JamKazam.  Click here to unsubscribe and update your profile settings. +

This email was sent to you because you have an account at JamKazam.  Click here to unsubscribe and update your profile settings.

diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb index 5c8262f63..49655e237 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb @@ -4,8 +4,8 @@ <%= yield %> <% end %> -<% unless @suppress_user_has_account_footer == true %> -This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile. +<% unless @user.nil? || @suppress_user_has_account_footer == true %> +This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. To unsubscribe: http://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>. <% end %> Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index bcf06b4c5..352048dc0 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -89,7 +89,7 @@ module JamRuby udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable sql =< b_weight + end + + position = 1 + sorted_tracks.each do |track| + track.position = position + position = position + 1 + end + + sorted_tracks[sorted_tracks.length - 1].position = 1000 + + sorted_tracks + end + + def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload) + + wav_files = fetch_wav_files(s3_path) + + tracks = [] + + wav_files.each do |wav_file| + track = JamTrackTrack.new + track.original_audio_s3_path = wav_file + + parsed_wav = parse_wav(wav_file) + + if parsed_wav[:master] + track.track_type = 'Master' + track.part = 'Master' + @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + else + if !parsed_wav[:instrument] || !parsed_wav[:part] + @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + else + @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + end + + track.instrument_id = parsed_wav[:instrument] || 'other' + track.track_type = 'Track' + track.part = parsed_wav[:part] || 'Other' + end + + tracks << track + end + + tracks = sort_tracks(tracks) + + jam_track.jam_track_tracks = tracks + + saved = jam_track.save + + if !saved + finish('invalid_audio', jam_track.errors.inspect) + return false + end + + return synchronize_audio_files(jam_track, skip_audio_upload) + end + + def synchronize_audio_files(jam_track, skip_audio_upload) + + begin + Dir.mktmpdir do |tmp_dir| + + 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) + + # 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" + + ogg_44100_s3_path = track.filename(ogg_44100_filename) + ogg_48000_s3_path = track.filename(ogg_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 + + # 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::s3_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) + + # convert entire master ogg file to mp3, and push both to public destination + preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) if track.track_type == 'Master' + + if !preview_succeeded + return false + end + + + end + + track.save! + end + end + rescue Exception => e + finish("sync_audio_exception", e.to_s) + return false + end + + return true + end + + def synchronize_duration(jam_track, ogg_44100) + duration_command = "soxi -D \"#{ogg_44100}\"" + output = `#{duration_command}` + + result_code = $?.to_i + + if result_code == 0 + duration = output.to_f.round + jam_track.duration = duration + else + @@log.warn("unable to determine duration for jam_track #{jam_track.name}. output #{output}") + end + true + end + + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) + + begin + mp3_44100 = File.join(tmp_dir, 'output-preview-44100.mp3') + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -ab 192k \"#{mp3_44100}\"" + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + mp3_digest = ::Digest::MD5.file(mp3_44100) + + track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + # upload 44100 ogg and mp3 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) + + + track.skip_uploader = true + + original_ogg_preview_url = track["preview_url"] + original_mp3_preview_url = track["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + track["preview_url"] = track.preview_filename(ogg_md5, 'ogg') + track["preview_length"] = File.new(ogg_44100).size + # 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_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_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"] + 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 fetch_all_files(s3_path) + JamTrackImporter::s3_manager.list_files(s3_path) + end + + def fetch_wav_files(s3_path) + files = fetch_all_files(s3_path) + files.select { |file| file.end_with?('.wav') } + end + + def synchronize(jam_track, metadata, metalocation, options) + + # metalocation should be audio/original artist/song name/meta.yml + + metadata ||= {} + + parsed_metalocation = parse_metalocation(metalocation) + + return unless parsed_metalocation + + original_artist = parsed_metalocation[1] + name = parsed_metalocation[2] + + success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name) + + return unless success + + synchronized_audio = synchronize_audio(jam_track, metadata, "audio/#{original_artist}/#{name}", options[:skip_audio_upload]) + + return unless synchronized_audio + + created_plan = synchronize_recurly(jam_track) + if created_plan + finish("success", nil) + end + + end + + def synchronize_recurly(jam_track) + begin + recurly = RecurlyClient.new + # no longer create JamTrack plans: VRFS-3028 + # recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track) + rescue RecurlyClientError => x + finish('recurly_create_plan', x.errors.to_s) + return false + end + + true + end + + class << self + + def s3_manager + @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def private_s3_manager + @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + + def dry_run + s3_manager.list_directories('audio').each do |original_artist| + @@log.debug("searching through artist directory '#{original_artist}'") + + songs = s3_manager.list_directories(original_artist) + songs.each do |song| + @@log.debug("searching through song directory' #{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + jam_track_importer = JamTrackImporter.new + + jam_track_importer.dry_run(metadata, metalocation) + end + end + + end + + def synchronize_jamtrack_master_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + master_track = jam_track.master_track + + if master_track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + if importer.synchronize_master_preview(master_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_previews + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_jamtrack_master_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" + @@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_duration(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + master_track = jam_track.master_track + if master_track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + + if importer.synchronize_duration(jam_track, ogg_44100) + jam_track.save! + importer.finish("success", nil) + end + end + else + importer.finish('no_duration', nil) + end + + importer + end + + def synchronize_durations + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_duration(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" + @@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 download_master(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + Dir.mkdir('tmp') unless Dir.exists?('tmp') + Dir.mkdir('tmp/jam_track_masters') unless Dir.exists?('tmp/jam_track_masters') + + master_track = jam_track.master_track + if master_track + ogg_44100 = File.join('tmp/jam_track_masters', "#{jam_track.original_artist} - #{jam_track.name}.ogg") + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + end + importer + end + + def download_masters + importers = [] + + JamTrack.all.each do |jam_track| + importers << download_master(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 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 synchronize_all(options) + importers = [] + + s3_manager.list_directories('audio').each do |original_artist| + @@log.debug("searching through artist directory '#{original_artist}'") + + songs = s3_manager.list_directories(original_artist) + songs.each do |song| + @@log.debug("searching through song directory' #{song}'") + + metalocation = "#{song}meta.yml" + + importer = synchronize_from_meta(metalocation, options) + importers << importer + end + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" + @@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 jam_track_dry_run(metalocation) + # see if we can find a JamTrack with this metalocation + jam_track = JamTrack.find_by_metalocation(metalocation) + + meta = load_metalocation(metalocation) + + if jam_track + @@log.debug("jamtrack #{jam_track.name} located by metalocation") + jam_track.dry_run(meta, metalocation) + else + jam_track = JamTrack.new + jam_track.dry_run(meta, metalocation) + end + end + + def load_metalocation(metalocation) + begin + data = s3_manager.read_all(metalocation) + return YAML.load(data) + rescue AWS::S3::Errors::NoSuchKey + return nil + end + end + + def create_from_metalocation(meta, metalocation, options = {skip_audio_upload: false}) + jam_track = JamTrack.new + sync_from_metadata(jam_track, meta, metalocation, options) + end + + def update_from_metalocation(jam_track, meta, metalocation, options) + sync_from_metadata(jam_track, meta, metalocation, options) + end + + def sync_from_metadata(jam_track, meta, metalocation, options) + jam_track_importer = JamTrackImporter.new + + JamTrack.transaction do + #begin + jam_track_importer.synchronize(jam_track, meta, metalocation, options) + #rescue Exception => e + # jam_track_importer.finish("unhandled_exception", e.to_s) + #end + + if jam_track_importer.reason != "success" + raise ActiveRecord::Rollback + end + end + + jam_track_importer + end + + def synchronize_from_meta(metalocation, options) + # see if we can find a JamTrack with this metalocation + jam_track = JamTrack.find_by_metalocation(metalocation) + + meta = load_metalocation(metalocation) + + jam_track_importer = nil + if jam_track + @@log.debug("jamtrack #{jam_track.name} located by metalocation") + jam_track_importer = update_from_metalocation(jam_track, meta, metalocation, options) + else + jam_track_importer = create_from_metalocation(meta, metalocation, options) + end + + if jam_track_importer.reason == "success" + @@log.info("#{jam_track_importer.name} successfully imported") + else + @@log.error("#{jam_track_importer.name} failed to import.") + @@log.error("#{jam_track_importer.name} reason=#{jam_track_importer.reason}") + @@log.error("#{jam_track_importer.name} detail=#{jam_track_importer.detail}") + end + + jam_track_importer + end + end + end +end diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index ee7318118..9322c3dbd 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -11,27 +11,33 @@ module JamRuby @@log = Logging.logger[JamTracksManager] + include JamRuby::S3ManagerMixin + class << self - def save_jam_track_jkz(user, jam_track) + + + def save_jam_track_jkz(user, jam_track, sample_rate=48) jam_track_right = jam_track.right_for_user(user) raise ArgumentError if jam_track_right.nil? - save_jam_track_right_jkz(jam_track_right) + save_jam_track_right_jkz(jam_track_right, sample_rate) end - def save_jam_track_right_jkz(jam_track_right) + def save_jam_track_right_jkz(jam_track_right, sample_rate=48) jam_track = jam_track_right.jam_track - #py_root = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "jamtracks")) py_root = APP_CONFIG.jamtracks_dir Dir.mktmpdir do |tmp_dir| jam_file_opts="" jam_track.jam_track_tracks.each do |jam_track_track| + + next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ + # use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata - nm = jam_track_track.id + File.extname(jam_track_track.filename) + nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate)) track_filename = File.join(tmp_dir, nm) - track_url = jam_track_track.sign_url + track_url = jam_track_track.sign_url(120, sample_rate) + @@log.info("downloading #{track_url} to #{track_filename}") copy_url_to_file(track_url, track_filename) - copy_url_to_file(track_url, File.join(".", nm)) - jam_file_opts << " -i '#{track_filename}+#{jam_track_track.part}'" + jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}" end #puts "LS + " + `ls -la '#{tmp_dir}'` @@ -39,10 +45,11 @@ module JamRuby title=jam_track.name output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz") py_file = File.join(py_root, "jkcreate.py") + version = jam_track.version @@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})" - + # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819: - cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}'" + cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output_jkz)} -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 @@ -51,11 +58,14 @@ module JamRuby #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")) - jam_track_right[:url] - + #raise ArgumentError, "output_jkz is empty #{output_jkz}" unless File.exists?(output_jkz) - - jam_track_right.url.store!(File.open(output_jkz, "rb")) + if sample_rate==48 + jam_track_right.url_48.store!(File.open(output_jkz, "rb")) + else + jam_track_right.url_44.store!(File.open(output_jkz, "rb")) + end + jam_track_right.signed=true jam_track_right.downloaded_since_sign=false jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem") @@ -73,7 +83,7 @@ module JamRuby http.request request do |response| response_code = response.code.to_i unless response_code >= 200 && response_code <= 299 - puts "Response from server was #{response_code} / #{response.message}" + @@log.info "Response from server was #{response_code} / #{response.message}" raise "bad status code: #{response_code}. body: #{response.body}" end response.read_body do |chunk| diff --git a/ruby/lib/jam_ruby/jmep_manager.rb b/ruby/lib/jam_ruby/jmep_manager.rb new file mode 100644 index 000000000..a4d0215bc --- /dev/null +++ b/ruby/lib/jam_ruby/jmep_manager.rb @@ -0,0 +1,55 @@ +require 'json' +require 'tempfile' +require 'open3' +require 'fileutils' +require 'open-uri' + +module JamRuby + + # Interact with external python tools to create jmep json + class JmepManager + + @@log = Logging.logger[JmepManager] + + class << self + + def execute(jmep_text) + + json = nil + + if jmep_text.blank? + return nil + end + + py_root = APP_CONFIG.jmep_dir + Dir.mktmpdir do |tmp_dir| + + output_json = File.join(tmp_dir, "jmep.json") + input_text = File.join(tmp_dir, "jmep.txt") + + # put JMEP text into input file + File.open(input_text, 'w') { |file| file.write(jmep_text) } + + py_file = File.join(py_root, "jmepgen.py") + @@log.info "Executing python source in #{py_file}, outputting to #{output_json})" + + # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819: + cli = "python #{py_file} -i '#{input_text}' -o '#{output_json}'" + Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid + exit_status = wait_thr.value + err = stderr.read(1000) + out = stdout.read(1000) + + raise ArgumentError, "#{out} #{err}" if exit_status != 0 + + json = File.read(output_json) + end + end + + json + end + + end + end +end diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index 3b6a6bf77..cf86fdc9b 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -44,6 +44,10 @@ module JamRuby s3_bucket.objects[key].url_for(operation, options).to_s end + def public_url(key, options = @@def_opts) + s3_bucket.objects[key].public_url(options).to_s + end + def presigned_post(key, options = @@def_opts) s3_bucket.objects[key].presigned_post(options) end @@ -72,8 +76,15 @@ module JamRuby s3_bucket.objects[filename].delete end - def upload(key, filename) - s3_bucket.objects[key].write(:file => filename) + def upload(key, filename, options={}) + options[:file] = filename + s3_bucket.objects[key].write(options) + end + + def cached_upload(key, filename, options={}) + options[:file] = filename + options.merge({expires: 5.years.from_now}) + s3_bucket.objects[key].write(filename, options) end def delete_folder(folder) @@ -88,6 +99,24 @@ module JamRuby end end + def read_all(key) + s = StringIO.new + s3_bucket.objects[key].read do |data| + s.write(data) + end + s.string + end + + def list_files(prefix) + tree = s3_bucket.as_tree(prefix: prefix) + tree.children.select(&:leaf?).collect(&:key) + end + + def list_directories(prefix) + tree = s3_bucket.as_tree(prefix: prefix) + tree.children.select(&:branch?).collect(&:prefix) + end + def exists?(filename) s3_bucket.objects[filename].exists? end diff --git a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb index 61781aaab..c294afe6d 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb @@ -10,8 +10,8 @@ module JamRuby end - def s3_manager(options={:public => false}) - @s3_manager ||= S3Manager.new(options[:public] ? app_config.aws_bucket_public : app_config.aws_bucket, app_config.aws_access_key_id, app_config.aws_secret_access_key) + def s3_manager(options={:bucket => nil, :public => false}) + @s3_manager ||= S3Manager.new(options[:bucket] ? options[:bucket] : (options[:public] ? app_config.aws_bucket_public : app_config.aws_bucket), app_config.aws_access_key_id, app_config.aws_secret_access_key) end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb b/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb new file mode 100644 index 000000000..6eba1990a --- /dev/null +++ b/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb @@ -0,0 +1,17 @@ +module JamRuby + module S3PublicManagerMixin + extend ActiveSupport::Concern + include AppConfig + + included do + end + + module ClassMethods + + end + + def s3_public_manager() + @s3_public_manager ||= S3Manager.new(app_config.aws_bucket_public, app_config.aws_access_key_id, app_config.aws_secret_access_key) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index c28ae8de2..2addd2266 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -7,7 +7,7 @@ module JamRuby self.table_name = 'active_music_sessions' - attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording + attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome, :jam_track_id belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" @@ -15,6 +15,9 @@ module JamRuby belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id" + belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id" + belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id" + has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id' has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id' belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id @@ -27,6 +30,10 @@ module JamRuby validate :creator_is_musician validate :validate_opening_recording, :if => :opening_recording validate :validate_opening_jam_track, :if => :opening_jam_track + validate :validate_opening_backing_track, :if => :opening_backing_track + + # not sure if this is helpful since if one opens, it always stays open + validate :validate_opening_metronome, :if => :opening_metronome after_create :started_session @@ -73,22 +80,47 @@ module JamRuby if is_jam_track_open? errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end + + if is_backing_track_open? + errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) + end + + if is_metronome_open? + errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN) + end end def validate_opening_jam_track + validate_other_audio(:jam_track) + end + + def validate_opening_backing_track + validate_other_audio(:backing_track) + end + + def validate_opening_metronome + validate_other_audio(:metronome) + end + + def validate_other_audio(error_key) + # validate that there is no backing track already open in this session + if backing_track_path_was.present? + errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) + end + # validate that there is no jam track already open in this session - unless jam_track_id_was.nil? - errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN) + if jam_track_id_was.present? + errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end # validate that there is no recording being made if is_recording? - errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) + errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) end # validate that there is no recording being played back to the session if is_playing_recording? - errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) + errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) end end @@ -593,6 +625,14 @@ module JamRuby !self.jam_track.nil? end + def is_backing_track_open? + self.backing_track_path.present? + end + + def is_metronome_open? + self.metronome_active.present? + end + # is this music session currently recording? def is_recording? recordings.where(:duration => nil).count > 0 @@ -742,6 +782,35 @@ module JamRuby self.save end + # @param backing_track_path is a relative path: + def open_backing_track(user, backing_track_path) + self.backing_track_path = backing_track_path + self.backing_track_initiator = user + self.opening_backing_track = true + self.save + self.opening_backing_track = false + end + + def close_backing_track + self.backing_track_path = nil + self.backing_track_initiator = nil + self.save + end + + def open_metronome(user) + self.metronome_active = true + self.metronome_initiator = user + self.opening_metronome = true + self.save + self.opening_metronome = false + end + + def close_metronome + self.metronome_active = false + self.metronome_initiator = nil + self.save + end + def self.sync(session_history) music_session = MusicSession.find_by_id(session_history.id) diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb new file mode 100644 index 000000000..0a843185a --- /dev/null +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -0,0 +1,29 @@ +# this was added to support the idea of an anonymous user interacting with our site; needed by the ShoppingCart +# over time it might make sense to beef this up and to use it conistently in anonymous interactions + +module JamRuby + class AnonymousUser + + attr_accessor :id + + def initialize(id) + @id = id + end + + def shopping_carts + ShoppingCart.where(anonymous_user_id: @id) + end + + def destroy_all_shopping_carts + ShoppingCart.destroy_all(anonymous_user_id: @id) + end + + def admin + false + end + + def has_redeemable_jamtrack + true + end + end +end diff --git a/ruby/lib/jam_ruby/models/backing_track.rb b/ruby/lib/jam_ruby/models/backing_track.rb new file mode 100644 index 000000000..fc5e6517b --- /dev/null +++ b/ruby/lib/jam_ruby/models/backing_track.rb @@ -0,0 +1,19 @@ +module JamRuby + class BackingTrack < ActiveRecord::Base + + self.table_name = "backing_tracks" + self.primary_key = 'id' + + default_scope order('created_at ASC') + + belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id' + validates :connection, presence: true + validates :client_track_id, presence: true + validates :filename, presence: true + + def user + self.connection.user + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 8a813f9ce..5a5378b91 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -18,15 +18,18 @@ module JamRuby belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all + has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all + validates :metronome_open, :inclusion => {:in => [true, false]} validates :as_musician, :inclusion => {:in => [true, false, nil]} validates :client_type, :inclusion => {:in => CLIENT_TYPES} - validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true + validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true validate :can_join_music_session, :if => :joining_session? validate :user_or_latency_tester_present - after_save :require_at_least_one_track_when_in_session, :if => :joining_session? + # this is no longer required with the new no-input profile + #after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create after_save :report_add_participant @@ -60,11 +63,11 @@ module JamRuby def state_message case self.aasm_state.to_sym when CONNECT_STATE - 'Connected' - when STALE_STATE - 'Stale' + 'Connected' + when STALE_STATE + 'Stale' else - 'Idle' + 'Idle' end end @@ -83,7 +86,7 @@ module JamRuby def joining_session? joining_session end - + def can_join_music_session # puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed? @@ -181,8 +184,8 @@ module JamRuby end def associate_tracks(tracks) + self.tracks.clear() unless tracks.nil? - self.tracks.clear() tracks.each do |track| t = Track.new t.instrument = Instrument.find(track["instrument_id"]) diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 7f383f5dc..f05643c86 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -10,73 +10,175 @@ module JamRuby PRODUCT_TYPE = 'JamTrack' - mount_uploader :url, JamTrackUploader + @@log = Logging.logger[JamTrack] + attr_accessor :uploading_preview attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type, :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :available, as: :admin + :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin validates :name, presence: true, uniqueness: true, length: {maximum: 200} + validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } validates :description, length: {maximum: 1000} - validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/ - validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES} + validates :time_signature, inclusion: {in: [nil] + [''] + TIME_SIGNATURES} # the empty string is needed because of activeadmin validates :status, inclusion: {in: [nil] + STATUS} validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE} validates :original_artist, length: {maximum: 200} validates :songwriter, length: {maximum: 1000} validates :publisher, length: {maximum: 1000} - validates :pro, inclusion: {in: [nil] + PRO} validates :sales_region, inclusion: {in: [nil] + SALES_REGION} validates_format_of :price, with: /^\d+\.*\d{0,2}$/ - validates :initial_play_silence, numericality: true, :allow_nil => true - + validates :version, presence: true + validates :pro_ascap, inclusion: {in: [true, false]} + validates :pro_bmi, inclusion: {in: [true, false]} + validates :pro_sesac, inclusion: {in: [true, false]} + validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :reproduction_royalty, inclusion: {in: [nil, true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} + validates :duration, numericality: {only_integer: true}, :allow_nil => true + validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/ validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,3}$/ - validates_format_of :pro_royalty_amount, with: /^\d+\.*\d{0,3}$/ - - before_save :sanitize_active_admin belongs_to :genre, class_name: "JamRuby::Genre" belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' - has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'position ASC' + has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC' has_many :jam_track_tap_ins, :class_name => "JamRuby::JamTrackTapIn", order: 'offset_time ASC' - has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" + has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" # ' + has_many :owners, :through => :jam_track_rights, :class_name => "JamRuby::User", :source => :user has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" + has_many :recordings, :class_name => "JamRuby::Recording" + + # VRFS-2916 jam_tracks.id is varchar: REMOVE + # has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy + # VRFS-2916 jam_tracks.id is varchar: ADD + has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy + accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true - class << self - def index(options, user) - limit = options[:limit] - limit ||= 20 - limit = limit.to_i + def duplicate_positions? + counter = {} + jam_track_tracks.each do |track| + count = counter[track.position] + if count.nil? + count = 0 + end + counter[track.position] = count + 1 + end + + duplicate = false + counter.each do|position, count| + if count > 1 + duplicate = true + break + end + end + duplicate + end + + def missing_previews? + missing_preview = false + self.jam_track_tracks.each do |track| + unless track.has_preview? + missing_preview = true + break + end + end + missing_preview + end + + def onboard_warnings + warnings = [] + warnings << 'POSITIONS' if duplicate_positions? + warnings << 'PREVIEWS'if missing_previews? + warnings << 'DURATION' if duration.nil? + warnings.join(',') + end + + def band_jam_track_count + JamTrack.where(original_artist: original_artist).count + end + + class << self + # @return array[artist_name(string)] + def all_artists + JamTrack.select("original_artist"). + group("original_artist"). + collect{|jam_track|jam_track.original_artist} + end + + # @return array[JamTrack] for given artist_name + def tracks_for_artist(artist_name) + JamTrack.where("original_artist=?", artist_name).all + end + + def index(options, user) + if options[:page] + page = options[:page].to_i + per_page = options[:per_page].to_i + + if per_page == 0 + # try and see if limit was specified + limit = options[:limit] + limit ||= 20 + limit = limit.to_i + else + limit = per_page + end + + start = (page -1 )* per_page + limit = per_page + else + limit = options[:limit] + limit ||= 20 + limit = limit.to_i + + start = options[:start].presence + start = start.to_i || 0 + + page = 1 + start/limit + per_page = limit + end - start = options[:start].presence - start = start.to_i || 0 query = JamTrack.joins(:jam_track_tracks) - .paginate(page: 1 + start/limit, per_page: limit) + .paginate(page: page, per_page: per_page) if options[:show_purchased_only] query = query.joins(:jam_track_rights) query = query.where("jam_track_rights.user_id = ?", user.id) end + + if options[:artist].present? + query = query.where("original_artist=?", options[:artist]) + end + + if options[:id].present? + query = query.where("jam_tracks.id=?", options[:id]) + end + + if options[:group_artist] + query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id") + query = query.group("original_artist") + query = query.order('jam_tracks.original_artist') + else + query = query.group("jam_tracks.id") + query = query.order('jam_tracks.name') + end - query = query.where("jam_tracks.available = ?", true) unless user.admin + query = query.where("jam_tracks.status = ?", 'Production') unless user.admin query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank? - query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? - query = query.group("jam_tracks.id") - query = query.order('jam_tracks.name') + query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + if query.length == 0 [query, nil] @@ -87,69 +189,19 @@ module JamRuby end end end - - # create storage directory that will house this jam_track, as well as - def store_dir - "jam_tracks/#{id}" - end - # create name of the file - def filename - "#{name}.jkz" - end - # creates a short-lived URL that has access to the object. - # the idea is that this is used when a user who has the rights to this tries to download this JamTrack - # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download - # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/jkz', :secure => false}) + def master_track + JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first end - def can_download?(user) owners.include?(user) end def right_for_user(user) jam_track_rights.where("user_id=?", user).first - end - - def self.list_downloads(user, limit = 100, since = 0) - since = 0 unless since || since == '' # guard against nil - downloads = [] - - user.jam_track_rights - .limit(limit) - .where('jam_track_rights.id > ?', since) - .each do |jam_track_right| - downloads << { - :type => "jam_track", - :id => jam_track_right.id.to_s, - :jam_track_id => jam_track_right.jam_track_id, - :length => jam_track_right.length, - :md5 => jam_track_right.md5, - :url => jam_track_right.url, - :created_at => jam_track_right.created_at, - :next => jam_track_right.id - } - end - - next_id = downloads[-1][:next] if downloads.length > 0 - next_id = since if next_id.nil? # echo back to the client the same value they passed in, if there are no results - - { - 'downloads' => downloads, - 'next' => next_id.to_s - } end - - private - - def sanitize_active_admin - self.genre_id = nil if self.genre_id == '' - self.licensor_id = nil if self.licensor_id == '' - end end end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index affee58a5..f5abb1353 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -3,19 +3,24 @@ module JamRuby # describes what users have rights to which tracks class JamTrackRight < ActiveRecord::Base include JamRuby::S3ManagerMixin - attr_accessible :user, :jam_track, :user_id, :jam_track_id, :url, :md5, :length, :download_count + attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count + attr_accessible :user_id, :jam_track_id, as: :admin + 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" validates :user, presence:true validates :jam_track, presence:true + validates :is_test_purchase, inclusion: {in: [true, false]} + validate :verify_download_count after_save :after_save validates_uniqueness_of :user_id, scope: :jam_track_id # Uploads the JKZ: - mount_uploader :url, JamTrackRightUploader + mount_uploader :url_48, JamTrackRightUploader + mount_uploader :url_44, JamTrackRightUploader before_destroy :delete_s3_files MAX_JAM_TRACK_DOWNLOADS = 1000 @@ -30,7 +35,7 @@ module JamRuby end def store_dir - "#{jam_track.store_dir}/rights" + "jam_track_rights/#{created_at.strftime('%m-%d-%Y')}/#{user_id}-#{id}" end # create name of the file @@ -59,12 +64,17 @@ module JamRuby else raise "Error sending notification #{self.errors}" end - end - def finish_sign(length, md5) + + def finish_sign(length, md5, bitrate) self.last_signed_at = Time.now - self.length = length - self.md5 = md5 + if bitrate==48 + self.length_48 = length + self.md5_48 = md5 + else + self.length_44 = length + self.md5_44 = md5 + end self.signed = true self.error_count = 0 self.error_reason = nil @@ -81,36 +91,50 @@ module JamRuby # the idea is that this is used when a user who has the rights to this tries to download this JamTrack # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:url], {:expires => expiration_time, :secure => false}) + def sign_url(expiration_time = 120, bitrate=48) + field_name = (bitrate==48) ? "url_48" : "url_44" + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => false}) end def delete_s3_files - remove_url! + remove_url_48! + remove_url_44! end - def enqueue + + def enqueue(sample_rate=48) begin JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil) - Resque.enqueue(JamTracksBuilder, self.id) + Resque.enqueue(JamTracksBuilder, self.id, sample_rate) true 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 + def enqueue_if_needed(sample_rate=48) state = signing_state - if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' false else - enqueue + enqueue(sample_rate) true end end + + + # @return true if signed && file exists for the sample_rate specifed: + def ready?(sample_rate=48) + if sample_rate==48 + self.signed && self.url_48.present? && self.url_48.file.exists? + else + self.signed && self.url_44.present? && self.url_44.file.exists? + end + end + # returns easy to digest state field # SIGNED - the package is ready to be downloaded # ERROR - the package was built unsuccessfully @@ -123,8 +147,6 @@ module JamRuby state = nil if signed state = 'SIGNED' - elsif error_count > 0 - state = 'ERROR' elsif signing_started_at if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time state = 'SIGNING_TIMEOUT' @@ -137,6 +159,8 @@ module JamRuby else state = 'QUEUED' end + elsif error_count > 0 + state = 'ERROR' else state = 'QUIET' # needs to be poked to go build end diff --git a/ruby/lib/jam_ruby/models/jam_track_tap_in.rb b/ruby/lib/jam_ruby/models/jam_track_tap_in.rb index 0bddeb8c6..fa0e63eda 100644 --- a/ruby/lib/jam_ruby/models/jam_track_tap_in.rb +++ b/ruby/lib/jam_ruby/models/jam_track_tap_in.rb @@ -35,7 +35,6 @@ module JamRuby else raise "format of offset time must be MM:SS:MLS" end - end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index 978ed29ab..41514b2f8 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -3,42 +3,90 @@ module JamRuby # describes an audio track (like the drums, or guitar) that comprises a JamTrack class JamTrackTrack < ActiveRecord::Base include JamRuby::S3ManagerMixin + include JamRuby::S3PublicManagerMixin # there should only be one Master per JamTrack, but there can be N Track per JamTrack - TRACK_TYPE = %w{Master Track} + TRACK_TYPE = %w{Track Master} - mount_uploader :url, JamTrackTrackUploader + @@log = Logging.logger[JamTrackTrack] - attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, :url, as: :admin + + # Because JamTrackImporter imports audio files now, and because also the mere presence of this causes serious issues when updating the model (because reset of url_44 to something bogus), I've removed these + #mount_uploader :url_48, JamTrackTrackUploader + #mount_uploader :url_44, JamTrackTrackUploader + + 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 validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000} - validates :part, length: {maximum: 20} + validates :part, length: {maximum: 25} validates :track_type, inclusion: {in: TRACK_TYPE } - validates_uniqueness_of :position, scope: :jam_track_id - validates_uniqueness_of :part, scope: :jam_track_id + 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 :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 + # create storage directory that will house this jam_track, as well as def store_dir - "#{jam_track.store_dir}/tracks" + "jam_track_tracks" end # create name of the file - def filename - track_type == 'Master' ? 'master.ogg' : "#{part}.ogg" + def filename(original_name) + "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{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}" + end + + def has_preview? + !self["preview_url"].nil? && !self['preview_mp3_url'].nil? + end + + # 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] + if url + s3_public_manager.public_url(url,{ :secure => false}) + else + nil + end + end + + def manually_uploaded_filename(mounted_as) + if track_type == 'Master' + filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") + else + filename("#{jam_track.name} Stem - #{instrument.description}-#{part}-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") + end + end + + def master? + track_type == 'Master' + end + + def url_by_sample_rate(sample_rate=48) + field_name = (sample_rate==48) ? "url_48" : "url_44" + self[field_name] + end # creates a short-lived URL that has access to the object. # the idea is that this is used when a user who has the rights to this tries to download this JamTrack # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + 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 => false}) end - + def can_download?(user) # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? jam_track.owners.include?(user) diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 8e8ab2ace..d5c7bd364 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -136,11 +136,62 @@ module JamRuby def manifest one_day = 60 * 60 * 24 + jam_track_offset = 0 + + if recording.timeline + recording_timeline_data = JSON.parse(recording.timeline) + + recording_start_time = recording_timeline_data["recording_start_time"] + jam_track_play_start_time = recording_timeline_data["jam_track_play_start_time"] + jam_track_recording_start_play_offset = recording_timeline_data["jam_track_recording_start_play_offset"] + + jam_track_offset = -jam_track_recording_start_play_offset + end + manifest = { "files" => [], "timeline" => [] } mix_params = [] + + + # this 'pick limiter' logic will ensure that we set a limiter on the 1st recorded_track we come across. + pick_limiter = false + if recording.is_jamtrack_recording? + # we only use the limiter feature if this is a JamTrack recording + # by setting this to true, the 1st recorded_track in the database will be the limiter + pick_limiter = true + end + recording.recorded_tracks.each do |recorded_track| - manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } - mix_params << { "level" => 100, "balance" => 0 } + manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0, limiter:pick_limiter } + pick_limiter = false + mix_params << { "level" => 1.0, "balance" => 0 } + end + + recording.recorded_backing_tracks.each do |recorded_backing_track| + manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } + mix_params << { "level" => 1.0, "balance" => 0 } + end + + recording.recorded_jam_track_tracks.each do |recorded_jam_track_track| + manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => jam_track_offset } + # let's look for level info from the client + level = 1.0 # default value - means no effect + if recorded_jam_track_track.timeline + + timeline_data = JSON.parse(recorded_jam_track_track.timeline) + + # always take the 1st entry for now + first = timeline_data[0] + + if first["mute"] + # mute equates to no noise + level = 0.0 + else + # otherwise grab the left channel... + level = first["vol_l"] + end + end + + mix_params << { "level" => level, "balance" => 0 } end manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index a29d0b2f9..e2954d9f3 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -307,7 +307,7 @@ module JamRuby filter_approved = only_approved ? 'AND rrrs.chosen = true' : '' MusicSession.where(%Q{music_sessions.canceled = FALSE AND - music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND + (music_sessions.create_type is NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}') AND (music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND music_sessions.id in ( select distinct(rs.music_session_id) 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 430c897a1..bca0ecccc 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -43,7 +43,7 @@ module JamRuby session_user_history.music_session_id = music_session_id session_user_history.user_id = user_id session_user_history.client_id = client_id - session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") + session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") if tracks session_user_history.save end diff --git a/ruby/lib/jam_ruby/models/playable_play.rb b/ruby/lib/jam_ruby/models/playable_play.rb index 4631bc4db..a04018689 100644 --- a/ruby/lib/jam_ruby/models/playable_play.rb +++ b/ruby/lib/jam_ruby/models/playable_play.rb @@ -2,9 +2,27 @@ module JamRuby class PlayablePlay < ActiveRecord::Base self.table_name = "playable_plays" - belongs_to :playable, :polymorphic => :true, :counter_cache => :play_count + belongs_to :playable, :polymorphic => :true + # VRFS-2916 jam_tracks.id is varchar: REMOVE + #belongs_to :jam_track, :foreign_key => :jam_track_id belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "player_id" belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id" + validate do + # VRFS-2916 jam_tracks.id is varchar: REMOVE + #if !playable_id && !jam_track_id + # self.errors[:base] << 'No playable instance detected' + #end + + # VRFS-2916 jam_tracks.id is varchar: ADD + if !playable_id + self.errors[:base] << 'No playable instance detected' + end + + if !user + self.errors[:base] << 'No user detected' + end + end + end end diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track.rb b/ruby/lib/jam_ruby/models/recorded_backing_track.rb new file mode 100644 index 000000000..0826d160c --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_backing_track.rb @@ -0,0 +1,196 @@ +module JamRuby + # BackingTrack analog to JamRuby::RecordedTrack + class RecordedBackingTrack < ActiveRecord::Base + + include JamRuby::S3ManagerMixin + + attr_accessor :marking_complete + attr_writer :current_user + + belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks + validates :filename, :presence => true + + validates :client_id, :presence => true # not a connection relation on purpose + validates :backing_track_id, :presence => true # not a track relation on purpose + validates :client_track_id, :presence => true + validates :md5, :presence => true, :if => :upload_starting? + validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely... + validates :user, presence: true + validates :download_count, presence: true + + before_destroy :delete_s3_files + validate :validate_fully_uploaded + validate :validate_part_complete + validate :validate_too_many_upload_failures + validate :verify_download_count + + def self.create_from_backing_track(backing_track, recording) + recorded_backing_track = self.new + recorded_backing_track.recording = recording + recorded_backing_track.client_id = backing_track.connection.client_id + recorded_backing_track.backing_track_id = backing_track.id + recorded_backing_track.client_track_id = "R" + backing_track.client_track_id # Matches behavior in RecordingManager.cpp#getWavComment + recorded_backing_track.user = backing_track.connection.user + recorded_backing_track.filename = backing_track.filename + recorded_backing_track.next_part_to_upload = 0 + recorded_backing_track.file_offset = 0 + recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id) + recorded_backing_track.save + recorded_backing_track + end + + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + end + + def can_download?(some_user) + claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user } + + if claimed_recording + !claimed_recording.discarded + else + false + end + end + + def too_many_upload_failures? + upload_failures >= APP_CONFIG.max_track_upload_failures + end + + def too_many_downloads? + (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin + end + + def upload_starting? + next_part_to_upload_was == 0 && next_part_to_upload == 1 + end + + def validate_too_many_upload_failures + if upload_failures >= APP_CONFIG.max_track_upload_failures + errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED) + end + end + + def validate_fully_uploaded + if marking_complete && fully_uploaded && fully_uploaded_was + errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED) + end + end + + def validate_part_complete + + # if we see a transition from is_part_uploading from true to false, we validate + if is_part_uploading_was && !is_part_uploading + if next_part_to_upload_was + 1 != next_part_to_upload + errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED) + end + + if file_offset > length + errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH) + end + elsif next_part_to_upload_was + 1 == next_part_to_upload + # this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start + if next_part_to_upload_was != 0 + # we see that the part number was ticked--but was is_part_upload set to true before this transition? + if !is_part_uploading_was && !is_part_uploading + errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED) + end + end + end + end + + def verify_download_count + if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin + errors.add(:download_count, "must be less than or equal to 100") + end + end + + + def upload_start(length, md5) + #self.upload_id set by the observer + self.next_part_to_upload = 1 + self.length = length + self.md5 = md5 + save + end + + # if for some reason the server thinks the client can't carry on with the upload, + # this resets everything to the initial state + def reset_upload + self.upload_failures = self.upload_failures + 1 + self.part_failures = 0 + self.file_offset = 0 + self.next_part_to_upload = 0 + self.upload_id = nil + self.md5 = nil + self.length = 0 + self.fully_uploaded = false + self.is_part_uploading = false + save :validate => false # skip validation because we need this to always work + end + + def upload_next_part(length, md5) + self.marking_complete = true + if next_part_to_upload == 0 + upload_start(length, md5) + end + self.is_part_uploading = true + save + end + + def upload_sign(content_md5) + s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id) + end + + def upload_part_complete(part, offset) + # validated by :validate_part_complete + self.marking_complete = true + self.is_part_uploading = false + self.next_part_to_upload = self.next_part_to_upload + 1 + self.file_offset = offset.to_i + self.part_failures = 0 + save + end + + def upload_complete + # validate from happening twice by :validate_fully_uploaded + self.fully_uploaded = true + self.marking_complete = true + save + end + + def increment_part_failures(part_failure_before_error) + self.part_failures = part_failure_before_error + 1 + RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'") + end + + def stored_filename + # construct a path from s3 + RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id) + end + + def update_download_count(count=1) + self.download_count = self.download_count + count + self.last_downloaded_at = Time.now + end + + def delete_s3_files + s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url]) + end + + def mark_silent + destroy + + # check if we have all the files we need, now that the recorded_backing_track is out of the way + recording.preconditions_for_mix? + end + + private + + def self.construct_filename(created_at, recording_id, client_track_id) + raise "unknown ID" unless client_track_id + "recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg" + end + end +end diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb new file mode 100644 index 000000000..1a2b8f291 --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb @@ -0,0 +1,91 @@ +module JamRuby + class RecordedBackingTrackObserver < ActiveRecord::Observer + + # if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here + observe JamRuby::RecordedBackingTrack + + def before_validation(recorded_backing_tracks) + + # if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded + if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading + begin + aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1) + # calling size on a part that does not exist will throw an exception... that's what we want + aws_part.size + rescue SocketError => e + raise # this should cause a 500 error, which is what we want. The client will retry later on 500. + rescue Exception => e + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + rescue RuntimeError => e + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + rescue + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + end + + end + + # if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together + if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded + + multipart_success = false + begin + recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id) + multipart_success = true + rescue SocketError => e + raise # this should cause a 500 error, which is what we want. The client will retry later. + rescue Exception => e + #recorded_track.reload + recorded_backing_tracks.reset_upload + recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD) + end + + # unlike RecordedTracks, only the person who uploaded can download it, so no need to notify + + # tell all users that a download is available, except for the user who just uploaded + # recorded_backing_tracks.recording.users.each do |user| + #Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user + # end + + end + end + + def after_commit(recorded_backing_track) + + end + + # here we tick upload failure counts, or revert the state of the model, as needed + def after_rollback(recorded_backing_track) + # if fully uploaded, don't increment failures + if recorded_backing_track.fully_uploaded + return + end + + # increment part failures if there is a part currently being uploaded + if recorded_backing_track.is_part_uploading_was + #recorded_track.reload # we don't want anything else that the user set to get applied + recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was) + if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures + # save upload id before we abort this bad boy + upload_id = recorded_backing_track.upload_id + begin + recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id) + rescue => e + puts e.inspect + end + recorded_backing_track.reset_upload + if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures + # do anything? + end + end + end + + end + + def before_save(recorded_backing_track) + # if we are on the 1st part, then we need to make sure we can save the upload_id + if recorded_backing_track.next_part_to_upload == 1 + recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url]) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb new file mode 100644 index 000000000..53bad104c --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb @@ -0,0 +1,22 @@ +module JamRuby + # BackingTrack analog to JamRuby::RecordedTrack + class RecordedJamTrackTrack < ActiveRecord::Base + + belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_jam_track_tracks + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_jam_track_tracks + belongs_to :jam_track_track, :class_name => "JamRuby::JamTrackTrack", :inverse_of => :recorded_jam_track_tracks + + validates :user, presence: true + validates :jam_track_track, presence:true + + def self.create_from_jam_track_track(jam_track_track, recording, user) + recorded_jam_track_track = self.new + recorded_jam_track_track.recording = recording + recorded_jam_track_track.jam_track_track = jam_track_track + recorded_jam_track_track.user = user + recorded_jam_track_track.save + recorded_jam_track_track + end + + end +end diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 49f4219f1..b7c683747 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -3,7 +3,7 @@ module JamRuby @@log = Logging.logger[Recording] - attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin + attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, :jam_track_id, as: :admin has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy @@ -11,6 +11,8 @@ module JamRuby has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy + has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy + has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy @@ -19,6 +21,9 @@ module JamRuby belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id' belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id + belongs_to :non_active_music_session, :class_name => "JamRuby::MusicSession", foreign_key: :music_session_id + belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id' + belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id' accepts_nested_attributes_for :recorded_tracks, :mixes, :claimed_recordings, allow_destroy: true @@ -45,6 +50,10 @@ module JamRuby self.comments.size end + def is_jamtrack_recording? + !jam_track_id.nil? + end + def high_quality_mix? has_final_mix end @@ -179,8 +188,16 @@ module JamRuby recorded_tracks.where(:user_id => user.id) end + def recorded_backing_tracks_for_user(user) + unless self.users.exists?(user) + raise PermissionError, "user was not in this session" + end + recorded_backing_tracks.where(:user_id => user.id) + end + + def has_access?(user) - users.exists?(user) + users.exists?(user) || plays.where("player_id=?", user).count != 0 end # Start recording a session. @@ -209,7 +226,21 @@ module JamRuby connection.video_sources.each do |video| recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording) end + + connection.backing_tracks.each do |backing_track| + recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording) + end end + + if music_session.jam_track + music_session.jam_track.jam_track_tracks.each do |jam_track_track| + recording.recorded_jam_track_tracks << RecordedJamTrackTrack.create_from_jam_track_track(jam_track_track, recording, owner) if jam_track_track.track_type == 'Track' + end + recording.jam_track = music_session.jam_track + recording.jam_track_initiator = music_session.jam_track_initiator + end + + recording.save end end @@ -321,8 +352,7 @@ module JamRuby } ) end - - latest_recorded_track = downloads[-1][:next] if downloads.length > 0 + latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0 Mix.joins(:recording).joins(:recording => :claimed_recordings) .order('mixes.id') @@ -345,16 +375,31 @@ module JamRuby } ) end + latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0 - latest_mix = downloads[-1][:next] if downloads.length > 0 - - if !latest_mix.nil? && !latest_recorded_track.nil? - next_date = [latest_mix, latest_recorded_track].max - elsif latest_mix.nil? - next_date = latest_recorded_track - else - next_date = latest_mix + RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings) + .order('recorded_backing_tracks.id') + .where('recorded_backing_tracks.fully_uploaded = TRUE') + .where('recorded_backing_tracks.id > ?', since) + .where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back + .where('all_discarded = false') + .where('deleted = false') + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track| + downloads.push( + { + :type => "recorded_backing_track", + :id => recorded_backing_track.client_track_id, + :recording_id => recorded_backing_track.recording_id, + :length => recorded_backing_track.length, + :md5 => recorded_backing_track.md5, + :url => recorded_backing_track[:url], + :next => recorded_backing_track.id + } + ) end + latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0 + + next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max if next_date.nil? next_date = since # echo back to the client the same value they passed in, if there are no results @@ -417,6 +462,20 @@ module JamRuby Arel::Nodes::As.new('stream_mix', Arel.sql('item_type')) ]).reorder("") + # Select fields for quick mix. Note that it must include + # the same number of fields as the track or video in order for + # the union to work: + backing_track_arel = RecordedBackingTrack.select([ + :id, + :recording_id, + :user_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + Arel::Nodes::As.new('backing_track', Arel.sql('item_type')) + ]).reorder("") + # Glue them together: union = track_arel.union(vid_arel) @@ -439,7 +498,25 @@ module JamRuby ]) # And repeat: - union_all = arel.union(quick_mix_arel) + union_quick = arel.union(quick_mix_arel) + utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick) + arel = arel.from(utable_quick) + + arel = arel.except(:select) + arel = arel.select([ + "recorded_items_quick.id", + :recording_id, + :user_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + :item_type + ]) + + + # And repeat for backing track: + union_all = arel.union(backing_track_arel) utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all) arel = arel.from(utable_all) @@ -455,7 +532,6 @@ module JamRuby :item_type ]) - # Further joining and criteria for the unioned object: arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \ .where('recorded_items_all.user_id' => user.id) \ @@ -492,6 +568,13 @@ module JamRuby :recording_id => recorded_item.recording_id, :next => recorded_item.id }) + elsif recorded_item.item_type == 'backing_track' + uploads << ({ + :type => "recorded_backing_track", + :recording_id => recorded_item.recording_id, + :client_track_id => recorded_item.client_track_id, + :next => recorded_item.id + }) else end @@ -513,6 +596,11 @@ module JamRuby recorded_tracks.each do |recorded_track| return false unless recorded_track.fully_uploaded end + + recorded_backing_tracks.each do |recorded_backing_track| + return false unless recorded_backing_track.fully_uploaded + end + true end @@ -608,6 +696,25 @@ module JamRuby self.save(:validate => false) end + def add_timeline(timeline) + global = timeline["global"] + raise JamArgumentError, "global must be specified" unless global + + tracks = timeline["tracks"] + raise JamArgumentError, "tracks must be specified" unless tracks + + Recording.where(id: self.id).update_all(timeline: global.to_json) + + jam_tracks = tracks.select {|track| track["type"] == "jam_track"} + jam_tracks.each do |client_jam_track| + RecordedJamTrackTrack.where(recording_id: id, jam_track_track_id: client_jam_track["id"]).update_all(timeline: client_jam_track["timeline"].to_json) + end + end + + def self.popular_recordings(limit = 100) + Recording.select('recordings.id').joins('inner join claimed_recordings ON claimed_recordings.recording_id = recordings.id AND claimed_recordings.is_public = TRUE').where(all_discarded: false).where(is_done: true).where(deleted: false).order('play_count DESC').limit(limit).group('recordings.id') + end + private def self.validate_user_is_band_member(user, band) unless band.users.exists? user diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb new file mode 100644 index 000000000..d94dca2e0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -0,0 +1,125 @@ +module JamRuby + class RecurlyTransactionWebHook < ActiveRecord::Base + + belongs_to :user, class_name: 'JamRuby::User' + belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions + belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions + + validates :recurly_transaction_id, presence: true + validates :action, presence: true + validates :status, presence: true + validates :amount_in_cents, numericality: {only_integer: true} + validates :user, presence: true + + + SUCCESSFUL_PAYMENT = 'payment' + FAILED_PAYMENT = 'failed_payment' + REFUND = 'refund' + VOID = 'void' + + def is_credit_type? + transaction_type == REFUND || transaction_type == VOID + end + + def is_voided? + transaction_type == VOID + end + + def is_refund? + transaction_type == REFUND + end + + def self.is_transaction_web_hook?(document) + + return false if document.root.nil? + case document.root.name + when 'successful_payment_notification' + true + when 'successful_refund_notification' + true + when 'failed_payment_notification' + true + when 'void_payment_notification' + true + else + false + end + end + + # see spec for examples of XML + def self.create_from_xml(document) + + transaction = RecurlyTransactionWebHook.new + + case document.root.name + when 'successful_payment_notification' + transaction.transaction_type = SUCCESSFUL_PAYMENT + when 'successful_refund_notification' + transaction.transaction_type = REFUND + when 'failed_payment_notification' + transaction.transaction_type = FAILED_PAYMENT + when 'void_payment_notification' + transaction.transaction_type = VOID + else + raise 'unknown document type ' + document.root.name + end + + transaction.recurly_transaction_id = document.at_css('transaction id').content + transaction.user_id = document.at_css('account account_code').content + transaction.subscription_id = document.at_css('subscription_id').content + transaction.invoice_id = document.at_css('invoice_id').content + transaction.invoice_number_prefix = document.at_css('invoice_number_prefix').content + transaction.invoice_number = document.at_css('invoice_number').content + transaction.action = document.at_css('action').content + transaction.status = document.at_css('status').content + transaction.transaction_at = Time.parse(document.at_css('date').content) + transaction.amount_in_cents = document.at_css('amount_in_cents').content + transaction.reference = document.at_css('reference').content + transaction.message = document.at_css('message').content + + 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 + jam_track = sale.sale_line_items[0].product + jam_track_right = jam_track.right_for_user(transaction.user) if jam_track + if jam_track_right + jam_track_right.destroy + AdminMailer.alerts({ + subject:"NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked", + body: "A void 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.alerts({ + 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.alerts({ + subject:"ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale", + body: "We received a refund 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.alerts({ + 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 + end + + end + transaction + end + end +end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb new file mode 100644 index 000000000..732bd45bb --- /dev/null +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -0,0 +1,343 @@ +module JamRuby + + # a sale is created every time someone tries to buy something + class Sale < ActiveRecord::Base + + JAMTRACK_SALE = 'jamtrack' + + belongs_to :user, class_name: 'JamRuby::User' + has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' + + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id' + + validates :order_total, numericality: {only_integer: false} + validates :user, presence: true + + @@log = Logging.logger[Sale] + + def self.index(user, params = {}) + + limit = params[:per_page] + limit ||= 20 + limit = limit.to_i + + query = Sale.limit(limit) + .includes([:recurly_transactions, :sale_line_items]) + .where('sales.user_id' => user.id) + .order('sales.created_at DESC') + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + { query: query, next_page: nil} + elsif query.length < limit # no more results + { query: query, next_page: nil} + else + { query: query, next_page: next_page } + end + + end + + def state + original_total = self.recurly_total_in_cents + + is_voided = false + refund_total = 0 + + recurly_transactions.each do |transaction| + if transaction.is_voided? + is_voided = true + else + + end + + if transaction.is_refund? + refund_total = refund_total + transaction.amount_in_cents + end + end + + # if refund_total is > 0, then you have a refund. + # if voided is true, then in theory the whole thing has been refunded + { + voided: is_voided, + original_total: original_total, + refund_total: refund_total + } + 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 + 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 + # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation) + # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028) + # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack + 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 + end + + jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks) + sales << jam_track_sale if jam_track_sale + + # TODO: process shopping_carts_subscriptions + + 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 + + # 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 + 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) + + client = RecurlyClient.new + + sale = nil + Sale.transaction do + sale = create_jam_track_sale(current_user) + + if sale.valid? + account = client.get_account(current_user) + if account.present? + + purge_pending_adjustments(account) + + created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + + # now invoice the sale ... almost done + + begin + invoice = account.invoice! + sale.recurly_invoice_id = invoice.uuid + sale.recurly_invoice_number = invoice.invoice_number + + # now slap in all the real tax/purchase totals + sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents + sale.recurly_tax_in_cents = invoice.tax_in_cents + sale.recurly_total_in_cents = invoice.total_in_cents + sale.recurly_currency = invoice.currency + + # and resolve against sale_line_items + sale.sale_line_items.each do |sale_line_item| + found_line_item = false + invoice.line_items.each do |line_item| + if line_item.uuid == sale_line_item.recurly_adjustment_uuid + sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents + sale_line_item.recurly_total_in_cents =line_item.total_in_cents + sale_line_item.recurly_currency = line_item.currency + sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents + found_line_item = true + break + end + + end + + if !found_line_item + @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") + puts "CANT FIND LINE ITEM" + end + end + + unless sale.save + raise RecurlyClientError, "Invalid sale (at end)." + end + rescue Recurly::Resource::Invalid => e + # this exception is thrown by invoice! if the invoice is invalid + sale.rollback_adjustments(current_user, created_adjustments) + sale = nil + raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic + end + else + raise RecurlyClientError, "Could not find account to place order." + end + else + raise RecurlyClientError, "Invalid sale." + end + end + sale + end + + def process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + + created_adjustments = [] + + begin + shopping_carts_jam_tracks.each do |shopping_cart| + process_jam_track(current_user, shopping_cart, account, created_adjustments) + end + rescue Recurly::Error, NoMethodError => x + # rollback any adjustments created if error + rollback_adjustments(user, created_adjustments) + raise RecurlyClientError, x.to_s + rescue Exception => e + # rollback any adjustments created if error + rollback_adjustments(user, created_adjustments) + raise e + end + + created_adjustments + end + + + def process_jam_track(current_user, shopping_cart, account, created_adjustments) + recurly_adjustment_uuid = nil + recurly_adjustment_credit_uuid = nil + + # we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info + shopping_cart.reload + + # get the JamTrack in this shopping cart + jam_track = 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 + end + + # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack + adjustments = shopping_cart.create_adjustment_attributes(current_user) + + adjustments.each do |adjustment| + + # create the adjustment at Recurly (this may not look like it, but it is a REST API) + created_adjustment = account.adjustments.new(adjustment) + created_adjustment.save + + # if the adjustment could not be made, bail + raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any? + + # keep track of adjustments we created for this order, in case we have to roll them back + created_adjustments << created_adjustment + + if ShoppingCart.is_product_purchase?(adjustment) + # this was a normal product adjustment, so track it as such + recurly_adjustment_uuid = created_adjustment.uuid + else + # this was a 'credit' adjustment, so track it as such + recurly_adjustment_credit_uuid = created_adjustment.uuid + end + end + + # create one sale line item for every jam track + sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) + + # 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}") + 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 + + # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks + User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if shopping_cart.free? + + # 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 + + # 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 + + + def rollback_adjustments(current_user, adjustments) + begin + adjustments.each { |adjustment| adjustment.destroy } + rescue Exception => e + AdminMailer.alerts({ + subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback", + body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}" + }).deliver + + end + end + + def self.purge_pending_adjustments(account) + account.adjustments.pending.find_each do |adjustment| + # we only pre-emptively destroy pending adjustments if they appear to be created by the server + adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment) + end + end + + def is_jam_track_sale? + sale_type == JAMTRACK_SALE + end + + def self.create_jam_track_sale(user) + sale = Sale.new + sale.user = user + sale.sale_type = JAMTRACK_SALE + sale.order_total = 0 + sale.save + sale + end + + # this checks just jamtrack sales appropriately + def self.check_integrity_of_jam_track_sales + Sale.select([:total, :voided]).find_by_sql( + "SELECT COUNT(sales.id) AS total, + COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided + FROM sales + LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id + WHERE sale_type = '#{JAMTRACK_SALE}'") + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb new file mode 100644 index 000000000..2022318b0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -0,0 +1,84 @@ +module JamRuby + class SaleLineItem < ActiveRecord::Base + + JAMBLASTER = 'JamBlaster' + JAMCLOUD = 'JamCloud' + JAMTRACK = 'JamTrack' + + belongs_to :sale, class_name: 'JamRuby::Sale' + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' + 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 :unit_price, numericality: {only_integer: false} + validates :quantity, numericality: {only_integer: true} + validates :free, numericality: {only_integer: true} + validates :sales_tax, numericality: {only_integer: false}, allow_nil: true + validates :shipping_handling, numericality: {only_integer: false} + validates :recurly_plan_code, presence:true + validates :sale, presence:true + + def product + if product_type == JAMTRACK + JamTrack.find_by_id(product_id) + else + raise 'unsupported product type' + end + end + + def product_info + item = product + { name: product.name } if item + end + + def state + voided = false + refunded = false + failed = false + succeeded = false + + recurly_transactions.each do |transaction| + if transaction.transaction_type == RecurlyTransactionWebHook::VOID + voided = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::REFUND + refunded = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::FAILED_PAYMENT + failed = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT + succeeded = true + end + end + + { + void: voided, + refund: refunded, + fail: failed, + success: succeeded + } + end + + + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) + product_info = shopping_cart.product_info + + sale.order_total = sale.order_total + product_info[:real_price] + + sale_line_item = SaleLineItem.new + sale_line_item.product_type = shopping_cart.cart_type + sale_line_item.unit_price = product_info[:price] + sale_line_item.quantity = product_info[:quantity] + sale_line_item.free = product_info[:marked_for_redeem] + sale_line_item.sales_tax = nil + sale_line_item.shipping_handling = 0 + sale_line_item.recurly_plan_code = product_info[:plan_code] + sale_line_item.product_id = shopping_cart.cart_id + sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid + sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid + sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid + sale.sale_line_items << sale_line_item + sale_line_item.save + sale_line_item + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index da1e567f6..ed74d2490 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -1,34 +1,170 @@ module JamRuby class ShoppingCart < ActiveRecord::Base + # just a normal purchase; used on the description field of a recurly adjustment + PURCHASE_NORMAL = 'purchase-normal' + # a free purchase; used on the description field of a recurly adjustment + PURCHASE_FREE = 'purchase-free' + # a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment + PURCHASE_FREE_CREDIT = 'purchase-free-credit' + + PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT] + attr_accessible :quantity, :cart_type, :product_info + validates_uniqueness_of :cart_id, scope: :cart_type + belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" validates :cart_id, presence: true validates :cart_type, presence: true validates :cart_class_name, presence: true + validates :marked_for_redeem, numericality: {only_integer: true} default_scope order('created_at DESC') def product_info product = self.cart_product - {name: product.name, price: product.price, product_id: cart_id} unless product.nil? + {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} unless product.nil? end + # multiply quantity by price + def total_price(product) + quantity * product.price + end + + # multiply (quantity - redeemable) by price + def real_price(product) + (quantity - marked_for_redeem) * product.price + end + + def cart_product - self.cart_class_name.classify.constantize.find_by_id self.cart_id unless self.cart_class_name.blank? + self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? end - def self.create user, product, quantity = 1 + def redeem(mark_redeem) + self.marked_for_redeem = mark_redeem ? 1 : 0 + end + + def free? + marked_for_redeem == quantity + end + + def self.create user, product, quantity = 1, mark_redeem = false cart = ShoppingCart.new - cart.user = user + if user.is_a?(User) + cart.user = user + else + cart.anonymous_user_id = user.id + end + cart.cart_type = product.class::PRODUCT_TYPE cart.cart_class_name = product.class.name cart.cart_id = product.id cart.quantity = quantity + cart.redeem(mark_redeem) cart.save cart end + + def is_jam_track? + cart_type == JamTrack::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? + + 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)", + tax_exempt: true + }, + { + accounting_code: PURCHASE_FREE, + currency: 'USD', + unit_amount_in_cents: (info[:total_price] * 100).to_i, + description: "JamTrack: " + info[:name], + tax_exempt: true + } + ] + else + [ + { + accounting_code: PURCHASE_NORMAL, + currency: 'USD', + unit_amount_in_cents: (info[:total_price] * 100).to_i, + description: "JamTrack: " + info[:name], + tax_exempt: false + } + ] + end + end + + def self.is_product_purchase?(adjustment) + (adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT) + end + + # recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment) + # this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).' + def self.is_server_pending_adjustment?(recurly_adjustment) + recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT)) + end + + # if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked. + # 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... + 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 + end + end + end + mark_redeem + end + + # adds a jam_track to cart, checking for promotions + def self.add_jam_track_to_cart(any_user, jam_track) + cart = nil + ShoppingCart.transaction do + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) + cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) + 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 + 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 mark_redeem && carts.length > 0 + carts[0].redeem(mark_redeem) + carts[0].save + end + end + + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb new file mode 100644 index 000000000..b415d45a4 --- /dev/null +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -0,0 +1,36 @@ +module JamRuby + + # some times someone comes to signup as a new user, but there is context to preserve. + # the AnyUser cookie is one way that we can track the user from pre-signup to post-signup + # anyway, once the signup is done, we check to see if there is a SignupHint, and if so, + # we use it to figure out what to do with the user after they signup + class SignupHint < ActiveRecord::Base + + + belongs_to :user, class_name: 'JamRuby::User' + + validates :redirect_location, length: {maximum: 1000} + validates :want_jamblaster, inclusion: {in: [nil, true, false]} + + def self.refresh_by_anoymous_user(anonymous_user, options = {}) + + hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id) + + unless hint + hint = SignupHint.new + end + + hint.anonymous_user_id = anonymous_user.id + hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) + hint.want_jamblaster = options[:want_jamblaster] if options.has_key?(:want_jamblaster) + hint.expires_at = 15.minutes.from_now + hint.save + hint + end + + def self.delete_old + SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all + end + + end +end diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb index 10b7ecb40..e430edb2d 100644 --- a/ruby/lib/jam_ruby/models/track.rb +++ b/ruby/lib/jam_ruby/models/track.rb @@ -55,80 +55,99 @@ module JamRuby return query end + def self.diff_track(track_class, existing_tracks, new_tracks, &blk) + result = [] + if new_tracks.length == 0 + existing_tracks.delete_all + else + + # we will prune from this as we find matching tracks + to_delete = Set.new(existing_tracks) + to_add = Array.new(new_tracks) + + existing_tracks.each do |existing_track| + new_tracks.each do |new_track| + + if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id + to_delete.delete(existing_track) + to_add.delete(new_track) + + blk.call(existing_track, new_track) + + result.push(existing_track) + + if existing_track.save + next + else + result = existing_track + raise ActiveRecord::Rollback + end + end + end + end + + + to_add.each do |new_track| + existing_track = track_class.new + + blk.call(existing_track, new_track) + + if existing_track.save + result.push(existing_track) + else + result = existing_track + raise ActiveRecord::Rollback + end + end + + to_delete.each do |delete_me| + delete_me.delete + end + end + result + end # this is a bit different from a normal track synchronization in that the client just sends up all tracks, # ... some may already exist - def self.sync(clientId, tracks) - result = [] + def self.sync(clientId, tracks, backing_tracks = [], metronome_open = false) + result = {} + + backing_tracks = [] unless backing_tracks Track.transaction do connection = Connection.find_by_client_id!(clientId) + # synchronize metronome_open on connection + if connection.metronome_open != metronome_open + Connection.where(:id => connection.id).update_all(:metronome_open => metronome_open) + end + # each time tracks are synced we have to update the entry in music_sessions_user_history msh = MusicSessionUserHistory.find_by_client_id!(clientId) instruments = [] - if tracks.length == 0 - connection.tracks.delete_all - else - connection_tracks = connection.tracks + tracks.each do |track| + instruments << track[:instrument_id] + end - # we will prune from this as we find matching tracks - to_delete = Set.new(connection_tracks) - to_add = Array.new(tracks) + result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info| + track_record.connection = connection + track_record.client_track_id = track_info[:client_track_id] + track_record.client_resource_id = track_info[:client_resource_id] + track_record.instrument_id = track_info[:instrument_id] + track_record.sound = track_info[:sound] + end - tracks.each do |track| - instruments << track[:instrument_id] - end + result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info| + track_record.connection = connection + track_record.client_track_id = track_info[:client_track_id] + track_record.client_resource_id = track_info[:client_resource_id] + track_record.filename = track_info[:filename] + end - connection_tracks.each do |connection_track| - tracks.each do |track| - - if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id - to_delete.delete(connection_track) - to_add.delete(track) - # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session - connection_track.instrument_id = track[:instrument_id] - connection_track.sound = track[:sound] - connection_track.client_track_id = track[:client_track_id] - connection_track.client_resource_id = track[:client_resource_id] - - result.push(connection_track) - - if connection_track.save - next - else - result = connection_track - raise ActiveRecord::Rollback - end - - end - end - end - - msh.instruments = instruments.join("|") - if !msh.save - raise ActiveRecord::Rollback - end - - to_add.each do |track| - connection_track = Track.new - connection_track.connection = connection - connection_track.instrument_id = track[:instrument_id] - connection_track.sound = track[:sound] - connection_track.client_track_id = track[:client_track_id] - connection_track.client_resource_id = track[:client_resource_id] - if connection_track.save - result.push(connection_track) - else - result = connection_track - raise ActiveRecord::Rollback - end - end - - to_delete.each do |delete_me| - delete_me.delete - end + msh.instruments = instruments.join("|") + if !msh.save + raise ActiveRecord::Rollback end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 30bde5246..be020d4b3 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -71,6 +71,11 @@ module JamRuby has_many :playing_claimed_recordings, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :claimed_recording_initiator has_many :playing_jam_tracks, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :jam_track_initiator + # VRFS-2916 jam_tracks.id is varchar: REMOVE + # has_many :jam_tracks_played, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id', :conditions => "jam_track_id IS NOT NULL" + # VRFS-2916 jam_tracks.id is varchar: ADD + has_many :jam_tracks_played, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id', :conditions => ["playable_type = 'JamRuby::JamTrack'"] + # self.id = user_id in likes table has_many :likings, :class_name => "JamRuby::Like", :inverse_of => :user, :dependent => :destroy @@ -122,7 +127,12 @@ module JamRuby # saved tracks has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user + has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user + has_many :recorded_jam_track_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedJamTrackTrack", :inverse_of => :user + + # jam track recordings started + has_many :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id', :class_name => "JamRuby::Recording", :inverse_of => :jam_track_initiator # invited users has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser" @@ -149,6 +159,9 @@ module JamRuby # score history has_many :from_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'from_user_id' has_many :to_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'to_user_id' + has_many :sales, :class_name => 'JamRuby::Sale', dependent: :destroy + has_many :recurly_transaction_web_hooks, :class_name => 'JamRuby::RecurlyTransactionWebHook', dependent: :destroy + # This causes the authenticate method to be generated (among other stuff) #has_secure_password @@ -166,6 +179,8 @@ module JamRuby validates_confirmation_of :password, :if => :should_validate_password? 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 :subscribe_email, :inclusion => {:in => [nil, true, false]} validates :musician, :inclusion => {:in => [true, false]} validates :show_whats_next, :inclusion => {:in => [nil, true, false]} @@ -356,6 +371,14 @@ module JamRuby MusicSession.scheduled_rsvp(self, true).length end + def purchased_jamtracks_count + self.purchased_jam_tracks.count + end + + def sales_count + self.sales.count + end + def joined_score return nil unless has_attribute?(:score) a = read_attribute(:score) @@ -852,6 +875,11 @@ module JamRuby user.email = user.update_email user.update_email_token = nil user.save + begin + RecurlyClient.new.update_account(user) + rescue Recurly::Error + @@log.debug("No recurly account found; continuing") + end return user end @@ -923,6 +951,8 @@ module JamRuby signup_confirm_url = options[:signup_confirm_url] affiliate_referral_id = options[:affiliate_referral_id] recaptcha_failed = options[:recaptcha_failed] + any_user = options[:any_user] + reuse_card = options[:reuse_card] user = User.new @@ -933,6 +963,7 @@ module JamRuby user.subscribe_email = true user.terms_of_service = terms_of_service user.musician = musician + user.reuse_card unless reuse_card.nil? # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. @@ -977,6 +1008,9 @@ module JamRuby user.photo_url = photo_url + # copy over the shopping cart to the new user, if a shopping cart is provided + user.shopping_carts = any_user.shopping_carts if any_user + unless fb_signup.nil? user.update_fb_authorization(fb_signup) @@ -1498,6 +1532,33 @@ module JamRuby stats['audio_latency_avg'] = result['last_jam_audio_latency_avg'].to_f stats end + + def destroy_all_shopping_carts + ShoppingCart.where("user_id=?", self).destroy_all + end + + def unsubscribe_token + self.class.create_access_token(self) + end + + # Verifier based on our application secret + def self.verifier + ActiveSupport::MessageVerifier.new(APP_CONFIG.secret_token) + end + + # Get a user from a token + def self.read_access_token(signature) + uid = self.verifier.verify(signature) + User.find_by_id uid + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + + # Class method for token generation + def self.create_access_token(user) + verifier.generate(user.id) + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/models/user_sync.rb b/ruby/lib/jam_ruby/models/user_sync.rb index 8cdeac13b..1e915b953 100644 --- a/ruby/lib/jam_ruby/models/user_sync.rb +++ b/ruby/lib/jam_ruby/models/user_sync.rb @@ -4,6 +4,7 @@ module JamRuby belongs_to :recorded_track belongs_to :mix belongs_to :quick_mix + belongs_to :recorded_backing_track def self.show(id, user_id) self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first @@ -22,7 +23,7 @@ module JamRuby raise 'no user id specified' if user_id.blank? query = UserSync - .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[]) + .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[]) .joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id") .where(user_id: user_id) .where(%Q{ diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb new file mode 100644 index 000000000..ec88cdaa5 --- /dev/null +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -0,0 +1,238 @@ +require 'recurly' +module JamRuby + class RecurlyClient + def initialize() + @log = Logging.logger[self] + end + + def create_account(current_user, billing_info) + options = account_hash(current_user, billing_info) + account = nil + begin + #puts "Recurly.api_key: #{Recurly.api_key}" + account = Recurly::Account.create(options) + raise RecurlyClientError.new(account.errors) if account.errors.any? + rescue Recurly::Error, NoMethodError => x + #puts "Error: #{x} : #{Kernel.caller}" + raise RecurlyClientError, x.to_s + else + if account + current_user.update_attribute(:recurly_code, account.account_code) + end + end + account + end + + def has_account?(current_user) + account = get_account(current_user) + !!account + end + + def delete_account(current_user) + account = get_account(current_user) + if (account) + begin + account.destroy + rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError, x.to_s + end + else + raise RecurlyClientError, "Could not find account to delete." + end + account + end + + def get_account(current_user) + current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil + rescue Recurly::Error => x + raise RecurlyClientError, x.to_s + end + + def update_account(current_user, billing_info=nil) + account = get_account(current_user) + if(account.present?) + options = account_hash(current_user, billing_info) + begin + account.update_attributes(options) + rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError, x.to_s + end + end + account + end + + def payment_history(current_user, options ={}) + + limit = params[:limit] + limit ||= 20 + limit = limit.to_i + + cursor = options[:cursor] + + payments = [] + account = get_account(current_user) + if(account.present?) + begin + + account.transaction.paginate(per_page:limit, cursor:cursor).each do |transaction| + # XXX this isn't correct because we create 0 dollar transactions too (for free stuff) + #if transaction.amount_in_cents > 0 # Account creation adds a transaction record + payments << { + :created_at => transaction.created_at, + :amount_in_cents => transaction.amount_in_cents, + :status => transaction.status, + :payment_method => transaction.payment_method, + :reference => transaction.reference, + :plan_code => transaction.plan_code + } + #end + end + rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError, x.to_s + end + end + payments + end + + def update_billing_info(current_user, billing_info=nil) + account = get_account(current_user) + if (account.present?) + begin + account.billing_info = billing_info + account.billing_info.save + rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError, x.to_s + end + + raise RecurlyClientError.new(account.errors) if account.errors.any? + else + raise RecurlyClientError, "Could not find account to update billing info." + end + account + end + + def refund_user_subscription(current_user, jam_track) + jam_track_right=JamRuby::JamTrackRight.where("user_id=? AND jam_track_id=?", current_user.id, jam_track.id).first + if jam_track_right + refund_subscription(jam_track_right) + else + raise RecurlyClientError, "The user #{current_user} does not have a subscription to #{jam_track}" + end + end + + def refund_subscription(jam_track_right) + account = get_account(jam_track_right.user) + if (account.present?) + terminated = false + begin + jam_track = jam_track_right.jam_track + account.subscriptions.find_each do |subscription| + #puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}" + if(subscription.plan.plan_code == jam_track.plan_code) + subscription.terminate(:full) + raise RecurlyClientError.new(subscription.errors) if subscription.errors.any? + terminated = true + end + end + + if terminated + jam_track_right.destroy() + else + raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund." + end + + rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError, x.to_s + end + + else + raise RecurlyClientError, "Could not find account to refund order." + end + account + end + + def find_jam_track_plan(jam_track) + plan = nil + begin + plan = Recurly::Plan.find(jam_track.plan_code) + rescue Recurly::Resource::NotFound + end + plan + end + + def create_jam_track_plan(jam_track) + plan = Recurly::Plan.create(accounting_code: "", + bypass_hosted_confirmation: false, + cancel_url: nil, + description: jam_track.description, + display_donation_amounts: false, + display_phone_number: false, + display_quantity: false, + name: "JamTrack: #{jam_track.name}", + payment_page_css: nil, + payment_page_tos_link: nil, + plan_code: jam_track.plan_code, + plan_interval_length: 1, + plan_interval_unit: "months", + setup_fee_in_cents: Recurly::Money.new(:USD => 0), # + success_url: "", + tax_exempt: false, + total_billing_cycles: 1, + trial_interval_length: 0, + trial_interval_unit: "days", + unit_amount_in_cents: Recurly::Money.new(:USD => 1_99), + unit_name: "unit" + ) + raise RecurlyClientError.new(plan.errors) if plan.errors.any? + end + + + def find_or_create_account(current_user, billing_info) + account = get_account(current_user) + + if(account.nil?) + account = create_account(current_user, billing_info) + else + update_billing_info(current_user, billing_info) + end + account + end + + private + def account_hash(current_user, billing_info) + options = { + account_code: current_user.id, + email: current_user.email, + first_name: current_user.first_name, + last_name: current_user.last_name, + address: { + city: current_user.city, + state: current_user.state, + country: current_user.country + } + } + + options[:billing_info] = billing_info if billing_info + options + end + end # class + + class RecurlyClientError < Exception + attr_accessor :errors + def initialize(data) + if data.respond_to?('has_key?') + self.errors = data + else + self.errors = {:message=>data.to_s} + end + end # initialize + + def to_s + s=super + s << ", errors: #{errors.inspect}" if self.errors.any? + s + end + + end # RecurlyClientError +end # module + \ No newline at end of file diff --git a/ruby/lib/jam_ruby/resque/audiomixer.rb b/ruby/lib/jam_ruby/resque/audiomixer.rb index e0a3b065e..fa987b69f 100644 --- a/ruby/lib/jam_ruby/resque/audiomixer.rb +++ b/ruby/lib/jam_ruby/resque/audiomixer.rb @@ -260,6 +260,10 @@ module JamRuby end @manifest = symbolize_keys(mix.manifest) + @@log.debug("manifest") + @@log.debug("--------") + @@log.debug(JSON.pretty_generate(@manifest)) + @manifest[:mix_id] = mix_id # slip in the mix_id so that the job can add it to the ogg comments # sanity check the manifest diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb index 19d836462..395d12084 100644 --- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb +++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb @@ -7,25 +7,24 @@ require 'digest/md5' module JamRuby class JamTracksBuilder extend JamRuby::ResqueStats - + attr_accessor :jam_track_right_id, :bitrate @queue = :jam_tracks_builder def log @log || Logging.logger[JamTracksBuilder] end - attr_accessor :jam_track_right_id - - def self.perform(jam_track_right_id) + def self.perform(jam_track_right_id, bitrate=48) jam_track_builder = JamTracksBuilder.new() jam_track_builder.jam_track_right_id = jam_track_right_id + jam_track_builder.bitrate=bitrate jam_track_builder.run end def run + self.bitrate ||= 48 begin - log.info("jam_track_builder job starting. jam_track_right_id #{jam_track_right_id}") - + log.info("jam_track_builder job starting. jam_track_right_id #{jam_track_right_id}, bitrate: #{self.bitrate}") begin @jam_track_right = JamTrackRight.find(jam_track_right_id) @@ -37,16 +36,14 @@ module JamRuby # track that it's started ( and avoid db validations ) JamTrackRight.where(:id => @jam_track_right.id).update_all(:signing_started_at => Time.now, :should_retry => false) + JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate) - JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right) - - length = @jam_track_right.url.size() + # If bitrate is 48 (the default), use that URL. Otherwise, use 44kHz: + length = (self.bitrate==48) ? @jam_track_right.url_48.size() : @jam_track_right.url_44.size() md5 = Digest::MD5.new + @jam_track_right.finish_sign(length, md5.to_s, self.bitrate) - @jam_track_right.finish_sign(length, md5.to_s) - - log.info "Signed jamtrack to #{@jam_track_right[:url]}" - + log.info "Signed #{self.bitrate}kHz jamtrack to #{@jam_track_right[:url]}" rescue Exception => e # record the error in the database post_error(e) diff --git a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb index ddfb6b28f..f46946886 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb @@ -11,6 +11,7 @@ module JamRuby @@log.debug("waking up") FacebookSignup.delete_old + SignupHint.delete_old @@log.debug("done") end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index bd52cc17f..1f4e3c662 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -18,6 +18,7 @@ FactoryGirl.define do musician true terms_of_service true last_jam_audio_latency 5 + reuse_card true #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -232,6 +233,11 @@ FactoryGirl.define do sequence(:client_resource_id) { |n| "resource_id#{n}"} end + factory :backing_track, :class => JamRuby::BackingTrack do + sequence(:client_track_id) { |n| "client_track_id#{n}"} + filename 'foo.mp3' + end + factory :video_source, :class => JamRuby::VideoSource do #client_video_source_id "test_source_id" sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"} @@ -250,6 +256,19 @@ FactoryGirl.define do association :recording, factory: :recording end + factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do + sequence(:client_id) { |n| "client_id-#{n}"} + sequence(:backing_track_id) { |n| "track_id-#{n}"} + sequence(:client_track_id) { |n| "client_track_id-#{n}"} + sequence(:filename) { |n| "filename-{#n}"} + sequence(:url) { |n| "/recordings/blah/#{n}"} + md5 'abc' + length 1 + fully_uploaded true + association :user, factory: :user + association :recording, factory: :recording + end + factory :recorded_video, :class => JamRuby::RecordedVideo do sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} fully_uploaded true @@ -258,6 +277,12 @@ FactoryGirl.define do association :recording, factory: :recording end + factory :recorded_jam_track_track, :class => JamRuby::RecordedJamTrackTrack do + association :user, factory: :user + association :recording, factory: :recording + association :jam_track_track, factory: :jam_track_track + end + factory :instrument, :class => JamRuby::Instrument do description { |n| "Instrument #{n}" } end @@ -559,6 +584,7 @@ FactoryGirl.define do end factory :playable_play, :class => JamRuby::PlayablePlay do + association :user, factory: :user end factory :recording_like, :class => JamRuby::RecordingLiker do @@ -700,22 +726,19 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } - bpm 100.1 time_signature '4/4' status 'Production' recording_type 'Cover' sequence(:original_artist) { |n| "original-artist-#{n}" } sequence(:songwriter) { |n| "songwriter-#{n}" } sequence(:publisher) { |n| "publisher-#{n}" } - pro 'ASCAP' sales_region 'United States' price 1.99 reproduction_royalty true public_performance_royalty true reproduction_royalty_amount 0.999 licensor_royalty_amount 0.999 - pro_royalty_amount 0.999 - available true + sequence(:plan_code) { |n| "jamtrack-#{n}" } genre JamRuby::Genre.first association :licensor, factory: :jam_track_licensor @@ -746,4 +769,30 @@ FactoryGirl.define do bpm 120 tap_in_count 3 end + + factory :sale, :class => JamRuby::Sale do + order_total 0 + association :user, factory:user + end + + factory :recurly_transaction_web_hook, :class => JamRuby::RecurlyTransactionWebHook do + + transaction_type JamRuby::RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT + sequence(:recurly_transaction_id ) { |n| "recurly-transaction-id-#{n}" } + sequence(:subscription_id ) { |n| "subscription-id-#{n}" } + sequence(:invoice_id ) { |n| "invoice-id-#{n}" } + sequence(:invoice_number ) { |n| 1000 + n } + invoice_number_prefix nil + action 'purchase' + status 'success' + transaction_at Time.now + amount_in_cents 199 + reference 100000 + message 'meh' + association :user, factory: :user + + factory :recurly_transaction_web_hook_failed do + transaction_type JamRuby::RecurlyTransactionWebHook::FAILED_PAYMENT + end + end end diff --git a/ruby/spec/files/off.ogg b/ruby/spec/files/off.ogg new file mode 100644 index 000000000..743d6e3aa Binary files /dev/null and b/ruby/spec/files/off.ogg differ diff --git a/ruby/spec/jam_ruby/jam_track_importer_spec.rb b/ruby/spec/jam_ruby/jam_track_importer_spec.rb new file mode 100644 index 000000000..5a0a89cf4 --- /dev/null +++ b/ruby/spec/jam_ruby/jam_track_importer_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe JamTrackImporter do + + let(:s3_manager) { S3Manager.new(app_config.aws_bucket_jamtracks, app_config.aws_access_key_id, app_config.aws_secret_access_key) } + + let(:sample_yml) { + { + "name" => "Back in Black", + "plan_code" => "jamtrack-acdc-backinblack", + "recording_type" => 'Cover', + "pro" => 'ASCAP', + "genre" => 'rock' + + } + } + describe "load_metalocation" do + + include UsesTempFiles + + metafile = 'meta.yml' + in_directory_with_file(metafile) + + before(:each) do + content_for_file(YAML.dump(sample_yml)) + end + + it "no meta" do + s3_metalocation = 'audio/Artist 1/Bogus Place/meta.yml' + JamTrackImporter.load_metalocation(s3_metalocation).should be_nil + end + + it "successfully" do + s3_metalocation = 'audio/Artist 1/Song 1/meta.yml' + s3_manager.upload(s3_metalocation, metafile) + + JamTrackImporter.load_metalocation(s3_metalocation).should eq(sample_yml) + end + end + + describe "synchronize" do + let(:jam_track) { JamTrack.new } + let(:importer) { JamTrackImporter.new } + let(:minimum_meta) { nil } + let(:metalocation) { 'audio/Artist 1/Song 1/meta.yml' } + let(:options) {{ skip_audio_upload:true }} + + it "bare minimum specification" do + importer.synchronize_metadata(jam_track, minimum_meta, metalocation, 'Artist 1', 'Song 1') + + jam_track.plan_code.should eq('jamtrack-artist1-song1') + jam_track.name.should eq("Song 1") + jam_track.description.should == "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the Artist 1 song \"Song 1\"." + jam_track.time_signature.should be_nil + jam_track.status.should eq('Staging') + jam_track.recording_type.should eq('Cover') + jam_track.original_artist.should eq('Artist 1') + jam_track.songwriter.should be_nil + jam_track.publisher.should be_nil + jam_track.sales_region.should eq('United States') + jam_track.price.should eq(1.99) + end + end + + describe "parse_wav" do + it "Guitar" do + result = JamTrackImporter.new.parse_wav('blah/Ready for Love Stem - Guitar - Main.wav') + result[:instrument].should eq('electric guitar') + result[:part].should eq('Main') + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb b/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb new file mode 100644 index 000000000..3e70a7c95 --- /dev/null +++ b/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' + +describe JamTracksManager do + + +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb index 392d70473..c117a65c2 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -745,6 +745,29 @@ describe ActiveMusicSession do @music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN] end + + it "disallow a claimed recording to be started when backing track is open" do + # open the backing track + @backing_track = "foo.mp3" + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + + # and try to open a recording for playback + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_true + @music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN] + end + + it "disallow a claimed recording to be started when metronome is open" do + # open the metronome + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + + # and try to open a recording for playback + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_true + @music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN] + end end end @@ -830,5 +853,134 @@ describe ActiveMusicSession do music_sessions[0].connections[0].tracks.should have(1).items end end + + describe "open_backing_track" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user1) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true) + # @music_session.connections << @connection + @music_session.save! + @connection.join_the_session(@music_session, true, nil, @user1, 10) + @backing_track = "foo/bar.mp3" + end + + it "allow a backing track to be associated" do + # simple success case; just open the backing track and observe the state of the session is correct + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.backing_track_path.should == @backing_track + @music_session.backing_track_initiator.should == @user1 + end + + it "allow a backing track to be closed" do + # simple success case; close an opened backing track and observe the state of the session is correct + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.close_backing_track + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.backing_track_path.should be_nil + @music_session.backing_track_initiator.should be_nil + end + + it "disallow a backing track to be opened when another is already opened" do + # if a backing track is open, don't allow another to be opened + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN] + end + + it "disallow a backing track to be opened when recording is ongoing" do + @recording = Recording.start(@music_session, @user1) + @music_session.errors.any?.should be_false + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS] + end + + it "disallow a backing track to be opened when recording is playing back" do + # create a recording, and open it for play back + @recording = Recording.start(@music_session, @user1) + @recording.errors.any?.should be_false + @recording.stop + @recording.reload + @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true) + @claimed_recording.errors.any?.should be_false + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + + # while it's open, try to open a jam track + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS] + end + + end + + describe "open_metronome" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user1) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true) + # @music_session.connections << @connection + @music_session.save! + @connection.join_the_session(@music_session, true, nil, @user1, 10) + end + + it "allow a metronome to be activated" do + # simple success case; just open the metronome and observe the state of the session is correct + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.metronome_active.should == true + @music_session.metronome_initiator.should == @user1 + end + + it "allow a metronome to be closed" do + # simple success case; close an opened metronome and observe the state of the session is correct + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + @music_session.close_metronome + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.metronome_active.should be_false + @music_session.metronome_initiator.should be_nil + end + + it "disallow a metronome to be opened when recording is ongoing" do + @recording = Recording.start(@music_session, @user1) + @music_session.errors.any?.should be_false + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_true + @music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS] + end + + it "disallow a metronome to be opened when recording is playing back" do + # create a recording, and open it for play back + @recording = Recording.start(@music_session, @user1) + @recording.errors.any?.should be_false + @recording.stop + @recording.reload + @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true) + @claimed_recording.errors.any?.should be_false + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + + # while it's open, try to open a jam track + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_true + @music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS] + end + + end + end 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 b92bfa04b..530535f8c 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -17,14 +17,6 @@ describe JamTrackRight do end - it "lists" do - jam_track_right = FactoryGirl.create(:jam_track_right) - jam_tracks = JamTrack.list_downloads(jam_track_right.user) - jam_tracks.should have_key('downloads') - jam_tracks.should have_key('next') - jam_tracks['downloads'].should have(1).items - end - describe "validations" do it "one purchase per user/jam_track combo" do user = FactoryGirl.create(:user) @@ -66,27 +58,28 @@ describe JamTrackRight do user = FactoryGirl.create(:user) jam_track_track = FactoryGirl.create(:jam_track_track) jam_track = jam_track_track.jam_track - - uploader = JamTrackTrackUploader.new(jam_track_track, :url) - uploader.store!(File.open(ogg_path, 'rb')) + + s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + + s3.upload(jam_track_track.manually_uploaded_filename(:url_48), ogg_path) + jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48) jam_track_track.save! - - jam_track_track[:url].should == jam_track_track.store_dir + '/' + jam_track_track.filename + + jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48) # verify it's on S3 - s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) - s3.exists?(jam_track_track[:url]).should be_true - s3.length(jam_track_track[:url]).should == File.size?(ogg_path) + s3.exists?(jam_track_track[:url_48]).should be_true + s3.length(jam_track_track[:url_48]).should == File.size?(ogg_path) jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track) #expect { JamRuby::JamTracksManager.save_jam_track_jkz(user, jam_track) #}.to_not raise_error(ArgumentError) jam_track_right.reload - jam_track_right[:url].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename # verify it's on S3 - url = jam_track_right[:url] + url = jam_track_right[:url_48] s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) s3.exists?(url).should be_true s3.length(url).should > File.size?(ogg_path) @@ -105,7 +98,7 @@ describe JamTrackRight do end it "bogus key" do - JamTrackRight.list_keys(user, [2112]).should eq([]) + JamTrackRight.list_keys(user, ['2112']).should eq([]) end it "valid track with no rights to it by querying user" do diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index 0c3ab5936..10cec40cd 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -14,6 +14,30 @@ describe JamTrack do jam_track.licensor.jam_tracks.should == [jam_track] end + describe 'plays' do + it "creates played instance properly" do + @jam_track = FactoryGirl.create(:jam_track) + play = PlayablePlay.new + + # VRFS-2916 jam_tracks.id is varchar: REMOVE + # play.jam_track = @jam_track + # VRFS-2916 jam_tracks.id is varchar: ADD + play.playable = @jam_track + + play.user = user + play.save! + expect(@jam_track.plays.count).to eq(1) + expect(@jam_track.plays[0].user.id).to eq(user.id) + expect(user.jam_tracks_played.count).to eq(1) + end + it "handles played errors" do + play = PlayablePlay.new + play.user = user + play.save + expect(play.errors.count).to eq(1) + end + end + describe "index" do it "empty query" do query, pager = JamTrack.index({}, user) @@ -55,25 +79,6 @@ describe JamTrack do end describe "validations" do - describe "bpm" do - it "1" do - FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true - end - - it "100" do - FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true - end - - it "100.1" do - FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true - end - - it "100.12" do - jam_track = FactoryGirl.build(:jam_track, bpm: 100.12) - jam_track.valid?.should be_false - jam_track.errors[:bpm].should == ['is invalid'] - end - end describe "price" do @@ -136,44 +141,5 @@ describe JamTrack do end end end - - describe "upload/download" do - JKA_NAME = 'blah.jkz' - - in_directory_with_file(JKA_NAME) - - before(:all) do - original_storage = JamTrackUploader.storage = :fog - end - - after(:all) do - JamTrackUploader.storage = @original_storage - end - - before(:each) do - content_for_file('abc') - end - - it "uploads to s3 with correct name, and then downloads via signed URL" do - jam_track = FactoryGirl.create(:jam_track) - uploader = JamTrackUploader.new(jam_track, :url) - uploader.store!(File.open(JKA_NAME)) # uploads file - jam_track.save! - - # verify that the uploader stores the correct path - jam_track[:url].should == jam_track.store_dir + '/' + jam_track.filename - - # verify it's on S3 - s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) - s3.exists?(jam_track[:url]).should be_true - s3.length(jam_track[:url]).should == 'abc'.length - - # download it via signed URL, and check contents - url = jam_track.sign_url - downloaded_contents = open(url).read - downloaded_contents.should == 'abc' - end - - end end diff --git a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb index 6cc5c3773..6fb4343f6 100644 --- a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb @@ -16,7 +16,7 @@ describe JamTrackTrack do jam_track_track_1 = FactoryGirl.create(:jam_track_track, position: 1, jam_track: jam_track) jam_track_track_2 = FactoryGirl.build(:jam_track_track, position: 1, jam_track: jam_track) jam_track_track_2.valid?.should == false - jam_track_track_2.errors[:position].should == ['has already been taken'] + #jam_track_track_2.errors[:position].should == ['has already been taken'] end it "jam_track required" do @@ -46,18 +46,20 @@ describe JamTrackTrack do end it "uploads to s3 with correct name, and then downloads via signed URL" do + s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + jam_track_track = FactoryGirl.create(:jam_track_track) - uploader = JamTrackTrackUploader.new(jam_track_track, :url) - uploader.store!(File.open(TRACK_NAME)) # uploads file + s3.upload(jam_track_track.manually_uploaded_filename(:url_48), TRACK_NAME) + jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48) jam_track_track.save! + jam_track_track.reload # verify that the uploader stores the correct path - jam_track_track[:url].should == jam_track_track.store_dir + '/' + jam_track_track.filename + jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48) # verify it's on S3 - s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) - s3.exists?(jam_track_track[:url]).should be_true - s3.length(jam_track_track[:url]).should == 'abc'.length + s3.exists?(jam_track_track[:url_48]).should be_true + s3.length(jam_track_track[:url_48]).should == 'abc'.length # download it via signed URL, and check contents url = jam_track_track.sign_url diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb index 52163bcc4..ecb2a9e4c 100644 --- a/ruby/spec/jam_ruby/models/latency_tester_spec.rb +++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb @@ -39,7 +39,7 @@ describe LatencyTester do latency_tester.connection.aasm_state = Connection::STALE_STATE.to_s latency_tester.save! - set_updated_at(latency_tester.connection, 1.days.ago) + set_updated_at(latency_tester.connection, 6.hours.ago) params[:client_id] = latency_tester.connection.client_id @@ -49,7 +49,7 @@ describe LatencyTester do # state should have refreshed from stale to connected found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s # updated_at needs to be poked on connection to keep stale non-stale - (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 24 # 1 day + (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 6 # 6hours end end end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 6273855d9..e152fed55 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -854,6 +854,13 @@ describe MusicSession do music_session_1.rsvp_slots[0].rsvp_requests_rsvp_slots[0].save! MusicSession.scheduled_rsvp(creator_1, true).should == [] end + + it "create_type = nil will still return RSVPs" do + music_session_1.create_type = nil + music_session_1.save! + + MusicSession.scheduled_rsvp(creator_1, true).should == [music_session_1] + end end end diff --git a/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb new file mode 100644 index 000000000..de65e22e1 --- /dev/null +++ b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb @@ -0,0 +1,228 @@ +require 'spec_helper' +require 'rest-client' + +describe RecordedBackingTrack do + + include UsesTempFiles + + before do + @user = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) + @recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user) + end + + it "should copy from a regular track properly" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + + @recorded_backing_track.user.id.should == @backing_track.connection.user.id + @recorded_backing_track.filename.should == @backing_track.filename + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.fully_uploaded.should == false + @recorded_backing_track.client_id = @connection.client_id + @recorded_backing_track.backing_track_id = @backing_track.id + end + + it "should update the next part to upload properly" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)" + @recorded_backing_track.errors[:md5][0].should == "can't be blank" + end + + it "properly finds a recorded track given its upload filename" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.save.should be_true + RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track + end + + it "gets a url for the track" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg" + end + + it "signs url" do + stub_const("APP_CONFIG", app_config) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.sign_url.should_not be_nil + end + + it "can not be downloaded if no claimed recording" do + user2 = FactoryGirl.create(:user) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.can_download?(user2).should be_false + @recorded_backing_track.can_download?(@user).should be_false + end + + it "can be downloaded if there is a claimed recording" do + @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false + @recorded_backing_track.can_download?(@user).should be_true + end + + + describe "aws-based operations", :aws => true do + + def put_file_to_aws(signed_data, contents) + + begin + RestClient.put( signed_data[:url], + contents, + { + :'Content-Type' => 'audio/ogg', + :Date => signed_data[:datetime], + :'Content-MD5' => signed_data[:md5], + :Authorization => signed_data[:authorization] + }) + rescue => e + puts e.response + raise e + end + + end + # create a test file + upload_file='some_file.ogg' + in_directory_with_file(upload_file) + + upload_file_contents="ogg binary stuff in here" + md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp + test_config = app_config + s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key) + + + before do + stub_const("APP_CONFIG", app_config) + # this block of code will fully upload a sample file to s3 + content_for_file(upload_file_contents) + s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging + + + end + + it "cant mark a part complete without having started it" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(1000, "abc") + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED + end + + it "no parts" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(1000, "abc") + @recorded_backing_track.upload_next_part(1000, "abc") + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS + end + + it "enough part failures reset the upload" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_false + APP_CONFIG.max_track_part_upload_failures.times do |i| + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS] + part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1 + expected_is_part_uploading = !part_failure_rollover + expected_part_failures = part_failure_rollover ? 0 : i + 1 + @recorded_backing_track.reload + @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading + @recorded_backing_track.part_failures.should == expected_part_failures + end + + @recorded_backing_track.reload + @recorded_backing_track.upload_failures.should == 1 + @recorded_backing_track.file_offset.should == 0 + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.upload_id.should be_nil + @recorded_backing_track.md5.should be_nil + @recorded_backing_track.length.should == 0 + end + + it "enough upload failures fails the upload forever" do + APP_CONFIG.stub(:max_track_upload_failures).and_return(1) + APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + APP_CONFIG.max_track_upload_failures.times do |j| + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_false + APP_CONFIG.max_track_part_upload_failures.times do |i| + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS] + part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1 + expected_is_part_uploading = part_failure_rollover ? false : true + expected_part_failures = part_failure_rollover ? 0 : i + 1 + @recorded_backing_track.reload + @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading + @recorded_backing_track.part_failures.should == expected_part_failures + end + @recorded_backing_track.upload_failures.should == j + 1 + end + + @recorded_backing_track.reload + @recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures + @recorded_backing_track.file_offset.should == 0 + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.upload_id.should be_nil + @recorded_backing_track.md5.should be_nil + @recorded_backing_track.length.should == 0 + + # try to poke it and get the right kind of error back + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED] + end + + describe "correctly uploaded a file" do + + before do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + signed_data = @recorded_backing_track.upload_sign(md5) + @response = put_file_to_aws(signed_data, upload_file_contents) + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.upload_complete + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.marking_complete = false + end + + it "can download an updated file" do + @response = RestClient.get @recorded_backing_track.sign_url + @response.body.should == upload_file_contents + end + + it "can't mark completely uploaded twice" do + @recorded_backing_track.upload_complete + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + + it "can't ask for a next part if fully uploaded" do + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + + it "can't ask for mark part complete if fully uploaded" do + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + end + end +end + diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index 303080174..3de7d09e1 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -15,6 +15,29 @@ describe Recording do @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) end + describe "popular_recordings" do + it "empty" do + Recording.popular_recordings.length.should eq(0) + end + + it "one public recording" do + claim = FactoryGirl.create(:claimed_recording) + + claim.recording.is_done = true + claim.recording.save! + recordings = Recording.popular_recordings + recordings.length.should eq(1) + recordings[0].id.should eq(claim.recording.id) + end + + it "one private recording" do + claim = FactoryGirl.create(:claimed_recording, is_public: true) + + recordings = Recording.popular_recordings + recordings.length.should eq(0) + end + end + describe "cleanup_excessive_storage" do sample_audio='sample.file' @@ -211,6 +234,20 @@ describe Recording do user1_recorded_tracks[0].discard = true user1_recorded_tracks[0].save! end + + it "should allow finding of backing tracks" do + user2 = FactoryGirl.create(:user) + connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session) + track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument) + backing_track = FactoryGirl.create(:backing_track, :connection => connection2) + + + @recording = Recording.start(@music_session, @user) + @recording.recorded_backing_tracks_for_user(@user).length.should eq(0) + user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2) + user2_recorded_tracks.length.should == 1 + user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0] + end it "should set up the recording properly when recording is started with 1 user in the session" do @music_session.is_recording?.should be_false @@ -547,6 +584,8 @@ describe Recording do @genre = FactoryGirl.create(:genre) @recording.claim(@user, "Recording", "Recording Description", @genre, true) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) + # We should have 2 items; a track and a video: uploads = Recording.list_uploads(@user) uploads["uploads"].should have(3).items @@ -1057,6 +1096,34 @@ describe Recording do RecordedVideo.find_by_id(video.id).should_not be_nil end end + + describe "add_timeline" do + + let!(:recorded_jam_track_track) {FactoryGirl.create(:recorded_jam_track_track)} + let(:recording) {recorded_jam_track_track.recording} + let(:timeline_data) {{"sample" => "data"}} + let(:good_timeline) { { + "global" => {"recording_start_time" => 0, "jam_track_play_start_time" => 0, "jam_track_recording_start_play_offset" => 0}, + "tracks" => [ + { + "id" => recorded_jam_track_track.jam_track_track.id, + "timeline" => timeline_data, + "type" => "jam_track" + } + ] + } + } + + it "applies timeline data correctly" do + recording.add_timeline good_timeline + recorded_jam_track_track.reload + JSON.parse(recorded_jam_track_track.timeline).should eq(timeline_data) + end + + it "fails if no tracks data" do + expect { recording.add_timeline({}) }.to raise_error(JamRuby::JamArgumentError) + end + end end diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb new file mode 100644 index 000000000..ba89aee20 --- /dev/null +++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb @@ -0,0 +1,260 @@ +require 'spec_helper' + +# verifies that all webhooks work, except for the failed_payment_notification hook, since I don't have an example of it. +# because the other 3 types work, I feel pretty confident it will work + +# testing with CURL: +# curl -X POST -d @filename.txt http://localhost:3000/api/recurly/webhook --header "Content-Type:text/xml" --user monkeytoesspeartoss:frizzyfloppymushface +# where @filename.txt is either empty (creates no row), or the contents of one of the create_from_xml tests below (replacing the account_code with a real user_id in our system) + +describe RecurlyTransactionWebHook do + + + let(:refund_xml) {' + + + 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d + + sethcall@gmail.com + Seth + Call + + + + 2de439790e8fceb7fc385a4a89b89883 + 2da71ad9c657adf9fe618e4f058c78bb + + 1033 + 2da71ad97c826a7b784c264ac59c04de + refund + 2015-04-01T14:41:40Z + 216 + success + Successful test transaction + 3819545 + subscription + + Street address and postal code match. + + + true + true + false + +' + } + + let(:void_xml) { +' + + + 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d + + sethcall@gmail.com + Seth + Call + + + + 2de4370332f709c768313d4f47a9af1d + 2da71ad9c657adf9fe618e4f058c78bb + + 1033 + 2da71ad97c826a7b784c264ac59c04de + refund + 2015-04-01T14:38:59Z + 216 + void + Successful test transaction + 3183996 + subscription + + Street address and postal code match. + + + true + false + false + +' + } + + let(:success_xml) { +' + + + 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d + + seth@jamkazam.com + Seth + Call + + + + 2de4448533db12d6d92b4c4b4e90a4f1 + 2de44484fa4528b504555f43ac8bf42f + + 1037 + 2de44484b460d95863799a431383b165 + purchase + 2015-04-01T14:53:44Z + 216 + success + Successful test transaction + 6249355 + subscription + + Street address and postal code match. + + + true + true + true + +' + } + describe "sales integrity maintanence" do + + before(:each) do + @user = FactoryGirl.create(:user, id: '56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d') + end + + it "deletes jam_track_right when refunded" do + + sale = Sale.create_jam_track_sale(@user) + sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb' + sale.recurly_total_in_cents = 216 + sale.save! + # create a jam_track right, which should be whacked as soon as we craete the web hook + jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'bleh') + + shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil) + + document = Nokogiri::XML(refund_xml) + + RecurlyTransactionWebHook.create_from_xml(document) + + JamTrackRight.find_by_id(jam_track_right.id).should be_nil + end + + it "deletes jam_track_right when voided" do + + sale = Sale.create_jam_track_sale(@user) + sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb' + sale.recurly_total_in_cents = 216 + sale.save! + # create a jam_track right, which should be whacked as soon as we craete the web hook + jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'blah') + + shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil) + + document = Nokogiri::XML(void_xml) + + RecurlyTransactionWebHook.create_from_xml(document) + + JamTrackRight.find_by_id(jam_track_right.id).should be_nil + end + end + + + describe "is_transaction_web_hook?" do + + it "successful payment" do + document = Nokogiri::XML('') + RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true + end + + it "successful refund" do + document = Nokogiri::XML('') + RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true + end + + it "failed payment" do + document = Nokogiri::XML('') + RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true + end + + it "void" do + document = Nokogiri::XML('') + RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true + end + + it "not a transaction web hook" do + document = Nokogiri::XML('') + RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_false + end + end + describe "create_from_xml" do + + before(:each) do + @user = FactoryGirl.create(:user, id: '56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d') + end + + it "successful payment" do + + document = Nokogiri::XML(success_xml) + + transaction = RecurlyTransactionWebHook.create_from_xml(document) + transaction.valid?.should be_true + transaction.user.should eq(@user) + transaction.transaction_type.should eq('payment') + transaction.subscription_id.should eq('2de44484b460d95863799a431383b165') + transaction.invoice_id.should eq('2de44484fa4528b504555f43ac8bf42f') + transaction.invoice_number_prefix.should eq('') + transaction.invoice_number.should eq(1037) + transaction.recurly_transaction_id.should eq('2de4448533db12d6d92b4c4b4e90a4f1') + transaction.action.should eq('purchase') + transaction.transaction_at.should eq(Time.parse('2015-04-01T14:53:44Z')) + transaction.amount_in_cents.should eq(216) + transaction.status.should eq('success') + transaction.message.should eq('Successful test transaction') + transaction.reference.should eq('6249355') + end + + it "successful refund" do + document = Nokogiri::XML(refund_xml) + + transaction = RecurlyTransactionWebHook.create_from_xml(document) + transaction.valid?.should be_true + transaction.user.should eq(@user) + transaction.transaction_type.should eq('refund') + transaction.subscription_id.should eq('2da71ad97c826a7b784c264ac59c04de') + transaction.invoice_id.should eq('2da71ad9c657adf9fe618e4f058c78bb') + transaction.invoice_number_prefix.should eq('') + transaction.invoice_number.should eq(1033) + transaction.recurly_transaction_id.should eq('2de439790e8fceb7fc385a4a89b89883') + transaction.action.should eq('refund') + transaction.transaction_at.should eq(Time.parse('2015-04-01T14:41:40Z')) + transaction.amount_in_cents.should eq(216) + transaction.status.should eq('success') + transaction.message.should eq('Successful test transaction') + transaction.reference.should eq('3819545') + + end + + it "successful void" do + document = Nokogiri::XML(void_xml) + + transaction = RecurlyTransactionWebHook.create_from_xml(document) + transaction.valid?.should be_true + transaction.user.should eq(@user) + transaction.transaction_type.should eq('void') + transaction.subscription_id.should eq('2da71ad97c826a7b784c264ac59c04de') + transaction.invoice_id.should eq('2da71ad9c657adf9fe618e4f058c78bb') + transaction.invoice_number_prefix.should eq('') + transaction.invoice_number.should eq(1033) + transaction.recurly_transaction_id.should eq('2de4370332f709c768313d4f47a9af1d') + transaction.action.should eq('refund') + transaction.transaction_at.should eq(Time.parse('2015-04-01T14:38:59Z')) + transaction.amount_in_cents.should eq(216) + transaction.status.should eq('void') + transaction.message.should eq('Successful test transaction') + transaction.reference.should eq('3183996') + end + end +end + +# https://github.com/killbilling/recurly-java-library/blob/master/src/main/java/com/ning/billing/recurly/model/push/payment/FailedPaymentNotification.java +# failed_payment_notification \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb new file mode 100644 index 000000000..334166734 --- /dev/null +++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb @@ -0,0 +1,41 @@ + +require 'spec_helper' + +describe SaleLineItem do + + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "associations" do + + it "can find associated recurly transaction web hook" 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) + transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid') + + sale_line_item.reload + sale_line_item.recurly_transactions.should eq([transaction]) + end + end + + + describe "state" do + + it "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) + 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 new file mode 100644 index 000000000..b08aad36d --- /dev/null +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -0,0 +1,329 @@ +require 'spec_helper' + +describe Sale do + + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "index" do + it "empty" do + result = Sale.index(user) + result[:query].length.should eq(0) + result[:next].should eq(nil) + end + + it "one" 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, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + + it "user filtered correctly" 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, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + + sale2 = Sale.create_jam_track_sale(user2) + shopping_cart = ShoppingCart.create(user2, jam_track) + sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + end + + + describe "place_order" do + + let(:user) {FactoryGirl.create(:user)} + let(:jamtrack) { FactoryGirl.create(:jam_track) } + let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i } + let(:client) { RecurlyClient.new } + let(:billing_info) { + info = {} + info[:first_name] = user.first_name + info[:last_name] = user.last_name + info[:address1] = 'Test Address 1' + info[:address2] = 'Test Address 2' + info[:city] = user.city + info[:state] = user.state + info[:country] = user.country + info[:zip] = '12345' + info[:number] = '4111-1111-1111-1111' + info[:month] = '08' + info[:year] = '2017' + info[:verification_value] = '111' + info + } + + after(:each) do + if user.recurly_code + account = Recurly::Account.find(user.recurly_code) + if account.present? + account.destroy + end + end + end + + + it "for a free jam track" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + 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(jam_track_price_in_cents) + 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 + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(jam_track_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(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) + 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_not be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(2).items + free_purchase= adjustments[0] + free_purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) + free_purchase.accounting_code.should eq(ShoppingCart::PURCHASE_FREE) + free_purchase.description.should eq("JamTrack: " + jamtrack.name) + free_purchase.state.should eq('invoiced') + free_purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + + free_credit = adjustments[1] + free_credit.unit_amount_in_cents.should eq(-(jamtrack.price * 100).to_i) + free_credit.accounting_code.should eq(ShoppingCart::PURCHASE_FREE_CREDIT) + free_credit.description.should eq("JamTrack: " + jamtrack.name + " (Credit)") + free_credit.state.should eq('invoiced') + free_credit.uuid.should eq(sale_line_item.recurly_adjustment_credit_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(2).items # should have both adjustments associated + invoice.line_items[0].should eq(free_credit) + invoice.line_items[1].should eq(free_purchase) + invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.total_in_cents.should eq(0) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + end + + it "for a normally priced jam track" do + user.has_redeemable_jamtrack = false + user.save! + shopping_cart = ShoppingCart.create user, jamtrack, 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(jam_track_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(jamtrack.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(jam_track_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(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(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(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.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 eq(user.jam_track_rights.last.recurly_adjustment_uuid) + + # 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((jamtrack.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTrack: " + jamtrack.name) + 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((jamtrack.price * 100).to_i) + invoice.total_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_false + user.has_redeemable_jamtrack.should be_false + end + + it "for a jamtrack already owned" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + sales = Sale.place_order(user, [shopping_cart]) + sales.should have(0).items + # also, verify that no earlier adjustments were affected + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should have(2).items + end + + # this test counts on the fact that two adjustments are made when buying a free JamTrack + # so if we make the second adjustment invalid from Recurly's standpoint, then + # we can see if the first one is ultimately destroyed + it "rolls back created adjustments if error" do + + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + + # grab the real response; we will modify it to make a nil accounting code + adjustment_attrs = shopping_cart.create_adjustment_attributes(user) + client.find_or_create_account(user, billing_info) + + adjustment_attrs[1][:unit_amount_in_cents] = nil # invalid amount + ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs) + + expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError) + + user.reload + user.sales.should have(0).items + + recurly_account = client.get_account(user) + recurly_account.adjustments.should have(0).items + end + + it "rolls back adjustments created before the order" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + # create a single adjustment on the account + adjustment_attrs = shopping_cart.create_adjustment_attributes(user) + recurly_account = client.get_account(user) + adjustment = recurly_account.adjustments.new (adjustment_attrs[0]) + adjustment.save + adjustment.errors.any?.should be_false + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should have(2).items # two adjustments are created for a free jamtrack; that should be all there is + end + end + + describe "check_integrity_of_jam_track_sales" do + + let(:user) { FactoryGirl.create(:user) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + + it "empty" do + check_integrity = Sale.check_integrity_of_jam_track_sales + check_integrity.length.should eq(1) + r = check_integrity[0] + r.total.to_i.should eq(0) + r.voided.to_i.should eq(0) + end + + it "one succeeded sale" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil) + + check_integrity = Sale.check_integrity_of_jam_track_sales + r = check_integrity[0] + r.total.to_i.should eq(1) + r.voided.to_i.should eq(0) + end + + + it "one voided sale" do + sale = Sale.create_jam_track_sale(user) + sale.recurly_invoice_id = 'some_recurly_invoice_id' + sale.save! + shopping_cart = ShoppingCart.create(user, jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil) + FactoryGirl.create(:recurly_transaction_web_hook, transaction_type: RecurlyTransactionWebHook::VOID, invoice_id: 'some_recurly_invoice_id') + + check_integrity = Sale.check_integrity_of_jam_track_sales + r = check_integrity[0] + r.total.to_i.should eq(1) + r.voided.to_i.should eq(1) + end + + end +end + diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index db3f4d75c..13f6777bc 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -4,6 +4,7 @@ describe ShoppingCart do let(:user) { FactoryGirl.create(:user) } let(:jam_track) {FactoryGirl.create(:jam_track) } + let(:jam_track2) {FactoryGirl.create(:jam_track) } before(:each) do ShoppingCart.delete_all @@ -20,4 +21,49 @@ describe ShoppingCart do user.shopping_carts[0].quantity.should == 1 end + it "should not add duplicate JamTrack to ShoppingCart" 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_track) + cart2.errors.any?.should be_true + + end + + describe "redeemable behavior" do + it "adds redeemable item to shopping cart" do + + user.has_redeemable_jamtrack.should be_true + + # first item added to shopping cart should be marked for redemption + cart = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart.marked_for_redeem.should eq(1) + + # but the second item should not + + user.reload + + cart = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart.marked_for_redeem.should eq(0) + end + + it "removes redeemable item to shopping cart" do + + user.has_redeemable_jamtrack.should be_true + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.should_not be_nil + + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(0) + ShoppingCart.remove_jam_track_from_cart(user, jam_track) + + user.shopping_carts.length.should eq(1) + cart2.reload + cart1.marked_for_redeem.should eq(1) + end + end end diff --git a/ruby/spec/jam_ruby/models/signup_hint_spec.rb b/ruby/spec/jam_ruby/models/signup_hint_spec.rb new file mode 100644 index 000000000..1ec79efef --- /dev/null +++ b/ruby/spec/jam_ruby/models/signup_hint_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe SignupHint do + + let(:user) {AnonymousUser.new(SecureRandom.uuid)} + + describe "refresh_by_anoymous_user" do + it "creates" do + hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'}) + hint.errors.any?.should be_false + hint.redirect_location.should eq('abc') + hint.want_jamblaster.should be_false + end + + it "updated" do + SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'}) + + hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: nil, want_jamblaster: true}) + hint.errors.any?.should be_false + hint.redirect_location.should be_nil + hint.want_jamblaster.should be_true + end + end +end diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb index 1e03d123b..4db0f59a5 100644 --- a/ruby/spec/jam_ruby/models/track_spec.rb +++ b/ruby/spec/jam_ruby/models/track_spec.rb @@ -7,8 +7,10 @@ describe Track do let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) } let (:track) { FactoryGirl.create(:track, :connection => connection)} let (:track2) { FactoryGirl.create(:track, :connection => connection)} + let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)} let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) } let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} } + let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} } before(:each) do msuh.touch @@ -16,7 +18,8 @@ describe Track do describe "sync" do it "create one track" do - tracks = Track.sync(connection.client_id, [track_hash]) + result = Track.sync(connection.client_id, [track_hash]) + tracks = result[:tracks] tracks.length.should == 1 track = tracks[0] track.client_track_id.should == track_hash[:client_track_id] @@ -25,7 +28,8 @@ describe Track do end it "create two tracks" do - tracks = Track.sync(connection.client_id, [track_hash, track_hash]) + result = Track.sync(connection.client_id, [track_hash, track_hash]) + tracks = result[:tracks] tracks.length.should == 2 track = tracks[0] track.client_track_id.should == track_hash[:client_track_id] @@ -40,7 +44,8 @@ describe Track do it "delete only track" do track.id.should_not be_nil connection.tracks.length.should == 1 - tracks = Track.sync(connection.client_id, []) + result = Track.sync(connection.client_id, []) + tracks = result[:tracks] tracks.length.should == 0 end @@ -49,7 +54,8 @@ describe Track do track.id.should_not be_nil track2.id.should_not be_nil connection.tracks.length.should == 2 - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -62,7 +68,8 @@ describe Track do track.id.should_not be_nil track2.id.should_not be_nil connection.tracks.length.should == 2 - tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -75,7 +82,8 @@ describe Track do track.id.should_not be_nil connection.tracks.length.should == 1 set_updated_at(track, 1.days.ago) - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -87,7 +95,8 @@ describe Track do it "updates a single track using .client_track_id to correlate" do track.id.should_not be_nil connection.tracks.length.should == 1 - tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -99,11 +108,85 @@ describe Track do track.id.should_not be_nil connection.tracks.length.should == 1 set_updated_at(track, 1.days.ago) - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] expect(found.id).to eq track.id expect(found.updated_at.to_i).to eq track.updated_at.to_i end + + describe "backing tracks" do + it "create one track and one backing track" do + result = Track.sync(connection.client_id, [track_hash], [backing_track_hash]) + tracks = result[:tracks] + tracks.length.should == 1 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 1 + track = backing_tracks[0] + track.client_track_id.should == backing_track_hash[:client_track_id] + end + + it "delete only backing_track" do + track.id.should_not be_nil + backing_track.id.should_not be_nil + connection.tracks.length.should == 1 + connection.backing_tracks.length.should == 1 + result = Track.sync(connection.client_id, + [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}], + []) + tracks = result[:tracks] + tracks.length.should == 1 + found = tracks[0] + expect(found.id).to eq track.id + expect(found.updated_at.to_i).to eq track.updated_at.to_i + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 0 + end + + it "does not touch updated_at when nothing changes" do + track.id.should_not be_nil + backing_track.id.should_not be_nil + connection.tracks.length.should == 1 + set_updated_at(track, 1.days.ago) + set_updated_at(backing_track, 1.days.ago) + result = Track.sync(connection.client_id, + [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}], + [{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}]) + tracks = result[:tracks] + tracks.length.should == 1 + found = tracks[0] + expect(found.id).to eq track.id + expect(found.updated_at.to_i).to eq track.updated_at.to_i + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 1 + found = backing_tracks[0] + expect(found.id).to eq backing_track.id + expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i + end + end + + describe "metronome_open" do + it "sets metronome_open to true" do + result = Track.sync(connection.client_id, [track_hash], [], true) + connection.reload + connection.metronome_open.should be_true + end + + it "sets metronome_open to false" do + connection.metronome_open = true + connection.save! + result = Track.sync(connection.client_id, [track_hash], [], false) + connection.reload + connection.metronome_open.should be_false + end + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 6d51c9b6e..1d6b0f40f 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'jam_ruby/recurly_client' RESET_PASSWORD_URL = "/reset_token" @@ -9,6 +10,7 @@ describe User do @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", password: "foobar", password_confirmation: "foobar", city: "Apex", state: "NC", country: "US", terms_of_service: true, musician: true) @user.musician_instruments << FactoryGirl.build(:musician_instrument, user: @user) + @recurly = RecurlyClient.new end subject { @user } @@ -434,6 +436,8 @@ describe User do describe "finalize email update" do before do + @recurly.has_account?(@user).should == false + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") UserMailer.deliveries.clear end @@ -464,6 +468,36 @@ describe User do end end + describe "finalize email updates recurly" do + before do + + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") + UserMailer.deliveries.clear + billing_info = { + first_name: @user.first_name, + last_name: @user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: @user.city, + state: @user.state, + country: @user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' + } + @recurly.find_or_create_account(@user, billing_info) + end + + it "should update recurly" do + @recurly.has_account?(@user).should == true + @recurly.get_account(@user).email.should_not == "somenewemail@blah.com" + @finalized = User.finalize_update_email(@user.update_email_token) + @recurly.get_account(@user).email.should == "somenewemail@blah.com" + end + end + describe "user_authorizations" do it "can create" do diff --git a/ruby/spec/jam_ruby/models/user_sync_spec.rb b/ruby/spec/jam_ruby/models/user_sync_spec.rb index 2bea2d766..b015e77f5 100644 --- a/ruby/spec/jam_ruby/models/user_sync_spec.rb +++ b/ruby/spec/jam_ruby/models/user_sync_spec.rb @@ -20,6 +20,49 @@ describe UserSync do data[:next].should be_nil end + describe "backing_tracks" do + + let!(:recording1) { + recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1) + recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false) + recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false) + recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false) + recording.save! + recording.reload + recording + } + + let(:sorted_tracks) { + Array.new(recording1.recorded_tracks).sort! {|a, b| + if a.created_at == b.created_at + a.id <=> b.id + else + a.created_at <=> b.created_at + end + } + } + + # backing tracks should only list download, or upload, for the person who opened it, for legal reasons + it "lists backing track for opener" do + data = UserSync.index({user_id: user1.id}) + data[:next].should be_nil + user_syncs = data[:query] + user_syncs.count.should eq(3) + user_syncs[0].recorded_track.should == sorted_tracks[0] + user_syncs[1].recorded_track.should == sorted_tracks[1] + user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0] + end + + it "does not list backing track for non-opener" do + data = UserSync.index({user_id: user2.id}) + data[:next].should be_nil + user_syncs = data[:query] + user_syncs.count.should eq(2) + user_syncs[0].recorded_track.should == sorted_tracks[0] + user_syncs[1].recorded_track.should == sorted_tracks[1] + end + end + it "one mix and quick mix" do mix = FactoryGirl.create(:mix) mix.recording.duration = 1 diff --git a/web/spec/managers/recurly_client_spec.rb b/ruby/spec/jam_ruby/recurly_client_spec.rb similarity index 73% rename from web/spec/managers/recurly_client_spec.rb rename to ruby/spec/jam_ruby/recurly_client_spec.rb index 8b1825cba..108fe1600 100644 --- a/web/spec/managers/recurly_client_spec.rb +++ b/ruby/spec/jam_ruby/recurly_client_spec.rb @@ -1,12 +1,11 @@ require 'spec_helper' -require "recurly_client" +require "jam_ruby/recurly_client" describe RecurlyClient do - let(:jamtrack) { FactoryGirl.create(:jam_track) } - #let(:client) { RecurlyClient.new } + let(:jamtrack) { FactoryGirl.create(:jam_track, plan_code: 'jamtrack-acdc-backinblack') } before :all do @client = RecurlyClient.new - @jamtrack = FactoryGirl.create(:jam_track) + @jamtrack = FactoryGirl.create(:jam_track, plan_code: 'jamtrack-acdc-backinblack') end before(:each) do @@ -87,21 +86,27 @@ describe RecurlyClient do found.state.should eq('closed') end - it "can place order" do - @client.find_or_create_account(@user, @billing_info) - expect{@client.place_order(@user, @jamtrack)}.not_to raise_error() - subs = @client.get_account(@user).subscriptions - subs.should_not be_nil - subs.should have(1).items - @user.jam_track_rights.should_not be_nil - @user.jam_track_rights.should have(1).items - @user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id) - end +=begin + it "can refund subscription" do + sale = Sale.create(@user) + shopping_cart = ShoppingCart.create @user, @jamtrack, 1 + @client.find_or_create_account(@user, @billing_info) + + # Place order: + expect{@client.place_order(@user, @jamtrack, shopping_cart, sale)}.not_to raise_error() + active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'} + @jamtrack.reload + @jamtrack.jam_track_rights.should have(1).items - it "detects error on double order" do - @client.find_or_create_account(@user, @billing_info) - expect{@client.place_order(@user, @jamtrack)}.not_to raise_error() - expect{@client.place_order(@user, @jamtrack)}.to raise_error(RecurlyClientError) + # Refund: + expect{@client.refund_user_subscription(@user, @jamtrack)}.not_to raise_error() + active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'} + active_subs.should have(0).items + + @jamtrack.reload + @jamtrack.jam_track_rights.should have(0).items end +=end + end diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb new file mode 100644 index 000000000..7ead35e3a --- /dev/null +++ b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe JamTracksBuilder do + include UsesTempFiles + include CarrierWave::Test::Matchers + + before (:all) do + @user = FactoryGirl.create(:user) + @jam_track = FactoryGirl.create(:jam_track) + original_storage = JamTrackTrackUploader.storage = :fog + original_storage = JamTrackRightUploader.storage = :fog + @s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + after(:all) do + JamTrackRightUploader.storage = @original_storage + JamTrackTrackUploader.storage = @original_storage + end + + before(:each) do + @s3.delete_folder('jam_tracks') + end + + it "should build" do + ogg_path = File.join('spec', 'files', 'on.ogg') + user = FactoryGirl.create(:user) + jam_track_track = FactoryGirl.create(:jam_track_track) + jam_track = jam_track_track.jam_track + + @s3.upload(jam_track_track.manually_uploaded_filename(:url_48), ogg_path) + jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48) + jam_track_track.save! + + jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48) + + # verify it's on S3 + @s3.exists?(jam_track_track[:url_48]).should be_true + @s3.length(jam_track_track[:url_48]).should == File.size?(ogg_path) + jam_track_track[:url_44].should be_nil + + # Check right + jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track) + jam_track_right[:url_48].should be_nil + jam_track_right[:url_44].should be_nil + JamTracksBuilder.perform(jam_track_right.id, 48) + jam_track_right.reload + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_track[:url_44].should be_nil + end + + describe "with bitrate 44" do + it "should build" do + ogg_path = File.join('spec', 'files', 'on.ogg') + user = FactoryGirl.create(:user) + + # Should build bitrate 44 and only bitrate 44: + jam_track_track = FactoryGirl.create(:jam_track_track) + jam_track = jam_track_track.jam_track + + # uploader = JamTrackTrackUploader.new(jam_track_track, :url_44) + # uploader.store!(File.open(ogg_path, 'rb')) + @s3.upload(jam_track_track.manually_uploaded_filename(:url_44), ogg_path) + jam_track_track[:url_44] = jam_track_track.manually_uploaded_filename(:url_44) + jam_track_track.save! + + jam_track_track[:url_44].should == jam_track_track.manually_uploaded_filename(:url_44) + + # verify it's on S3 + @s3.exists?(jam_track_track[:url_44]).should be_true + @s3.length(jam_track_track[:url_44]).should == File.size?(ogg_path) + jam_track_track[:url_48].should be_nil + + # Check right + jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track) + jam_track_right[:url_44].should be_nil + jam_track_right[:url_48].should be_nil + JamTracksBuilder.perform(jam_track_right.id, 44) + jam_track_right.reload + jam_track_right[:url_44].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right.url_44.should_not be_nil + jam_track_track[:url_48].should be_nil + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb index 36cb5a067..1c900bc52 100644 --- a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb +++ b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb @@ -26,20 +26,20 @@ describe JamTracksCleaner do jam_track_right.signed=true jam_track_right - jam_track_right.url.store!(File.open(RIGHT_NAME)) + jam_track_right.url_48.store!(File.open(RIGHT_NAME)) jam_track_right.downloaded_since_sign=true jam_track_right.save! - jam_track_right[:url].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename jam_track_right.reload # Should exist after uploading: - url = jam_track_right[:url] + url = jam_track_right[:url_48] s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) url.should_not be_nil s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) - s3.exists?(jam_track_right[:url]).should be_true + s3.exists?(jam_track_right[:url_48]).should be_true JamRuby::JamTracksCleaner.perform s3.exists?(url).should be_true diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index 62b1472c6..c3d041060 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -12,6 +12,7 @@ describe UserMailer do let(:user) { FactoryGirl.create(:user) } before(:each) do + stub_const("APP_CONFIG", app_config) UserMailer.deliveries.clear end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 890a195c0..16ac84514 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance ActiveRecord::Base.add_observer UserObserver.instance ActiveRecord::Base.add_observer FeedbackObserver.instance ActiveRecord::Base.add_observer RecordedTrackObserver.instance +ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance ActiveRecord::Base.add_observer QuickMixObserver.instance #RecordedTrack.observers.disable :all # only a few tests want this observer active diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index fda7ec00a..b22dd4467 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -3,10 +3,22 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws def app_config klass = Class.new do + def email_alerts_alias + 'alerts@jamkazam.com' + end + + def email_generic_from + 'nobody@jamkazam.com' + end + def aws_bucket JAMKAZAM_TESTING_BUCKET end + def aws_bucket_jamtracks + 'jamkazam-jamtracks-test' + end + def aws_access_key_id 'AKIAJESQY24TOT542UHQ' end @@ -162,6 +174,18 @@ def app_config 20 # 20 seconds end + def one_free_jamtrack_per_user + true + end + + def secret_token + 'foobar' + end + + def unsubscribe_token + 'blah' + end + private def audiomixer_workspace_path @@ -231,4 +255,4 @@ end def friend(user1, user2) FactoryGirl.create(:friendship, user: user1, friend: user2) FactoryGirl.create(:friendship, user: user2, friend: user1) -end \ No newline at end of file +end diff --git a/web/Gemfile b/web/Gemfile index e5c3f1583..8aa825f40 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -51,11 +51,12 @@ gem 'twitter' gem 'fb_graph', '2.5.9' gem 'sendgrid', '1.2.0' gem 'filepicker-rails', '0.1.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'aasm', '3.0.16' gem 'carrierwave', '0.9.0' gem 'carrierwave_direct' gem 'fog' +gem 'jquery-payment-rails' gem 'haml-rails' gem 'unf' #optional fog dependency gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) @@ -75,7 +76,7 @@ gem 'netaddr' gem 'quiet_assets', :group => :development gem 'bugsnag' gem 'multi_json', '1.9.0' -gem 'rest_client' +gem 'rest-client' gem 'iso-639' gem 'language_list' gem 'rubyzip' @@ -86,6 +87,7 @@ gem 'recurly' gem 'guard', '2.7.3' gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' +gem 'sitemap_generator' group :development, :test do gem 'rspec-rails', '2.14.2' diff --git a/web/README.md b/web/README.md index ae45c560b..975118c93 100644 --- a/web/README.md +++ b/web/README.md @@ -1,15 +1,5 @@ -TODO: -==== - Jasmine Javascript Unit Tests ============================= -1. Ensure you have the jasmine Gem installed; -$ bundle - -2. Start the jasmine server (defaults to :8888) -$ rake jasmine - -Open browser to localhost:8888 - +Open browser to localhost:3000/teaspoon diff --git a/web/Rakefile b/web/Rakefile index f4019acfc..882005f42 100644 --- a/web/Rakefile +++ b/web/Rakefile @@ -6,6 +6,7 @@ #require 'resque/scheduler/tasks' require 'resque/tasks' require 'resque/scheduler/tasks' +require 'sitemap_generator/tasks' require File.expand_path('../config/application', __FILE__) SampleApp::Application.load_tasks diff --git a/web/app/assets/images/content/bkg_home_guitar.jpg b/web/app/assets/images/content/bkg_home_guitar.jpg new file mode 100644 index 000000000..b67797dd8 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_guitar.jpg differ diff --git a/web/app/assets/images/content/bkg_home_guitar_x.jpg b/web/app/assets/images/content/bkg_home_guitar_x.jpg new file mode 100644 index 000000000..e76ec6a83 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_guitar_x.jpg differ diff --git a/web/app/assets/images/content/checkmark.png b/web/app/assets/images/content/checkmark.png index 8ce5e42ee..628f05a1f 100644 Binary files a/web/app/assets/images/content/checkmark.png and b/web/app/assets/images/content/checkmark.png differ diff --git a/web/app/assets/images/content/icon_metronome.png b/web/app/assets/images/content/icon_metronome.png new file mode 100644 index 000000000..2e8d963b2 Binary files /dev/null and b/web/app/assets/images/content/icon_metronome.png differ diff --git a/web/app/assets/images/content/icon_metronome_small.png b/web/app/assets/images/content/icon_metronome_small.png new file mode 100644 index 000000000..b24f11fdb Binary files /dev/null and b/web/app/assets/images/content/icon_metronome_small.png differ diff --git a/web/app/assets/images/content/icon_shopping_cart.png b/web/app/assets/images/content/icon_shopping_cart.png index 24bf9b09f..511fe7c77 100644 Binary files a/web/app/assets/images/content/icon_shopping_cart.png and b/web/app/assets/images/content/icon_shopping_cart.png differ diff --git a/web/app/assets/images/content/shopping-cart.png b/web/app/assets/images/content/shopping-cart.png index eef1a4c69..62e6cc2fb 100644 Binary files a/web/app/assets/images/content/shopping-cart.png and b/web/app/assets/images/content/shopping-cart.png differ diff --git a/web/app/assets/images/up_arrow.png b/web/app/assets/images/up_arrow.png new file mode 100644 index 000000000..6f3c95d68 Binary files /dev/null and b/web/app/assets/images/up_arrow.png differ diff --git a/web/app/assets/images/web/back-us-kickstarter.png b/web/app/assets/images/web/back-us-kickstarter.png new file mode 100644 index 000000000..d2883c8a3 Binary files /dev/null and b/web/app/assets/images/web/back-us-kickstarter.png differ diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png new file mode 100644 index 000000000..8650fe322 Binary files /dev/null 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 new file mode 100644 index 000000000..998d89b75 Binary files /dev/null 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 new file mode 100644 index 000000000..dba8d499b Binary files /dev/null and b/web/app/assets/images/web/button_cta_platform.png differ diff --git a/web/app/assets/images/web/carousel_community.png b/web/app/assets/images/web/carousel_community.png new file mode 100644 index 000000000..ace54546b Binary files /dev/null and b/web/app/assets/images/web/carousel_community.png differ diff --git a/web/app/assets/images/web/carousel_jamblaster.png b/web/app/assets/images/web/carousel_jamblaster.png new file mode 100644 index 000000000..636ede273 Binary files /dev/null and b/web/app/assets/images/web/carousel_jamblaster.png differ diff --git a/web/app/assets/images/web/carousel_overview.png b/web/app/assets/images/web/carousel_overview.png new file mode 100644 index 000000000..f8493c1f4 Binary files /dev/null and b/web/app/assets/images/web/carousel_overview.png differ diff --git a/web/app/assets/images/web/thumbnail_buzz.jpg b/web/app/assets/images/web/thumbnail_buzz.jpg new file mode 100644 index 000000000..737a17873 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_buzz.jpg differ diff --git a/web/app/assets/images/web/thumbnail_jamblaster.jpg b/web/app/assets/images/web/thumbnail_jamblaster.jpg new file mode 100644 index 000000000..7eef7d267 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamblaster.jpg differ diff --git a/web/app/assets/images/web/thumbnail_jamtracks.jpg b/web/app/assets/images/web/thumbnail_jamtracks.jpg new file mode 100644 index 000000000..98bb27db5 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamtracks.jpg differ diff --git a/web/app/assets/images/web/thumbnail_platform.jpg b/web/app/assets/images/web/thumbnail_platform.jpg new file mode 100644 index 000000000..8adacef59 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_platform.jpg differ diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 73997c0a7..61b889eb2 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -529,10 +529,12 @@ if(server.connecting) { logger.error("server.connect should never be called if we are already connecting. cancelling.") + // XXX should return connectDeferred, but needs to be tested/vetted return; } if(server.connected) { logger.error("server.connect should never be called if we are already connected. cancelling.") + // XXX should return connectDeferred, but needs to be tested/vetted return; } @@ -678,7 +680,12 @@ logger.info("server.send(" + jsMessage + ")"); } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { - server.socket.send(jsMessage); + try { + server.socket.send(jsMessage); + } + catch(err) { + logger.warn("error when sending on websocket: " + err) + } } else { logger.warn("Dropped message because server connection is closed."); } diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 92edf171a..a3b75f9b9 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -35,6 +35,10 @@ } } + function licenseDetail(userDetail) { + return (userDetail.purchased_jamtracks_count==0) ? "You don't currently own any JamTracks" : 'You currently own a license to use ' + userDetail.purchased_jamtracks_count + " JamTracks" + } + function populateAccount(userDetail) { var validProfiles = prettyPrintAudioProfiles(context.JK.getGoodConfigMap()); @@ -49,15 +53,18 @@ var $template = $(context._.template($('#template-account-main').html(), { email: userDetail.email, name: userDetail.name, + licenseDetail: licenseDetail(userDetail), location : userDetail.location, session : sessionSummary, + paymentMethod: "mastercard", instruments : prettyPrintInstruments(userDetail.instruments), photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), validProfiles : validProfiles, invalidProfiles : invalidProfiles, isNativeClient: gon.isNativeClient, musician: context.JK.currentUserMusician, - webcamName: webcam + webcamName: webcam, + sales_count: userDetail.sales_count } , { variable: 'data' })); $('#account-content-scroller').html($template); @@ -103,8 +110,10 @@ // events for main screen function events() { - // wire up main panel clicks + // wire up main panel clicks: $('#account-content-scroller').on('click', '#account-scheduled-sessions-link', function(evt) { evt.stopPropagation(); navToScheduledSessions(); return false; } ); + $('#account-content-scroller').on('click', '#account-my-jamtracks-link', function(evt) { evt.stopPropagation(); navToMyJamTracks(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-identity-link', function(evt) { evt.stopPropagation(); navToEditIdentity(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } ); @@ -112,12 +121,18 @@ $('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-video-link', function(evt) { evt.stopPropagation(); navToEditVideo(); return false; } ); $('#account-content-scroller').on('avatar_changed', '#profile-avatar', function(evt, newAvatarUrl) { evt.stopPropagation(); updateAvatar(newAvatarUrl); return false; }) + + // License dialog: + $("#account-content-scroller").on('click', '#account-view-license-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-license-dialog'); return false; } ); + $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } ); } function renderAccount() { + app.user().done(function() { rest.getUserDetail() - .done(populateAccount) - .error(app.ajaxError) + .done(populateAccount) + .error(app.ajaxError) + }) } function navToScheduledSessions() { @@ -125,6 +140,11 @@ window.location = '/client#/account/sessions' } + function navToMyJamTracks() { + resetForm(); + window.location = '/client#/account/jamtracks' + } + function navToEditIdentity() { resetForm() window.location = '/client#/account/identity' @@ -136,7 +156,7 @@ } function navToEditSubscriptions() { - + window.location = '/client#/account/profile' } function navToEditPayments() { @@ -153,6 +173,10 @@ window.location = "/client#/account/video" } + function navToPaymentHistory() { + window.location = '/client#/account/paymentHistory' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee new file mode 100644 index 000000000..f7bdef10f --- /dev/null +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -0,0 +1,93 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.AccountJamTracks = class AccountJamTracks + constructor: (@app) -> + @rest = context.JK.Rest() + @client = context.jamClient + @logger = context.JK.logger + @screen = null + @userId = context.JK.currentUserId; + + initialize:() => + screenBindings = + 'beforeShow': @beforeShow + 'afterShow': @afterShow + @app.bindScreen('account/jamtracks', screenBindings) + @screen = $('#account-jamtracks') + + beforeShow:() => + rest.getPurchasedJamTracks({}) + .done(@populateJamTracks) + .fail(@app.ajaxError); + + afterShow:() => + + populateJamTracks:(data) => + if (data.jamtracks? && data.jamtracks.length > 0) + @screen.find(".no-jamtracks-found").addClass("hidden") + @appendJamTracks context._.template($('#template-account-jamtrack').html(), {jamtracks:data.jamtracks}, { variable: 'data' }) + @screen.find('.jamtrack-solo-session').on 'click', @soloSession + @screen.find('.jamtrack-group-session').on 'click', @groupSession + else + @screen.find(".no-jamtracks-found").removeClass("hidden") + + appendJamTracks:(template) => + $('#account-my-jamtracks table tbody').replaceWith template + + soloSession:(e) => + #context.location="client#/createSession" + jamRow = $(e.target).parents("tr") + @createSession(jamRow.data(), true) + + groupSession:(e) => + #context.location="client#/createSession" + jamRow = $(e.target).parents("tr") + @createSession(jamRow.data(), false) + + createSession:(sessionData, solo) => + tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient) + + if (context.JK.guardAgainstBrowser(@app)) + 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 = !solo + data.fan_access = false + data.fan_chat = false + data.genre = [sessionData.genre] + data.genres = [sessionData.genre] + # 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.location = '/client#/session/' + newSessionId + # Re-loading the session settings will cause the form to reset with the right stuff in it. + # This is an extra xhr call, but it keeps things to a single codepath + loadSessionSettings() + context.JK.GA.trackSessionCount data.musician_access, data.fan_access, invitationCount + 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' + \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee new file mode 100644 index 000000000..05fc089d7 --- /dev/null +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -0,0 +1,179 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen + LIMIT = 20 + + constructor: (@app) -> + @logger = context.JK.logger + @rest = context.JK.Rest() + @screen = null + @scroller = null + @genre = null + @artist = null + @instrument = null + @availability = null + @nextPager = null + @noMoreSales = null + @currentPage = 0 + @next = null + @tbody = null + @rowTemplate = null + + beforeShow:(data) => + + + afterShow:(data) => + @refresh() + + events:() => + @backBtn.on('click', @onBack) + + onBack:() => + window.location = '/client#/account' + return false + + clearResults:() => + @currentPage = 0 + @tbody.empty() + @noMoreSales.hide() + @next = null + + + + refresh:() => + @currentQuery = this.buildQuery() + @rest.getSalesHistory(@currentQuery) + .done(@salesHistoryDone) + .fail(@salesHistoryFail) + + + renderPayments:(response) => + if response.entries? && response.entries.length > 0 + for sale in response.entries + amt = sale.recurly_total_in_cents + amt = 0 if !amt? + + original_total = sale.state.original_total + refund_total = sale.state.refund_total + + refund_state = null + if original_total != 0 # the enclosed logic does not work for free purchases + if refund_total == original_total + refund_state = 'refunded' + else if refund_total != 0 and refund_total < original_total + refund_state = 'partial refund' + + + displayAmount = (amt/100).toFixed(2) + status = 'paid' + + if sale.state.voided + status = 'voided' + displayAmount = (0).toFixed(2) + else if refund_state? + status = refund_state + displayAmount = (amt/100).toFixed(2) + " (refunded: #{(refund_total/100).toFixed(2)})" + + description = [] + for line_item in sale.line_items + description.push(line_item.product_info?.name) + + payment = { + date: context.JK.formatDate(sale.created_at, true) + amount: displayAmount + status: status + payment_method: 'Credit Card', + description: description.join(', ') + } + + tr = $(context._.template(@rowTemplate, payment, { variable: 'data' })); + @tbody.append(tr); + else + tr = "No payments found" + @tbody.append(tr); + + salesHistoryDone:(response) => + + # Turn in to HTML rows and append: + #@tbody.html("") + @next = response.next_page + @renderPayments(response) + if response.next_page == null + # if we less results than asked for, end searching + @scroller.infinitescroll 'pause' + @logger.debug("end of history") + if @currentPage > 0 + @noMoreSales.show() + # there are bugs with infinitescroll not removing the 'loading'. + # it's most noticeable at the end of the list, so whack all such entries + $('.infinite-scroll-loader').remove() + else + @currentPage++ + this.buildQuery() + this.registerInfiniteScroll() + + + salesHistoryFail:(jqXHR)=> + @noMoreSales.show() + @app.notifyServerError jqXHR, 'Payment History Unavailable' + + defaultQuery:() => + query = + per_page: LIMIT + page: @currentPage+1 + if @next + query.since = @next + query + + buildQuery:() => + @currentQuery = this.defaultQuery() + + + registerInfiniteScroll:() => + that = this + @scroller.infinitescroll { + behavior: 'local' + navSelector: '#account-payment-history .btn-next-pager' + nextSelector: '#account-payment-history .btn-next-pager' + binder: @scroller + dataType: 'json' + appendCallback: false + prefill: false + bufferPx: 100 + loading: + msg: $('
Loading ...
') + img: '/assets/shared/spinner.gif' + path: (page) => + '/api/sales?' + $.param(that.buildQuery()) + + }, (json, opts) => + this.salesHistoryDone(json) + @scroller.infinitescroll 'resume' + + initialize:() => + screenBindings = + 'beforeShow': this.beforeShow + 'afterShow': this.afterShow + @app.bindScreen 'account/paymentHistory', screenBindings + @screen = $('#account-payment-history') + @scroller = @screen.find('.content-body-scroller') + @nextPager = @screen.find('a.btn-next-pager') + @noMoreSales = @screen.find('.end-of-payments-list') + @tbody = @screen.find("table.payment-table tbody") + @rowTemplate = $('#template-payment-history-row').html() + @backBtn = @screen.find('.back') + + if @screen.length == 0 + throw new Error('@screen must be specified') + if @scroller.length == 0 + throw new Error('@scroller must be specified') + if @tbody.length == 0 + throw new Error('@tbody must be specified') + if @noMoreSales.length == 0 + throw new Error('@noMoreSales must be specified') + + this.events() + + diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 037c2c1b7..bc065cf22 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -35,6 +35,8 @@ //= require jquery.browser //= require jquery.custom-protocol //= require jquery.exists +//= require jquery.payment +//= require howler.core.js //= require jstz //= require class //= require AAC_underscore diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js new file mode 100644 index 000000000..fbe41c665 --- /dev/null +++ b/web/app/assets/javascripts/checkout_order.js @@ -0,0 +1,364 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.CheckoutOrderScreen = function (app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; + + var $screen = null; + var $navigation = null; + var $templateOrderContent = null; + var $templatePurchasedJamTrack = 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 downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; + var $backBtn = null; + var $orderPrompt = null; + var $emptyCartPrompt = null; + var $noAccountInfoPrompt = null; + + + function beforeShow() { + beforeShowOrder(); + } + + + function afterShow(data) { + + } + + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; + } + + function beforeShowOrder() { + $orderPrompt.addClass('hidden') + $emptyCartPrompt.addClass('hidden') + $noAccountInfoPrompt.addClass('hidden') + $orderPanel.removeClass("hidden") + $thanksPanel.addClass("hidden") + $screen.find(".place-order").addClass('disabled').off('click', placeOrder) + $("#order_error").text('').addClass("hidden") + step = 3; + renderNavigation(); + populateOrderPage(); + } + + + function populateOrderPage() { + clearOrderPage(); + + rest.getShoppingCarts() + .done(function(carts) { + rest.getBillingInfo() + .done(function(billingInfo) { + renderOrderPage(carts, billingInfo) + }) + .fail(function(jqXHR) { + if(jqXHR.status == 404) { + // no account for this user + $noAccountInfoPrompt.removeClass('hidden') + app.notify({ title: "No account information", + text: "Please restart the checkout process." }, + null, + true); + } + }) + }) + .fail(app.ajaxError); + } + + + function renderOrderPage(carts, recurlyAccountInfo) { + logger.debug("rendering order page") + var data = {} + + var sub_total = 0.0 + var taxes = 0.0 + $.each(carts, function(index, cart) { + sub_total += parseFloat(cart.product_info.real_price) + }); + if(carts.length == 0) { + data.grand_total = '-.--' + data.sub_total = '-.--' + data.taxes = '-.--' + data.shipping_handling = '-.--' + } + else { + data.grand_total = 'Calculating...' + data.sub_total = '$' + sub_total.toFixed(2) + data.taxes = 'Calculating...' + data.shipping_handling = '$0.00' + } + + data.carts = carts + data.billing_info = recurlyAccountInfo.billing_info + data.shipping_info = recurlyAccountInfo.address + + data.shipping_as_billing = true; //jamTrackUtils.compareAddress(data.billing_info, data.shipping_info); + + var orderContentHtml = $( + context._.template( + $templateOrderContent.html(), + data, + {variable: 'data'} + ) + ) + + $orderContent.append(orderContentHtml) + $orderPanel.find(".change-payment-info").on('click', moveToPaymentInfo) + + var $placeOrder = $screen.find(".place-order") + if(carts.length == 0) { + $orderPrompt.addClass('hidden') + $emptyCartPrompt.removeClass('hidden') + $noAccountInfoPrompt.addClass('hidden') + $placeOrder.addClass('disabled') + } + else { + logger.debug("cart has " + carts.length + " items in it") + $orderPrompt.removeClass('hidden') + $emptyCartPrompt.addClass('hidden') + $noAccountInfoPrompt.addClass('hidden') + $placeOrder.removeClass('disabled').on('click', placeOrder) + + var planPricing = {} + + + var priceElement = $screen.find('.order-right-page .plan.jamtrack') + + if(priceElement.length == 0) { + logger.error("unable to find price element for jamtrack"); + app.notify({title: "Error Encountered", text: "Unable to find plan info for jam track"}) + return false; + } + + logger.debug("creating recurly pricing element for plan: " + gon.recurly_tax_estimate_jam_track_plan) + + var effectiveQuantity = 0 + + context._.each(carts, function(cart) { + effectiveQuantity += cart.product_info.quantity - cart.product_info.marked_for_redeem + }) + + var pricing = context.recurly.Pricing(); + pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan; + pricing.resolved = false; + pricing.effective_quantity = 1 + + // this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it. + pricing.on('change', function(price) { + + var totalTax = 0; + var totalPrice = 0; + + var unitTax = Number(pricing.price.now.tax) * effectiveQuantity; + totalTax += unitTax; + + var totalUnitPrice = Number(pricing.price.now.total) * effectiveQuantity; + totalPrice += totalUnitPrice; + + $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)) + }) + + pricing.attach(priceElement.eq(0)) + } + } + + function moveToPaymentInfo() { + context.location = '/client#/checkoutPayment'; + return false; + } + + function placeOrder(e) { + e.preventDefault(); + $screen.find(".place-order").off('click').addClass('disabled') + $("#order_error").text('').addClass("hidden") + rest.placeOrder() + .done(moveToThanks) + .fail(orderErrorHandling); + } + + + function orderErrorHandling(xhr, ajaxOptions, thrownError) { + if (xhr && xhr.responseJSON) { + var message = "Error submitting payment: " + $.each(xhr.responseJSON.errors, function (key, error) { + message += key + ": " + error + }) + $("#order_error").text(message).removeClass("hidden") + } + else { + $("#order_error").text(xhr.responseText).removeClass("hidden") + } + $screen.find(".place-order").on('click', placeOrder).removeClass('disabled') + } + + function moveToThanks(purchaseResponse) { + checkoutUtils.deletePreserveBillingInfo() + $("#order_error").addClass("hidden") + $orderPanel.addClass("hidden") + $thanksPanel.removeClass("hidden") + jamTrackUtils.checkShoppingCart() + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + } + } + + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.') + } + } + + function clearOrderPage() { + $orderContent.empty(); + } + + function renderNavigation() { + $navigation.html(""); + var navigationHtml = $( + context._.template( + $('#template-checkout-navigation').html(), + {current: step}, + {variable: 'data'} + ) + ); + + $navigation.append(navigationHtml); + } + + function events() { + $backBtn.on('click', function(e) { + e.preventDefault(); + + context.location = '/client#/checkoutPayment' + }) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide': beforeHide + }; + app.bindScreen('checkoutOrder', screenBindings); + + $screen = $("#checkoutOrderScreen"); + $navigation = $screen.find(".checkout-navigation-bar"); + $templateOrderContent = $("#template-order-content"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $orderPanel = $screen.find(".order-panel"); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $backBtn = $screen.find('.back'); + $orderPrompt = $screen.find('.order-prompt'); + $emptyCartPrompt = $screen.find('.empty-cart-prompt'); + $noAccountInfoPrompt = $screen.find('.no-account-info-prompt'); + $orderContent = $orderPanel.find(".order-content"); + + if ($screen.length == 0) throw "$screen must be specified"; + if ($navigation.length == 0) throw "$navigation must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +}) +(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js new file mode 100644 index 000000000..10afdb70d --- /dev/null +++ b/web/app/assets/javascripts/checkout_payment.js @@ -0,0 +1,706 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.CheckoutPaymentScreen = function(app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; + + var $screen = null; + var $navigation = null; + var $billingInfo = null; + var $shippingInfo = null; + var $paymentMethod = null; + var $shippingAddress = null; + var $shippingAsBilling = null; + var $paymentInfoPanel = null; + var $accountSignup = null; + var userDetail = null; + var step = null; + var billing_info = null; + var shipping_info = null; + var shipping_as_billing = null; + var $reuseExistingCard = null; + var $reuseExistingCardChk = null; + var $existingCardEndsWith = null; + var $newCardInfo = null; + var selectCountry = null; + var selectCountryLoaded = false; + var $freeJamTrackPrompt = null; + var $noFreeJamTrackPrompt = null; + var $alreadyEnteredJamTrackPrompt = null; + + function afterShow() { + + beforeShowPaymentInfo(); + } + + function beforeShowPaymentInfo() { + step = 2; + renderNavigation(); + renderAccountInfo(); + } + + + function renderAccountInfo() { + + $paymentInfoPanel.addClass('hidden') + $reuseExistingCard.addClass('hidden'); + $newCardInfo.removeClass('hidden'); + $freeJamTrackPrompt.addClass('hidden'); + $noFreeJamTrackPrompt.addClass('hidden'); + $alreadyEnteredJamTrackPrompt.addClass('hidden') + $("#payment_error").addClass('hidden').text('') + + + if(checkoutUtils.shouldPreserveBillingInfo()) { + logger.debug("showing 'user has already set up billing info' because 'preserve billing' session is active") + checkoutUtils.refreshPreserveBillingInfo() + $alreadyEnteredJamTrackPrompt.removeClass('hidden') + return + } + + $paymentInfoPanel.removeClass('hidden') + + + var selectCountryReady = selectCountry.ready(); + if(!selectCountryReady) { + // one time init of country dropdown + selectCountryReady = selectCountry.load('US', null, null); + } + + selectCountryReady.done(function() { + var user = rest.getUserDetail() + if(user) { + user.done(populateAccountInfo).error(app.ajaxError); + } + else { + $reuseExistingCardChk.iCheck('uncheck').attr('checked', false) + if(gon.global.one_free_jamtrack_per_user) { + $freeJamTrackPrompt.removeClass('hidden') + } + else { + $noFreeJamTrackPrompt.removeClass('hidden') + } + } + }) + } + + function populateAccountInfo(user) { + userDetail = user; + + $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) { + $freeJamTrackPrompt.removeClass('hidden') + } + else { + $noFreeJamTrackPrompt.removeClass('hidden') + } + + if (userDetail.has_recurly_account) { + + rest.getBillingInfo() + .done(function(response) { + + if(userDetail.reuse_card) { + $reuseExistingCard.removeClass('hidden'); + toggleReuseExistingCard.call($reuseExistingCardChk) + $existingCardEndsWith.text(response.billing_info.last_four); + } + + var isSameAsShipping = true // jamTrackUtils.compareAddress(response.billing_info, response.address); + + $shippingAsBilling.iCheck(isSameAsShipping ? 'check' : 'uncheck').attr('checked', isSameAsShipping) + + $billingInfo.find("#billing-first-name").val(response.billing_info.first_name); + $billingInfo.find("#billing-last-name").val(response.billing_info.last_name); + $billingInfo.find("#billing-address1").val(response.billing_info.address1); + $billingInfo.find("#billing-address2").val(response.billing_info.address2); + $billingInfo.find("#billing-city").val(response.billing_info.city); + $billingInfo.find("#billing-state").val(response.billing_info.state); + $billingInfo.find("#billing-zip").val(response.billing_info.zip); + $billingInfo.find("#billing-country").val(response.billing_info.country); + + //$shippingAddress.find("#shipping-first-name").val(response.billing_info.first_name); + //$shippingAddress.find("#shipping-last-name").val(response.billing_info.last_name); + //$shippingAddress.find("#shipping-address1").val(response.address.address1); + //$shippingAddress.find("#shipping-address2").val(response.address.address2); + //$shippingAddress.find("#shipping-city").val(response.address.city); + //$shippingAddress.find("#shipping-state").val(response.address.state); + //$shippingAddress.find("#shipping-zip").val(response.address.zip); + //$shippingAddress.find("#shipping-country").val(response.address.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 beforeShow(data) { + } + + function beforeHide() { + + } + + function beforeHide() { + + } + + // TODO: Refactor: this function is long and fraught with many return points. + function next(e) { + + // check if we are showing the 'change payment info' pass; if so, just move on to checkoutOrder + if($alreadyEnteredJamTrackPrompt.is(':visible')) { + logger.debug("skipping payment logic ") + context.location = '/client#/checkoutOrder' + return false; + } + $paymentInfoPanel.find('.error-text').remove(); + $paymentInfoPanel.find('.error').removeClass('error'); + e.preventDefault(); + $("#payment_error").addClass("hidden").text('') + + var reuse_card_this_time = $reuseExistingCardChk.is(':checked'); + var reuse_card_next_time = $paymentMethod.find('#save-card').is(':checked'); + + // 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(); + + var billingInfoValid = true; + if (!billing_first_name) { + $billingInfo.find('#divBillingFirstName .error-text').remove(); + $billingInfo.find('#divBillingFirstName').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-first-name').after("
  • First Name is required
"); + + logger.info("no billing first name"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingFirstName').removeClass("error"); + } + + if (!billing_last_name) { + $billingInfo.find('#divBillingLastName .error-text').remove(); + $billingInfo.find('#divBillingLastName').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-last-name').after("
  • Last Name is required
"); + + logger.info("no billing last name"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingLastName').removeClass("error"); + } + + if (!billing_address1) { + $billingInfo.find('#divBillingAddress1 .error-text').remove(); + $billingInfo.find('#divBillingAddress1').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-address1').after("
  • Address is required
"); + + logger.info("no billing address line 1"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingAddress1').removeClass("error"); + } + + if (!billing_zip) { + $billingInfo.find('#divBillingZip .error-text').remove(); + $billingInfo.find('#divBillingZip').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-zip').after("
  • Zip Code is required
"); + + logger.info("no billing zip"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingZip').removeClass("error"); + } + + if (!billing_state) { + $billingInfo.find('#divBillingState .error-text').remove(); + $billingInfo.find('#divBillingState').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-state').after("
  • State is required
"); + + logger.info("no billing state"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingState').removeClass("error"); + } + + if (!billing_city) { + $billingInfo.find('#divBillingCity .error-text').remove(); + $billingInfo.find('#divBillingCity').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-city').after("
  • City is required
"); + + logger.info("no billing city"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingCity').removeClass("error"); + } + + if (!billing_country) { + $billingInfo.find('#divBillingCountry .error-text').remove(); + $billingInfo.find('#divBillingCountry').addClass("error").addClass("transparent"); + $billingInfo.find('#billing-country').after("
  • Country is required
"); + + logger.info("no billing country"); + billingInfoValid = false; + } + else { + $billingInfo.find('#divBillingCountry').removeClass("error"); + } + + /** + shipping_as_billing = $shippingAsBilling.is(":checked"); + var shipping_first_name, shipping_last_name, shipping_address1, shipping_address2; + var shipping_city, shipping_state, shipping_zip, shipping_country; + + if (!shipping_as_billing) { + shipping_first_name = $shippingAddress.find("#shipping-first-name").val(); + shipping_last_name = $shippingAddress.find("#shipping-last-name").val(); + shipping_address1 = $shippingAddress.find("#shipping-address1").val(); + shipping_address2 = $shippingAddress.find("#shipping-address2").val(); + shipping_city = $shippingAddress.find("#shipping-city").val(); + shipping_state = $shippingAddress.find("#shipping-state").val(); + shipping_zip = $shippingAddress.find("#shipping-zip").val(); + shipping_country = $shippingAddress.find("#shipping-country").val(); + + if (!shipping_first_name) { + $shippingAddress.find('#divShippingFirstName .error-text').remove(); + $shippingAddress.find('#divShippingFirstName').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-first-name').after("
  • First Name is required
"); + + logger.info("no address first name"); + return false; + } + else { + $shippingInfo.find('#divShippingFirstName').removeClass("error"); + } + + if (!shipping_last_name) { + $shippingAddress.find('#divShippingLastName .error-text').remove(); + $shippingAddress.find('#divShippingLastName').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-last-name').after("
  • Last Name is required
"); + + logger.info("no last name"); + return false; + } + else { + $shippingInfo.find('#divShippingLastName').removeClass("error"); + } + + if (!shipping_address1) { + $shippingAddress.find('#divShippingAddress1 .error-text').remove(); + $shippingAddress.find('#divShippingAddress1').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-address1').after("
  • Address is required
"); + + logger.info("no shipping address 1"); + return false; + } + else { + $shippingInfo.find('#divShippingAddress1').removeClass("error"); + } + + if (!shipping_zip) { + $shippingAddress.find('#divShippingZip .error-text').remove(); + $shippingAddress.find('#divShippingZip').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-zip').after("
  • Zip code is required
"); + + logger.info("no shipping address 2"); + return false; + } + else { + $shippingInfo.find('#divShippingZip').removeClass("error"); + } + + if (!shipping_state) { + $shippingAddress.find('#divShippingState .error-text').remove(); + $shippingAddress.find('#divShippingState').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-zip').after("
  • State is required
"); + + logger.info("no shipping state"); + return false; + } + else { + $shippingInfo.find('#divShippingState').removeClass("error"); + } + + if (!shipping_city) { + $shippingAddress.find('#divShippingCity .error-text').remove(); + $shippingAddress.find('#divShippingCity').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-city').after("
  • City is required
"); + + logger.info("no shipping city"); + return false; + } + else { + $shippingInfo.find('#divShippingCity').removeClass("error"); + } + + if (!shipping_country) { + $shippingAddress.find('#divShippingCountry .error-text').remove(); + $shippingAddress.find('#divShippingCountry').addClass("error").addClass("transparent"); + $shippingAddress.find('#shipping-country').after("
  • Country is required
"); + + logger.info("no shipping country"); + return false; + } + else { + $shippingAddress.find('#divShippingCountry').removeClass("error"); + } + } + */ + + var card_name = $paymentMethod.find("#card-name").val(); + var card_number = $paymentMethod.find("#card-number").val(); + var card_year = $paymentMethod.find("#card_expire-date_1i").val(); + var card_month = $paymentMethod.find("#card_expire-date_2i").val(); + var card_verify = $paymentMethod.find("#card-verify").val(); + + /** + if (!card_name) { + $paymentMethod.find('#divCardName .error-text').remove(); + $paymentMethod.find('#divCardName').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-name').after("
  • Card Name is required
"); + return false; + } else { + $paymentMethod.find('#divCardName').removeClass("error"); + }*/ + + // don't valid card form fields when reuse card selected + if(!reuse_card_this_time) { + if (!card_number) { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-number').after("
  • Card Number is required
"); + logger.info("no card number"); + billingInfoValid = false; + } else if (!$.payment.validateCardNumber(card_number)) { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-number').after("
  • Card Number is not valid
"); + logger.info("invalid card number"); + billingInfoValid = false; + } else { + $paymentMethod.find('#divCardNumber').removeClass("error"); + } + + if (!$.payment.validateCardExpiry(card_month, card_year)) { + $paymentMethod.find('#divCardExpiry .error-text').remove(); + $paymentMethod.find('#divCardExpiry').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-expiry').after("
  • Card Number is not valid
"); + logger.info("invalid card expiry"); + billingInfoValid = false; + } else { + $paymentMethod.find('#divCardExpiry').removeClass("error"); + } + + if (!card_verify) { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-verify').after("
  • Card Verification Value is required
"); + + logger.info("no card verify"); + billingInfoValid = false; + } else if (!$.payment.validateCardCVC(card_verify)) { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-verify').after("
  • Card Verification Value is not valid.
"); + + logger.info("bad card CVC"); + billingInfoValid = false; + } else { + $paymentMethod.find('#divCardVerify').removeClass("error"); + } + } + + if(!billingInfoValid) { + logger.debug("billing info is invalid. returning"); + return false; + } + + billing_info = {}; + shipping_info = {}; + billing_info.first_name = billing_first_name; + billing_info.last_name = billing_last_name; + billing_info.address1 = billing_address1; + billing_info.address2 = billing_address2; + billing_info.city = billing_city; + billing_info.state = billing_state; + billing_info.country = billing_country; + billing_info.zip = billing_zip; + billing_info.number = card_number; + billing_info.month = card_month; + billing_info.year = card_year; + billing_info.verification_value = card_verify; + + /** + if (shipping_as_billing) { + shipping_info = $.extend({},billing_info); + delete shipping_info.number; + delete shipping_info.month; + delete shipping_info.year; + delete shipping_info.verification_value; + } else { + shipping_info.first_name = shipping_first_name; + shipping_info.last_name = shipping_last_name; + shipping_info.address1 = shipping_address1; + shipping_info.address2 = shipping_address2; + shipping_info.city = shipping_city; + shipping_info.state = shipping_state; + shipping_info.country = shipping_country; + shipping_info.zip = shipping_zip; + }*/ + + var email = null; + var password = null; + var terms = false; + var isLoggedIn = context.JK.currentUserId; + if(!isLoggedIn) { + email = $accountSignup.find('input[name="email"]').val() + password = $accountSignup.find('input[name="password"]').val() + terms = $accountSignup.find('input[name="terms-of-service"]').is(':checked'); + } + + $screen.find("#payment-info-next").addClass("disabled"); + $screen.find("#payment-info-next").off("click"); + rest.createRecurlyAccount({billing_info: billing_info, terms_of_service: terms, email: email, password: password, reuse_card_this_time: reuse_card_this_time, reuse_card_next_time: reuse_card_next_time}) + .done(function() { + // so the user can hit back in checkoutOrder and not have to re-enter billing info right away + checkoutUtils.setPreserveBillingInfo(); + $screen.find("#payment-info-next").on("click", next); + + if(isLoggedIn) { + context.location = '/client#/checkoutOrder' + } + else { + // this means the account was created; we need to reload the page for this to take effect + context.JK.currentUserId = 'something' // this is to trick layout.js from getting involved and redirecting to home screen + context.location = '/client#/checkoutOrder' + context.location.reload() + } + }) + .fail(errorHandling) + .always(function(){ + $screen.find("#payment-info-next").removeClass("disabled"); + }) + } + + function errorHandling(xhr, ajaxOptions, thrownError) { + logger.debug("error handling", xhr.responseJSON) + if(xhr.responseJSON && xhr.responseJSON.errors) { + $.each(xhr.responseJSON.errors, function(key, error) { + if (key == 'number') { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-number').after("
  • " + error + "
"); + } + else if (key == 'verification_value') { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error").addClass("transparent"); + $paymentMethod.find('#card-verify').after("
  • " + error + "
"); + } + else if(key == 'email') { + var $email = $accountSignup.find('input[name="email"]') + var $field = $email.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error").addClass("transparent"); + $email.after("
  • " + error + "
"); + } + else if(key == 'password') { + var $password = $accountSignup.find('input[name="password"]') + var $field = $password.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error").addClass("transparent"); + $password.after("
  • " + error + "
"); + } + else if(key == 'terms_of_service') { + var $terms = $accountSignup.find('input[name="terms-of-service"]') + var $field = $terms.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error").addClass("transparent"); + $accountSignup.find('.terms-of-service-label-holder').append("
  • " + error + "
"); + } + else if(key == 'message') { + $("#payment_error").text(error).removeClass('hidden') + } + }); + } + else { + $("#payment_error").text(xhr.responseText).removeClass('hidden') + } + + + $screen.find("#payment-info-next").on('click', next); + } + + function beforeShowOrder() { + step = 3; + renderNavigation(); + populateOrderPage(); + } + + + function populateOrderPage() { + + rest.getShoppingCarts() + .done(renderOrderPage) + .fail(app.ajaxError); + } + + function toggleShippingAsBilling(e) { + e.preventDefault(); + + var shipping_as_billing = $(e.target).is(':checked'); + + if (!shipping_as_billing) { + $shippingAddress.removeClass("hidden"); + } + else { + $shippingAddress.addClass("hidden"); + } + } + + function changeBillingInfo(e) { + if(e) { + e.preventDefault(); + } + + logger.debug("change billing info requested") + + // clear out the skip billing info behavior + checkoutUtils.deletePreserveBillingInfo() + + renderAccountInfo(); + + return false; + } + function toggleReuseExistingCard(e) { + if(e) { + e.preventDefault(); + } + + logger.debug("toggle reuse existing card") + + var reuse_existing = $(this).is(':checked'); + + $('#billing-first-name').prop('disabled', reuse_existing); + $('#billing-last-name').prop('disabled', reuse_existing); + $('#billing-address1').prop('disabled', reuse_existing); + $('#billing-address2').prop('disabled', reuse_existing); + $('#billing-city').prop('disabled', reuse_existing); + $('#billing-state').prop('disabled', reuse_existing); + $('#billing-zip').prop('disabled', reuse_existing); + $('#billing-country').prop('disabled', reuse_existing); + + $('#card-name').prop('disabled', reuse_existing); + $('#card-number').prop('disabled', reuse_existing); + $('#card_expire-date_1i').prop('disabled', reuse_existing); + $('#card_expire-date_2i').prop('disabled', reuse_existing); + $('#card-verify').prop('disabled', reuse_existing); + + } + + function events() { + $screen.find("#payment-info-next").on('click', next); + $shippingAsBilling.on('ifChanged', toggleShippingAsBilling); + $reuseExistingCardChk.on('ifChanged', toggleReuseExistingCard); + $alreadyEnteredJamTrackPrompt.find('.change-payment-info').on('click', changeBillingInfo) + } + + function reset() { + + } + + function renderNavigation() { + $navigation.html(""); + var navigationHtml = $( + context._.template( + $('#template-checkout-navigation').html(), + {current: step}, + {variable: 'data'} + ) + ); + + $navigation.append(navigationHtml); + } + + function initializeControls() { + $("form.payment-info").addClass(context.JK.currentUserId ? 'signed-in' : 'not-signed-in').iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); + + // Use jquery.payment to limit characters and length: + $paymentMethod.find("#card-number").payment('formatCardNumber'); + $paymentMethod.find("#card-verify").payment('formatCardCVC'); + + selectCountry = new context.JK.SelectLocation($('#billing-country'), null, null, app, false) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide' : beforeHide + }; + app.bindScreen('checkoutPayment', screenBindings); + + $screen = $("#checkoutPaymentScreen"); + $paymentInfoPanel = $screen.find("#checkout-payment-info"); + $navigation = $screen.find(".checkout-navigation-bar"); + $billingInfo = $paymentInfoPanel.find(".billing-address"); + $shippingInfo = $paymentInfoPanel.find(".shipping-address"); + $paymentMethod = $paymentInfoPanel.find(".payment-method"); + $accountSignup = $paymentInfoPanel.find('.jamkazam-account-signup') + $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); + $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); + $reuseExistingCard = $paymentInfoPanel.find('.reuse-existing-card') + $reuseExistingCardChk = $paymentInfoPanel.find('#reuse-existing-card') + $existingCardEndsWith = $paymentInfoPanel.find('.existing-card-ends-with') + $newCardInfo = $paymentInfoPanel.find('.new-card-info') + $freeJamTrackPrompt = $screen.find('.payment-prompt.free-jamtrack') + $noFreeJamTrackPrompt = $screen.find('.payment-prompt.no-free-jamtrack') + $alreadyEnteredJamTrackPrompt = $screen.find('.payment-prompt.already-entered') + + + if($screen.length == 0) throw "$screen must be specified"; + if($navigation.length == 0) throw "$navigation must be specified"; + + initializeControls(); + + events(); + } + + this.initialize = initialize; + + return this; + } +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js index beb536d3d..2416375c4 100644 --- a/web/app/assets/javascripts/checkout_signin.js +++ b/web/app/assets/javascripts/checkout_signin.js @@ -5,6 +5,7 @@ context.JK.CheckoutSignInScreen = function(app) { var logger = context.JK.logger; + var rest = context.JK.Rest(); var $screen = null; var $navigation = null; @@ -14,54 +15,104 @@ var $password = null; var $signinBtn = null; var $signupBtn = null; + var $inputElements = null; + var $contentHolder = null; + var $btnNext = null; + var $btnFacebook = null; function beforeShow(data) { renderNavigation(); + renderLoggedInState(); } function afterShow(data) { } - function events() { - $signinBtn.on('click', login); - $signupBtn.on('click', signup); + + function renderLoggedInState(){ + if(isLoggedIn()) { + $contentHolder.removeClass('not-signed-in').addClass('signed-in') + } + else { + $contentHolder.removeClass('signed-in').addClass('not-signed-in') + } } - function signup(e) { - app.layout.showDialog('signup-dialog'); - return false; + function isLoggedIn() { + return !!context.JK.currentUserId; + } + + function events() { + $signinForm.on('submit', login); + $signinBtn.on('click', login); + $btnNext.on('click', moveNext); + $btnFacebook.on('click', facebookSignup); } function reset() { - $signinForm.removeClass('login-error'); - - $email.val(''); - $password.val(''); + $inputElements.removeClass('login-error'); } + function moveNext() { + window.location = '/client#/checkoutPayment'; + + return false; + } + + function facebookSignup() { + + var $btn = $(this); + + if($btn.is('.disabled')) { + logger.debug("ignoring fast attempt at facebook signup") + return false; + } + + $btn.addClass('disabled') + + rest.createSignupHint({redirect_location: '/client#/checkoutPayment'}) + .done(function() { + // send the user on to facebook signin + window.location = $btn.attr('href'); + }) + .fail(function() { + app.notify({text:"Facebook Signup is not working properly"}); + }) + .always(function() { + $btn.removeClass('disabled') + }) + return false; + } function login() { + if($signinBtn.is('.disabled')) { + return false; + } + var email = $email.val(); var password = $password.val(); reset(); - $signinBtn.text('TRYING...'); + $signinBtn.text('TRYING...').addClass('disabled') - rest.login({email: email, password: password, remember_me: false}) + rest.login({email: email, password: password, remember_me: true}) .done(function() { - window.location = '/client#/order' + window.location = '/client#/checkoutPayment' + window.location.reload(); }) .fail(function(jqXHR) { if(jqXHR.status == 422) { - $signinForm.addClass('login-error') + $inputElements.addClass('login-error') } else { app.notifyServerError(jqXHR, "Unable to log in") } }) .always(function() { - $signinBtn.text('SIGN IN') + $signinBtn.text('SIGN IN').removeClass('disabled') }) + + return false; } function renderNavigation() { @@ -83,15 +134,18 @@ 'beforeShow': beforeShow, 'afterShow': afterShow }; - app.bindScreen('signin', screenBindings); + app.bindScreen('checkoutSignin', screenBindings); - $screen = $("#signInScreen"); + $screen = $("#checkoutSignInScreen"); $navigation = $screen.find(".checkout-navigation-bar"); $signinForm = $screen.find(".signin-form"); $signinBtn = $signinForm.find('.signin-submit'); - $email = $signinForm.find('input[name="session[email]"]'); - $password = $signinForm.find('input[name="session[password]"]'); - $signupBtn = $signinForm.find('.show-signup-dialog'); + $email = $signinForm.find('input[name="email"]'); + $password = $signinForm.find('input[name="password"]'); + $inputElements = $signinForm.find('.input-elements'); + $contentHolder = $screen.find('.content-holder'); + $btnNext = $screen.find('.btnNext'); + $btnFacebook = $screen.find('.signin-facebook') if($screen.length == 0) throw "$screen must be specified"; if($navigation.length == 0) throw "$navigation must be specified"; diff --git a/web/app/assets/javascripts/checkout_signin.js.coffee b/web/app/assets/javascripts/checkout_signin.js.coffee new file mode 100644 index 000000000..fd8cc5288 --- /dev/null +++ b/web/app/assets/javascripts/checkout_signin.js.coffee @@ -0,0 +1,62 @@ +$ = jQuery +context = window +context.JK ||= {}; + +# events emitted: +# EVENTS.CHECKOUT_SIGNED_IN +# EVENTS.CHECKOUT_SKIP_SIGN_IN + +context.JK.CheckoutSignin = class CheckoutSignin + constructor: (app, root) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + @app = app + @root = root + @emailInput = @root.find('input[name="email"]') + @passwordInput = @root.find('input[name="password"]') + @signinBtn = @root.find('.signin-submit') + @skipSigninBtn = @root.find('.btnNext') + @signinForm = @root.find('form.signin-form') + + # TODO: add Facebook login support + + throw "no root element" if not @root.exists() + throw "no email element" if not @emailInput.exists() + throw "no password element" if not @passwordInput.exists() + throw "no signin btn" if not @signinBtn.exists() + throw "no skip signin btn" if not @skipSigninBtn.exists() + + @signinForm.submit(@onSigninSubmit) + @skipSigninBtn.click(@onSkipSignin) + + removeErrors: () => + @signinForm.removeClass('login-error'); + + onSigninSubmit: () => + @logger.debug("attempting to signin") + + @removeErrors() + + @signinBtn.addClass('disabled') + + email = @emailInput.val() + password = @passwordInput.val() + + @rest.login({email: email, password: password, remember_me: true}) + .done(@onLoginDone) + .fail(@onLoginFail) + + onLoginDone: () => + @signinBtn.removeClass('disabled') + $(this).triggerHandler(@EVENTS.CHECKOUT_SIGNED_IN, {}) + + onLoginFail: (jqXHR) => + @signinBtn.removeClass('disabled') + if jqXHR.status == 422 + @signinForm.addClass('login-error') + else + @app.notifyServerError(jqXHR, "Unable to log in. Try again later.") + + onSkipSignin: () => + $(this).triggerHandler(@EVENTS.CHECKOUT_SKIP_SIGN_IN, {}) \ No newline at end of file diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee new file mode 100644 index 000000000..4505725ad --- /dev/null +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -0,0 +1,41 @@ +$ = jQuery +context = window +context.JK ||= {}; + +class CheckoutUtils + constructor: () -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + @cookie_name = "preserve_billing" + init: () => + + refreshPreserveBillingInfo:() => + if @shouldPreserveBillingInfo + @logger.debug("refreshing preserve billing info timer") + @setPreserveBillingInfo() + + setPreserveBillingInfo:() => + date = new Date(); + minutes = 10; + date.setTime(date.getTime() + (minutes * 60 * 1000)) + $.removeCookie(@cookie_name, { path: '/' }) + $.cookie(@cookie_name, "jam", { expires: date, path: '/' }) + + deletePreserveBillingInfo:() => + $.removeCookie(@cookie_name, { path: '/' }) + + @logger.debug("deleted preserve billing"); + + unless $.cookie(@cookie_name)? + @logger.error("after deleting the preserve billing cookie, it still exists!") + + + # existance of cookie means we should preserve billing + shouldPreserveBillingInfo:() => + value = $.cookie(@cookie_name) + value? + + + +# global instance +context.JK.CheckoutUtilsInstance = new CheckoutUtils() \ No newline at end of file diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee new file mode 100644 index 000000000..20d0e76cc --- /dev/null +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -0,0 +1,18 @@ +# one time init stuff for the /client view + + +$ = jQuery +context = window +context.JK ||= {}; + +context.JK.ClientInit = class ClientInit + constructor: () -> + @logger = context.JK.logger + @gearUtils = context.JK.GearUtils + + init: () => + if context.gon.isNativeClient + this.nativeClientInit() + + nativeClientInit: () => + @gearUtils.bootstrapDefaultPlaybackProfile(); diff --git a/web/app/assets/javascripts/configureTracksHelper.js b/web/app/assets/javascripts/configureTracksHelper.js index 32f220366..ba16f1d28 100644 --- a/web/app/assets/javascripts/configureTracksHelper.js +++ b/web/app/assets/javascripts/configureTracksHelper.js @@ -355,7 +355,7 @@ context._.each(tracks.tracks, function(track) { if(!track.instrument_id) { logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); - context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.'); + context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.'); return false; } }); diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 2ced92f0c..7e0fc0f12 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -135,7 +135,11 @@ if(!button.name) throw "button.name must be specified"; if(!button.click) throw "button.click must be specified"; - var buttonStyle = options.buttons.length == i + 1 ? 'button-orange' : 'button-grey'; + var buttonStyle = button.buttonStyle; + if(!buttonStyle) { + buttonStyle = options.buttons.length == i + 1 ? 'button-orange' : 'button-grey'; + } + var $btn = $('' + button.name + ''); $btn.click(function() { diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 6fc114f74..0faa28699 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -9,6 +9,9 @@ var $dialog = null; var $dontShowAgain = null; var $setupGearBtn = null; + var $browserJamTrackBtn = null; + var $jamTrackSection = null; + var $jamTracksLimitedTime = null; function handleStartAudioQualification() { @@ -45,6 +48,12 @@ return false; }) + $browserJamTrackBtn.click(function() { + app.layout.closeDialog('getting-started') + window.location = '/client#/jamtrackBrowse' + return false; + }) + $('#getting-started-dialog a.facebook-invite').on('click', function (e) { invitationDialog.showFacebookDialog(e); }); @@ -59,13 +68,21 @@ } function beforeShow() { + app.user().done(function(user) { + var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' + $jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule) + if(user.free_jamtrack) { + $jamTracksLimitedTime.removeClass('hidden') + } + }) } function beforeHide() { + var showWhatsNext = !$dontShowAgain.is(':checked') + app.user().done(function(user) { + app.updateUserModel({show_whats_next: showWhatsNext, show_whats_next_count: user.show_whats_next_count + 1}) + }) - if ($dontShowAgain.is(':checked')) { - app.updateUserModel({show_whats_next: false}) - } } function initializeButtons() { @@ -84,6 +101,9 @@ $dialog = $('#getting-started-dialog'); $dontShowAgain = $dialog.find('#show_getting_started'); $setupGearBtn = $dialog.find('.setup-gear-btn') + $browserJamTrackBtn = $dialog.find('.browse-jamtrack'); + $jamTrackSection = $dialog.find('.get-a-free-jamtrack-section') + $jamTracksLimitedTime = $dialog.find('.jamtracks-limited-time') registerEvents(); diff --git a/web/app/assets/javascripts/dialog/jamtrackLicenseDialog.js b/web/app/assets/javascripts/dialog/jamtrackLicenseDialog.js new file mode 100644 index 000000000..c74d71e86 --- /dev/null +++ b/web/app/assets/javascripts/dialog/jamtrackLicenseDialog.js @@ -0,0 +1,46 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.JamtrackLicenseDialog = function(app) { + var logger = context.JK.logger; + var $dialog = null; + var dialogId = 'jamtrack-license-dialog'; + + function beforeShow(data) { + } + + function afterShow(data) { + } + + function afterHide() { + } + + function showDialog() { + return app.layout.showDialog(dialogId); + } + + function events() { + } + + function initialize() { + + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $dialog = $('[layout-id="' + dialogId + '"]'); + + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + }; + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/jamtrack_payment_history_dialog.js.coffee b/web/app/assets/javascripts/dialog/jamtrack_payment_history_dialog.js.coffee new file mode 100644 index 000000000..a73460c58 --- /dev/null +++ b/web/app/assets/javascripts/dialog/jamtrack_payment_history_dialog.js.coffee @@ -0,0 +1,61 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.JamtrackPaymentHistoryDialog = class JamtrackPaymentHistoryDialog + constructor: (@app) -> + @rest = context.JK.Rest() + @client = context.jamClient + @logger = context.JK.logger + @screen = null + @dialogId = 'jamtrack-payment-history-dialog'; + @dialog = null; + + initialize:() => + dialogBindings = { + 'beforeShow' : @beforeShow, + 'afterShow' : @afterShow + } + + @dialog = $('[layout-id="' + @dialogId + '"]'); + @app.bindDialog(@dialogId, dialogBindings); + @tbody = @dialog.find("table.payment-table tbody") + @rowTemplate = $('#template-payment-history-row').html() + + beforeShow:() => + # Get payment history from jamrest + payments = [ + {date: new Date(2013, 4, 5), amount: 372.33}, + {date: new Date(2014, 5, 5), amount: 338.44} + ] + + @rest.getPaymentHistory() + .done(@showPaymentHistory) + .fail(@app.ajaxError) + + showPaymentHistory:(data) => + # Turn in to HTML rows and append: + @tbody.html("") + if data.payments? && data.payments.length > 0 + for p in data.payments + amt = p.amount + amt = 0 if !amt? + payment = { + date: context.JK.formatDateYYYYMMDD(p.created_at) + amount: (amt * 100).toFixed(2) + status: p.status + payment_method: p.payment_method.replace("_", " ") + reference: p.reference + } + tr = $(context._.template(@rowTemplate, payment, { variable: 'data' })); + @tbody.append(tr); + else + tr = "No payments found" + @tbody.append(tr); + + afterShow:() => + + showDialog:() => + @app.layout.showDialog(@dialogId) + + \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 27e1bc3e8..10fdae9a1 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -7,6 +7,7 @@ var rest = context.JK.Rest(); var showing = false; var perPage = 10; + var openingRecording = false; function tbody() { return $('#local-recordings-dialog table.local-recordings tbody'); @@ -22,6 +23,7 @@ function beforeShow() { + openingRecording = false; emptyList(); resetPagination(); showing = true; @@ -89,6 +91,12 @@ function registerStaticEvents() { $('#local-recordings-dialog table.local-recordings tbody').on('click', 'tr', function(e) { + if(openingRecording) { + // prevent double-click spam + logger.debug("localRecordingDialog: ignoring duplicate open attempt") + return false; + } + var localState = $(this).attr('data-local-state'); if(localState == 'MISSING') { @@ -109,9 +117,15 @@ { var claimedRecording = $(this).data('server-model'); + openingRecording = true; + // tell the server we are about to start a recording rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) .done(function(response) { + + // update session info + context.JK.CurrentSessionModel.updateSession(response); + var recordingId = $(this).attr('data-recording-id'); var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); @@ -142,6 +156,9 @@ app.notifyServerError(jqXHR, "Unable to Open Recording For Playback"); }) + .always(function() { + openingRecording = false; + }) } diff --git a/web/app/assets/javascripts/dialog/loginRequiredDialog.js b/web/app/assets/javascripts/dialog/loginRequiredDialog.js new file mode 100644 index 000000000..6795614c1 --- /dev/null +++ b/web/app/assets/javascripts/dialog/loginRequiredDialog.js @@ -0,0 +1,45 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.LoginRequiredDialog = function(app) { + var logger = context.JK.logger; + var $dialog = null; + var dialogId = 'login-required-dialog'; + + function beforeShow(data) { + } + + function afterShow(data) { + } + + function afterHide() { + } + + function events() { + $dialog.find('.go-to-jamtracks').click(function() { + app.layout.closeDialog(dialogId) + context.location.href = $(this).attr('href') + }) + } + + function initialize() { + + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $dialog = $('#' + dialogId); + + events(); + } + + this.initialize = initialize; + }; + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js new file mode 100644 index 000000000..bd0d136aa --- /dev/null +++ b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js @@ -0,0 +1,145 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.OpenBackingTrackDialog = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var showing = false; + var perPage = 10; + var $dialog = null; + var $tbody = null; + var $paginatorHolder = null; + var $templateOpenBackingTrackRow = null; + var $downloadedTrackHelp = null; + var $whatAreBackingTracks = null; + var $displayAudioFileFolder = null; + + + function emptyList() { + $tbody.empty(); + } + + function resetPagination() { + $dialog.find('.paginator').remove(); + } + + function beforeShow() { + emptyList(); + resetPagination(); + showing = true; + getBackingTracks(); + $dialog.data('result', null); + // .done(function(data, textStatus, jqXHR) { + // // initialize pagination + // var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected) + // $paginatorHolder.append($paginator); + // }); + } + + function afterHide() { + showing = false; + } + + + function onPageSelected(targetPage) { + return getBackingTracks(targetPage); + } + + function getBackingTracks(page) { + + var result = context.jamClient.getBackingTrackList(); + var backingTracks = result.backing_tracks; + + if (!backingTracks || backingTracks.length == 0) { + $tbody.append("No Tracks found"); + } else { + $.each(backingTracks, function(index, backingTrack) { + var extension = backingTrack.name + var options = { + backingTrackState: null, + name: backingTrack.name, + type: getExtension(backingTrack.name), + length: displaySize(backingTrack.size) + } + var $tr = $(context._.template($templateOpenBackingTrackRow.html(), options, { variable: 'data' })); + $tr.data('server-model', backingTrack); + $tbody.append($tr); + }); + }//end + } + + // from http://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript + function getExtension(filename) { + return filename.substr((~-filename.lastIndexOf(".") >>> 0) + 2) + } + + // from seth: + function displaySize(length) { + var size = (length==null || typeof(length)=='undefined') ? 0 : Number(length) + return (Math.round(size * 10 / (1024 * 1024) ) / 10).toString() + "M" + } + + function registerStaticEvents() { + $tbody.on('click', 'tr', function(e) { + var backingTrack = $(this).data('server-model'); + + // tell the server we are about to open a backing track: + rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name}) + .done(function(response) { + var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false); + + // TODO: Possibly actually check the result. Investigate + // what real client returns: + // // if(result) { + // let callers see which backing track was chosen + $dialog.data('result', backingTrack); + app.layout.closeDialog('open-backing-track-dialog'); + // } + // else { + // logger.error("unable to open backing track") + // } + context.JK.CurrentSessionModel.refreshCurrentSession(true); + + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback"); + }) + + return false; + }) + + context.JK.helpBubble($whatAreBackingTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog}) + $whatAreBackingTracks.on('click', false) // no help yet + + $displayAudioFileFolder.on('click', function(e) { + e.stopPropagation(); + context.jamClient.OpenBackingTracksDirectory(); + }) + } + + function initialize(){ + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('open-backing-track-dialog', dialogBindings); + + $dialog = $('#open-backing-track-dialog'); + $tbody = $dialog.find('table.open-backing-tracks tbody'); + $paginatorHolder = $dialog.find('.paginator-holder'); + $templateOpenBackingTrackRow = $('#template-backing-track-row') + $whatAreBackingTracks = $dialog.find('.what-are-backingtracks') + $displayAudioFileFolder = $dialog.find('.display-backingtracks-folder') + + registerStaticEvents(); + }; + + + this.initialize = initialize; + this.isShowing = function isShowing() { return showing; } + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index b3d485876..9f0fc5ce6 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -24,6 +24,7 @@ } function beforeShow() { + $dialog.data('result', null) emptyList(); resetPagination(); showing = true; @@ -57,7 +58,8 @@ options.jamTrackId = jamTrack.id; options.name = jamTrack.name; options.artist = jamTrack.original_artist; - options.downloaded = 'Yes' + var detail = context.jamClient.JamTrackGetTrackDetail(jamTrack.id) || {} + options.downloaded = detail.key_state == 'ready' ? 'Yes' : 'No' var $tr = $(context._.template($templateOpenJamTrackRow.html(), options, { variable: 'data' })); $tr.data('server-model', jamTrack); @@ -77,18 +79,8 @@ // tell the server we are about to start a recording rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done(function(response) { - context.jamClient.JamTrackStopPlay(); - var result = context.jamClient.JamTrackPlay('t'); - - logger.debug("JamTrackPlay response: %o", result); - - if(result) { - app.layout.closeDialog('open-jam-track-dialog'); - } - else { - logger.error("unable to open jam track") - } - + $dialog.data('result', {success:true, jamTrack: jamTrack}) + app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback"); diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index f0d24a27d..f78022877 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -67,7 +67,7 @@ else { // load recording - var openRecordingResult = context.jamClient.OpenRecording(recording); + var openRecordingResult = context.jamClient.PreviewRecording(recording); logger.debug("OpenRecording response: %o", openRecordingResult); @@ -78,6 +78,23 @@ "icon_url": "/assets/content/icon_alert_big.png" }); } + else { + // hunt for missing backing tracks; if so, mark them as silent + context._.each(openRecordingResult.backing_tracks, function(backingTrack) { + if(backingTrack.local_state == "MISSING") { + // mark this as deleted + logger.debug("marking recorded track as deleted") + rest.markRecordedBackingTrackSilent({recording_id: openRecordingResult.recording_id, backing_track_id: backingTrack.client_track_id}) + .fail(function() { + app.notify({ + "title": "Unable to Mark Backing Track", + "text": "A backing track was never played, but we could not tell the server to remove it from the recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + }) + } + }) + } playbackControls.startMonitor(); } @@ -88,7 +105,7 @@ function afterHide() { recording = null; playbackControls.stopMonitor(); - context.jamClient.CloseRecording(); + context.jamClient.ClosePreviewRecording(); } function discardRecording(e) { diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index ea18fcd01..0c47eb20e 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -3,6 +3,7 @@ context.JK = context.JK || {}; context.JK.SessionSettingsDialog = function(app, sessionScreen) { var logger = context.JK.logger; + var gearUtils = context.JK.GearUtilsInstance; var $dialog; var $screen = $('#session-settings'); var $selectedFilenames = $screen.find('#selected-filenames'); @@ -15,6 +16,8 @@ function beforeShow(data) { + var canPlayWithOthers = gearUtils.canPlayWithOthers(); + context.JK.GenreSelectorHelper.render('#session-settings-genre'); $dialog = $('[layout-id="session-settings"]'); @@ -72,6 +75,10 @@ context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); + + var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable' + $('#session-settings-musician-access').easyDropDown(easyDropDownState) + $('#session-settings-fan-access').easyDropDown(easyDropDownState) } function saveSettings(evt) { diff --git a/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee b/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee new file mode 100644 index 000000000..d94a21d6a --- /dev/null +++ b/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee @@ -0,0 +1,57 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.SinglePlayerProfileGuardDialog = class SinglePlayerProfileGuardDialog + constructor: (@app) -> + @rest = context.JK.Rest() + @client = context.jamClient + @logger = context.JK.logger + @gearUtils = context.JK.GearUtilsInstance + @screen = null + @dialogId = 'single-player-profile-dialog'; + @dialog = null; + + initialize:() => + dialogBindings = { + 'beforeShow' : @beforeShow, + 'afterShow' : @afterShow + } + + @dialog = $('[layout-id="' + @dialogId + '"]'); + @app.bindDialog(@dialogId, dialogBindings); + @content = @dialog.find(".dialog-inner") + @audioLatency = @dialog.find('.audio-latency') + @btnPrivateSession = @dialog.find('.btn-private-session') + @btnGearSetup = @dialog.find('.btn-gear-setup') + + @btnPrivateSession.on('click', @onPrivateSessionChoice) + @btnGearSetup.on('click', @onGearSetupChoice) + + beforeShow:() => + @dialog.data('result', { choice: null}) + + + afterShow:() => + canPlayWithOthers = @gearUtils.canPlayWithOthers() + + if canPlayWithOthers.isNoInputProfile + @content.removeClass('high-latency').addClass('has-no-inputs') + else + @content.removeClass('has-no-input').addClass('high-latency') + + latency = '?' + if canPlayWithOthers.audioLatency? + latency = canPlayWithOthers.audioLatency + + @audioLatency.text("#{latency} milliseconds.") + + onPrivateSessionChoice: () => + @dialog.data('result', { choice: 'private_session'}) + @app.layout.closeDialog(@dialogId) + return false + + onGearSetupChoice: () => + @dialog.data('result', { choice: 'gear_setup'}) + @app.layout.closeDialog(@dialogId) + return false \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/videoDialog.js b/web/app/assets/javascripts/dialog/videoDialog.js index 3d5a41707..01ab3dc6b 100644 --- a/web/app/assets/javascripts/dialog/videoDialog.js +++ b/web/app/assets/javascripts/dialog/videoDialog.js @@ -14,7 +14,7 @@ if (!context.jamClient || !context.jamClient.IsNativeClient()) { $('#video-dialog-header').html($self.data('video-header') || $self.attr('data-video-header')); - $('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.atr('data-video-url')); + $('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.attr('data-video-url')); app.layout.showDialog('video-dialog'); e.stopPropagation(); e.preventDefault(); @@ -29,6 +29,7 @@ function events() { $('.carousel .slides').on('click', '.slideItem', videoClick); $('.video-slide').on('click', videoClick); + $('.video-item').on('click', videoClick); $(dialogId + '-close').click(function (e) { app.layout.closeDialog('video-dialog'); diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 75e684bc6..8442fec02 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -2,10 +2,492 @@ $ = jQuery context = window context.JK ||= {}; -context.JK.DownloadJamTrack = class SyncViewer - constructor: (@app) -> +# This is the sequence of how this widget works: +# checkState() is the heart of the state machine; it is called to get things going, and is called whenevr a state ends +# checkState() checks first against what the client thinks about the state of the JamTrack; +# if it on the disk then the state machine may enter one of: +# * synchronized +# * keying +# +# if it's still on the server, then the state machine may be: +# * packaging +# * downloading +# +# errored state can be entered from @jamTrack.jam_track_right_id +# +# other state; you augment the error to the user by suppling @errorMessage before transitioning +# +# no-client is the way the widget behaves when you are in a normal browser (i.e., nothing happens other than tell the user to use the client) +# +# Discussion of the different states: +# There are different states that a JamTrack can be in. +# The final success state is that the JamTrack is on disk and loadable. (show synchronized state) +# But there are others until you get there: +# The JamTrack does not exist on the server, so we will create it (packaging state) +# The JamTrack exists on the server, but not on disk, so we will download it (downloading state) +# The JamTrack is on the disk, but does not yet have keys, so we will fetch them (keying) + +context.JK.DownloadJamTracks = {} +context.JK.DownloadJamTrack = class DownloadJamTrack + constructor: (@app, jamTrack, size = 'large') -> @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() + @logger = context.JK.logger + @jamTrack = jamTrack + @size = size + @attemptedEnqueue = false + @errorReason = null + @errorMessage = null + @transitionTimer = null + @downloadTimer = null + @trackDetail = null + @stateHolder = null + @active = false + @startTime = null + @attempts = 0 + @tracked = false + @ajaxEnqueueAborted = false + @ajaxGetJamTrackRightAborted = false + throw "no JamTrack specified" unless @jamTrack? + throw "invalid size" if @size != 'large' && @size != 'small' + throw "no JamTrack version" unless @jamTrack.version? + + @path = [] + @states = { + no_client: { name: 'no-client', show: @showNoClient, leaf: true }, + synchronized: { name: 'synchronized', show: @showSynchronized, leaf: true}, + packaging: { name: 'packaging', show: @showPackaging }, + downloading: { name: 'downloading', show: @showDownloading }, + keying: { name: 'keying', show: @showKeying, max_time: 10000 }, + initial: { name: 'initial', show: @showInitial }, + quiet: { name: 'quiet', show: @showQuiet }, + errored: { name: 'errored', show: @showError, leaf: true} + } + + context.JK.DownloadJamTracks[@jamTrack.id] = this + downloadJamTrackTemplate = $('#template-download-jamtrack') + throw "no download jamtrack template" if not downloadJamTrackTemplate.exists() + + @root = $(downloadJamTrackTemplate.html()) + @stateHolder = @root.find('.state') + @root.on('remove', this.destroy) # automatically destroy self when removed from DOM + + # populate in template and visual transition functions + for state, data of @states + data.template = $("#template-download-jamtrack-state-#{data.name}") + + # start off in quiet state, but don't do it through transition system. The transition system expects a change, not initial state + @state = @states.quiet + + this.showState() + + + # after you've created the DownloadJamTrack widget, call synchronize which will begin ensuring that the jamtrack + # is downloaded and ready to open init: () => - @root = $($('#template-download-jamtrack').html()) + @active = true + @root.addClass('active') + this.reset() + + # check if we are in a browser or client + if !gon.isNativeClient + this.transition(@states.no_client) + else + this.transition(@states.initial) + + # when done with the widget, call destroy; this ensures it's not still active, and tracks final metrics + destroy: () => + $(this).off() + @active = false + @root.removeClass('active') + this.trackProgress() + # since we are not in a leave node, we need to report a state since this is effectively our end state + this.reset() + + clear: () => + # reset attemptedEnqueue to false, to allow one attempt to enqueue + @attemptedEnqueue = false + this.clearDownloadTimer() + this.clearTransitionTimer() + this.abortEnqueue() + this.abortGetJamTrackRight() + for state, data of @states + if data.timer? + clearInterval(data.timer) + data.timer = null + + reset: () => + @path = [] + @attempts = 0 + @tracked = false + @startTime = new Date() + this.clear() + + + abortEnqueue: () => + if @ajaxEnqueueAborted + @logger.debug("DownloadJamTrack: aborting ajax enqueue") + # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline + ajax = @ajaxEnqueueAborted + @ajaxEnqueueAborted = true + ajax.abort() + + abortGetJamTrackRight: () => + if @ajaxGetJamTrackRightAborted + @logger.debug("DownloadJamTrack: aborting ajax GetJamTrackRight") + # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline + ajax = @ajaxGetJamTrackRightAborted + @ajaxGetJamTrackRightAborted = true + ajax.abort() + + showState: () => + @state.stateStartTime = new Date(); + @stateHolder.children().remove() + @stateHolder.append(context._.template(@state.template.html(), @jamTrack, { variable: 'data' })) + @stateHolder.find('.' + @size).removeClass('hidden') + @state.show() + + # report a stat now that we've reached the end of this widget's journey + trackProgress: () => + + # do not double-report + if @tracked + return + + if @path.length == 0 + return + + unless @state.leaf + # we've been asked to report at a non-leaf node, meaning the user must have cancelled + @path.push('user-cancelled') + + flattened_path = @path.join('-') + + data = { + value: 1, + path: flattened_path, + duration: (new Date().getTime() - @startTime.getTime()) / 1000, + attempts: @attempts, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName} + if @state == @states.errored + data.result = 'error' + data.detail = @errorReason + else + data.result = 'success' + + context.stats.write('web.jamtrack.downloader', data) + @tracked = true + + showPackaging: () => + @logger.debug("showing #{@state.name}") + this.expectTransition() + + showDownloading: () => + @logger.debug("showing #{@state.name}") + # while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually + context.jamClient.JamTrackDownload(@jamTrack.id, this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback()) + + showKeying: () => + @logger.debug("showing #{@state.name}") + context.jamClient.JamTrackKeysRequest() + this.waitForState() + + showQuiet: () => + @logger.debug("showing #{@state.name}") + + showInitial: () => + @logger.debug("showing #{@state.name}") + @attempts = @attempts + 1 + this.expectTransition() + context.JK.SubscriptionUtils.subscribe('jam_track_right', @jamTrack.jam_track_right_id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onJamTrackRightEvent) + this.checkState() + + showError: () => + @logger.debug("showing #{@state.name}") + context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id) + + if @size == 'large' + @stateHolder.find('.msg').text(@errorMessage) + @stateHolder.find('.retry-button').click(this.retry) + else + @stateHolder.find('.msg').text(@jamTrack.name + ' (error)') + @stateHolder.find('.errormsg').text(@errorMessage) + @stateHolder.find('.retry-button').on('click', this.retry) + + retryMsg = '' + if @attempts > 1 + retryMsg = 'Continue retrying or contact support@jamkazam.com' + + @stateHolder.find('.retry').text(retryMsg) + + showSynchronized: () => + @logger.debug("showing #{@state.name}") + context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id) + + showNoClient: () => + @logger.debug("showing #{@state.name}") + + downloadCheck: () => + @logger.debug "downloadCheck" + + retry: () => + @path = [] + @path.push('retry') + this.clear() + # just switch to the initial state again, causing the loop to start again + this.transition(@states.initial) + return false + + clearStateTimer: () => + if @state.timer? + clearInterval(@state.timer) + @state.timer = null + + stateIntervalCheck: () => + this.checkState() + + # if the timer is null now, then it must have been whacked due to a state change + # if not, then let's see if we have timed out + if @state.timer? + if (new Date()).getTime() - @state.stateStartTime.getTime() > @state.max_time + @logger.debug("The current step (#{@state.name}) took too long") + + if @state == @states.keying + # specific message + this.transitionError("#{@state.name}-timeout", "It took too long for the JamTrack to be keyed.") + else + # generic message + this.transitionError("#{@state.name}-timeout", "The current step (#{@state.name}) took too long") + + + # sets an interval timer for every second, waiting for the status to change + waitForState: () => + unless @active + @logger.error("DownloadJamTrack: ignoring waitForState because we are not active") + + @state.timer = setInterval(this.stateIntervalCheck, 1000) + + + # unused atm; the backend is good about always signalling. we still should though + expectDownload: () => + unless @active + @logger.error("DownloadJamTrack: ignoring expectDownload because we are not active") + + # every 10 seconds, wake up and check the server and see if we missed a state transition + this.clearDownloadTimer() + @downloadTimer = setTimeout(this.downloadCheck, 10000) + + clearDownloadTimer: () => + if @downloadTimer? + clearTimeout(@downloadTimer) + @downloadTimer = null + + transitionError: (reasonCode, errorMessage) => + @errorReason = reasonCode + @errorMessage = errorMessage + this.transition(@states.errored) + + transitionCheck: () => + this.checkState() + + # this should be called every time something changes statefully, to restart a 12 second timer to hit the server for update. + # if everything is moving snappily, we won't have to query the server much, because we are also getting subscription events + # about any changes to the status of the jam track. But, we could miss a message or there could be a path in the server where + # we don't get an event, so that's why, after 12 seconds, we'll still go to the server and check. + # exception: this should not be runngi + expectTransition: () => + unless @active + @logger.error("DownloadJamTrack: ignoring expectTransition because we are not active") + + # every 12 seconds, wake up and check the server and see if we missed a state transition + this.clearTransitionTimer() + @transitionTimer = setTimeout(this.transitionCheck, 12000) + + clearTransitionTimer: () => + if @transitionTimer? + clearTimeout(@transitionTimer) + @transitionTimer = null + + transition: (newState) => + unless @active + @logger.error("DownloadJamTrack: ignoring state change because we are not active") + return + + if newState == @state + @logger.debug("DownloadJamTrack: ignoring state change #{@state.name}") + return + + if @state? + @logger.debug("DownloadJamTrack: state change: #{@state.name} => #{newState.name}") + # make sure there is no timer running on the old state + this.clearTransitionTimer() + this.clearStateTimer() + this.abortEnqueue() + @logger.debug("aborting getJamTrack right on state change") + this.abortGetJamTrackRight() + else + @logger.debug("DownloadJamTrack: initial state: #{newState.name}") + + @state = newState + + # track which states were taken + @path.push(@state.name) + + if @state.leaf + this.trackProgress() + + this.showState() + + $(this).triggerHandler(@EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, {state: @state}) + + checkState: () => + # check for the success state against the local state of the client... if it's playable, then we should be OK + @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + + @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail.key_state: " + @trackDetail.key_state) + + # first check if the version is not the same; if so, invalidate. + + if @trackDetail.version? + if @jamTrack.version != @trackDetail.version + @logger.info("DownloadJamTrack: JamTrack on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.version}. Invalidating") + context.jamClient.InvalidateJamTrack(@jamTrack.id) + @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + + if @trackDetail.version? + @logger.error("after invalidating package, the version is still wrong!") + throw "after invalidating package, the version is still wrong!" + + switch @trackDetail.key_state + when 'pending' + this.transition(@states.keying) + when 'not authorized' + # TODO: if not authorized, do we need to re-initiate a keying attempt? + this.transition(@states.keying) + when 'ready' + this.transition(@states.synchronized) + when 'unknown' + @ajaxGetJamTrackRightAborted = false + @rest.getJamTrackRight({id: @jamTrack.id}) + .done(this.processJamTrackRight) + .fail(this.processJamTrackRightFail) + + + processSigningState: (signingState) => + @logger.debug("DownloadJamTrack: processSigningState: " + signingState) + + switch signingState + when 'QUIET' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("enqueue-timeout", "The server has not begun building your JamTrack.") + else + this.expectTransition() + + this.attemptToEnqueue() + when 'QUEUED' + # when it's queued, there is nothing to do except wait. + this.transition(@states.packaging) + when 'QUEUED_TIMEOUT' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("queued-timeout", "The server took too long to begin processing your JamTrack.") + else + this.expectTransition() + + this.attemptToEnqueue() + when 'SIGNING' + this.expectTransition() + this.transition(@states.packaging) + when 'SIGNING_TIMEOUT' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("signing-timeout", "The server took too long to create your JamTrack.") + else + this.expectTransition() + + this.attemptToEnqueue() + when 'SIGNED' + this.transition(@states.downloading) + when 'ERROR' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("package-error", "The server failed to create your package.") + else + this.expectTransition() + + this.attemptToEnqueue() + else + @logger.error("unknown state: " + signingState) + this.transitionError("unknown-state-#{signingState}", "The server sent an unknown state message: " + signingState) + + attemptToEnqueue: () => + @attemptedEnqueue = true + @ajaxEnqueueAborted = false + + sampleRate = context.jamClient.GetSampleRate() + + @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: sampleRate}) + .done(this.processEnqueueJamTrack) + .fail(this.processEnqueueJamTrackFail) + + + processJamTrackRight: (myJamTrack) => + unless @ajaxGetJamTrackRightAborted + this.processSigningState(myJamTrack.signing_state) + else + @logger.debug("DownloadJamTrack: ignoring processJamTrackRight response") + + processJamTrackRightFail: () => + unless @ajaxGetJamTrackRightAborted? + this.transitionError("status-check-error", "Unable to check with the server on the status of your JamTrack.") + else + @logger.debug("DownloadJamTrack: ignoring processJamTrackRightFail response") + + processEnqueueJamTrack: (enqueueResponse) => + unless @ajaxEnqueueAborted + this.expectTransition() # the act of enqueuing should send down events to the client. we wait... + else + @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response") + + processEnqueueJamTrackFail: () => + unless @ajaxEnqueueAborted + this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") + else + @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response") + + onJamTrackRightEvent: (e, data) => + @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type) + this.expectTransition() + this.processSigningState(data.body.signing_state) + + downloadProgressCallback: (bytesReceived, bytesTotal, downloadSpeedMegSec, timeRemaining) => + bytesReceived = Number(bytesReceived) + bytesTotal = Number(bytesTotal) + # bytesTotal from Qt is not trust worthy; trust server's answer instead + #progressWidth = ((bytesReceived / updateSize) * 100).toString() + "%"; + # $('#progress-bar').width(progressWidth) + + downloadSuccessCallback: (updateLocation) => + # is the package loadable yet? + @logger.debug("DownloadJamTrack: download complete - on to keying") + this.transition(@states.keying) + + downloadFailureCallback: (errorMsg) => + + this.transitionError("download-error", errorMsg) + + # makes a function name for the backend + makeDownloadProgressCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadProgressCallback" + + # makes a function name for the backend + makeDownloadSuccessCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadSuccessCallback" + + # makes a function name for the backend + makeDownloadFailureCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadFailureCallback" + diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index f89aeacba..c5e6e742a 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -7,6 +7,7 @@ //= require backend_alerts //= require stun //= require influxdb-latest +//= require jam_track_utils (function (context, $) { @@ -17,6 +18,8 @@ var ALERT_NAMES = context.JK.ALERT_NAMES; var logger = context.JK.logger; var stun = null; + var rest = context.JK.Rest(); + $(document).on('JAMKAZAM_CONSTRUCTED', function(e, data) { @@ -29,6 +32,8 @@ updateScoringIntervals(); initializeInfluxDB(); + + trackNewUser(); }) $(document).on('JAMKAZAM_READY', function() { @@ -51,6 +56,8 @@ operationalEvents(app); handleGettingStarted(app); + + initShoppingCart(app); }); function watchPreferencesEvent(app) { @@ -123,6 +130,9 @@ var clientPreferencesDialog = new JK.ClientPreferencesDialog(app); clientPreferencesDialog.initialize(); + + var loginRequiredDialog = new JK.LoginRequiredDialog(app); + loginRequiredDialog.initialize(); } // wait 10 seconds @@ -197,8 +207,9 @@ var user = app.user() if(user) { user.done(function(userProfile) { - if (userProfile.show_whats_next && + if (userProfile.show_whats_next && userProfile.show_whats_next_count < 10 && window.location.pathname.indexOf(gon.client_path) == 0 && + window.location.hash.indexOf('/checkout') == -1 && !app.layout.isDialogShowing('getting-started')) { app.layout.showDialog('getting-started'); @@ -207,4 +218,30 @@ } } + function initShoppingCart(app) { + context.JK.JamTrackUtils.checkShoppingCart(); + } + + function trackNewUser() { + var cookie = $.cookie('new_user') + + if(cookie) { + try { + cookie = JSON.parse(cookie) + + context.JK.signup = {} + context.JK.signup = cookie + + $(function() { + // ga() object isn't ready until the page is loaded + $.removeCookie('new_user') + context.JK.GA.trackRegister(cookie.musician, cookie.registrationType); + }); + } + catch(e) { + logger.error("unable to deserialize new_user cookie") + } + } + } + })(window, jQuery); diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index 7beca59db..ba66a44ca 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -20,7 +20,8 @@ e.stopPropagation(); var $fader = $(this); - + var sessionModel = window.JK.CurrentSessionModel || null; + var mediaControlsDisabled = $fader.data('media-controls-disabled'); if(mediaControlsDisabled) { var mediaTrackOpener = $fader.data('media-track-opener'); @@ -28,6 +29,20 @@ return false; } + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') { + window.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $fader.closest('.screen')}) + return false; + } + + if($fader.data('showHelpAboutMediaMixers')) { + if(sessionModel) { + if(!sessionModel.hasShownAudioMediaMixerHelp()) { + window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')}) + sessionModel.markShownAudioMediaMixerHelp() + } + } + } + draggingOrientation = $fader.attr('orientation'); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -129,14 +144,30 @@ var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener'); - + var sessionModel = window.JK.CurrentSessionModel || null; + if(mediaControlsDisabled) { return false; } + + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $draggingFaderHandle.closest('.session-track').data('track_data').type == 'jam_track') { + return false; + } + return true; } function onFaderDragStop(e, ui) { + + if($draggingFader.data('showHelpAboutMediaMixers')) { + if(window.JK.CurrentSessionModel) { + if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) { + window.JK.prodBubble($draggingFader, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $draggingFader.closest('.screen')}) + window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp() + } + } + } + var faderPct = faderValue($draggingFader, e, ui.position); // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows @@ -179,7 +210,10 @@ selector.html(g._.template(templateSource, options)); - selector.find('div[control="fader"]').data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener')) + selector.find('div[control="fader"]') + .data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) selector.find('div[control="fader-handle"]').draggable({ drag: onFaderDrag, @@ -187,7 +221,9 @@ stop: onFaderDragStop, containment: "parent", axis: options.faderType === 'horizontal' ? 'x' : 'y' - }).data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener')) + }).data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) // Embed any custom styles, applied to the .fader below selector if ("style" in options) { diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 12d0e4e4a..0e21c3a63 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -22,6 +22,13 @@ var fakeJamClientRecordings = null; var p2pCallbacks = null; var videoShared = false; + var metronomeActive=false; + var metronomeBPM=false; + var metronomeSound=false; + var metronomeMeter=0; + var backingTrackPath = ""; + var backingTrackLoop = false; + var simulateNoInputs = false; function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } @@ -42,7 +49,13 @@ function FTUEPageLeave() {} function FTUECancel() {} function FTUEGetMusicProfileName() { - return "FTUEAttempt-1" + + if(simulateNoInputs) { + return "System Default (Playback Only)" + } + else { + return "FTUEAttempt-1" + } } function FTUESetMusicProfileName() { @@ -293,6 +306,10 @@ return false; } + function FTUECreateUpdatePlayBackProfile() { + return true; + } + function RegisterVolChangeCallBack(functionName) { dbg('RegisterVolChangeCallBack'); } @@ -419,6 +436,14 @@ } // Session Functions + + function SessionCurrrentJamTrackPlayPosMs() { + return 0; + } + + function SessionGetJamTracksPlayDurationMs() { + return 60000; + } function SessionAddTrack() {} function SessionAudioResync() { @@ -431,30 +456,56 @@ } function SessionGetControlState(mixerIds, isMasterOrPersonal) { dbg("SessionGetControlState"); - var groups = [0, 1, 2, 3, 7, 9]; + var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12]; var names = [ "FW AP Multi", "FW AP Multi", "FW AP Multi", "FW AP Multi", "", - "" + "", + "", + "", + "", + "" ]; + + var media_types = [ + "Master", + "Monitor", + "AudioInputMusic", + "AudioInputChat", + "StreamOutMusic", + "UserMusicInput", + "PeerAudioInputMusic", + "PeerMediaTrack", + "JamTrack", + "MetronomeTrack" + ] var clientIds = [ "", "", "", "", "3933ebec-913b-43ab-a4d3-f21dc5f8955b", + "", + "", + "", + "", "" ]; var response = []; for (var i=0; i 0.5 + } + } + + function getPeerState(clientId) { + return { + ntp_stable: Math.random() > 0.5 + } + } + + // stun function NetworkTestResult() { return {remote_udp_blocked: false} } @@ -750,6 +860,14 @@ fire(); } + + function getBackingTrackList() { + return {backing_tracks: [ + {name:"This is a really long name for a song dude.mp3", size:4283}, + {name:"foo.mp3",size:325783838} + ]}; + } + function ClientUpdateStartUpdate(path, successCallback, failureCallback) {} // ------------------------------- @@ -829,7 +947,11 @@ function OpenRecording(claimedRecording) { return {success: true} } + function PreviewRecording(claimedRecording) { + return OpenRecording(claimedRecording); + } function CloseRecording() {} + function ClosePreviewRecording() {CloseRecording();} function OnDownloadAvailable() {} function SaveToClipboard(text) {} function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ return false; } @@ -946,9 +1068,12 @@ this.FTUELoadAudioConfiguration = FTUELoadAudioConfiguration; this.FTUEClearChannelAssignments = FTUEClearChannelAssignments; this.FTUEClearChatInput = FTUEClearChatInput; + this.FTUECreateUpdatePlayBackProfile = FTUECreateUpdatePlayBackProfile; // Session this.SessionAddTrack = SessionAddTrack; + this.SessionCurrrentJamTrackPlayPosMs = SessionCurrrentJamTrackPlayPosMs; + this.SessionGetJamTracksPlayDurationMs = SessionGetJamTracksPlayDurationMs; this.SessionGetControlState = SessionGetControlState; this.SessionGetAllControlState = SessionGetAllControlState; this.SessionSetUserName = SessionSetUserName; @@ -1006,10 +1131,31 @@ this.TrackGetChatUsesMusic = TrackGetChatUsesMusic; this.TrackSetChatUsesMusic = TrackSetChatUsesMusic; + this.JamTrackStopPlay = JamTrackStopPlay; + this.JamTrackPlay = JamTrackPlay; + this.JamTrackIsPlayable = JamTrackIsPlayable; + this.JamTrackGetTrackDetail = JamTrackGetTrackDetail; + this.JamTrackKeysRequest = JamTrackKeysRequest; + this.JamTrackDownload = JamTrackDownload; + // Scoring Knobs this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval; this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval; + // Backing tracks: + this.getBackingTrackList = getBackingTrackList; + this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile; + this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile; + this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop; + + // Metronome: + this.SessionCloseMetronome = SessionCloseMetronome; + this.SessionOpenMetronome = SessionOpenMetronome; + this.SessionSetMetronome = SessionSetMetronome; + this.setMetronomeOpenCallback = setMetronomeOpenCallback; + this.getMyNetworkState = getMyNetworkState; + this.getPeerState = getPeerState; + // Client Update this.IsAppInWritableVolume = IsAppInWritableVolume; this.ClientUpdateVersion = ClientUpdateVersion; @@ -1033,6 +1179,8 @@ this.GetLocalRecordingState = GetLocalRecordingState; this.OpenRecording = OpenRecording; this.CloseRecording = CloseRecording; + this.PreviewRecording = PreviewRecording; + this.ClosePreviewRecording = ClosePreviewRecording; this.OnDownloadAvailable = OnDownloadAvailable; // Video functionality: diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index f54feb866..d3c4638c2 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -57,9 +57,11 @@ }) .fail(function(xhr, textStatus, errorMessage) { if (xhr.status === 404) { + logger.warn("unable to list active sessions (404)") // swallow 404 } else { + logger.warn("unable to list active sessions") app.ajaxError(xhr, textStatus, errorMessage); } }) diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index 4fe5e565d..8e51a081e 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -6,6 +6,7 @@ "use strict"; context.JK = context.JK || {}; + var rest = context.JK.Rest(); var logger = context.JK.logger; // types @@ -130,6 +131,20 @@ jkComment : 'jkComment' }; + // JamTrack categories and actions: + var jamTrackAvailabilityTypes = { + worldwide: 'JamTrackGlobal', + usa: 'JamTrackUSA' + } + var jamTrackActions = { + isPublic: 'PublicPerformance', + isPrivate: 'PrivateUse' + } + var jamTrackSessionLabels = { + nonSession: 'NonSession', + inSession: 'InSession' + } + function translatePlatformForGA(platform) { assertOneOf(platform, context.JK.OS); @@ -239,6 +254,36 @@ context.ga('send', 'event', categories.findSession, sessionCountRollup, numSessionsFound); } + function trackJamTrackPlaySession(sessionId, inSession) { + rest.getSession(sessionId).done(function(session) { + if (session && session.jam_track ) { + rest.getJamTracks({id:session.jam_track.id}).done(function(response) { + if (response.jamtracks && response.jamtracks.length!=0) { + var jamtrack = response.jamtracks[0] + trackJamTrackPlay( + jamtrack.sales_region!=context.JK.AVAILABILITY_US, + session.participants.length > 1, + inSession); + }// if + })// rest.getJamTracks + }// if + })// rest.getSession + } + + function trackJamTrackPlay(isGlobal, isPublic, inSession) { + assertBoolean(isGlobal) + assertBoolean(isPublic) + assertBoolean(inSession) + context.ga( + 'send', + 'event', + (isGlobal) ? jamTrackAvailabilityTypes.worldwide : jamTrackAvailabilityTypes.usa, + (isPublic) ? jamTrackActions.isPublic : jamTrackActions.isPrivate, + (inSession) ? jamTrackSessionLabels.inSession : jamTrackSessionLabels.nonSession + ) + logger.debug("Tracked Jam Track Play") + } + // if you want to pass in no title, either omit it from the arg list when u invoke virtualPageView, or pass in undefined, NOT null function virtualPageView(page, title) { @@ -271,7 +316,8 @@ } // when someone plays a recording - function trackRecordingPlay(recordingAction) { + function trackRecordingPlay(recording, recordingAction) { + if (!recordingAction) { recordingAction = _defaultPlayAction(); } @@ -279,10 +325,20 @@ var label = JK.currentUserId ? userLabels.registeredUser : userLabels.visitor; context.ga('send', 'event', categories.recordingPlay, recordingAction, label); + + if (recording.jam_track) { + rest.getJamTracks({id:recording.jam_track_id}).done(function(response) { + if (response.jamtracks && response.jamtracks.length==1) { + var jamtrack = response.jamtracks[0] + trackJamTrackPlay(jamtrack.sales_region!=context.JK.AVAILABILITY_US, recording.fan_access, false); + } + }) + } } // when someone plays a live session broadcast - function trackSessionPlay(recordingAction) { + function trackSessionPlay(session, recordingAction) { + logger.debug("Tracking session play: ", session) if (!recordingAction) { recordingAction = _defaultPlayAction(); } @@ -379,7 +435,8 @@ GA.trackSessionQuality = trackSessionQuality; GA.trackServiceInvitations = trackServiceInvitations; GA.trackFindSessions = trackFindSessions; - GA.virtualPageView = virtualPageView; + GA.trackJamTrackPlay = trackJamTrackPlay; + GA.trackJamTrackPlaySession = trackJamTrackPlaySession; GA.trackFriendConnect = trackFriendConnect; GA.trackMakeRecording = trackMakeRecording; GA.trackShareRecording = trackShareRecording; @@ -387,8 +444,8 @@ GA.trackSessionPlay = trackSessionPlay; GA.trackBand = trackBand; GA.trackJKSocial = trackJKSocial; - - + GA.virtualPageView = virtualPageView; + context.JK.GA = GA; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 584eba17b..175a96fd6 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -27,6 +27,9 @@ CHAT: "1" }; + context.JK.AVAILABILITY_US = "United States"; + context.JK.MASTER_TRACK = "Master"; + context.JK.EVENTS = { DIALOG_CLOSED : 'dialog_closed', SHOW_SIGNUP : 'show_signup', @@ -45,9 +48,18 @@ SUBSCRIBE_NOTIFICATION: 'subscribe_notification', CONNECTION_UP: 'connection_up', CONNECTION_DOWN: 'connection_down', - SCREEN_CHANGED: 'screen_changed' + SCREEN_CHANGED: 'screen_changed', + JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state', + METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected', + CHECKOUT_SIGNED_IN: 'checkout_signed_in', + CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in' }; + context.JK.PLAYBACK_MONITOR_MODE = { + MEDIA_FILE: 'MEDIA_FILE', + JAMTRACK: 'JAMTRACK', + METRONOME: 'METRONOME' + } context.JK.ALERT_NAMES = { NO_EVENT : 0, BACKEND_ERROR : 1, //generic error - eg P2P message error diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js index 997927dbd..ec7b86d50 100644 --- a/web/app/assets/javascripts/homeScreen.js +++ b/web/app/assets/javascripts/homeScreen.js @@ -6,6 +6,7 @@ context.JK.HomeScreen = function(app) { var logger = context.JK.logger; var isFtueComplete = false; + var $screen = null; function beforeShow(data) { } @@ -86,9 +87,20 @@ var screenBindings = { 'beforeShow': beforeShow }; app.bindScreen('home', screenBindings); events(); + $screen = $('.screen[layout-id="home"]') - $('.profile').on('click', function() { + $screen.find('.profile').on('click', function() { + var $destination = $('[layout-id="profile"]'); + if(!context.JK.currentUserId && !$destination.is('.no-login-required')) { + // if there is no user and login is required, then stop user from clicknig through + app.layout.showDialog('login-required-dialog') + } + else + { context.location = '/client#/profile/' + context.JK.currentUserId; + } + + }); }; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 29b5fe284..4983e6410 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -287,11 +287,6 @@ } function addPlayablePlay(playableId, playableType, claimedRecordingId, userId) { - if (playableType == 'JamRuby::Recording') { - context.JK.GA.trackRecordingPlay(); - } else if (playableType == 'JamRuby::MusicSession') { - context.JK.GA.trackSessionPlay(); - } return $.ajax({ url: '/api/users/' + playableId + "/plays", type: "POST", @@ -473,7 +468,7 @@ processData: false, contentType: 'application/json', data: JSON.stringify(options) - }); + }) } function getUserDetail(options) { @@ -1043,6 +1038,18 @@ }) } + function markRecordedBackingTrackSilent(options) { + var recordingId = options["recording_id"]; + var trackId = options["backing_track_id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + data: {}, + url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + '/silent' + }); + } function getRecordedTrack(options) { var recordingId = options["recording_id"]; var trackId = options["track_id"]; @@ -1055,6 +1062,18 @@ }); } + function getRecordedBackingTrack(options) { + var recordingId = options["recording_id"]; + var trackId = options["track_id"]; + + return $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + }); + } + function getRecording(options) { var recordingId = options["id"]; @@ -1157,6 +1176,32 @@ }) } + function openBackingTrack(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/backing_tracks/open", + data: JSON.stringify(options) + }) + } + + function closeBackingTrack(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/backing_tracks/close", + data: JSON.stringify(options) + }) + } + function openJamTrack(options) { var musicSessionId = options["id"]; var jamTrackId = options["jam_track_id"]; @@ -1172,6 +1217,15 @@ }) } + function playJamTrack(jamTrackId) { + return $.ajax({ + type: "POST", + url: '/api/jamtracks/played/' + jamTrackId, + dataType: "json", + contentType: 'application/json' + }); + } + function closeJamTrack(options) { var musicSessionId = options["id"]; delete options["id"]; @@ -1185,6 +1239,32 @@ }) } + function openMetronome(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/metronome/open", + data: JSON.stringify(options) + }) + } + + function closeMetronome(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/metronome/close", + data: JSON.stringify(options) + }) + } + function discardRecording(options) { var recordingId = options["id"]; @@ -1349,7 +1429,26 @@ }); } - function getJamtracks(options) { + function getJamTrack(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/' + options['plan_code'] + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getJamTrackWithArtistInfo(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/band/' + options['plan_code'] + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getJamTracks(options) { + logger.debug("GETTING JAMTRACKS") return $.ajax({ type: "GET", url: '/api/jamtracks?' + $.param(options), @@ -1358,6 +1457,28 @@ }); } + function getJamTrackRight(options) { + var jamTrackId = options['id']; + + return $.ajax({ + type: "GET", + url: '/api/jamtracks/rights/' + jamTrackId + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }) + } + + function enqueueJamTrack(options) { + var jamTrackId = options['id']; + + return $.ajax({ + type: "POST", + url: '/api/jamtracks/enqueue/' + jamTrackId + '?' + $.param(options), + dataType: "json", + contentType: 'applications/json' + }); + } + function getPurchasedJamTracks(options) { return $.ajax({ type: "GET", @@ -1367,6 +1488,33 @@ }); } + function getPaymentHistory(options) { + return $.ajax({ + type: "GET", + url: '/api/recurly/payment_history', + dataType: "json", + contentType: 'application/json' + }); + } + + function getSalesHistory(options) { + return $.ajax({ + type: "GET", + url: '/api/sales?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getBackingTracks(options) { + return $.ajax({ + type: "GET", + url: '/api/backing_tracks?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function addJamtrackToShoppingCart(options) { return $.ajax({ type: "POST", @@ -1440,10 +1588,10 @@ }); } - function placeOrder(options) { + function placeOrder() { return $.ajax({ type: "POST", - url: '/api/recurly/place_order?' + $.param(options), + url: '/api/recurly/place_order', dataType: "json", contentType: 'application/json' }); @@ -1478,7 +1626,35 @@ }); } - function initialize() { + function validateUrlSite(url, sitetype) { + return $.ajax({ + type: "GET", + url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url), + contentType: 'application/json' + }); + } + + function addRecordingTimeline(recordingId, data) { + return $.ajax({ + type: "POST", + url: '/api/recordings/' + recordingId + '/timeline', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + + function createSignupHint(data) { + return $.ajax({ + type: "POST", + url: '/api/signup_hints', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + + function initialize() { return self; } @@ -1555,6 +1731,7 @@ this.stopRecording = stopRecording; this.getRecording = getRecording; this.getRecordedTrack = getRecordedTrack; + this.getRecordedBackingTrack = getRecordedBackingTrack; this.getClaimedRecordings = getClaimedRecordings; this.getClaimedRecording = getClaimedRecording; this.updateClaimedRecording = updateClaimedRecording; @@ -1563,8 +1740,13 @@ this.claimRecording = claimRecording; this.startPlayClaimedRecording = startPlayClaimedRecording; this.stopPlayClaimedRecording = stopPlayClaimedRecording; - this.openJamTrack = openJamTrack; + this.openJamTrack = openJamTrack + this.openBackingTrack = openBackingTrack + this.closeBackingTrack = closeBackingTrack + this.closeMetronome = closeMetronome; this.closeJamTrack = closeJamTrack; + this.openMetronome = openMetronome; + this.closeMetronome = closeMetronome; this.discardRecording = discardRecording; this.putTrackSyncChange = putTrackSyncChange; this.createBand = createBand; @@ -1593,8 +1775,15 @@ this.createDiagnostic = createDiagnostic; this.getLatencyTester = getLatencyTester; this.updateAudioLatency = updateAudioLatency; - this.getJamtracks = getJamtracks; + this.getJamTrack = getJamTrack; + this.getJamTrackWithArtistInfo = getJamTrackWithArtistInfo; + this.getJamTracks = getJamTracks; this.getPurchasedJamTracks = getPurchasedJamTracks; + this.getPaymentHistory = getPaymentHistory; + this.getSalesHistory = getSalesHistory; + this.getJamTrackRight = getJamTrackRight; + this.enqueueJamTrack = enqueueJamTrack; + this.getBackingTracks = getBackingTracks; this.addJamtrackToShoppingCart = addJamtrackToShoppingCart; this.getShoppingCarts = getShoppingCarts; this.removeShoppingCart = removeShoppingCart; @@ -1608,6 +1797,11 @@ this.resendBandInvitation = resendBandInvitation; this.getMount = getMount; this.createSourceChange = createSourceChange; + this.validateUrlSite = validateUrlSite; + this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent; + this.addRecordingTimeline = addRecordingTimeline; + this.playJamTrack = playJamTrack; + this.createSignupHint = createSignupHint; return this; }; diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee new file mode 100644 index 000000000..926a8d824 --- /dev/null +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -0,0 +1,125 @@ +$ = jQuery +context = window +context.JK ||= {}; + + +context.JK.JamTrackPreview = {} +context.JK.JamTrackPreview = class JamTrackPreview + constructor: (app, $root, jamTrack, jamTrackTrack, options) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + @options = options || {master_shows_duration: false} + @app = app + @jamTrack = jamTrack + @jamTrackTrack = jamTrackTrack + @root = $root + @playButton = null + @stopButton = null + @instrumentIcon = null + @instrumentName = null + @part = null + + template = $('#template-jam-track-preview') + throw "no jam track preview template" if not template.exists() + + @root.html($(template.html())) + @playButton = @root.find('.play-button') + @stopButton = @root.find('.stop-button') + @instrumentIcon = @root.find('.instrument-icon') + @instrumentName = @root.find('.instrument-name') + @part = @root.find('.part') + + @playButton.on('click', @play) + @stopButton.on('click', @stop) + + @root.attr('data-track-type', @jamTrackTrack.track_type).attr('data-id', @jamTrackTrack.id) + instrumentId = null + instrumentDescription = '?' + if @jamTrackTrack.track_type == 'Track' + if @jamTrackTrack.instrument + instrumentId = @jamTrackTrack.instrument.id + instrumentDescription = @jamTrackTrack.instrument.description + else + instrumentId = 'other' + instrumentDescription= 'Master Mix' + + instrument_src = context.JK.getInstrumentIcon24(instrumentId) + + @instrumentIcon.attr('data-instrument-id', instrumentId).attr('src', instrument_src) + @instrumentName.text(instrumentDescription) + #context.JK.bindInstrumentHover(@root) + + part = '' + + if @jamTrackTrack.track_type == 'Track' + part = "#{@jamTrackTrack.part}" if @jamTrackTrack.part? && @jamTrackTrack.part != instrumentDescription + + else + if @options.master_shows_duration + duration = 'entire song' + if @jamTrack.duration + duration = "0:00 - #{context.JK.prettyPrintSeconds(@jamTrack.duration)}" + part = duration + else + part = @jamTrack.name + ' by ' + @jamTrack.original_artist + + @part.text("(#{part})") if part != '' + + if @jamTrackTrack.preview_mp3_url? + + urls = [@jamTrackTrack.preview_mp3_url] + if @jamTrackTrack.preview_ogg_url? + urls.push(@jamTrackTrack.preview_ogg_url) + + @no_audio = false + @sound = new Howl({ + src: urls, + autoplay: false, + loop: false, + volume: 1.0, + onend: @onHowlerEnd}) + else + @no_audio = true + + if @no_audio + @playButton.addClass('disabled') + @stopButton.addClass('disabled') + + onHowlerEnd: () => + @logger.debug("on end $(this)", $(this)) + @stopButton.addClass('hidden') + @playButton.removeClass('hidden') + + play: (e) => + if e? + e.stopPropagation() + + if @no_audio + context.JK.prodBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + logger.debug("play issued for jam track preview") + @sound.play() + @playButton.addClass('hidden') + @stopButton.removeClass('hidden') + + return false + + stop: (e) => + if e? + e.stopPropagation() + + if @no_audio + context.JK.helpBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + logger.debug("stop issued for jam track preview") + @sound.stop() + @stopButton.addClass('hidden') + @playButton.removeClass('hidden') + + return false + + + + + diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee new file mode 100644 index 000000000..a0a2d4d83 --- /dev/null +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -0,0 +1,273 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.JamTrackScreen=class JamTrackScreen + LIMIT = 10 + instrument_logo_map = context.JK.getInstrumentIconMap24() + + constructor: (@app) -> + @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 + + beforeShow:(data) => + this.setFilterFromURL() + this.refresh() + + afterShow:(data) => + + 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 + + setFilterFromURL:() => + # Grab parms from URL for artist, instrument, and availability + parms=this.getParams() + + if(parms.artist?) + @artist.val(parms.artist) + if(parms.instrument?) + @instrument.val(parms.instrument) + if(parms.availability?) + @availability.val(parms.availability) + 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 + + refresh:() => + @currentQuery = this.buildQuery() + that = this + rest.getJamTracks(@currentQuery).done((response) => + that.clearResults() + that.handleJamtrackResponse(response) + ).fail (jqXHR) => + that.clearResults() + that.noMoreJamtracks.show() + that.app.notifyServerError jqXHR, 'Jamtrack Unavailable' + + 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 '
No JamTracks found.
' + if @currentPage > 0 + @noMoreJamtracks.show() + # there are bugs with infinitescroll not removing the 'loading'. + # it's most noticeable at the end of the list, so whack all such entries + $('.infinite-scroll-loader').remove() + else + @currentPage++ + this.buildQuery() + this.registerInfiniteScroll() + + + registerInfiniteScroll:() => + that = this + @scroller.infinitescroll { + behavior: 'local' + navSelector: '#jamtrackScreen .btn-next-pager' + nextSelector: '#jamtrackScreen .btn-next-pager' + binder: @scroller + dataType: 'json' + appendCallback: false + prefill: false + bufferPx: 100 + loading: + msg: $('
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() + params = id: $(e.target).attr('data-jamtrack-id') + rest.addJamtrackToShoppingCart(params).done((response) => + context.location = '/client#/shoppingCart' + ).fail @app.ajaxError + + licenseUSWhy:(e) => + e.preventDefault() + @app.layout.showDialog 'jamtrack-availability-dialog' + + registerEvents:() => + #@screen.find('.jamtrack-detail-btn').on 'click', this.showJamtrackDescription + @screen.find('.play-button').on 'click', this.playJamtrack + @screen.find('.jamtrack-add-cart').on 'click', this.addToCartJamtrack + @screen.find('.license-us-why').on 'click', this.licenseUSWhy + @screen.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 + + 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 = [] + for track in jamtrack.tracks + if track.track_type != 'Master' + 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 + ')' + + options = + jamtrack: trackRow + expanded: jamtrackExpanded + @jamtrackItem = $(context._.template($('#template-jamtrack').html(), options, variable: 'data')) + that.renderJamtrack(@jamtrackItem, jamtrack) + + this.registerEvents() + + renderJamtrack:(jamtrackElement, jamTrack) => + @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") + new JK.JamTrackPreview(@app, previewElement, jamTrack, track, {master_shows_duration: 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") + if this.expanded==jamTrackId + this.expanded = null + else + this.expanded = jamTrackId + this.rerenderJamtracks() + + initialize:() => + screenBindings = + 'beforeShow': this.beforeShow + 'afterShow': this.afterShow + @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/jam_track_utils.js.coffee b/web/app/assets/javascripts/jam_track_utils.js.coffee new file mode 100644 index 000000000..d3da3180a --- /dev/null +++ b/web/app/assets/javascripts/jam_track_utils.js.coffee @@ -0,0 +1,34 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class JamTrackUtils + constructor: () -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + + init: () => + + # check if the shopping cart should be shown + checkShoppingCart: () => + @rest.getShoppingCarts().done(this.displayCartIcon) + + displayCartIcon: (carts) => + cartLink = $("#header-shopping-cart") + if carts.length > 0 + cartLink.removeClass("hidden") + else + cartLink.addClass("hidden") + + compareAddress: (billing, shipping) => + billing.address1 == shipping.address1 && + billing.address2 == shipping.address2 && + billing.zip == shipping.zip && + billing.city == shipping.city && + billing.country == shipping.country; + + +# global instance +context.JK.JamTrackUtils = new JamTrackUtils() \ No newline at end of file diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index db0615510..2bc31b957 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -132,6 +132,9 @@ logger.error("Unexpected ajax error: " + textStatus + ", msg:" + errorMessage); app.notify({title: "Oops!", text: "What you were looking for is gone now."}); } + else if(jqXHR.status === 403) { + logger.debug("not logged in"); + } else if (jqXHR.status === 422) { logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText); // present a nicer message @@ -282,20 +285,37 @@ var hash = context.location.hash; + var screen = 'home' try { - context.RouteMap.parse(hash); + var location = context.RouteMap.parse(hash); + screen = location.page.substring(1); // remove leading slash } catch (e) { logger.debug("ignoring bogus screen name: %o", hash) hash = null; } - var url = '/client#/home'; + + var $destination = $('[layout-id="' + screen + '"]'); + + if(!context.JK.currentUserId && !$destination.is('.no-login-required')) { + logger.debug("not logged in so redirected to login from screen: " + screen) + var redirectPath= '?redirect-to=' + encodeURIComponent(JK.locationPath()); + if(gon.isNativeClient) { + window.location.href = '/signin' + redirectPath; + } + else { + window.location.href = '/' + redirectPath; + } + return; + } + + var url = '/client#/' + screen; if (hash) { url = hash; } - logger.debug("Changing screen to " + url); + logger.debug("jamkazam: Changing screen to " + url + " (hash=" + hash + ")") ; context.location = url; } @@ -377,7 +397,11 @@ app.notify({title: "Unable to Load User", text: "You should reload the page."}) }); } - } // if userDeferred + } + else { + userDeferred = new $.Deferred(); + userDeferred.reject('not_logged_in'); + } $(document).triggerHandler('JAMKAZAM_READY', {app:app}) diff --git a/web/app/assets/javascripts/jamtrack.js b/web/app/assets/javascripts/jamtrack.js deleted file mode 100644 index a0771762a..000000000 --- a/web/app/assets/javascripts/jamtrack.js +++ /dev/null @@ -1,258 +0,0 @@ -(function(context,$) { - - "use strict"; - context.JK = context.JK || {}; - context.JK.JamTrackScreen = function(app) { - - var logger = context.JK.logger; - - var $screen = null; - var $content = null; - var $scroller = null; - var $genre = null; - var $instrument = null; - var $availability = null; - var $nextPager = null; - var $noMoreJamtracks = null; - - var currentQuery = defaultQuery(); - var currentPage = 0; - var LIMIT = 10; - var next = null; - var instrument_logo_map = context.JK.getInstrumentIconMap24(); - - function beforeShow(data) { - refresh(); - } - - function afterShow(data) { - } - - function events() { - $genre.on("change", search); - $instrument.on("change", search); - $availability.on("change", search); - } - - function clearResults() { - //logger.debug("CLEARING CONTENT") - currentPage = 0; - $content.empty(); - $noMoreJamtracks.hide(); - next = null; - } - - function refresh() { - currentQuery = buildQuery(); - rest.getJamtracks(currentQuery) - .done(function(response) { - clearResults(); - handleJamtrackResponse(response); - }) - .fail(function(jqXHR) { - clearResults(); - $noMoreJamtracks.show(); - app.notifyServerError(jqXHR, 'Jamtrack Unavailable') - }) - } - - function search() { - logger.debug("Searching for jamtracks..."); - refresh(); - return false; - } - - function defaultQuery() { - var query = { limit:LIMIT, page:currentPage}; - - if(next) { - query.since = next; - } - - return query; - } - - function buildQuery() { - currentQuery = defaultQuery(); - - // genre filter - var genres = $screen.find('#jamtrack_genre').val(); - if (genres !== undefined) { - currentQuery.genre = genres; - } - - // instrument filter - var instrument = $instrument.val(); - if (instrument !== undefined) { - currentQuery.instrument = instrument; - } - - // availability filter - var availability = $availability.val(); - if (availability !== undefined) { - currentQuery.availability = availability; - } - - return currentQuery; - } - - function handleJamtrackResponse(response) { - //logger.debug("Handling response", JSON.stringify(response)) - next = response.next; - - renderJamtracks(response); - - if(response.next == null) { - // if we less results than asked for, end searching - $scroller.infinitescroll('pause'); - logger.debug("end of jamtracks"); - - if(currentPage == 0 && response.jamtracks.length == 0) { - $content.append("
There's no jamtracks.
") ; - } - - if(currentPage > 0) { - $noMoreJamtracks.show(); - // there are bugs with infinitescroll not removing the 'loading'. - // it's most noticeable at the end of the list, so whack all such entries - $('.infinite-scroll-loader').remove(); - } - } - else { - currentPage++; - buildQuery(); - registerInfiniteScroll(); - } - } - - function registerInfiniteScroll() { - $scroller.infinitescroll({ - behavior: 'local', - navSelector: '#jamtrackScreen .btn-next-pager', - nextSelector: '#jamtrackScreen .btn-next-pager', - binder: $scroller, - dataType: 'json', - appendCallback: false, - prefill: false, - bufferPx: 100, - loading: { - msg: $('
Loading ...
'), - img: '/assets/shared/spinner.gif' - }, - path: function(page) { - return '/api/jamtracks?' + $.param(buildQuery()); - } - },function(json, opts) { - handleJamtrackResponse(json); - }); - $scroller.infinitescroll('resume'); - } - - function playJamtrack(e) { - e.preventDefault(); - } - - function addToCartJamtrack(e) { - e.preventDefault(); - - var params = {id: $(e.target).attr("data-jamtrack-id")}; - rest.addJamtrackToShoppingCart(params) - .done(function(response) { - context.location = "/client#/shoppingCart"; - }) - .fail(app.ajaxError); - } - - function licenseUSWhy(e) { - e.preventDefault(); - - app.layout.showDialog('jamtrack-availability-dialog'); - } - - function registerEvents() { - $screen.find('.jamtrack-detail-btn').on("click", showJamtrackDescription); - $screen.find('.play-button').on('click', playJamtrack); - $screen.find('.jamtrack-add-cart').on('click', addToCartJamtrack); - $screen.find('.license-us-why').on('click', licenseUSWhy); - } - - function renderJamtracks(data) { - $.each(data.jamtracks, function(i, jamtrack) { - $.each(jamtrack.tracks, function (index, track) { - var inst = '../assets/content/icon_instrument_default24.png'; - if (track.instrument.id in instrument_logo_map) { - inst = instrument_logo_map[track.instrument.id].asset; - } - track.instrument_url = inst; - - track.instrument_desc = track.instrument.description; - if (track.part != "") { - track.instrument_desc += " ( " + track.part + " )"; - } - }); - - var options = { - jamtrack: jamtrack - }; - - var $jamtrackItem = $( - context._.template( - $('#template-jamtrack').html(), - options, - {variable: 'data'} - ) - ); - renderJamtrack($jamtrackItem ); - }); - - registerEvents(); - } - - function showJamtrackDescription(e) { - e.preventDefault(); - - var $description = $(e.target).parent(".detail-arrow").next(); - if ($description.css("display") == "none") { - $description.show(); - } - else { - $description.hide(); - } - } - - function renderJamtrack(jamtrack) { - $content.append(jamtrack); - } - - function initialize() { - var screenBindings = { - 'beforeShow': beforeShow, - 'afterShow': afterShow - }; - app.bindScreen('jamtrack', screenBindings); - - $screen = $("#jamtrack-find-form"); - $scroller = $screen.find('.content-body-scroller'); - $content = $screen.find(".jamtrack-content"); - $genre = $screen.find("#jamtrack_genre"); - $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 "$screen must be specified"; - if($scroller.length == 0) throw "$scroller must be specified"; - if($content.length == 0) throw "$content must be specified"; - if($noMoreJamtracks.length == 0) throw "$noMoreJamtracks must be specified"; - if($genre.length == 0) throw "$genre must be specified"; - if($instrument.length == 0) throw "$instrument must be specified"; - if($availability.length ==0) throw "$availability must be specified"; - - events(); - } - - this.initialize = initialize; - - return this; - } -})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jamtrack_landing.js.coffee b/web/app/assets/javascripts/jamtrack_landing.js.coffee new file mode 100644 index 000000000..57534d7f1 --- /dev/null +++ b/web/app/assets/javascripts/jamtrack_landing.js.coffee @@ -0,0 +1,53 @@ +$ = 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 + + initialize:() => + screenBindings = + 'beforeShow': @beforeShow + 'afterShow': @afterShow + @app.bindScreen('jamtrackLanding', screenBindings) + @screen = $('#jamtrackLanding') + + beforeShow:() => + # Get artist names and build links + @rest.getJamTracks({group_artist: true}) + .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() + afterShow:() => + + buildArtistLinks:(response) => + # Get artist names and build links + jamtracks = response.jamtracks + $("#band_list>li:not('#no_bands_found')").remove() + if jamtracks.length==0 + $("#no_bands_found").removeClass("hidden") + else + $("#no_bands_found").addClass("hidden") + + # client#/jamtrack + for jamtrack in jamtracks + artistLink = "#{jamtrack.original_artist}" + $("#band_list").append("
  • #{artistLink}
  • ") + + # We don't want to do a full page load if this is clicked on here: + bindArtistLinks:() => + band_list=$("ul#band_list") + that=this + band_list.on "click", "a.artist-link", (event)-> + context.location="client#/jamtrackBrowse" + window.history.replaceState({}, "", this.href) + event.preventDefault() + + handleFailure:(error) => diff --git a/web/app/assets/javascripts/jquery.listenRecording.js b/web/app/assets/javascripts/jquery.listenRecording.js index 84a7628f7..1303f50f7 100644 --- a/web/app/assets/javascripts/jquery.listenRecording.js +++ b/web/app/assets/javascripts/jquery.listenRecording.js @@ -78,6 +78,10 @@ audioDomElement.play(); isPlaying = true; rest.addPlayablePlay(recordingId, 'JamRuby::Recording', claimedRecordingId, context.JK.currentUserId); + rest.getRecording({id: recordingId}) + .done(function(recording) { + context.JK.GA.trackRecordingPlay(recording); + }) }) } diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js index 73837da8f..89c5184dd 100644 --- a/web/app/assets/javascripts/jquery.listenbroadcast.js +++ b/web/app/assets/javascripts/jquery.listenbroadcast.js @@ -74,6 +74,9 @@ var playState = PlayStateNone; // tracks if the stream is actually playing + var CANNOT_BROADCAST_TITLE = 'Unable to Broadcast Session'; + var CANNOT_BROADCAST_MSG = 'This session cannot be broadcasted. The session organizer may have configured it to be private.'; + function play(e) { if(e) { e.preventDefault(); @@ -96,28 +99,33 @@ destroy(); } else { - recreateAudioElement(); + try { + recreateAudioElement(); - audioDomElement.load(); + audioDomElement.load(); - retryAttempts = 0; + retryAttempts = 0; - transition(PlayStateInitializing); + transition(PlayStateInitializing); - // keep this after transition, because any transition clears this timer - waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); - logger.debug("setting buffering timeout"); - rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId); - - if(needsCanPlayGuard()) { - $audio.bind('canplay', function() { + // keep this after transition, because any transition clears this timer + waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + logger.debug("setting buffering timeout"); + rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId); + context.JK.GA.trackJamTrackPlaySession(musicSessionId, false) + + if(needsCanPlayGuard()) { + $audio.bind('canplay', function() { + audioDomElement.play(); + }) + } + else { audioDomElement.play(); - }) + } } - else { - audioDomElement.play(); + catch (err) { + logger.log("Catching error = %o", err); } - } }) } @@ -137,7 +145,12 @@ transition(PlayStateNone); - recreateAudioElement(); + try { + recreateAudioElement(); + } + catch (err) { + logger.log("Catching error = %o", err); + } } function destroy() { @@ -145,7 +158,12 @@ //$audio.remove(); //$audio = null; //audioDomElement = null; - recreateAudioElement() + try { + recreateAudioElement(); + } + catch (err) { + logger.log("Catching error = %o", err); + } // destroyed = true; //} } @@ -157,14 +175,21 @@ } function createAudioElementHtml() { - if (sessionInfo == null) throw "no session info"; - if (sessionInfo.mount == null) throw "no session mount info"; + if (sessionInfo == null) { + logger.log("no session info"); + throw "no session info"; + } + + if (sessionInfo.mount == null) { + logger.log("no session mount info"); + throw "no session mount info"; + } $audio = $('') - $parent.append($audio) + ''); + $parent.append($audio); audioDomElement = $audio.get(0); audioBind(); @@ -256,24 +281,28 @@ // tell audio to stop/start, in attempt to retry //audioDomElement.pause(); - recreateAudioElement(); + try { + recreateAudioElement(); - audioDomElement.load(); - if(needsCanPlayGuard()) { - $audio.bind('canplay', function() { + audioDomElement.load(); + if(needsCanPlayGuard()) { + $audio.bind('canplay', function() { + audioDomElement.play(); + }) + } + else { audioDomElement.play(); - }) - } - else { - audioDomElement.play(); - } + } - transition(PlayStateRetrying); + transition(PlayStateRetrying); - waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + } + catch (err) { + logger.log("Catching error = %o", err); + } } - }) - + }); } } @@ -378,21 +407,25 @@ function checkServer() { return rest.getSession(musicSessionId) .done(function(response) { - console.log("assigning sessionInfo") + logger.log("assigning sessionInfo") sessionInfo = response; }) .fail(function(jqXHR) { - if(jqXHR.status == 404 || jqXHR.status == 403) { + if(jqXHR.status == 404) { transition(PlayStateSessionOver); destroy(); } + else if (jqXHR.status == 403) { + logger.debug("session is private"); + context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG}); + } else if(jqXHR.status >= 500 && jqXHR.status <= 599){ transition(PlayStateServerError); } else { transition(PlayStateNetworkError); } - }) + }); } function triggerStateChange() { @@ -485,7 +518,7 @@ if(!sessionInfo.mount) { transition(PlayStateSessionOver); destroy(); - }}) + }}); } } @@ -677,31 +710,31 @@ } function openBubble() { - checkServer().done(function(response) { + checkServer() + .done(function(response) { - var mountId = sessionInfo.mount ? sessionInfo.mount.id : null + var mountId = sessionInfo.mount ? sessionInfo.mount.id : null; - if(mountId) { - rest.getMount({id: mountId}) - .done(function (mount) { - mountInfo = mount - $parent.data('mount-id', mountId) - context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent) - $parent.btOn() - }) - .fail(context.JK.app.ajaxError) - } - else { - mountInfo = null; - - transition(PlayStateSessionOver); - destroy(); - - context.JK.app.layout.notify('This session can not currently broadcast') - } - }) - .fail(function() { - logger.debug("session is over") + if(mountId) { + rest.getMount({id: mountId}) + .done(function (mount) { + mountInfo = mount; + $parent.data('mount-id', mountId); + context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent); + $parent.btOn(); + }) + .fail(context.JK.app.ajaxError) + } + else { + mountInfo = null; + destroy(); + context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG}); + } + }) + .fail(function(response) { + if (response.status == 404) { + logger.debug("session is over"); + } }) } diff --git a/web/app/assets/javascripts/jquery.metronomePlaybackMode.js b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js new file mode 100644 index 000000000..720cea539 --- /dev/null +++ b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js @@ -0,0 +1,103 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + + // creates an iconic/graphical instrument selector. useful when there is minimal real-estate + + function setValue(val, $target) { + if(val == "cricket") { + $target.html("Play cluster test") + } + else { + $target.html("Play metronome") + } + } + + $.fn.metronomeSetPlaybackMode = function(val) { + return this.each(function (index) { + setValue(val, $(this)) + }); + }; + + $.fn.metronomePlaybackMode = function(options) { + + options = options || {mode: 'self'} + + return this.each(function(index) { + + function close() { + $parent.btOff(); + $parent.focus(); + } + + + var $parent = $(this); + var value = options.mode; + setValue(options.mode, $parent) + + function onModeSelected() { + var $li = $(this); + var playbackMode = $li.attr('data-playback-option'); + + if(playbackMode) { + value = playbackMode; + close(); + $parent.triggerHandler(context.JK.EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, {playbackMode: playbackMode}); + } + else { + // if no playback mode, then this must be an attempt to open metronome window + close(); + context.jamClient.SessionShowMetronomeGui(); + } + 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-metronome-playback-mode').html(), { + trigger:'click', + cssClass: 'metronome-playback-mode-selector-popup', + spikeGirth:0, + spikeLength:0, + width:180, + closeWhenOthersOpen: true, + offsetParent: $parent.offsetParent(), + positions:['top'], + preShow: function() { + $parent.find('.down-arrow').removeClass('down-arrow').addClass('up-arrow') + }, + postShow:function(container) { + $(container).find('li').click(onModeSelected) + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + //waitForBubbleHover($(container)) + //timeout = setTimeout(function() {$parent.btOff()}, 3000) + }, + postHide:function() { + setValue(value, $parent) + } + }); + }); + } + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/landing/init.js b/web/app/assets/javascripts/landing/init.js index bc2a5c696..ade6a14ec 100644 --- a/web/app/assets/javascripts/landing/init.js +++ b/web/app/assets/javascripts/landing/init.js @@ -3,7 +3,8 @@ "use strict"; $(function() { - context.JK.popExternalLinks(); + // commented out because JamKazam.js does this, and it's included everywhere that this file is + //scontext.JK.popExternalLinks(); }) })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 723e75650..ac49684e6 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -95,6 +95,8 @@ layoutHeader(width, height); layoutNotify(width, height); layoutFooter(width, height); + + $(document).triggerHandler('layout_resized'); } function layoutCurtain(screenWidth, screenHeight) { @@ -407,8 +409,15 @@ } var destination = $(evt.currentTarget).attr('layout-link'); - var destinationType = $('[layout-id="' + destination + '"]').attr("layout"); + var $destination = $('[layout-id="' + destination + '"]'); + + var destinationType = $destination.attr("layout"); if (destinationType === "screen") { + if(!context.JK.currentUserId && !$destination.is('.no-login-required')) { + // there is no user, and this item does not support 'no-login', so warn user + showDialog('login-required-dialog'); + return; + } context.location = '/client#/' + destination; } else if (destinationType === "dialog") { showDialog(destination); @@ -540,7 +549,7 @@ var accepted = screenEvent(previousScreen, 'beforeHide', data); if(accepted === false) return; - logger.debug("Changing screen to " + currentScreen); + logger.debug("layout: changing screen to " + currentScreen); $(document).triggerHandler(EVENTS.SCREEN_CHANGED, {previousScreen: previousScreen, newScreen: currentScreen}) @@ -687,6 +696,7 @@ return null; } logger.debug("opening dialog: " + dialog) + var $overlay = $('.dialog-overlay') if (opts.sizeOverlayToContent) { @@ -719,6 +729,12 @@ function panelHeaderClicked(evt) { evt.preventDefault(); + + if(!context.JK.currentUserId) { + showDialog('login-required-dialog'); + return false; + } + expandedPanel = $(evt.currentTarget).closest('[layout="panel"]').attr("layout-id"); layout(); return false; diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index c295f1123..61273a2db 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -26,6 +26,7 @@ var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout'; var $startNetworkTestBtn = null; + var $foreverNetworkTestBtn = null; var $testResults = null; var $testScore = null; var $testText = null; @@ -51,6 +52,8 @@ var operatingSystem = null; var PRIME_PUMP_TIME = 1; + var forever = false; + // these try to make it such that we only pass a NetworkTest Pass/Failed one time in a new user flow var trackedPass = false; var lastNetworkFailure = null; @@ -313,6 +316,9 @@ if (success) { $self.triggerHandler(NETWORK_TEST_DONE) + if(forever) { + prepareNetworkTest(); + } } else { $self.triggerHandler(NETWORK_TEST_FAIL) @@ -875,6 +881,7 @@ inGearWizard = _inGearWizard; $startNetworkTestBtn = $step.find('.start-network-test'); + $foreverNetworkTestBtn = $step.find('.forever-network-test') if ($startNetworkTestBtn.length == 0) throw 'no start network test button found in network-test' @@ -889,8 +896,16 @@ $subscore = $step.find('.subscore'); $watchVideo = $step.find('.watch-video'); $startNetworkTestBtn.on('click', function () { + forever = false; prepareNetworkTest(); }); + if(context.JK.currentUserAdmin) { + $foreverNetworkTestBtn.on('click', function() { + forever = true; + prepareNetworkTest(); + }).show(); + } + operatingSystem = context.JK.GetOSAsString(); initializeVideoWatchButton(); diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index fdf0ac3ee..c6e1f25bc 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -213,7 +213,7 @@ }) .fail(function() { isLoading = false; - app.ajaxError(); + app.ajaxError(arguments); }) } @@ -221,9 +221,9 @@ $.each(response, function(index, val) { // this means the session no longer exists - if (response.fan_access == null && response.musician_access == null) { - return; - } + //if (response.fan_access == null && response.musician_access == null) { + // return; + //} if(val.description == context.JK.MessageType.TEXT_MESSAGE) { val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); @@ -1375,7 +1375,9 @@ events(); - populate(); + app.user().done(function(){ + populate(); + }) }; this.initialize = initialize; diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js index fb741ad31..23360c626 100644 --- a/web/app/assets/javascripts/order.js +++ b/web/app/assets/javascripts/order.js @@ -4,9 +4,12 @@ context.JK = context.JK || {}; context.JK.OrderScreen = function(app) { + var EVENTS = context.JK.EVENTS; var logger = context.JK.logger; var $screen = null; + var $templateOrderContent = null; + var $templatePurchasedJamTrack = null; var $navigation = null; var $billingInfo = null; var $shippingInfo = null; @@ -16,21 +19,36 @@ var $paymentInfoPanel = null; var $orderPanel = null; var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; var $orderContent = null; var userDetail = null; var step = null; var billing_info = null; var shipping_info = null; var shipping_as_billing = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; function beforeShow() { - beforeShowPaymentInfo(); + beforeShowPaymentInfo(); + resetJamTrackDownloadInfo(); } function beforeShowPaymentInfo() { step = 2; renderNavigation(); renderAccountInfo(); + $("#order_error").addClass("hidden") + } + + function resetJamTrackDownloadInfo() { + $purchasedJamTrack.addClass('hidden'); + $purchasedJamTracks.children().remove() + $jamTrackInBrowser.hide('hidden'); } function renderAccountInfo() { @@ -81,8 +99,24 @@ } 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") @@ -276,10 +310,8 @@ $paymentMethod.find('#divCardName .error-text').remove(); $paymentMethod.find('#divCardName').addClass("error"); $paymentMethod.find('#card-name').after("
    • Card Name is required
    "); - return false; - } - else { + } else { $paymentMethod.find('#divCardName').removeClass("error"); } @@ -287,21 +319,37 @@ $paymentMethod.find('#divCardNumber .error-text').remove(); $paymentMethod.find('#divCardNumber').addClass("error"); $paymentMethod.find('#card-number').after("
    • Card Number is required
    "); - return false; - } - else { + } else if (!$.payment.validateCardNumber(card_number)) { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error"); + $paymentMethod.find('#card-number').after("
    • Card Number is not valid
    "); + return false; + } else { $paymentMethod.find('#divCardNumber').removeClass("error"); } + if (!$.payment.validateCardExpiry(card_month, card_year)) { + $paymentMethod.find('#divCardExpiry .error-text').remove(); + $paymentMethod.find('#divCardExpiry').addClass("error"); + $paymentMethod.find('#card-expiry').after("
    • Card Number is not valid
    "); + } else { + $paymentMethod.find('#divCardExpiry').removeClass("error"); + } + if (!card_verify) { $paymentMethod.find('#divCardVerify .error-text').remove(); $paymentMethod.find('#divCardVerify').addClass("error"); - $paymentMethod.find('#card_verify').after("
    • Card Verification Value is required
    "); + $paymentMethod.find('#card-verify').after("
    • Card Verification Value is required
    "); return false; - } - else { + } else if(!$.payment.validateCardCVC(card_verify)) { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error"); + $paymentMethod.find('#card-verify').after("
    • Card Verification Value is not valid.
    "); + + return false; + } else { $paymentMethod.find('#divCardVerify').removeClass("error"); } @@ -412,7 +460,7 @@ data.shipping_as_billing = shipping_as_billing var orderContentHtml = $( context._.template( - $('#template-order-content').html(), + $templateOrderContent.html(), data, {variable: 'data'} ) @@ -430,13 +478,87 @@ beforeShowOrder(); } - function moveToThanks() { + function moveToThanks(purchaseResponse) { $("#order_error").addClass("hidden") $paymentInfoPanel.addClass("hidden") $orderPanel.addClass("hidden") $thanksPanel.removeClass("hidden") rest.clearShoppingCart() beforeShowOrder() + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + } + } + + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.') + } } function moveToPaymentInfo(e) { @@ -501,26 +623,37 @@ radioClass: 'iradio_minimal', inheritClass: true }); + + // Use jquery.payment to limit characters and length: + $paymentMethod.find("#card-number").payment('formatCardNumber'); + $paymentMethod.find("#card-verify").payment('formatCardCVC'); } function initialize() { var screenBindings = { 'beforeShow': beforeShow, - 'afterShow': afterShow + 'afterShow': afterShow, + 'beforeHide' : beforeHide }; app.bindScreen('order', screenBindings); - $screen = $("#orderScreen"); - $paymentInfoPanel = $screen.find(".checkout-payment-info"); - $orderPanel = $screen.find(".order-panel"); - $thanksPanel = $screen.find(".thanks-panel"); - $navigation = $screen.find(".checkout-navigation-bar"); - $billingInfo = $paymentInfoPanel.find(".billing-address"); - $shippingInfo = $paymentInfoPanel.find(".shipping-address"); - $paymentMethod = $paymentInfoPanel.find(".payment-method"); - $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); - $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); - $orderContent = $orderPanel.find(".order-content"); + $screen = $("#orderScreen"); + $templateOrderContent = $("#template-order-content"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $paymentInfoPanel = $screen.find(".checkout-payment-info"); + $orderPanel = $screen.find(".order-panel"); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $navigation = $screen.find(".checkout-navigation-bar"); + $billingInfo = $paymentInfoPanel.find(".billing-address"); + $shippingInfo = $paymentInfoPanel.find(".shipping-address"); + $paymentMethod = $paymentInfoPanel.find(".payment-method"); + $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); + $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); + $orderContent = $orderPanel.find(".order-content"); if($screen.length == 0) throw "$screen must be specified"; if($navigation.length == 0) throw "$navigation must be specified"; diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index e05c185ae..cfbcf9c5b 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -25,6 +25,8 @@ logger.debug("no $parentElement specified in PlaybackControls"); } + var PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE; + var $playButton = $('.play-button img.playbutton', $parentElement); var $pauseButton = $('.play-button img.pausebutton', $parentElement); var $currentTime = $('.recording-current', $parentElement); @@ -32,6 +34,7 @@ var $sliderBar = $('.recording-playback', $parentElement); var $slider = $('.recording-slider', $parentElement); var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement); + var $jamTrackGetReady = $('.jam-track-get-ready', $parentElement); var $self = $(this); @@ -47,18 +50,25 @@ var canUpdateBackend = false; var playbackMode = PlaybackMode.EveryWhere; var monitorPlaybackTimeout = null; + var playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; function startPlay() { updateIsPlaying(true); if(endReached) { update(0, playbackDurationMs, playbackPlaying); } - $self.triggerHandler('play', {playbackMode: playbackMode}); + $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); + + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + var sessionModel = context.JK.CurrentSessionModel || null; + context.JK.GA.trackJamTrackPlaySession(sessionModel.id(), true) + } } - function stopPlay() { + function stopPlay(endReached) { updateIsPlaying(false); - $self.triggerHandler('pause'); + $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); } function updateOffsetBasedOnPosition(offsetLeft) { @@ -67,7 +77,7 @@ playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); updateCurrentTimeText(playbackPositionMs); if(canUpdateBackend) { - $self.triggerHandler('change-position', {positionMs: playbackPositionMs}); + $self.triggerHandler('change-position', {positionMs: playbackPositionMs, playbackMonitorMode: playbackMonitorMode}); canUpdateBackend = false; } } @@ -100,13 +110,25 @@ } $playButton.on('click', function(e) { - startPlay(); - return false; + var sessionModel = context.JK.CurrentSessionModel || null; + //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { + // context.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $playButton}) + // return false; + //} + + startPlay(); + return false; }); $pauseButton.on('click', function(e) { - stopPlay(); - return false; + var sessionModel = context.JK.CurrentSessionModel || null; + //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { + // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) + // return false; + //} + + stopPlay(); + return false; }); $sliderBar.on('click', function(e) { @@ -143,17 +165,67 @@ setPlaybackMode(playmode); }); - function monitorRecordingPlayback() { - var isPlaying = context.jamClient.isSessionTrackPlaying(); - var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); - var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + function styleControls() { + $jamTrackGetReady.attr('data-mode', playbackMonitorMode); + + $parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode'); + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) { + $parentElement.addClass('mediafile-mode'); + } + else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + $parentElement.addClass('jamtrack-mode'); + } + else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { + $parentElement.addClass('metronome-mode'); + } + else + { + throw "unknown playbackMonitorMode: " + playbackMonitorMode; + } + } + function monitorRecordingPlayback() { + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); + var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + var durationMs = duration.media_len; + var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins + } + else { + var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + } + + var isPlaying = context.jamClient.isSessionTrackPlaying(); + + if(positionMs < 0) { + // bug in backend? + positionMs = 0; + } + + if(playbackMonitorMode = PLAYBACK_MONITOR_MODE.JAMTRACK) { + + if(isPlaying) { + $jamTrackGetReady.attr('data-current-time', positionMs) + } + else { + // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing + $jamTrackGetReady.attr('data-current-time', -1) + } + + } + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { + updateIsPlaying(isPlaying); + } + else { + update(positionMs, durationMs, isPlaying); + } - update(positionMs, durationMs, isPlaying); monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); } - function update(currentTimeMs, durationTimeMs, isPlaying) { + function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) { if(dragging) { return; @@ -166,7 +238,7 @@ isPlaying = false; durationTimeMs = playbackDurationMs; currentTimeMs = playbackDurationMs; - stopPlay(); + stopPlay(true); endReached = true; logger.debug("end reached"); } @@ -175,6 +247,10 @@ } } + + if(currentTimeMs < offsetStart) { + currentTimeMs = 0; // this is to squelch movement during tap-in period + } updateDurationTime(durationTimeMs); updateCurrentTime(currentTimeMs); updateIsPlaying(isPlaying); @@ -247,11 +323,23 @@ } } - function startMonitor() { + function startMonitor(_playbackMonitorMode) { + + if(_playbackMonitorMode === undefined || _playbackMonitorMode === null) { + playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; + } + else { + playbackMonitorMode = _playbackMonitorMode; + } + + logger.debug("playbackControl.startMonitor " + playbackMonitorMode + "") + + styleControls(); monitorRecordingPlayback(); } function stopMonitor() { + logger.debug("playbackControl.stopMonitor") if(monitorPlaybackTimeout!= null) { clearTimeout(monitorPlaybackTimeout); monitorPlaybackTimeout = null; diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index 078a41c4d..a0e275f48 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -132,7 +132,6 @@ $self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments}); } }); - }); return true; } diff --git a/web/app/assets/javascripts/scheduled_session.js.erb b/web/app/assets/javascripts/scheduled_session.js.erb index 969f9449d..a9ab74fa2 100644 --- a/web/app/assets/javascripts/scheduled_session.js.erb +++ b/web/app/assets/javascripts/scheduled_session.js.erb @@ -5,7 +5,7 @@ context.JK = context.JK || {}; context.JK.CreateScheduledSession = function(app) { - var gearUtils = context.JK.GearUtils; + var gearUtils = context.JK.GearUtilsInstance; var sessionUtils = context.JK.SessionUtils; var logger = context.JK.logger; var rest = JK.Rest(); @@ -597,7 +597,9 @@ if(willOptionStartSession()) { - gearUtils.guardAgainstInvalidConfiguration(app) + var shouldVerifyNetwork = createSessionSettings.musician_access.value != 'only-rsvp'; + + gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork) .fail(function() { $btn.removeClass('disabled') app.notify( @@ -908,6 +910,13 @@ createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>'; } + function optionRequiresMultiplayerProfile() { + return createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_RSVP %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_SCHEDULE_FUTURE %>'; + } + function next(event) { if(willOptionStartSession()) { if(!context.JK.guardAgainstBrowser(app)) { @@ -915,6 +924,12 @@ } } + if(optionRequiresMultiplayerProfile()) { + if(context.JK.guardAgainstSinglePlayerProfile(app).canPlay == false) { + return false; + } + } + var valid = beforeMoveStep(); if (!valid) { return false; @@ -1054,7 +1069,7 @@ context.JK.GenreSelectorHelper.render('#create-session-genre'); - inviteMusiciansUtil.loadFriends(); + //inviteMusiciansUtil.loadFriends(); context.JK.dropdown($screen.find('#session-musician-access')); context.JK.dropdown($screen.find('#session-fans-access')); diff --git a/web/app/assets/javascripts/selectLocation.js b/web/app/assets/javascripts/selectLocation.js index 4d5b36711..93cde1b12 100644 --- a/web/app/assets/javascripts/selectLocation.js +++ b/web/app/assets/javascripts/selectLocation.js @@ -6,7 +6,7 @@ context.JK.SelectLocation = Class.extend({ - init: function ($countries, $regions, $cities, app) { + init: function ($countries, $regions, $cities, app, useEasyDropdown) { this.api = context.JK.Rest(); this.logger = context.JK.logger; this.loadingCitiesData = false; @@ -14,9 +14,12 @@ this.loadingCountriesData = false; this.nilOptionStr = ''; this.nilOptionText = 'n/a'; + this.countriesLoaded = false; this.$countries = $countries; this.$regions = $regions; this.$cities = $cities; + this.$deferred = null; + this.useEasyDropdown = useEasyDropdown === undefined ? true : useEasyDropdown; this.app = app; $countries.on('change', function (evt) { @@ -24,11 +27,24 @@ this.handleCountryChanged(); return false; }.bind(this)); - $regions.on('change', function (evt) { - evt.stopPropagation(); - this.handleRegionChanged(); - return false; - }.bind(this)); + if($regions) { + $regions.on('change', function (evt) { + evt.stopPropagation(); + this.handleRegionChanged(); + return false; + }.bind(this)); + } + }, + selectCountry: function (country) { + if(this.useEasyDropdown) { + this.$countries.easyDropDown('select', country, true) + } + else { + this.$countries.val(country) + } + }, + ready: function() { + return this.$deferred; }, load: function (country, region, city) { @@ -42,13 +58,9 @@ country = 'US'; } - this.loadingCountriesData = true; - this.loadingRegionsData = true; - this.loadingCitiesData = true; - // make the 3 slower requests, which only matter if the user wants to affect their ISP or location - - this.api.getCountries() + this.loadingCountriesData = true; + this.$deferred = this.api.getCountries() .done(function (countriesx) { this.populateCountriesx(countriesx["countriesx"], country); }.bind(this)) @@ -57,7 +69,9 @@ this.loadingCountriesData = false; }.bind(this)) - if (country) { + + if (country && this.$regions) { + this.loadingRegionsData = true; this.api.getRegions({ country: country }) .done(function (regions) { this.populateRegions(regions["regions"], region); @@ -67,7 +81,8 @@ this.loadingRegionsData = false; }.bind(this)) - if (region) { + if (region && this.$cities) { + this.loadingCitiesData = true; this.api.getCities({ country: country, region: region }) .done(function (cities) { this.populateCities(cities["cities"], this.city) @@ -78,9 +93,15 @@ }.bind(this)) } } + return this.$deferred; }, handleCountryChanged: function () { var selectedCountry = this.$countries.val() + + if(!this.$regions) { + return; + } + var selectedRegion = this.$regions.val() var cityElement = this.$cities @@ -144,7 +165,9 @@ else { cityElement.children().remove(); cityElement.append($(this.nilOptionStr).text(this.nilOptionText)); - context.JK.dropdown(cityElement); + if(this.useEasyDropdown) { + context.JK.dropdown(cityElement); + } } }, @@ -159,7 +182,7 @@ if (!countryx.countrycode) return; var option = $(this.nilOptionStr); - option.text(countryx.countryname); + option.text(countryx.countryname ? countryx.countryname : countryx.countrycode); option.attr("value", countryx.countrycode); if (countryx.countrycode == this.country) { @@ -170,6 +193,8 @@ }, populateCountriesx: function (countriesx) { + this.countriesLoaded = true; + // countriesx has the format [{countrycode: "US", countryname: "United States"}, ...] this.foundCountry = false; @@ -194,8 +219,9 @@ this.$countries.val(this.country); this.$countries.attr("disabled", null).easyDropDown('enable'); - - context.JK.dropdown(this.$countries); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$countries); + } }, writeRegion: function (index, region) { @@ -220,7 +246,9 @@ this.$regions.val(userRegion) this.$regions.attr("disabled", null).easyDropDown('enable'); - context.JK.dropdown(this.$regions); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$regions); + } }, writeCity: function (index, city) { @@ -245,7 +273,9 @@ this.$cities.val(userCity) this.$cities.attr("disabled", null).easyDropDown('enable'); - context.JK.dropdown(this.$cities); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$cities); + } }, regionListFailure: function (jqXHR, textStatus, errorThrown) { diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 6b56facc9..e32c5bd95 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -4,6 +4,7 @@ context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { + var TEMPOS = context.JK.TEMPOS; var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; @@ -12,6 +13,58 @@ var modUtils = context.JK.ModUtils; var logger = context.JK.logger; var self = this; + + var defaultParticipant = { + tracks: [{ + instrument_id: "unknown" + }], + user: { + first_name: 'Unknown', + last_name: 'User', + photo_url: null + } + }; + + // Be sure to copy/extend these instead of modifying in place + var trackVuOpts = { + vuType: "vertical", + lightCount: 13, + lightWidth: 3, + lightHeight: 17 + }; + // Must add faderId key to this + var trackFaderOpts = { + faderType: "vertical", + height: 83 + }; + + // Recreate ChannelGroupIDs ENUM from C++ + var ChannelGroupIds = { + "MasterGroup": 0, + "MonitorGroup": 1, + "AudioInputMusicGroup": 2, + "AudioInputChatGroup": 3, + "MediaTrackGroup": 4, + "StreamOutMusicGroup": 5, + "StreamOutChatGroup": 6, + "UserMusicInputGroup": 7, + "UserChatInputGroup": 8, + "PeerAudioInputMusicGroup": 9, + "PeerMediaTrackGroup": 10, + "JamTrackGroup": 11, + "MetronomeGroup": 12 + }; + + var METRO_SOUND_LOOKUP = { + 0 : "BuiltIn", + 1 : "SineWave", + 2 : "Beep", + 3 : "Click", + 4 : "Kick", + 5 : "Snare", + 6 : "MetroFile" + } + var sessionModel = null; var sessionId; var tracks = {}; @@ -38,67 +91,54 @@ var startTimeDate = null; var startingRecording = false; // double-click guard var claimedRecording = null; + var backing_track_path = null; + var jamTrack = null; + + var metronomeMixer = null; var playbackControls = null; var promptLeave = false; var rateSessionDialog = null; var friendInput = null; var sessionPageDone = null; + var metroTempo = 120; + var metroCricket = false; + var metroSound = "Beep"; var $recordingManagerViewer = null; var $screen = null; var $mixModeDropdown = null; var $templateMixerModeChange = null; + var $myTracksNoTracks = null; + var $otherAudioContainer = null; + var $myTracksContainer = null; + var $liveTracksContainer = null; + var downloadJamTrack = null; + var $closePlaybackRecording = null; + var $openBackingTrack = null; + var $metronomePlaybackSelect = null; + var $metronomePlaybackHelp = null; + var $templatePendingMetronome = null; + var $myTracks = null; + var $liveTracks = null; + var $audioTracks = null; + var $fluidTracks = null; + var $voiceChat = null; + var $openFtue = null; + var $tracksHolder = null; + + var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; + var muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; + var rest = context.JK.Rest(); var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there. - var defaultParticipant = { - tracks: [{ - instrument_id: "unknown" - }], - user: { - first_name: 'Unknown', - last_name: 'User', - photo_url: null - } - }; - - // Be sure to copy/extend these instead of modifying in place - var trackVuOpts = { - vuType: "vertical", - lightCount: 13, - lightWidth: 3, - lightHeight: 17 - }; - // Must add faderId key to this - var trackFaderOpts = { - faderType: "vertical", - height: 83 - }; - - // Recreate ChannelGroupIDs ENUM from C++ - var ChannelGroupIds = { - "MasterGroup": 0, - "MonitorGroup": 1, - "AudioInputMusicGroup": 2, - "AudioInputChatGroup": 3, - "MediaTrackGroup": 4, - "StreamOutMusicGroup": 5, - "StreamOutChatGroup": 6, - "UserMusicInputGroup": 7, - "UserChatInputGroup": 8, - "PeerAudioInputMusicGroup": 9, - "PeerMediaTrackGroup": 10, - "JamTrackGroup": 11, - "MetronomeGroup": 12 - }; - function beforeShow(data) { sessionId = data.id; if(!sessionId) { window.location = '/client#/home'; } promptLeave = true; - $('#session-mytracks-container').empty(); + $myTracksContainer.empty(); displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); @@ -132,6 +172,9 @@ function afterShow(data) { + $fluidTracks.addClass('showing'); + $openBackingTrack.removeClass('disabled'); + if(!context.JK.JamServer.connected) { promptLeave = false; app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); @@ -159,57 +202,85 @@ // body-scoped drag handlers can go active screenActive = true; - gearUtils.guardAgainstInvalidConfiguration(app) - .fail(function() { - promptLeave = false; - window.location = '/client#/home' - }) - .done(function(){ - var result = sessionUtils.SessionPageEnter(); + rest.getSessionHistory(data.id) + .done(function(musicSession) { - gearUtils.guardAgainstActiveProfileMissing(app, result) - .fail(function(data) { + var singlePlayerCheckOK = true; + // to know whether we are allowed to be in this session, we have to check if we are the creator when checking against single player functionality + if(musicSession.user_id != context.JK.currentUserId) { + + var canPlay = context.JK.guardAgainstSinglePlayerProfile(app, function () { promptLeave = false; - if(data && data.reason == 'handled') { - if(data.nav == 'BACK') { - window.history.go(-1); - } - else { - window.location = data.nav; - } - } - else { - window.location = '/client#/home'; - } - }) - .done(function(){ + }); - sessionModel.waitForSessionPageEnterDone() - .done(function(userTracks) { + singlePlayerCheckOK = canPlay.canPlay; + } + if(singlePlayerCheckOK) { - context.JK.CurrentSessionModel.setUserTracks(userTracks); + var shouldVerifyNetwork = musicSession.musician_access; + gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork) + .fail(function() { + promptLeave = false; + window.location = '/client#/home' + }) + .done(function(){ + var result = sessionUtils.SessionPageEnter(); - initializeSession(); - }) - .fail(function(data) { - if(data == "timeout") { - context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') - } - else if(data == 'session_over') { - // do nothing; session ended before we got the user track info. just bail - } - else { - contetx.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) - } + gearUtils.guardAgainstActiveProfileMissing(app, result) + .fail(function(data) { + promptLeave = false; + if(data && data.reason == 'handled') { + if(data.nav == 'BACK') { + window.history.go(-1); + } + else { + window.location = data.nav; + } + } + else { + window.location = '/client#/home'; + } + }) + .done(function(){ + + sessionModel.waitForSessionPageEnterDone() + .done(function(userTracks) { + + context.JK.CurrentSessionModel.setUserTracks(userTracks); + + initializeSession(); + }) + .fail(function(data) { + if(data == "timeout") { + context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') + } + else if(data == 'session_over') { + // do nothing; session ended before we got the user track info. just bail + } + else { + context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) + } + promptLeave = false; + window.location = '/client#/home' + }); + }) + }) + } + else { + if(canPlay.dialog) { + canPlay.dialog.one(EVENTS.DIALOG_CLOSED, function(e, data) { + if(data.canceled) { promptLeave = false; window.location = '/client#/home' - }); - - }) + } + }) + } + } + }) + .fail(function() { }) - } function notifyWithUserInfo(title , text, clientId) { @@ -237,6 +308,7 @@ $(sessionModel.recordingModel) .on('startingRecording', function(e, data) { displayStartingRecording(); + lockControlsforJamTrackRecording(); }) .on('startedRecording', function(e, data) { if(data.reason) { @@ -282,12 +354,30 @@ { displayStartedRecording(); displayWhoCreated(data.clientId); + lockControlsforJamTrackRecording(); } }) .on('stoppingRecording', function(e, data) { displayStoppingRecording(data); + unlockControlsforJamTrackRecording(); }) .on('stoppedRecording', function(e, data) { + + unlockControlsforJamTrackRecording(); + if(sessionModel.selfOpenedJamTracks()) { + + var timeline = context.jamClient.GetJamTrackTimeline(); + + rest.addRecordingTimeline(data.recordingId, timeline) + .fail(function(){ + app.notify( + { title: "Unable to Add JamTrack Volume Data", + text: "The volume of the JamTrack will not be correct in the recorded mix." }, + null, + true); + }) + } + if(data.reason) { logger.warn("Recording Discarded: ", data); var reason = data.reason; @@ -411,7 +501,7 @@ } function beforeHide(data) { - + $fluidTracks.removeClass('showing'); if(screenActive) { // this path is possible if FTUE is invoked on session page, and they cancel sessionModel.leaveCurrentSession() @@ -427,23 +517,77 @@ sessionUtils.SessionPageLeave(); } - function handleTransitionsInRecordingPlayback() { - // let's see if we detect a transition to start playback or stop playback + function getMetronomeMasterMixers() { + return _mixersForGroupId(ChannelGroupIds.MetronomeGroup, MIX_MODES.MASTER); + } - var currentSession = sessionModel.getCurrentSession(); - - if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { - // this is a 'started with a claimed_recording' transition. - // we need to start a timer to watch for the state of the play session - playbackControls.startMonitor(); - } - else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { - playbackControls.stopMonitor(); - } - - claimedRecording = currentSession == null ? null : currentSession.claimed_recording; + function checkMetronomeTransition() { + // trust backend over server + if(sessionModel.jamTracks() !== null || sessionModel.recordedJamTracks() !== null) { + // ignore all metronome events when jamtracks are open, because backend opens metronome mixer to play jamtrack tap-ins + logger.debug("ignore checkMetronomeTransition because JamTrack is open") + return; } + + var metronomeMasterMixers = getMetronomeMasterMixers(); + + if (metronomeMixer == null && metronomeMasterMixers.length > 0) { + playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.METRONOME) + } + else if (metronomeMixer != null && metronomeMasterMixers.length == 0) { + playbackControls.stopMonitor(); + } + metronomeMixer = metronomeMasterMixers.length > 0 ? metronomeMasterMixers : null; + } + + function checkJamTrackTransition(currentSession) { + // handle jam tracks + if (jamTrack == null && (currentSession && currentSession.jam_track != null)) { + playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK); + } + else if (jamTrack && (currentSession == null || currentSession.jam_track == null)) { + playbackControls.stopMonitor(); + } + jamTrack = currentSession == null ? null : currentSession.jam_track; + } + + function checkBackingTrackTransition(currentSession) { + // handle backing tracks + if (backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) { + playbackControls.startMonitor(); + } + else if (backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) { + playbackControls.stopMonitor(); + } + backing_track_path = currentSession == null ? null : currentSession.backing_track_path; + } + + + function checkRecordingTransition(currentSession) { + // handle claimed recordings + if (claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { + // this is a 'started with a claimed_recording' transition. + // we need to start a timer to watch for the state of the play session + playbackControls.startMonitor(); + } + else if (claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { + playbackControls.stopMonitor(); + } + claimedRecording = currentSession == null ? null : currentSession.claimed_recording; + } + + function handleTransitionsInRecordingPlayback() { + // let's see if we detect a transition to start playback or stop playback + + var currentSession = sessionModel.getCurrentSession(); + + checkRecordingTransition(currentSession); + checkBackingTrackTransition(currentSession); + checkJamTrackTransition(currentSession); + checkMetronomeTransition(); + } + function sessionChanged() { handleTransitionsInRecordingPlayback(); @@ -459,8 +603,13 @@ * you must iterate. Convenience method to locate a particular * mixer by id. */ - function getMixer(mixerId) { - return allMixers[mixerId]; + function getMixer(mixerId, mode) { + + if(mode === undefined) { + mode = sessionModel.getMixMode(); + } + + return allMixers[(mode ? 'M' : 'P') + mixerId]; } function getMixerByResourceId(resourceId, mode) { @@ -499,10 +648,31 @@ } } + function resetOtherAudioContent() { + if ($('.session-recordings .track').length === 0 && $('.session-recordings .download-jamtrack').length === 0 && $('.session-recordings .pending-metronome').length === 0) { + $('.session-recordings .when-empty').show(); + $('.session-recording-name-wrapper').hide(); + $('.session-recordings .recording-controls').hide(); + $closePlaybackRecording.show(); + $('.session-recordings .session-recording-name').text('(No audio loaded)') + $('.session-recordings').attr('media-state', 'closed') + $('.session-livetracks').attr('media-state', 'closed') + } + } + function didSelfOpenMedia() { + var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER); + + // if we find any local media mixers, then we are the opener of media + return localMediaMixers.length > 0; + } + + function checkShowCloseControl() { + didSelfOpenMedia() ? $closePlaybackRecording.show() : $closePlaybackRecording.hide(); + } + function renderSession() { - $('#session-mytracks-container').empty(); + $myTracksContainer.empty(); $('.session-track').remove(); // Remove previous tracks - var $voiceChat = $('#voice-chat'); $voiceChat.hide(); _updateMixers(); _renderTracks(); @@ -511,15 +681,32 @@ _wireTopMix(); _addVoiceChat(); _initDialogs(); + if ($('.session-livetracks .track').length === 0) { $('.session-livetracks .when-empty').show(); } + checkPendingMetronome(); + resetOtherAudioContent(); + resizeFluid(); + /** if ($('.session-recordings .track').length === 0) { $('.session-recordings .when-empty').show(); $('.session-recording-name-wrapper').hide(); $('.session-recordings .recording-controls').hide(); + // should we show the close button? Only if the user opened the media + checkShowCloseControl(); + } else { + $('.session-recordings .when-empty').hide(); + $('.session-recording-name-wrapper').show(); + $('.session-recordings .recording-controls').show(); + checkShowCloseControl(); } - } + */ + + // Handle long labels: + $(".track-label").dotdotdot() + $(".session-recording-name").dotdotdot() + } // renderSession function _initDialogs() { configureTrackDialog.initialize(); @@ -546,7 +733,7 @@ var i; for(i = 0; i < masterMixers.length; i++) { var masterMixer = masterMixers[i]; - allMixers[masterMixer.id] = masterMixer; // populate allMixers by mixer.id + allMixers['M' + masterMixer.id] = masterMixer; // populate allMixers by mixer.id // populate mixer pair var mixerPair = {} @@ -557,11 +744,7 @@ for(i = 0; i < personalMixers.length; i++) { var personalMixer = personalMixers[i]; - if(personalMixer.group_id == ChannelGroupIds.MediaTrackGroup) { - continue; - } - - allMixers[personalMixer.id] = personalMixer + allMixers['P' + personalMixer.id] = personalMixer // populate other side of mixer pair @@ -674,7 +857,6 @@ //logger.debug("clientId", clientId, "groupIds", groupIds, "mixers", mixers) var foundMixers = {}; var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers; - // console.log("_groupedMixersForClientId", mixers) $.each(mixers, function(index, mixer) { if (mixer.client_id === clientId) { for (var i=0; i 0) { renderRecordingTracks(recordingTrackMixers) @@ -871,22 +1117,158 @@ if(jamTrackMixers.length > 0) { renderJamTracks(jamTrackMixers); } - if(metronomeTrackMixers.length > 0) { - renderMetronomeTracks(jamTrackMixers); + if(metronomeTrackMixers.length > 0 && sessionModel.jamTracks() === null && sessionModel.recordedJamTracks() == null) { + renderMetronomeTracks(metronomeTrackMixers); } if(adhocTrackMixers.length > 0) { logger.warn("some tracks are open that we don't know how to show") } + + checkMetronomeTransition(); } - + // this method is pretty complicated because it forks on a key bit of state: + // sessionModel.isPlayingRecording() + // a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks) + // than a backing track opend ad-hoc (connection.backing_tracks) function renderBackingTracks(backingTrackMixers) { - logger.error("do not know how to draw backing tracks yet") + + var backingTracks = [] + if(sessionModel.isPlayingRecording()) { + // only return managed mixers for recorded backing tracks + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return mixer.managed || mixer.managed === undefined}) + backingTracks = sessionModel.recordedBackingTracks(); + } + else { + // only return un-managed (ad-hoc) mixers for normal backing tracks + backingTracks = sessionModel.backingTracks(); + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return !mixer.managed}) + if(backingTrackMixers.length > 1) { + logger.error("multiple, managed backing track mixers encountered", backingTrackMixers) + app.notify({ + title: "Multiple Backing Tracks Encountered", + text: "Only one backing track can be open a time.", + icon_url: "/assets/content/icon_alert_big.png" + }); + return false; + } + } + + var noCorrespondingTracks = false; + $.each(backingTrackMixers, function(index, mixer) { + + // find the track or tracks that correspond to the mixer + var correspondingTracks = [] + + var noCorrespondingTracks = false; + if(sessionModel.isPlayingRecording()) { + $.each(backingTracks, function (i, backingTrack) { + if(mixer.persisted_track_id == backingTrack.client_track_id || // occurs if this client is the one that opened the track + mixer.id == 'L' + backingTrack.client_track_id) { // occurs if this client is a remote participant + correspondingTracks.push(backingTrack) + } + }); + } + else + { + // if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours + correspondingTracks.push(backingTracks[0]) + } + + if (correspondingTracks.length == 0) { + noCorrespondingTracks = true; + logger.debug("renderBackingTracks: could not map backing tracks") + app.notify({ + title: "Unable to Open Backing Track", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png" + }); + return false; + } + + // now we have backing track and mixer in hand; we can render + var backingTrack = correspondingTracks[0] + + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener + var isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup; + + if(isOpener) { + var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + var mixerId = mixer.id + "," + oppositeMixer.id + } + else { + var mixerId = mixer.id; + } + + var shortFilename = context.JK.getNameOfFile(backingTrack.filename); + + if(!sessionModel.isPlayingRecording()) { + // if a recording is being played back, do not set this header, because renderRecordedTracks already did + // ugly. + $('.session-recording-name').text(shortFilename); + } + + var instrumentIcon = context.JK.getInstrumentIcon45(backingTrack.instrument_id); + var photoUrl = "/assets/content/icon_recording.png"; + + + // Default trackData to participant + no Mixer state. + var trackData = { + type: 'backing_track', + trackId: backingTrack.id, + clientId: backingTrack.client_id, + name: 'Backing', + filename: backingTrack.filename, + instrumentIcon: instrumentIcon, + avatar: photoUrl, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + showLoop: isOpener && !sessionModel.isPlayingRecording(), + loopState: mixer.loop, + mixerId: "", + avatarClass: 'avatar-recording', + preMasteredClass: "" + }; + + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; + } + + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) + trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = isOpener; + trackData.mediaControlsDisabled = !isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; + + _addRecordingTrack(trackData, mixer, oppositeMixer); + }); } function renderJamTracks(jamTrackMixers) { - log.debug("rendering jam tracks") - var jamTracks = sessionModel.jamTracks(); + logger.debug("rendering jam tracks") + + var jamTracks = [] + var jamTrackName = 'JamTrack'; + if(sessionModel.isPlayingRecording()) { + // only return managed mixers for recorded backing tracks + jamTracks = sessionModel.recordedJamTracks(); + jamTrackName = sessionModel.recordedJamTrackName(); + } + else { + // only return un-managed (ad-hoc) mixers for normal backing tracks + jamTracks = sessionModel.jamTracks(); + jamTrackName = sessionModel.jamTrackName(); + } + + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) // if it's a locally opened track (JamTrackGroup), then we can say this person is the opener @@ -894,28 +1276,22 @@ // using the server's info in conjuction with the client's, draw the recording tracks if(jamTracks) { - $('.session-recording-name').text(sessionModel.getCurrentSession().jam_track.name); + $('.session-recording-name').text(jamTrackName); var noCorrespondingTracks = false; $.each(jamTrackMixers, function(index, mixer) { var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] - console.log("mixer", mixer) $.each(jamTracks, function(i, jamTrack) { - if(mixer.id.indexOf("L") == 0) { - if(mixer.id.substring(1) == jamTrack.id) { + if(mixer.id == jamTrack.id) { correspondingTracks.push(jamTrack); } - else { - // this should not be possible - alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id"); - } - } }); if(correspondingTracks.length == 0) { noCorrespondingTracks = true; + logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks) app.notify({ title: "Unable to Open JamTrack", text: "Could not correlate server and client tracks", @@ -929,16 +1305,25 @@ }); var oneOfTheTracks = correspondingTracks[0]; - var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); + var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument.id); var photoUrl = "/assets/content/icon_recording.png"; var name = oneOfTheTracks.part if (!name) { - name = oneOfTheTracks.instrument; + name = ''; + } + + if(isOpener) { + var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + var mixerId = mixer.id + "," + oppositeMixer.id + } + else { + var mixerId = mixer.id; } // Default trackData to participant + no Mixer state. var trackData = { + type: 'jam_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, @@ -960,15 +1345,14 @@ } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; - trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = isOpener; + trackData.mediaControlsDisabled = !isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; - if(sessionModel.isPersonalMixMode() || !isOpener) { - trackData.mediaControlsDisabled = true; - trackData.mediaTrackOpener = isOpener; - } - _addRecordingTrack(trackData); + _addRecordingTrack(trackData, mixer, oppositeMixer); }); if(!noCorrespondingTracks && jamTracks.length > 0) { @@ -981,13 +1365,90 @@ } function renderMetronomeTracks(metronomeTrackMixers) { - logger.error("do not know how to draw metronome tracks yet") + logger.debug("rendering metronome track") + + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener + var name = "Metronome" + + // using the server's info in conjuction with the client's, draw the recording tracks + if(metronomeTrackMixers.length > 0) { + var metronome = {} + $('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path); + + var noCorrespondingTracks = false; + var mixer = metronomeTrackMixers[0] + var preMasteredClass = ""; + // find the track or tracks that correspond to the mixer + var correspondingTracks = [] + correspondingTracks.push(metronome); + + if(correspondingTracks.length == 0) { + noCorrespondingTracks = true; + app.notify({ + title: "Unable to Open Metronome", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_metronome_small.png"}); + return false; + } + + // prune found recorded tracks + // Metronomes = $.grep(Metronomes, function(value) { + // return $.inArray(value, correspondingTracks) < 0; + // }); + + var oneOfTheTracks = correspondingTracks[0]; + var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); + var photoUrl = "/assets/content/icon_metronome_small.png"; + + var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + var mixerId = mixer.id + "," + oppositeMixer.id + + // Default trackData to participant + no Mixer state. + var trackData = { + type: 'metronome', + trackId: "MS" + oneOfTheTracks.id, + clientId: oneOfTheTracks.client_id, + name: "Metronome", + instrumentIcon: photoUrl, + avatar: instrumentIcon, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + mixerId: "", + avatarClass : 'avatar-recording', + preMasteredClass: "", + showMetronomeControls: true + }; + + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; + } + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) + trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = true + trackData.mediaControlsDisabled = false + trackData.showHelpAboutMediaMixers = false + + _addRecordingTrack(trackData, mixer, oppositeMixer); + }// if + setFormFromMetronome() + metroCricket = context.jamClient.getMetronomeCricketTestState(); + setMetronomePlaybackMode() + $closePlaybackRecording.show(); } function renderRecordingTracks(recordingMixers) { // get the server's info for the recording var recordedTracks = sessionModel.recordedTracks(); + var recordedBackingTracks = sessionModel.recordedBackingTracks(); // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between Local vs Peer) // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener @@ -1023,6 +1484,7 @@ if(correspondingTracks.length == 0) { noCorrespondingTracks = true; + logger.debug("unable to correlate all recorded tracks", recordingMixers, recordedTracks) app.notify({ title: "Unable to Open Recording", text: "Could not correlate server and client tracks", @@ -1044,8 +1506,17 @@ name = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name; } + if(isOpener) { + var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + var mixerId = mixer.id + "," + oppositeMixer.id + } + else { + var mixerId = mixer.id; + } + // Default trackData to participant + no Mixer state. var trackData = { + type: 'recorded_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, @@ -1067,15 +1538,14 @@ } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; - trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaControlsDisabled = !isOpener; + trackData.mediaTrackOpener = isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; - if(sessionModel.isPersonalMixMode() || !isOpener) { - trackData.mediaControlsDisabled = true; - trackData.mediaTrackOpener = isOpener; - } - _addRecordingTrack(trackData); + _addRecordingTrack(trackData, mixer, oppositeMixer); }); if(!noCorrespondingTracks && recordedTracks.length > 0) { @@ -1099,7 +1569,6 @@ var mixer = $muteControl.data('mixer') var oppositeMixer = $muteControl.data('opposite-mixer') - logger.debug("muting tracks. current mixer id=" + mixer.id + ", opposite mixer id=" + oppositeMixer.id) var mixerPair = {} @@ -1132,6 +1601,7 @@ var mixMode = sessionModel.getMixMode(); if(myTrack) { + // when it's your track, look it up by the backend resource ID mixer = getMixerByTrackId(track.client_track_id, mixMode) vuMixer = mixer; @@ -1220,108 +1690,138 @@ var myTrack = app.clientId == participant.client_id; + // special case; if it's me and I have no tracks, show info about this sort of use of the app + if (myTrack && participant.tracks.length == 0) { + $tracksHolder.addClass('no-local-tracks') + } + else { + $tracksHolder.removeClass('no-local-tracks') + } + // loop through all tracks for each participant - $.each(participant.tracks, function(index, track) { - var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); - var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); + $.each(participant.tracks, function (index, track) { + var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); + var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); - // Default trackData to participant + no Mixer state. - var trackData = { - trackId: track.id, - connection_id: track.connection_id, - client_track_id: track.client_track_id, - client_resource_id: track.client_resource_id, - clientId: participant.client_id, - name: name, - instrumentIcon: instrumentIcon, - avatar: photoUrl, - latency: "good", - gainPercent: 0, - muteClass: 'muted', - mixerId: "", - avatarClass: 'avatar-med', - preMasteredClass: "", - myTrack: myTrack - }; + // Default trackData to participant + no Mixer state. + var trackData = { + trackId: track.id, + connection_id: track.connection_id, + client_track_id: track.client_track_id, + client_resource_id: track.client_resource_id, + clientId: participant.client_id, + name: name, + instrumentIcon: instrumentIcon, + avatar: photoUrl, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + mixerId: "", + avatarClass: 'avatar-med', + preMasteredClass: "", + myTrack: myTrack + }; - var mixerData = findMixerForTrack(participant.client_id, track, myTrack) - var mixer = mixerData.mixer; - var vuMixer = mixerData.vuMixer; - var muteMixer = mixerData.muteMixer; - var oppositeMixer = mixerData.oppositeMixer; + var mixerData = findMixerForTrack(participant.client_id, track, myTrack) + var mixer = mixerData.mixer; + var vuMixer = mixerData.vuMixer; + var muteMixer = mixerData.muteMixer; + var oppositeMixer = mixerData.oppositeMixer; - - if (mixer && oppositeMixer) { - myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup); - if(!myTrack) { - // it only makes sense to track 'audio established' for tracks that don't belong to you - sessionModel.setAudioEstablished(participant.client_id, true); - } - - var gainPercent = percentFromMixerValue( - mixer.range_low, mixer.range_high, mixer.volume_left); - var muteClass = "enabled"; - if (mixer.mute) { - muteClass = "muted"; - } - - trackData.gainPercent = gainPercent; - trackData.muteClass = muteClass; - trackData.mixerId = mixer.id; - trackData.vuMixerId = vuMixer.id; - trackData.oppositeMixer = oppositeMixer; - trackData.muteMixerId = muteMixer.id; - trackData.noaudio = false; - trackData.group_id = mixer.group_id; - context.jamClient.SessionSetUserName(participant.client_id,name); - - } else { // No mixer to match, yet - lookingForMixers.push({track: track, clientId: participant.client_id}) - trackData.noaudio = true; - if (!(lookingForMixersTimer)) { - logger.debug("waiting for mixer to show up for track: " + track.id) - lookingForMixersTimer = context.setInterval(lookForMixers, 500); - } + if (mixer && oppositeMixer) { + myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup); + if (!myTrack) { + // it only makes sense to track 'audio established' for tracks that don't belong to you + sessionModel.setAudioEstablished(participant.client_id, true); } - var allowDelete = myTrack && index > 0; - _addTrack(allowDelete, trackData, mixer, oppositeMixer); - - // Show settings icons only for my tracks - if (myTrack) { - myTracks.push(trackData); + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; } + + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixer.id; + trackData.vuMixerId = vuMixer.id; + trackData.oppositeMixer = oppositeMixer; + trackData.muteMixerId = muteMixer.id; + trackData.noaudio = false; + trackData.group_id = mixer.group_id; + context.jamClient.SessionSetUserName(participant.client_id, name); + + } else { // No mixer to match, yet + lookingForMixers.push({track: track, clientId: participant.client_id}) + trackData.noaudio = true; + if (!(lookingForMixersTimer)) { + logger.debug("waiting for mixer to show up for track: " + track.id) + lookingForMixersTimer = context.setInterval(lookForMixers, 500); + } + } + + var allowDelete = myTrack && index > 0; + _addTrack(allowDelete, trackData, mixer, oppositeMixer); + + // Show settings icons only for my tracks + if (myTrack) { + myTracks.push(trackData); + } }); + }); configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel); addNewGearDialog = new context.JK.AddNewGearDialog(app, self); } - function connectTrackToMixer(trackSelector, track, mixerId, gainPercent, groupId) { + function connectTrackToMixer(trackSelector, track, mixerId, gainPercent, groupId, mixer, oppositeMixer) { var vuOpts = $.extend({}, trackVuOpts); var faderOpts = $.extend({}, trackFaderOpts); faderOpts.faderId = mixerId; var vuLeftSelector = trackSelector + " .track-vu-left"; var vuRightSelector = trackSelector + " .track-vu-right"; var faderSelector = trackSelector + " .track-gain"; - var $fader = $(faderSelector).attr('mixer-id', mixerId).data('groupId', groupId) + var $fader = $(faderSelector).attr('mixer-id', mixerId).data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer); if(track.mediaControlsDisabled) { $fader.data('media-controls-disabled', true).data('media-track-opener', track.mediaTrackOpener) // this we be applied later to the fader handle $element } + $fader.data('showHelpAboutMediaMixers', track.showHelpAboutMediaMixers) + + var $track = $(trackSelector); // Set mixer-id attributes and render VU/Fader - context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); - $track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId) - context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); - $track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId) + + if (!track.hideVU) { + context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); + $track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer) + context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); + $track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer) + } + + if (track.showMetronomeControls) { + $track.find('.metronome-selects').removeClass("hidden") + } else { + $track.find('.metronome-selects').addClass("hidden") + } + + // if (track.showMetroSound) { + // $track.find('.metro-sound-select').removeClass("hidden") + // } + context.JK.FaderHelpers.renderFader($fader, faderOpts); // Set gain position context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent); - $fader.on('fader_change', faderChanged); - return $track; + if(track.faderChanged) { + $fader.on('fader_change', track.faderChanged); + } else { + $fader.on('fader_change', faderChanged); + } + + return $track; } // Function called on an interval when participants change. Mixers seem to @@ -1359,7 +1859,7 @@ mixer.range_low, mixer.range_high, mixer.volume_left); var trackSelector = 'div.track[track-id="' + track.id + '"]'; - connectTrackToMixer(trackSelector, track, mixer.id, gainPercent, mixer.group_id); + connectTrackToMixer(trackSelector, track, mixer.id, gainPercent, mixer.group_id, mixer, oppositeMixer); var $track = $('div.track[client-id="' + clientId + '"]'); var $trackIconMute = $track.find('.track-icon-mute') $trackIconMute.attr('mixer-id', muteMixer.id).data('mixer', mixer).data('opposite-mixer', oppositeMixer) @@ -1414,19 +1914,23 @@ var selector; var pureMixerId = mixerId.replace("_vul", ""); pureMixerId = pureMixerId.replace("_vur", ""); - var mixer = getMixer(pureMixerId); + var mixer = getMixer(pureMixerId, sessionModel.getMixMode()); + if(!mixer) { + // try again, in the opposite mode (awful that this is necessary) + mixer = getMixer(pureMixerId, !sessionModel.getMixMode()); + } if (mixer) { if (!(mixer.stereo)) { // mono track if (mixerId.substr(-4) === "_vul") { // Do the left - selector = $('#tracks [mixer-id="' + pureMixerId + '_vul"]'); + selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vul"]'); context.JK.VuHelpers.updateVU(selector, value); // Do the right - selector = $('#tracks [mixer-id="' + pureMixerId + '_vur"]'); + selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vur"]'); context.JK.VuHelpers.updateVU(selector, value); } // otherwise, it's a mono track, _vur event - ignore. } else { // stereo track - selector = $('#tracks [mixer-id="' + mixerId + '"]'); + selector = $tracksHolder.find('[mixer-id="' + mixerId + '"]'); context.JK.VuHelpers.updateVU(selector, value); } } @@ -1434,15 +1938,14 @@ function _addTrack(allowDelete, trackData, mixer, oppositeMixer) { - var parentSelector = '#session-mytracks-container'; - var $destination = $(parentSelector); + var $destination = $myTracksContainer; if (trackData.clientId !== app.clientId) { - parentSelector = '#session-livetracks-container'; - $destination = $(parentSelector); + $destination = $liveTracksContainer $('.session-livetracks .when-empty').hide(); } var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); + newTrack.data('track_data', trackData) var audioOverlay = $('.disabled-track-overlay', newTrack); var $trackIconMute = newTrack.find('.track-icon-mute') $trackIconMute.muteSelector().on(EVENTS.MUTE_SELECTED, trackMuteSelected) @@ -1452,9 +1955,9 @@ $destination.append(newTrack); // Render VU meters and gain fader - var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]'; + var trackSelector = $destination.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; - connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id); + connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id, mixer, oppositeMixer); var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]'); if (!allowDelete) { @@ -1468,30 +1971,87 @@ tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } + // something is being shown now in the other audio area + function otherAudioFilled() { + $('.session-recordings .when-empty').hide(); + $('.session-recording-name-wrapper').show(); + $('.session-recordings').attr('media-state', 'open'); + $('.session-livetracks').attr('media-state', 'open'); + } - function _addRecordingTrack(trackData) { + function resizeFluid() { + var trackWidth = 78; // 70 width + 8 margin + var trackPadding = 30; // 15px left and right + + var numLiveTracks = $liveTracks.find('.track').length; + var numAudioTracks = $audioTracks.find('.track').length; + var totalWidth = $fluidTracks.width(); + + + // calculate desired audio tracks width + var minimumLiveTrackWidth = numLiveTracks * trackWidth + trackPadding; + var otherAudioWidth = numAudioTracks * trackWidth + trackPadding; + var liveTrackWidth = totalWidth - otherAudioWidth; + + // live tracks get precedence over audio tracks, if there is a content over width usage + if(liveTrackWidth < minimumLiveTrackWidth) { + logger.debug("live track width trumping mode") + liveTrackWidth = minimumLiveTrackWidth; + otherAudioWidth = totalWidth - liveTrackWidth; + } + + var otherAudioWidthPct = Math.floor(100 * otherAudioWidth/totalWidth); + var liveTrackWidthPct = Math.ceil(100 * liveTrackWidth/totalWidth); + + logger.debug("resizeFluid: ", minimumLiveTrackWidth, otherAudioWidth, otherAudioWidthPct, liveTrackWidthPct, liveTrackWidthPct) + + $audioTracks.css('width', otherAudioWidthPct + '%'); + $liveTracks.css('width', liveTrackWidthPct + '%'); + } + + function _addRecordingTrack(trackData, mixer, oppositeMixer) { + + otherAudioFilled(); + + $('.session-recordings .recording-controls').show(); + var parentSelector = '#session-recordedtracks-container'; var $destination = $(parentSelector); - $('.session-recordings .when-empty').hide(); - $('.session-recording-name-wrapper').show(); - $('.session-recordings .recording-controls').show(); var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); - $destination.append(newTrack); + newTrack.data('track_data', trackData); + $otherAudioContainer.append(newTrack); if(trackData.preMasteredClass) { context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')}); } // Render VU meters and gain fader - var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]'; + var trackSelector = $otherAudioContainer.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; var $track = connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, null); var $trackIconMute = $track.find('.track-icon-mute') if(trackData.mediaControlsDisabled) { $trackIconMute.data('media-controls-disabled', true).data('media-track-opener', trackData.mediaTrackOpener) } + $trackIconMute.data('mixer', mixer).data('opposite-mixer', oppositeMixer) + $trackIconMute.data('showHelpAboutMediaMixers', trackData.showHelpAboutMediaMixers) + + + if(trackData.showLoop) { + var $trackIconLoop = $track.find('.track-icon-loop') + var $trackIconLoopCheckbox = $trackIconLoop.find('input'); + $trackIconLoopCheckbox.prop('checked', trackData.loopState); + + context.JK.checkbox($trackIconLoopCheckbox) + $trackIconLoopCheckbox.on('ifChanged', function() { + var loop = $trackIconLoopCheckbox.is(':checked') + _toggleAudioLoop(mixer.id, loop, getMixer(mixer.id).mode) + }); + $trackIconLoop.show() + } + // is this used? tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } @@ -1505,10 +2065,20 @@ var faderId = $target.attr('mixer-id'); var groupId = $target.data('groupId'); var mixerIds = faderId.split(','); + + // media tracks are the only controls that sometimes set two mixers right now + var hasMasterAndPersonalControls = mixerIds.length == 2; + $.each(mixerIds, function(i,v) { var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast - fillTrackVolumeObject(v, broadcast); - setMixerVolume(v, data.percentage); + + var mode = undefined; + if(hasMasterAndPersonalControls) { + mode = i == 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; + } + var mixer = fillTrackVolumeObject(v, mode, broadcast); + + setMixerVolume(mixer, data.percentage); if(groupId == ChannelGroupIds.UserMusicInputGroup) { // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well @@ -1517,6 +2087,33 @@ }); } + // function tempoFaderChanged(e, data) { + // var $target = $(this); + // var faderId = $target.attr('mixer-id'); + // var groupId = $target.data('groupId'); + // var mixerIds = faderId.split(','); + // $.each(mixerIds, function(i,v) { + // // TODO Interpolate tempo values if we decide to go this way: + // if(groupId == ChannelGroupIds.UserMusicInputGroup) { + // // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well + // } + // }); + // } + + function handleMetronomeCallback(args) { + logger.debug("MetronomeCallback: ", args) + metroTempo = args.bpm + metroCricket = args.cricket; + metroSound = METRO_SOUND_LOOKUP[args.sound]; + + setMetronomePlaybackMode(); + setFormFromMetronome(); + + // This isn't actually there, so we rely on the metroSound as set from select on form: + // metroSound = args.sound + context.JK.CurrentSessionModel.refreshCurrentSession(true); + } + function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) { // Visually update mixer // There is no need to actually set the back-end mixer value as the @@ -1592,7 +2189,42 @@ } } + function handleBackingTrackSelectedCallback(result) { + $openBackingTrack.removeClass('disabled'); + + if(!sessionModel.inSession()) { + return; + } + + if(result.success) { + logger.debug("backing track selected: " + result.file); + + rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: result.file}) + .done(function(response) { + var openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false); + + if(openResult) { + sessionModel.setBackingTrack(result.file); + } + else { + app.notify({ + "title": "Couldn't Open Backing Track", + "text": "Is the file a valid audio file?", + "icon_url": "/assets/content/icon_alert_big.png" + }); + closeBackingTrack(); + } + + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback"); + }) + } + else { + logger.debug("no backing track selected") + } + } function deleteSession(evt) { var sessionId = $(evt.currentTarget).attr("action-id"); if (sessionId) { @@ -1625,7 +2257,7 @@ } function _toggleAudioMute(mixerId, muting, mode) { - fillTrackVolumeObject(mixerId); + fillTrackVolumeObject(mixerId, mode); context.trackVolumeObject.mute = muting; if(mode === undefined) { @@ -1634,6 +2266,16 @@ context.jamClient.SessionSetControlState(mixerId, mode); } + function _toggleAudioLoop(mixerId, loop, mode) { + fillTrackVolumeObject(mixerId, mode); + context.trackVolumeObject.loop = loop; + + if(mode === undefined) { + mode = sessionModel.getMixMode(); + } + context.jamClient.SessionSetControlState(mixerId, mode); + } + function showMuteDropdowns($control) { $control.btOn(); } @@ -1653,6 +2295,20 @@ return false; } + if(sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') { + context.JK.prodBubble($control, 'jamtrack-controls-disabled', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) + return false; + } + + if($control.data('showHelpAboutMediaMixers')) { + if(!sessionModel.hasShownAudioMediaMixerHelp()) { + context.JK.prodBubble($control, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) + sessionModel.markShownAudioMediaMixerHelp() + } + } + + + $.each(mixerIds, function(i,v) { var mixerId = v; // behavior: if this is the user's track in personal mode, then we mute the track globally @@ -1661,17 +2317,19 @@ var mixer = $control.data('mixer'); var oppositeMixer = $control.data('opposite-mixer') - if(mixer && oppositeMixer && mixer.group_id == ChannelGroupIds.AudioInputMusicGroup) { + if(mixer && oppositeMixer && (muteBothMasterAndPersonalGroups.indexOf(mixer.group_id) > -1)) { // this is the user's local track; mute both personal and master mode - _toggleAudioMute(mixer.id, muting, getMixer(mixer.id).mode) - _toggleAudioMute(oppositeMixer.id, muting, getMixer(oppositeMixer.id).mode) + logger.debug("muting both master and personal mode mixers") + _toggleAudioMute(mixer.id, muting, mixer.mode) + _toggleAudioMute(oppositeMixer.id, muting, oppositeMixer.mode) } else { - _toggleAudioMute(mixer.id, muting, getMixer(mixer.id).mode) + logger.debug("muting mixer") + _toggleAudioMute(mixer.id, muting, mixer.mode) } // look for all controls matching this mixer id (important when it's personal mode + UserMusicInputGroup) - var $controls = $screen.find('.track-icon-mute[mixer-id=' + mixerId +']'); + var $controls = $screen.find('.track-icon-mute[mixer-id="' + mixerId +'"]'); _toggleVisualMuteControl($controls, muting); }); } @@ -1697,13 +2355,13 @@ } - function fillTrackVolumeObject(mixerId, broadcast) { + function fillTrackVolumeObject(mixerId, mode, broadcast) { _updateMixers(); var _broadcast = true; if (broadcast !== undefined) { _broadcast = broadcast; } - var mixer = getMixer(mixerId); + var mixer = getMixer(mixerId, mode); context.trackVolumeObject.clientID = mixer.client_id; context.trackVolumeObject.broadcast = _broadcast; context.trackVolumeObject.master = mixer.master; @@ -1712,10 +2370,16 @@ context.trackVolumeObject.name = mixer.name; context.trackVolumeObject.record = mixer.record; context.trackVolumeObject.volL = mixer.volume_left; - context.trackVolumeObject.volR = mixer.volume_right; + + // today we treat all tracks as mono, but this is required to make a stereo track happy + //context.trackVolumeObject.volR = mixer.volume_right; + context.trackVolumeObject.volR = mixer.volume_left; + + context.trackVolumeObject.loop = mixer.loop; // trackVolumeObject doesn't have a place for range min/max currentMixerRangeMin = mixer.range_low; currentMixerRangeMax = mixer.range_high; + return mixer; } // Given a mixer's min/max and current value, return it as @@ -1750,7 +2414,7 @@ // Given a volume percent (0-100), set the underlying // audio volume level of the passed mixerId to the correct // value. - function setMixerVolume(mixerId, volumePercent) { + function setMixerVolume(mixer, volumePercent) { // The context.trackVolumeObject has been filled with the mixer values // that go with mixerId, and the range of that mixer // has been set in currentMixerRangeMin-Max. @@ -1762,13 +2426,17 @@ context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); // Special case for L2M mix: - if (mixerId === '__L2M__') { + if (mixer.id === '__L2M__') { logger.debug("L2M volumePercent=" + volumePercent); var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent); context.jamClient.SessionSetMasterLocalMix(dbValue); // context.jamClient.SessionSetMasterLocalMix(sliderValue); } else { - context.jamClient.SessionSetControlState(mixerId, sessionModel.getMixMode()); + //var isMediaMixer = mediaTrackGroups.indexOf(mixer.group_id) > -1; + + // if this is a media file (Metronome, JamTrack, BackingTrack, RecordedTrack), then we only modify master + //var mixMode = isMediaMixer ? MIX_MODES.MASTER : sessionModel.getMixMode(); + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); } } @@ -1878,6 +2546,14 @@ $('#recording-status').text("Make a Recording"); } + function lockControlsforJamTrackRecording() { + sessionModel.lockControlsforJamTrackRecording(); + } + + function unlockControlsforJamTrackRecording() { + sessionModel.unlockControlsforJamTrackRecording(); + } + function displayWhoCreated(clientId) { if(app.clientId != clientId) { // don't show to creator sessionModel.findUserBy({clientId: clientId}) @@ -1911,6 +2587,52 @@ .fail(app.ajaxError); } + function checkPendingMetronome() { + logger.debug("checkPendingMetronome", sessionModel.isMetronomeOpen(), getMetronomeMasterMixers().length) + if(sessionModel.isMetronomeOpen() && getMetronomeMasterMixers().length == 0) { + var pendingMetronome = $($templatePendingMetronome.html()) + + // hide the open options + otherAudioFilled(); + // fill out the 'media' name + $('.session-recordings .session-recording-name').text('Metronome') + // and hide the close button + $closePlaybackRecording.hide(); + + // avoid double addition of pending metronome + if($otherAudioContainer.find('.pending-metronome').length === 0) { + $otherAudioContainer.append(pendingMetronome) + } + + } + else { + $('.session-recordings .pending-metronome').remove() + } + + } + + function openBackingTrack(e) { + + if($openBackingTrack.is('.disabled')) { + logger.debug("backing track dialog already open") + return false; + } + + // just ignore the click if they are currently recording for now + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a backing track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } + + $openBackingTrack.addClass('disabled'); + context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback"); + return false; + } + function openJamTrack(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { @@ -1922,11 +2644,194 @@ return false; } - app.layout.showDialog('open-jam-track-dialog'); + app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) { + + // once the dialog is closed, see if the user has a jamtrack selected + if(!data.canceled && data.result.jamTrack) { + + var jamTrack = data.result.jamTrack; + + $('.session-recording-name').text(''); + + // hide 'other audio' placeholder + otherAudioFilled(); + + if(downloadJamTrack) { + // if there was one showing before somehow, destroy it. + logger.warn("destroying existing JamTrack") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null + } + + downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack synchronized; hide widget and show tracks") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null; + + // XXX: test with this removed; it should be unnecessary + context.jamClient.JamTrackStopPlay(); + + if(jamTrack.jmep) + { + logger.debug("setting jmep data") + context.jamClient.JamTrackLoadJmep(jamTrack.id, jamTrack.jmep) + } + else { + logger.debug("no jmep data for jamtrack") + } + + // JamTrackPlay means 'load' + var result = context.jamClient.JamTrackPlay(jamTrack.id); + + if(!result) { + app.notify( + { title: "JamTrack Can Not Open", + text: "Unable to open your JamTrack. Please contact support@jamkazam.com" + }, null, true); + } else { + playJamTrack(jamTrack.id); + } + } + }) + + // show it on the page + $otherAudioContainer.append(downloadJamTrack.root) + + // kick off the download JamTrack process + downloadJamTrack.init() + } + else { + logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) + } + }) return false; } + function playJamTrack(jamTrackId) { + var participantCnt=sessionModel.participants().length + rest.playJamTrack(jamTrackId) + context.stats.write('web.jamtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) + }// function + + function openBackingTrackFile(e) { + + // just ignore the click if they are currently recording for now + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a backing track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } else { + context.jamClient.openBackingTrackFile(sessionModel.backing_track) + context.stats.write('web.backingtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) + //context.JK.CurrentSessionModel.refreshCurrentSession(true); + } + return false; + } + + function unstableNTPClocks() { + var unstable = [] + + // This should be handled in the below loop, actually: + var myState = context.jamClient.getMyNetworkState() + + var map; + + $.each(sessionModel.participants(), function(index, participant) { + + var isSelf = participant.client_id == app.clientId; + + if(isSelf) { + var isStable = myState.ntp_stable; + } + else { + map = context.jamClient.getPeerState(participant.client_id) + var isStable = map.ntp_stable; + } + + if (!isStable) { + var name = participant.user.name; + if (!(name)) { + name = participant.user.first_name + ' ' + participant.user.last_name; + } + + if (isSelf) { + name += " (this computer)" + } + + unstable.push(name) + } + }); + + return unstable + } + + function openMetronome(e) { + // just ignore the click if they are currently recording for now + + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a metronome while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } else { + var unstable = unstableNTPClocks() + if (sessionModel.participants().length > 1 && unstable.length > 0) { + var names = unstable.join(", ") + logger.debug("Unstable clocks: ", names, unstable) + context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' })); + } else { + var data = { + value: 1, + session_size: sessionModel.participants().length, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName } + context.stats.write('web.metronome.open', data) + var bpm = 120; + logger.debug("opening the metronome with bpm: " + bpm + ", sound:" + metroSound) + rest.openMetronome({id: sessionModel.id()}) + .done(function() { + context.jamClient.SessionStopPlay(); + context.jamClient.SessionOpenMetronome(bpm, metroSound, 1, 0); + }) + .fail(function(jqXHR) { + logger.debug(jqXHR, jqXHR) + app.notify({ + "title": "Couldn't open metronome", + "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + } + + + return false; + } + } + + function openRecording(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { @@ -1949,18 +2854,59 @@ if(sessionModel.recordedTracks()) { closeRecording(); } - else if(sessionModel.jamTracks()) { + else if(sessionModel.jamTracks() || downloadJamTrack) { closeJamTrack(); } - else { - logger.error("don't know how to close open media (backing track?)"); + else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) { + closeBackingTrack(); } + else if(getMetronomeMasterMixers().length > 0) { + closeMetronomeTrack(); + } + else { + logger.error("don't know how to close open media"); + } + return false; } + function closeBackingTrack() { + rest.closeBackingTrack({id: sessionModel.id()}) + .done(function() { + //sessionModel.refreshCurrentSession(true); + }) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Close Backing Track", + "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + + // '' closes all open backing tracks + context.jamClient.SessionStopPlay(); + context.jamClient.SessionCloseBackingTrackFile(''); + + return false; + } + function closeJamTrack() { + + logger.debug("closing recording"); + + if(downloadJamTrack) { + logger.debug("closing DownloadJamTrack widget") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null; + + // this is necessary because a syncing widget means no jamtracks are loaded; + // so removing the widget will not cause a backend media change event (and so renderSession will not be called, ultimately) + resetOtherAudioContent(); + } + rest.closeJamTrack({id: sessionModel.id()}) .done(function() { - sessionModel.refreshCurrentSession(); + sessionModel.refreshCurrentSession(true); }) .fail(function(jqXHR) { app.notify({ @@ -1975,10 +2921,30 @@ return false; } + function closeMetronomeTrack() { + rest.closeMetronome({id: sessionModel.id()}) + .done(function() { + context.jamClient.SessionCloseMetronome(); + sessionModel.refreshCurrentSession(true); + }) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Close MetronomeTrack", + "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + return false; + } + function closeRecording() { + logger.debug("closing recording"); + rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) - .done(function() { - sessionModel.refreshCurrentSession(); + .done(function(response) { + //sessionModel.refreshCurrentSession(true); + // update session info + context.JK.CurrentSessionModel.updateSession(response); }) .fail(function(jqXHR) { app.notify({ @@ -1993,9 +2959,12 @@ return false; } - function onPause() { - logger.debug("calling jamClient.SessionStopPlay"); - context.jamClient.SessionStopPlay(); + function onPause(e, data) { + logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached); + + if(!data.endReached) { + context.jamClient.SessionStopPlay(); + } } function onPlay(e, data) { @@ -2004,11 +2973,41 @@ } function onChangePlayPosition(e, data){ - logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")"); - context.jamClient.SessionTrackSeekMs(data.positionMs); + + var seek = data.positionMs; + + if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) { + // if positionMs == 0, then seek it back to whatever the earliest play start is to catch all the prelude + + if(seek == 0) { + var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + seek = duration.start; + } + } + + logger.debug("calling jamClient.SessionTrackSeekMs(" + seek + ")"); + + if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) { + context.jamClient.SessionJamTrackSeekMs(seek); + } + else { + context.jamClient.SessionTrackSeekMs(seek); + } } function startStopRecording() { + + // check first if a jamtrack is loaded, and playing; if so, tell user to stop the play + /**if(sessionModel.jamTracks() && context.jamClient.isSessionTrackPlaying()) { + app.notify( + { title: "Can't Recording a Play JamTrack", + text: "Stop the JamTrack before trying to recording." }, + null, + true); + + return; + }*/ + if(sessionModel.recordingModel.isRecording()) { sessionModel.recordingModel.stopRecording(); } @@ -2022,12 +3021,55 @@ sessionId); inviteMusiciansUtil.loadFriends(); $(friendInput).show(); - } + } - function onMixerModeChanged(e, data) - { + function setFormFromMetronome() { + $("select.metro-tempo").val(metroTempo) + $("select.metro-sound").val(metroSound) + } + + function setMetronomePlaybackMode() { + $metronomePlaybackSelect.metronomeSetPlaybackMode(metroCricket ? 'cricket' : 'self') + } + + function setMetronomeFromForm() { + var tempo = $("select.metro-tempo:visible option:selected").val() + var sound = $("select.metro-sound:visible option:selected").val() + + var t = parseInt(tempo) + var s + if (tempo==NaN || tempo==0 || tempo==null) { + t = 120 + } + + if (sound==null || typeof(sound)=='undefined' || sound=="") { + s = "Beep" + } else { + s = sound + } + + logger.debug("Setting tempo and sound:", t, s) + metroTempo = t + metroSound = s + context.jamClient.SessionSetMetronome(t, s, 1, 0); + } + + function onMetronomeChanged(e, data) { + setMetronomeFromForm() + } + + function metronomePlaybackModeChanged(e, data) { + + var mode = data.playbackMode; // will be either 'self' or 'cricket' + + logger.debug("setting metronome playback mode: ", mode) + + var isCricket = mode == 'cricket'; + context.jamClient.setMetronomeCricketTestState(isCricket); + } + + function onMixerModeChanged(e, data) { $mixModeDropdown.easyDropDown('select', data.mode, true); - setTimeout(renderSession, 1); } @@ -2046,23 +3088,43 @@ return true; } - function events() { + function showFTUEWhenNoInputs( ) { + //app.afterFtue = function() { window.location.reload }; + app.layout.startNewFtue(); + } + + function events() { $('#session-leave').on('click', sessionLeave); $('#session-resync').on('click', sessionResync); $('#session-contents').on("click", '[action="delete"]', deleteSession); - $('#tracks').on('click', 'div[control="mute"]', toggleMute); + $tracksHolder.on('click', 'div[control="mute"]', toggleMute); $('#recording-start-stop').on('click', startStopRecording); $('#open-a-recording').on('click', openRecording); $('#open-a-jamtrack').on('click', openJamTrack); + $openBackingTrack.on('click', openBackingTrack); + $('#open-a-metronome').on('click', openMetronome); $('#session-invite-musicians').on('click', inviteMusicians); $('#session-invite-musicians2').on('click', inviteMusicians); $('#track-settings').click(function() { + + if(gearUtils.isNoInputProfile()) { + // show FTUE + showFTUEWhenNoInputs(); + return false; + } + else { configureTrackDialog.refresh(); configureTrackDialog.showVoiceChatPanel(true); configureTrackDialog.showMusicAudioPanel(true); + } }); - $('#close-playback-recording').on('click', closeOpenMedia); + $openFtue.click(function() { + showFTUEWhenNoInputs(); + return false; + }) + + $closePlaybackRecording.on('click', closeOpenMedia); $(playbackControls) .on('pause', onPause) .on('play', onPlay) @@ -2070,6 +3132,12 @@ $(friendInput).focus(function() { $(this).val(''); }) $(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged) $mixModeDropdown.change(onUserChangeMixMode) + $(document).on("change", ".metronome-select", onMetronomeChanged) + $metronomePlaybackSelect.metronomePlaybackMode().on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, metronomePlaybackModeChanged) + context.JK.helpBubble($metronomePlaybackHelp, 'metromone-playback-modes', {} , {offsetParent: $screen, width:'400px'}); + $(document).on('layout_resized', function() { + resizeFluid(); + }); } this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) { @@ -2080,6 +3148,7 @@ context.jamClient.SetVURefreshRate(150); context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback"); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); + context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback") var screenBindings = { 'beforeShow': beforeShow, @@ -2094,6 +3163,23 @@ $screen = $('#session-screen'); $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); + $otherAudioContainer = $('#session-recordedtracks-container'); + $myTracksNoTracks = $('#session-mytracks-notracks') + $openFtue = $screen.find('.open-ftue-no-tracks') + $myTracksContainer = $('#session-mytracks-container') + $liveTracksContainer = $('#session-livetracks-container'); + $closePlaybackRecording = $('#close-playback-recording') + $openBackingTrack = $('#open-a-backingtrack'); + $metronomePlaybackSelect = $('#metronome-playback-select') + $metronomePlaybackHelp = $('#metronome-playback-help') + $templatePendingMetronome = $('#template-pending-metronome'); + $myTracks = $screen.find('.session-mytracks'); + $liveTracks = $screen.find('.session-livetracks'); + $audioTracks = $screen.find('.session-recordings'); + $fluidTracks = $screen.find('.session-fluidtracks'); + $voiceChat = $screen.find('#voice-chat'); + $tracksHolder = $screen.find('#tracks') + events(); @@ -2121,7 +3207,9 @@ } context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; + context.JK.HandleMetronomeCallback = handleMetronomeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; + context.JK.HandleBackingTrackSelectedCallback = handleBackingTrackSelectedCallback; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 5a02eb429..9662b83ff 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -224,29 +224,11 @@ } if (showJoinLink) { - // wire up the Join Link to the T&Cs dialog + // wire up the Join Link to the T&Cs dialog $('.join-link', $parentRow).click(function(evt) { - if(!context.JK.guardAgainstBrowser(app)) { - return false; - } - - if (!context.JK.JamServer.connected) { - app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); - return false; - } - - gearUtils.guardAgainstInvalidConfiguration(app) - .fail(function() { - app.notify( - { title: "Unable to Join Session", - text: "You can only join a session once you have working audio gear and a tested internet connection." - }) - }) - .done(function(){ - sessionUtils.joinSession(session.id); - }) - - return false; + sessionUtils.ensureValidClient(app, gearUtils, function() { + sessionUtils.joinSession(session.id); + }); }); } } @@ -368,7 +350,7 @@ $('a.more.rsvps', $parentRow).click(toggleRsvps); var showRsvpLink = true; - var noLinkText = ''; + var sessionLinkText = ''; $('.rsvp-link-text', $parentRow).hide(); function showStartSessionButton(scheduledStart) { @@ -380,8 +362,8 @@ if (session.creator.id === context.JK.currentUserId) { showRsvpLink = false; - noLinkText = $('Start session now?'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Start session now?'); + sessionLinkText.find('a').click(function() { ui.launchSessionStartDialog(session); return false; }); @@ -390,18 +372,18 @@ showRsvpLink = false; if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) { - noLinkText = $('Start session now? | Cancel RSVP'); - noLinkText.find('a.start').click(function() { + sessionLinkText = $('Start session now? | Cancel RSVP'); + sessionLinkText.find('a.start').click(function() { ui.launchSessionStartDialog(session); return false; }); } else { - noLinkText = $('Cancel RSVP'); + sessionLinkText = $('Cancel RSVP'); } // wire cancel link - noLinkText.find('a.cancel').click(function() { + sessionLinkText.find('a.cancel').click(function() { ui.launchRsvpCancelDialog(session.id, approvedRsvpId) .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) @@ -419,8 +401,8 @@ showRsvpLink = false; if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) { - noLinkText = $('Start session now?'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Start session now?'); + sessionLinkText.find('a').click(function() { ui.launchSessionStartDialog(session); return false; }); @@ -428,8 +410,8 @@ } else if (pendingRsvpId) { showRsvpLink = false; - noLinkText = $('Cancel RSVP'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Cancel RSVP'); + sessionLinkText.find('a').click(function() { ui.launchRsvpCancelDialog(session.id, pendingRsvpId) .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) @@ -445,11 +427,11 @@ } else if (!openSlots) { showRsvpLink = false; - noLinkText = 'No more openings in this session.'; + sessionLinkText = 'No more openings in this session.'; } else if (!openRsvps && !hasInvitation) { showRsvpLink = false; - noLinkText = 'You need an invitation to RSVP to this session.'; + sessionLinkText = 'You need an invitation to RSVP to this session.'; } if (showRsvpLink) { @@ -472,7 +454,7 @@ }); } else { - $('.rsvp-msg', $parentRow).html(noLinkText).show(); + $('.rsvp-msg', $parentRow).html(sessionLinkText).show(); $('.rsvp-link', $parentRow).hide(); } } diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 92d77d2bb..3c0afa636 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -12,6 +12,7 @@ var ALERT_TYPES = context.JK.ALERT_TYPES; var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; + var gearUtils = context.JK.GearUtilsInstance; var userTracks = null; // comes from the backend var clientId = client.clientID; @@ -34,6 +35,10 @@ var sessionPageEnterTimeout = null; var startTime = null; var joinDeferred = null; + var previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}; + var openBackingTrack = null; + var shownAudioMediaMixerHelp = false; + var controlsLockedForJamTrackRecording = false; var mixerMode = MIX_MODES.PERSONAL; @@ -64,10 +69,23 @@ } } + // if any participant has the metronome open, then we say this session has the metronome open + function isMetronomeOpen() { + var metronomeOpen = false; + context._.each(participants(), function(participant) { + if(participant.metronome_open) { + metronomeOpen = true; + return false; + } + }) + + return metronomeOpen; + } + function isPlayingRecording() { // this is the server's state; there is no guarantee that the local tracks // requested from the backend will have corresponding track information - return currentSession && currentSession.claimed_recording; + return !!(currentSession && currentSession.claimed_recording); } function recordedTracks() { @@ -79,9 +97,74 @@ } } + function recordedBackingTracks() { + if(currentSession && currentSession.claimed_recording) { + return currentSession.claimed_recording.recording.recorded_backing_tracks + } + else { + return null; + } + } + + function backingTracks() { + var backingTracks = [] + // this may be wrong if we loosen the idea that only one person can have a backing track open. + // but for now, the 1st person we find with a backing track open is all there is to find... + context._.each(participants(), function(participant) { + if(participant.backing_tracks.length > 0) { + backingTracks = participant.backing_tracks; + return false; // break + } + }) + return backingTracks; + } + function jamTracks() { if(currentSession && currentSession.jam_track) { - return currentSession.jam_track.tracks + return currentSession.jam_track.tracks.filter(function(track) { return track.track_type == 'Track'}) + } + else { + return null; + } + } + + function recordedJamTracks() { + if(currentSession && currentSession.claimed_recording) { + return currentSession.claimed_recording.recording.recorded_jam_track_tracks + } + else { + return null; + } + } + + function jamTrackName() { + if (currentSession && currentSession.jam_track) { + return currentSession.jam_track.name; + } + else { + return null; + } + } + + function recordedJamTrackName() { + if(currentSession && currentSession.claimed_recording && currentSession.claimed_recording.recording.jam_track) { + return currentSession.claimed_recording.recording.jam_track.name; + } + else { + return null; + } + } + // did I open up the current JamTrack? + function selfOpenedJamTracks() { + return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId) + } + + function backingTrack() { + if(currentSession) { + // TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668 + return { + path: currentSession.backing_track_path + } } else { return null; @@ -106,6 +189,18 @@ return inSession; } + function lockControlsforJamTrackRecording() { + controlsLockedForJamTrackRecording = true; + } + + function unlockControlsforJamTrackRecording() { + controlsLockedForJamTrackRecording = false; + } + + function areControlsLockedForJamTrackRecording() { + return controlsLockedForJamTrackRecording; + } + function onMixerModeChanged(newMixerMode) { mixerMode = newMixerMode; @@ -119,7 +214,8 @@ // see if we already have tracks; if so, we need to run with these var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); - if(inputTracks.length > 0) { + + if(inputTracks.length > 0 || gearUtils.isNoInputProfile() ) { logger.debug("on page enter, tracks are already available") sessionPageEnterDeferred.resolve(inputTracks); var deferred = sessionPageEnterDeferred; @@ -303,6 +399,10 @@ } currentSessionId = null; currentParticipants = {} + previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} + openBackingTrack = null + shownAudioMediaMixerHelp = false + controlsLockedForJamTrackRecording = false; } // you should only update currentSession with this function @@ -321,6 +421,25 @@ } } + function updateSession(response) { + updateSessionInfo(response, null, true); + } + + function updateSessionInfo(response, callback, force) { + if(force === true || currentTrackChanges < response.track_changes_counter) { + logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) + currentTrackChanges = response.track_changes_counter; + sendClientParticipantChanges(currentSession, response); + updateCurrentSession(response); + if(callback != null) { + callback(); + } + } + else { + logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); + } + } + /** * Reload the session data from the REST server, calling * the provided callback when complete. @@ -344,18 +463,7 @@ type: "GET", url: url, success: function(response) { - if(force === true || currentTrackChanges < response.track_changes_counter) { - logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) - currentTrackChanges = response.track_changes_counter; - sendClientParticipantChanges(currentSession, response); - updateCurrentSession(response); - if(callback != null) { - callback(); - } - } - else { - logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); - } + updateSessionInfo(response, callback, force); }, error: function(jqXHR) { if(jqXHR.status != 404) { @@ -532,9 +640,50 @@ return mixerMode == MIX_MODES.PERSONAL; } - function getMixMode() { - return mixerMode; - } + function getMixMode() { + return mixerMode; + } + + function syncTracks(allTracks) { + // double check that we are in session, since a bunch could have happened since then + if(!inSession()) { + logger.debug("dropping queued up sync tracks because no longer in session"); + return null; + } + + if(allTracks === undefined) { + allTracks = context.JK.TrackHelpers.getTrackInfo(context.jamClient); + } + + var inputTracks = allTracks.userTracks; + var backingTracks = allTracks.backingTracks; + var metronomeTracks = allTracks.metronomeTracks; + + // create a trackSync request based on backend data + var syncTrackRequest = {}; + syncTrackRequest.client_id = app.clientId; + syncTrackRequest.tracks = inputTracks; + syncTrackRequest.backing_tracks = backingTracks; + syncTrackRequest.metronome_open = metronomeTracks.length > 0; + syncTrackRequest.id = id(); + + return rest.putTrackSyncChange(syncTrackRequest) + .done(function() { + }) + .fail(function(jqXHR) { + if(jqXHR.status != 404) { + app.notify({ + "title": "Can't Sync Local Tracks", + "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + else { + logger.debug("Unable to sync local tracks because session is gone.") + } + + }) + } function onWebsocketDisconnected(in_error) { // kill the streaming of the session immediately @@ -678,45 +827,35 @@ // wait until we are fully in session before trying to sync tracks to server if(joinDeferred) { joinDeferred.done(function() { - - // double check that we are in session, since a bunch could have happened since then - if(!inSession()) { - logger.debug("dropping queued up sync tracks because no longer in session"); - return; - } - - // this is a local change to our tracks. we need to tell the server about our updated track information - var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); - - // create a trackSync request based on backend data - var syncTrackRequest = {}; - syncTrackRequest.client_id = app.clientId; - syncTrackRequest.tracks = inputTracks; - syncTrackRequest.id = id(); - - rest.putTrackSyncChange(syncTrackRequest) - .done(function() { - }) - .fail(function(jqXHR) { - if(jqXHR.status != 404) { - app.notify({ - "title": "Can't Sync Local Tracks", - "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.", - "icon_url": "/assets/content/icon_alert_big.png" - }); - } - else { - logger.debug("Unable to sync local tracks because session is gone.") - } - - }) + syncTracks(); }) } }, 100); } else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) { - refreshCurrentSession(true); + + var allTracks = context.JK.TrackHelpers.getTrackInfo(context.jamClient); + var backingTracks = allTracks.backingTracks; + var previousBackingTracks = previousAllTracks.backingTracks; + var metronomeTracks = allTracks.metronomeTracks; + var previousMetronomeTracks = previousAllTracks.metronomeTracks; + + // the way we know if backing tracks changes, or recordings are opened, is via this event. + // but we want to report to the user when backing tracks change; so we need to detect change on our own + if(!(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks) { + logger.debug("backing tracks changed", previousBackingTracks, backingTracks) + syncTracks(allTracks); + } + else if(!(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks) { + logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks) + syncTracks(allTracks); + } + else { + refreshCurrentSession(true); + } + + previousAllTracks = allTracks; } else if(inSession() && (text == 'Global Peer Input Mixer Mode')) { setMixerMode(MIX_MODES.MASTER); @@ -729,13 +868,20 @@ // Public interface this.id = id; this.start = start; + this.backingTrack = backingTrack; + this.backingTracks = backingTracks; + this.recordedBackingTracks = recordedBackingTracks; + this.recordedJamTracks = recordedJamTracks; this.setUserTracks = setUserTracks; this.recordedTracks = recordedTracks; + this.jamTrackName = jamTrackName; + this.recordedJamTrackName = recordedJamTrackName; this.jamTracks = jamTracks; this.participants = participants; this.joinSession = joinSession; this.leaveCurrentSession = leaveCurrentSession; this.refreshCurrentSession = refreshCurrentSession; + this.updateSession = updateSession; this.subscribe = subscribe; this.participantForClientId = participantForClientId; this.isPlayingRecording = isPlayingRecording; @@ -748,6 +894,11 @@ this.isMasterMixMode = isMasterMixMode; this.isPersonalMixMode = isPersonalMixMode; this.getMixMode = getMixMode; + this.selfOpenedJamTracks = selfOpenedJamTracks; + this.isMetronomeOpen = isMetronomeOpen; + this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording; + this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording; + this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording; // ALERT HANDLERS this.onBackendMixerChanged = onBackendMixerChanged; @@ -767,6 +918,19 @@ this.getParticipant = function(clientId) { return participantsEverSeen[clientId] }; + this.setBackingTrack = function(backingTrack) { + openBackingTrack = backingTrack; + }; + this.getBackingTrack = function() { + return openBackingTrack; + }; + this.hasShownAudioMediaMixerHelp = function() { + return shownAudioMediaMixerHelp; + } + this.markShownAudioMediaMixerHelp = function() { + shownAudioMediaMixerHelp = true; + } + // call to report if the current user was able to establish audio with the specified clientID this.setAudioEstablished = function(clientId, audioEstablished) { diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index 071f05567..4e3b13594 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -125,7 +125,35 @@ } } + sessionUtils.ensureValidClient = function(app, gearUtils, successCallback) { + + if(!context.JK.guardAgainstBrowser(app)) { + return false; + } + + if (!context.JK.JamServer.connected) { + app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); + return false; + } + + if(context.JK.guardAgainstSinglePlayerProfile(app).canPlay) { + gearUtils.guardAgainstInvalidConfiguration(app) + .fail(function() { + app.notify( + { title: "Unable to Join Session", + text: "You can only join a session once you have working audio gear and a tested internet connection." + }); + }) + .done(function() { + if (successCallback) { + successCallback(); + } + }); + } + } + sessionUtils.joinSession = function(sessionId) { + var hasInvitation = false; var session = null; // we need to do a real-time check of the session in case the settings have diff --git a/web/app/assets/javascripts/shopping_cart.js b/web/app/assets/javascripts/shopping_cart.js index ac02b8b39..86157b96d 100644 --- a/web/app/assets/javascripts/shopping_cart.js +++ b/web/app/assets/javascripts/shopping_cart.js @@ -5,6 +5,7 @@ context.JK.ShoppingCartScreen = function(app) { var logger = context.JK.logger; + var jamTrackUtils = context.JK.JamTrackUtils; var $screen = null; var $content = null; @@ -16,6 +17,10 @@ function afterShow(data) { } + function afterHide() { + jamTrackUtils.checkShoppingCart(); + } + function events() { $screen.find("a.remove-cart").on('click', removeCart); $screen.find("a.proceed-checkout").on('click', proceedCheckout); @@ -25,10 +30,17 @@ e.preventDefault(); if (!context.JK.currentUserId) { - window.location = '/client#/signin'; + window.location = '/client#/checkoutSignin'; } else { - window.location = '/client#/order'; + app.user().done(function(user) { + if(user.has_recurly_account && user.reuse_card) { + window.location = '/client#/checkoutOrder'; + } + else { + window.location = '/client#/checkoutPayment'; + } + }) } } @@ -94,7 +106,8 @@ function initialize() { var screenBindings = { 'beforeShow': beforeShow, - 'afterShow': afterShow + 'afterShow': afterShow, + 'afterHide' : afterHide }; app.bindScreen('shoppingCart', screenBindings); diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index d60d59868..cf733c451 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -13,6 +13,7 @@ var notificationPanel = null; var chatPanel = null; var me = null; + var $sidebar = null; function initializeSearchPanel() { $('#search_text_type').change(function() { @@ -39,7 +40,9 @@ function initializeFriendsPanel() { $('#sidebar-search-header').hide(); - refreshFriends(); + app.user().done(function() { + refreshFriends(); + }) return false; } @@ -406,11 +409,24 @@ me = this; invitationDialog = invitationDialogInstance; textMessageDialog = textMessageDialogInstance; - events(); - initializeSearchPanel(); - initializeFriendsPanel(); - initializeChatPanel(); - initializeNotificationsPanel(); + $sidebar = $('#sidebar-div') + app.user() + .done(function() { + events(); + initializeSearchPanel(); + initializeFriendsPanel(); + initializeChatPanel(); + initializeNotificationsPanel(); + }) + .fail(function(arg1) { + if(arg1 == "not_logged_in") { + $('#search-input').attr('disabled', 'disabled') + $('.sidebar .invite-friend-row').click(function() { + app.layout.showDialog('login-required-dialog') + }); + $sidebar.addClass('not-logged-in') + } + }) }; this.refreshFriends = refreshFriends; diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee new file mode 100644 index 000000000..268d87f0f --- /dev/null +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -0,0 +1,167 @@ +$ = jQuery +context = window +context.JK ||= {}; + +context.JK.SiteValidator = class SiteValidator + + constructor: (site_type) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @site_type = site_type + @input_div = $(".site_validator#"+site_type+"_validator") + @data_input = @input_div.find('input') + @logger = context.JK.logger + @spinner = @input_div.find('span.spinner-small') + @checkmark = @input_div.find('.validate-checkmark') + this.setSiteStatus(null) + this.showFormatStatus() + @is_rec_src = false + @deferred_status_check = null + @is_validating = false + + init: () => + this.renderErrors({}) + @spinner.hide() + validator = this + @data_input.on 'blur', -> + validator.didBlur() + @data_input.on 'focus', -> + validator.showFormatStatus() + @data_input.on 'change', -> + @site_status = null + + dataToValidate: () => + url = @data_input.val() + if 0 < url.length + url.substring(0,2000) + else + null + + showFormatStatus: () => + data = this.dataToValidate() + yn = true + if data && ('url' == @site_type || @is_rec_src) + regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ + yn = regexp.test(this.dataToValidate()) + unless yn + @checkmark.hide() + yn + + didBlur: () => + if this.showFormatStatus() + this.validateSite() + + validateSite: () => + unless data = this.dataToValidate() + return null + this.setSiteStatus(null) + @spinner.show() + @rest.validateUrlSite(data, @site_type) + .done(this.processSiteCheckSucceed) + .fail(this.processSiteCheckFail) + + processSiteCheckSucceed: (response) => + @spinner.hide() + + if 'Valid Site' == response.message + this.setSiteStatus(true) + this.renderErrors({}) + if @deferred_status_check + @deferred_status_check.resolve() + else + this.setSiteStatus(false) + this.renderErrors(response) + if @deferred_status_check + @deferred_status_check.reject() + @deferred_status_check = null + @logger.debug("site_status = "+@site_status) + + processSiteCheckFail: (response) => + @logger.error("site check error") + this.setSiteStatus(false) + if @deferred_status_check + @deferred_status_check.reject() + @deferred_status_check = null + + setSiteStatus: (status) => + @site_status = status + @spinner.hide() + if true == status + @checkmark.show() + else + @checkmark.hide() + + siteIsValid: () => + this.setSiteStatus(true) + + siteIsInvalid: () => + this.setSiteStatus(false) + + renderErrors: (errors) => + errdiv = @input_div.find('.error') + if errmsg = context.JK.format_errors("site", errors) + errdiv.show() + errdiv.html(errmsg) + else + errdiv.hide() + errdiv.html('') + + state: () => + dfr = $.Deferred() + if null == @site_status + @deferred_status_check = dfr + this.validateSite() + else + if true == @site_status + dfr.resolve() + else + dfr.reject() + return dfr.promise() + + +context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator + constructor: (site_type) -> + super(site_type) + @recording_sources = [] + @is_rec_src = true + @add_btn = @input_div.find('a.add-recording-source') + + init: (sources) => + super() + if sources + @recording_sources = sources + @add_btn.on 'click', => + this.attemptAdd() + + processSiteCheckSucceed: (response) => + super(response) + @add_btn.removeClass('disabled') + @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + + processSiteCheckFail: (response) => + super(response) + @add_btn.removeClass('disabled') + + didBlur: () => + # do nothing, validate on add only + + validateSite: () => + @add_btn.addClass('disabled') + super() + + attemptAdd: () => + if data = this.dataToValidate() + unless this.containsRecordingUrl(data) + this.validateSite() + + removeRecordingId: (recording_id) => + start_len = @recording_sources.length + @recording_sources = $.grep @recording_sources, (src_data) -> + src_data['recording_id'] != recording_id + start_len != @recording_sources.length + + containsRecordingUrl: (url) => + vals = $.grep @recording_sources, (src_data) -> + src_data['url'] == url + 0 < vals.length + diff --git a/web/app/assets/javascripts/subscription_utils.js.coffee b/web/app/assets/javascripts/subscription_utils.js.coffee index afd7852ab..b9b3e7823 100644 --- a/web/app/assets/javascripts/subscription_utils.js.coffee +++ b/web/app/assets/javascripts/subscription_utils.js.coffee @@ -84,6 +84,7 @@ class SubscriptionUtils # call subscribe, and use the returned object to listen for events of name context.JK.EVENTS.SUBSCRIBE_NOTIFICATION subscribe: (type, id) => + id = id.toString() key = this.genKey(type, id) @logger.debug("subscribing for any notifications for #{key}") @@ -104,6 +105,7 @@ class SubscriptionUtils # TODO: this should not send a unsubscribe message to the server it's the last listener for the specific type/id combo unsubscribe: (type, id) => + id = id.toString() key = this.genKey(type, id) @logger.debug("unsubscribing for any notifications for #{key}") diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 3215359e2..0bf819d2e 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer @list = @root.find('.list') @logList = @root.find('.log-list') @templateRecordedTrack = $('#template-sync-viewer-recorded-track') + @templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track') @templateStreamMix = $('#template-sync-viewer-stream-mix') @templateMix = $('#template-sync-viewer-mix') @templateNoSyncs = $('#template-sync-viewer-no-syncs') @templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details') @templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track') + @templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track') @templateHoverMix = $('#template-sync-viewer-hover-mix') @templateDownloadReset = $('#template-sync-viewer-download-progress-reset') @templateUploadReset = $('#template-sync-viewer-upload-progress-reset') @templateGenericCommand = $('#template-sync-viewer-generic-command') @templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command') + @templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command') @templateLogItem = $('#template-sync-viewer-log-item') @tabSelectors = @root.find('.dialog-tabs .tab') @tabs = @root.find('.tab-content') @@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer them_upload_soon: 'them-upload-soon' missing: 'missing', me_uploaded: 'me-uploaded', - them_uploaded: 'them-uploaded' + them_uploaded: 'them-uploaded', + not_mine: 'not-mine' } @clientStates = { unknown: 'unknown', @@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer hq: 'hq', sq: 'sq', missing: 'missing', - discarded: 'discarded' + discarded: 'discarded', + not_mine: 'not-mine' } throw "no sync-viewer" if not @root.exists() @@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer $clientRetry.hide() $uploadRetry.hide() + updateBackingTrackState: ($track) => + clientInfo = $track.data('client-info') + serverInfo = $track.data('server-info') + myTrack = serverInfo.user.id == context.JK.currentUserId + + # determine client state + clientStateMsg = 'UNKNOWN' + clientStateClass = 'unknown' + clientState = @clientStates.unknown + + if serverInfo.mine + if serverInfo.download.should_download + if serverInfo.download.too_many_downloads + clientStateMsg = 'EXCESS DOWNLOADS' + clientStateClass = 'error' + clientState = @clientStates.too_many_uploads + else + if clientInfo? + if clientInfo.local_state == 'HQ' + clientStateMsg = 'HIGHEST QUALITY' + clientStateClass = 'hq' + clientState = @clientStates.hq + else if clientInfo.local_state == 'MISSING' + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'DISCARDED' + clientStateClass = 'discarded' + clientState = @clientStates.discarded + else + clientStateMsg = 'NOT MINE' + clientStateClass = 'not_mine' + clientState = @clientStates.not_mine + + # determine upload state + uploadStateMsg = 'UNKNOWN' + uploadStateClass = 'unknown' + uploadState = @uploadStates.unknown + + if serverInfo.mine + if !serverInfo.fully_uploaded + if serverInfo.upload.too_many_upload_failures + uploadStateMsg = 'UPLOAD FAILURE' + uploadStateClass = 'error' + uploadState = @uploadStates.too_many_upload_failures + else + if myTrack + if clientInfo? + if clientInfo.local_state == 'HQ' + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.me_upload_soon + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.them_upload_soon + else + uploadStateMsg = 'UPLOADED' + uploadStateClass = 'uploaded' + if myTrack + uploadState = @uploadStates.me_uploaded + else + uploadState = @uploadStates.them_uploaded + else + uploadStateMsg = 'NOT MINE' + uploadStateClass = 'not_mine' + uploadState = @uploadStates.not_mine + + + $clientState = $track.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + $clientStateProgress = $clientState.find('.progress') + $uploadState = $track.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + $uploadStateProgress = $uploadState.find('.progress') + + $clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass) + $clientStateMsg.text(clientStateMsg) + $clientStateProgress.css('width', '0') + $uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass) + $uploadStateMsg.text(uploadStateMsg) + $uploadStateProgress.css('width', '0') + + # this allows us to make styling decisions based on the combination of both client and upload state. + $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}") + + $clientRetry = $clientState.find('.retry') + $uploadRetry = $uploadState.find('.retry') + + if gon.isNativeClient + # handle client state + + # only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already + if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + $clientRetry.show() + else + $clientRetry.hide() + + # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it + if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon) + $uploadRetry.show() + else + $uploadRetry.hide() + else + $clientRetry.hide() + $uploadRetry.hide() + + associateClientInfo: (recording) => for clientInfo in recording.local_tracks $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") $track.data('client-info', clientInfo) $track.data('total-size', recording.size) + for clientInfo in recording.backing_tracks + $track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") + $track.data('client-info', clientInfo) + $track.data('total-size', recording.size) + $track = @list.find(".mix[data-recording-id='#{recording.recording_id}']") $track.data('client-info', recording.mix) $track.data('total-size', recording.size) @@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer uploadStateClass: uploadStateClass} {variable: 'data'}) - onHoverOfStateIndicator: () -> + displayBackingTrackHover: ($recordedTrack) => + $clientState = $recordedTrack.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + clientStateClass = $clientState.data('custom-class') + clientState = $clientState.attr('data-state') + clientInfo = $recordedTrack.data('client-info') + + $uploadState = $recordedTrack.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + uploadStateClass = $uploadState.data('custom-class') + uploadState = $uploadState.attr('data-state') + serverInfo = $recordedTrack.data('server-info') + + # decide on special case strings first + + summary = '' + if clientState == @clientStates.not_mine && @uploadStates.them_uploaded + # this is not our backing track + summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you." + else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon + # this is not our backing track + summary = "#{serverInfo.user.name} has not yet uploaded their backing track." + else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded + # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon! + summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing." + else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.discarded + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded ) + summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made." + + clientStateDefinition = switch clientState + when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed." + when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track." + when @clientStates.missing then "MISSING means you do not have this backing track anymore." + when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over." + when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track." + else 'There is no help for this state' + + uploadStateDefinition = switch uploadState + when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted." + when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon." + when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this backing track soon." + when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track." + when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track." + when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either." + when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track." + + context._.template(@templateHoverRecordedBackingTrack.html(), + {summary: summary, + clientStateDefinition: clientStateDefinition, + uploadStateDefinition: uploadStateDefinition, + clientStateMsg: $clientStateMsg.text(), + uploadStateMsg: $uploadStateMsg.text(), + clientStateClass: clientStateClass, + uploadStateClass: uploadStateClass} + {variable: 'data'}) + + onTrackHoverOfStateIndicator: () -> $recordedTrack = $(this).closest('.recorded-track.sync') self = $recordedTrack.data('sync-viewer') self.displayTrackHover($recordedTrack) + onBackingTrackHoverOfStateIndicator: () -> + $recordedTrack = $(this).closest('.recorded-backing-track.sync') + self = $recordedTrack.data('sync-viewer') + self.displayBackingTrackHover($recordedTrack) + onStreamMixHover: () -> $streamMix = $(this).closest('.stream-mix.sync') self = $streamMix.data('sync-viewer') @@ -512,6 +709,39 @@ context.JK.SyncViewer = class SyncViewer return false + retryDownloadRecordedBackingTrack: (e) => + $retry = $(e.target) + $track = $retry.closest('.recorded-backing-track') + serverInfo = $track.data('server-info') + + console.log("track serverInfo", $track, serverInfo) + this.sendCommand($retry, { + type: 'recorded_backing_track', + action: 'download' + queue: 'download', + recording_id: serverInfo.recording_id + track_id: serverInfo.client_track_id + }) + + return false + + retryUploadRecordedBackingTrack: (e) => + $retry = $(e.target) + $track = $retry.closest('.recorded-backing-track') + serverInfo = $track.data('server-info') + + console.log("track serverInfo", $track, serverInfo) + + this.sendCommand($retry, { + type: 'recorded_backing_track', + action: 'upload' + queue: 'upload', + recording_id: serverInfo.recording_id + track_id: serverInfo.client_track_id + }) + + return false + createMix: (userSync) => recordingInfo = null if userSync == 'fake' @@ -548,8 +778,26 @@ context.JK.SyncViewer = class SyncViewer $uploadStateRetry.click(this.retryUploadRecordedTrack) context.JK.bindHoverEvents($track) context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); - context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) - context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) + context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) + context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) + $clientState.addClass('is-native-client') if gon.isNativeClient + $uploadState.addClass('is-native-client') if gon.isNativeClient + $track + + createBackingTrack: (userSync) => + $track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'})) + $track.data('server-info', userSync) + $track.data('sync-viewer', this) + $clientState = $track.find('.client-state') + $uploadState = $track.find('.upload-state') + $clientStateRetry = $clientState.find('.retry') + $clientStateRetry.click(this.retryDownloadRecordedBackingTrack) + $uploadStateRetry = $uploadState.find('.retry') + $uploadStateRetry.click(this.retryUploadRecordedBackingTrack) + context.JK.bindHoverEvents($track) + context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); + context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) + context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) $clientState.addClass('is-native-client') if gon.isNativeClient $uploadState.addClass('is-native-client') if gon.isNativeClient $track @@ -687,6 +935,8 @@ context.JK.SyncViewer = class SyncViewer for userSync in response.entries if userSync.type == 'recorded_track' @list.append(this.createTrack(userSync)) + if userSync.type == 'recorded_backing_track' + @list.append(this.createBackingTrack(userSync)) else if userSync.type == 'mix' @list.append(this.createMix(userSync)) else if userSync.type == 'stream_mix' @@ -707,6 +957,8 @@ context.JK.SyncViewer = class SyncViewer for track in @list.find('.recorded-track.sync') this.updateTrackState($(track)) + for track in @list.find('.recorded-backing-track.sync') + this.updateBackingTrackState($(track)) for streamMix in @list.find('.stream-mix.sync') this.updateStreamMixState($(streamMix)) @@ -726,6 +978,18 @@ context.JK.SyncViewer = class SyncViewer deferred.resolve(matchingTrack.data('server-info')) return deferred + resolveBackingTrack: (commandMetadata) => + recordingId = commandMetadata['recording_id'] + clientTrackId = commandMetadata['track_id'] + + matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + if matchingTrack.length == 0 + return @rest.getRecordedBackingTrack({recording_id: recordingId, track_id: clientTrackId}) + else + deferred = $.Deferred(); + deferred.resolve(matchingTrack.data('server-info')) + return deferred + renderFullUploadRecordedTrack: (serverInfo) => $track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'})) $busy = @uploadProgress.find('.busy') @@ -738,6 +1002,18 @@ context.JK.SyncViewer = class SyncViewer $busy.empty().append($track) @downloadProgress.find('.progress').css('width', '0%') + renderFullUploadRecordedBackingTrack: (serverInfo) => + $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'})) + $busy = @uploadProgress.find('.busy') + $busy.empty().append($track) + @uploadProgress.find('.progress').css('width', '0%') + + renderFullDownloadRecordedBackingTrack: (serverInfo) => + $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'})) + $busy = @downloadProgress.find('.busy') + $busy.empty().append($track) + @downloadProgress.find('.progress').css('width', '0%') + # this will either show a generic placeholder, or immediately show the whole track renderDownloadRecordedTrack: (commandId, commandMetadata) => # try to find the info in the list; if we can't find it, then resolve it @@ -756,6 +1032,23 @@ context.JK.SyncViewer = class SyncViewer deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") ) + # this will either show a generic placeholder, or immediately show the whole track + renderDownloadRecordedBackingTrack: (commandId, commandMetadata) => + # try to find the info in the list; if we can't find it, then resolve it + deferred = this.resolveBackingTrack(commandMetadata) + if deferred.state() == 'pending' + this.renderGeneric(commandId, 'download', commandMetadata) + + deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") ) + + renderUploadRecordedBackingTrack: (commandId, commandMetadata) => + # try to find the info in the list; if we can't find it, then resolve it + deferred = this.resolveBackingTrack(commandMetadata) + if deferred.state() == 'pending' + this.renderGeneric(commandId, 'upload', commandMetadata) + + deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") ) + renderGeneric: (commandId, category, commandMetadata) => commandMetadata.displayType = this.displayName(commandMetadata) @@ -794,6 +1087,8 @@ context.JK.SyncViewer = class SyncViewer @downloadProgress.addClass('busy') if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download' this.renderDownloadRecordedTrack(commandId, commandMetadata) + else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download' + this.renderDownloadRecordedBackingTrack(commandId, commandMetadata) else this.renderGeneric(commandId, 'download', commandMetadata) else if commandMetadata.queue == 'upload' @@ -803,6 +1098,8 @@ context.JK.SyncViewer = class SyncViewer @uploadProgress.addClass('busy') if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload' this.renderUploadRecordedTrack(commandId, commandMetadata) + else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload' + this.renderUploadRecordedBackingTrack(commandId, commandMetadata) else this.renderGeneric(commandId, 'upload', commandMetadata) else if commandMetadata.queue == 'cleanup' @@ -820,6 +1117,12 @@ context.JK.SyncViewer = class SyncViewer $track.data('server-info', userSync) this.associateClientInfo(clientRecordings.recordings[0]) this.updateTrackState($track) + else if userSync.type == 'recorded_backing_track' + $track = @list.find(".sync[data-id='#{userSync.id}']") + continue if $track.length == 0 + $track.data('server-info', userSync) + this.associateClientInfo(clientRecordings.recordings[0]) + this.updateBackingTrackState($track) else if userSync.type == 'mix' # check if there is a virtual mix 1st; if so, update it $mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']") @@ -839,20 +1142,6 @@ context.JK.SyncViewer = class SyncViewer updateSingleRecording: (recording_id) => @rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording) - updateSingleRecordedTrack: ($track) => - serverInfo = $track.data('server-info') - @rest.getUserSync({user_sync_id: serverInfo.id}) - .done((userSync) => - # associate new server-info with this track - $track.data('server-info', userSync) - # associate new client-info with this track - clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSync.recording]) - this.associateClientInfo(clientRecordings.recordings[0]) - - this.updateTrackState($track) - ) - .fail(@app.ajaxError) - updateProgressOnSync: ($track, queue, percentage) => state = if queue == 'upload' then '.upload-state' else '.client-state' $progress = $track.find("#{state} .progress") @@ -892,10 +1181,10 @@ context.JK.SyncViewer = class SyncViewer $progress = @downloadProgress.find('.progress') $progress.css('width', percentage + '%') - if @downloadMetadata.type == 'recorded_track' + if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track' clientTrackId = @downloadMetadata['track_id'] recordingId = @downloadMetadata['recording_id'] - $matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") if $matchingTrack.length > 0 this.updateProgressOnSync($matchingTrack, 'download', percentage) @@ -903,10 +1192,10 @@ context.JK.SyncViewer = class SyncViewer $progress = @uploadProgress.find('.progress') $progress.css('width', percentage + '%') - if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload' + if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_track') and @uploadMetadata.action == 'upload' clientTrackId = @uploadMetadata['track_id'] recordingId = @uploadMetadata['recording_id'] - $matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") if $matchingTrack.length > 0 this.updateProgressOnSync($matchingTrack, 'upload', percentage) else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload' @@ -977,15 +1266,15 @@ context.JK.SyncViewer = class SyncViewer this.logResult(data.commandMetadata, false, data.commandReason, true) displayName: (metadata) => - if metadata.type == 'recorded_track' && metadata.action == 'download' + if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download' return 'DOWNLOADING TRACK' - else if metadata.type == 'recorded_track' && metadata.action == 'upload' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'upload' return 'UPLOADING TRACK' else if metadata.type == 'mix' && metadata.action == 'download' return 'DOWNLOADING MIX' - else if metadata.type == 'recorded_track' && metadata.action == 'convert' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert' return 'COMPRESSING TRACK' - else if metadata.type == 'recorded_track' && metadata.action == 'delete' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete' return 'CLEANUP TRACK' else if metadata.type == 'stream_mix' && metadata.action == 'upload' return 'UPLOADING STREAM MIX' diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index 6b30035e0..b434d80a9 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -14,9 +14,28 @@ // take all necessary arguments to complete its work. context.JK.TrackHelpers = { - getTracks: function(jamClient, groupId) { + getTrackInfo: function(jamClient) { + + var allTracks = context.jamClient.SessionGetAllControlState(true); + + var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, allTracks); + var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, allTracks); + var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, 12); + + return { + userTracks: userTracks, + backingTracks: backingTracks, + metronomeTracks: metronomeTracks + } + }, + + // allTracks is the result of SessionGetAllControlState; as an optimization + getTracks: function(jamClient, groupId, allTracks) { var tracks = []; - var allTracks = context.jamClient.SessionGetAllControlState(true); + + if(!allTracks) { + allTracks = context.jamClient.SessionGetAllControlState(true); + } //var trackIds = jamClient.SessionGetIDs(); //var allTracks = jamClient.SessionGetControlState(trackIds, true); @@ -30,17 +49,38 @@ return tracks; }, + // allTracks is the result of SessionGetAllControlState; as an optimization + getBackingTracks: function(jamClient, allTracks) { + var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4, allTracks); + + var backingTracks = [] + context._.each(mediaTracks, function(mediaTrack) { + // the check for 'not managed' means this is not a track opened by a recording, basically + // we do not try and sync these sorts of backing tracks to the server, because they + // are already encompassed by + if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) { + var track = {}; + track.client_track_id = mediaTrack.persisted_track_id; + track.client_resource_id = mediaTrack.rid; + track.filename = mediaTrack.filename; + backingTracks.push(track); + } + }) + + return backingTracks; + }, + /** * This function resolves which tracks to configure for a user * when creating or joining a session. By default, tracks are pulled * from jamClient. If none exist there, the first instrument from the * user's profile is used. */ - getUserTracks: function(jamClient) { + getUserTracks: function(jamClient, allTracks) { var localMusicTracks = []; var i; - localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2); + localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2, allTracks); var trackObjects = []; diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 9213c43cb..01f204bfb 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -6,6 +6,7 @@ context.JK.UIHelper = function(app) { var logger = context.JK.logger; var rest = new context.JK.Rest(); + var sessionUtils = context.JK.SessionUtils; function addSessionLike(sessionId, userId, $likeCountSelector, $likeButtonSelector) { rest.addSessionLike(sessionId, userId) @@ -54,9 +55,11 @@ } function launchSessionStartDialog(session) { - var sessionStartDialog = new JK.SessionStartDialog(JK.app, session); - sessionStartDialog.initialize(); - return sessionStartDialog.showDialog(); + sessionUtils.ensureValidClient(app, context.JK.GearUtils, function() { + var sessionStartDialog = new JK.SessionStartDialog(JK.app, session); + sessionStartDialog.initialize(); + return sessionStartDialog.showDialog(); + }); } this.addSessionLike = addSessionLike; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 052b7d20e..b91ab71af 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -128,12 +128,17 @@ } } else { - var $template = $('#template-help-' + templateName) - if($template.length == 0) { - var helpText = templateName; + try { + var $template = $('#template-help-' + templateName) + if ($template.length == 0) { + var helpText = templateName; + } + else { + var helpText = context._.template($template.html(), data, { variable: 'data' }); + } } - else { - var helpText = context._.template($template.html(), data, { variable: 'data' }); + catch(e) { + var helpText = templateName; } holder = $('
    '); @@ -616,11 +621,12 @@ } // returns Fri May 20, 2013 - context.JK.formatDate = function (dateString) { + context.JK.formatDate = function (dateString, suppressDay) { var date = new Date(dateString); - return days[date.getDay()] + ' ' + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); + return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); } + context.JK.formatDateYYYYMMDD = function(dateString) { var date = new Date(dateString); return date.getFullYear() + '-' + context.JK.padString((date.getMonth() + 1).toString(), 2) + '-' + context.JK.padString(date.getDate(), 2); @@ -910,6 +916,10 @@ return null; } + context.JK.makeAbsolute = function(path) { + return window.location.protocol + '//' + window.location.host + path; + } + context.JK.popExternalLinks = function ($parent) { if(!$parent) $parent = $('body'); @@ -972,6 +982,19 @@ }) } + + context.JK.flash = function(msg, options) { + options = options || {} + + var $flash = $(context._.template($('#template-flash-notice').html(), {}, { variable: 'data' })); + $flash.find('.flash-content').html(msg); + $('body').prepend($flash) + if(options.hide) { + // slide down (take 1 sec to do it), sit for 5, then leave over 1 second + setTimeout(function() {$flash.slideUp(1000, 'swing') }, options.hide * 1000) + } + } + context.JK.hasFlash = function () { var hasFlash = false; @@ -987,6 +1010,15 @@ return hasFlash; } + context.JK.getNameOfFile = function(filename) { + + var index = filename.lastIndexOf('/'); + if(index == -1) { + index = filename.lastIndexOf('\\'); + } + return index == -1 ? filename : filename.substring(index + 1, filename.length) + } + context.JK.hasOneConfiguredDevice = function () { var result = context.jamClient.FTUEGetGoodConfigurationList(); logger.debug("hasOneConfiguredDevice: ", result); @@ -1080,7 +1112,7 @@ context.JK.guardAgainstBrowser = function(app, args) { if(!gon.isNativeClient) { - logger.debug("guarding against normal browser on screen thaht requires native client") + logger.debug("guarding against normal browser on screen that requires native client") app.layout.showDialog('launch-app-dialog', args) .one(EVENTS.DIALOG_CLOSED, function() { if(args && args.goHome) { @@ -1093,6 +1125,111 @@ return true; } + context.JK.guardAgainstSinglePlayerProfile = function(app, beforeCallback) { + + var canPlayWithOthers = context.JK.GearUtilsInstance.canPlayWithOthers(); + + if(!canPlayWithOthers.canPlay) { + logger.debug("guarding against single player profile") + + var $dialog = app.layout.showDialog('single-player-profile-dialog'); + + // so that callers can check dialog result + canPlayWithOthers.dialog = $dialog; + + // allow callers to take action before default behavior + if(beforeCallback) { + $dialog.one(EVENTS.DIALOG_CLOSED, beforeCallback); + } + + $dialog.one(EVENTS.DIALOG_CLOSED, function(e, data) { + + if(!data.canceled) { + if(data.result.choice == 'private_session') { + var data = { + createType: 'quick-start', + timezone: {}, + recurring_mode: {}, + language: {}, + band: {}, + musician_access: {}, + fans_access: {}, + rsvp_slots: [], + open_rsvps: false + }; + + context.JK.privateSessionSettings(data) + + context.JK.createSession(app, data) + .done(function(response) { + var sessionId = response.id; + + context.JK.GA.trackSessionCount(true, true, 0); + + // we redirect to the session screen, which handles the REST call to POST /participants. + logger.debug("joining session screen: " + sessionId) + context.location = '/client#/session/' + sessionId; + }) + .fail(function(jqXHR) { + logger.debug("unable to schedule a private session") + app.notifyServerError(jqXHR, "Unable to schedule a private session"); + }) + } + else if(data.result.choice == 'gear_setup') { + window.location = '/client#/account/audio' + } + else + { + logger.error("unknown choice: " + data.result.choice) + alert("unknown choice: " + data.result.choice) + } + } + }) + } + + return canPlayWithOthers; + } + + context.JK.createSession = function(app, data) { + + // auto pick an 'other' instrument + var otherId = context.JK.server_to_client_instrument_map.Other.server_id; // get server ID + var otherInstrumentInfo = context.JK.instrument_id_to_instrument[otherId]; // get display name + var beginnerLevel = 1; // default to beginner + var instruments = [ {id: otherId, name: otherInstrumentInfo.display, level: beginnerLevel} ]; + $.each(instruments, function(index, instrument) { + var slot = {}; + slot.instrument_id = instrument.id; + slot.proficiency_level = instrument.level; + slot.approve = true; + data.rsvp_slots.push(slot); + }); + + data.isUnstructuredRsvp = true; + + return rest.createScheduledSession(data) + } + + context.JK.privateSessionSettings = function(createSessionSettings) { + createSessionSettings.genresValues = ['Pop']; + createSessionSettings.genres = ['pop']; + createSessionSettings.timezone = 'Central Time (US & Canada),America/Chicago' + createSessionSettings.name = "Private Test Session"; + createSessionSettings.description = "Private session set up just to test things out in the session interface by myself."; + createSessionSettings.notations = []; + createSessionSettings.language = 'eng' + createSessionSettings.legal_policy = 'Standard'; + createSessionSettings.musician_access = false + createSessionSettings.fan_access = false + createSessionSettings.fan_chat = false + createSessionSettings.approval_required = false + createSessionSettings.legal_terms = true + createSessionSettings.recurring_mode = 'once'; + createSessionSettings.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false); + createSessionSettings.duration = "60"; + createSessionSettings.open_rsvps = false + createSessionSettings.rsvp_slots = []; + } /* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. diff --git a/web/app/assets/javascripts/web/congratulations.js b/web/app/assets/javascripts/web/congratulations.js index 576759f17..31ddf40d4 100644 --- a/web/app/assets/javascripts/web/congratulations.js +++ b/web/app/assets/javascripts/web/congratulations.js @@ -8,13 +8,6 @@ if(musician) { context.JK.Downloads.listClients(true); } - - if(registrationType) { - $(function() { - // ga() object isn't ready until the page is loaded - context.JK.GA.trackRegister(musician, registrationType); - }); - } } context.congratulations = congratulations; diff --git a/web/app/assets/javascripts/web/downloads.js b/web/app/assets/javascripts/web/downloads.js index 82ef0a1da..6f01037e5 100644 --- a/web/app/assets/javascripts/web/downloads.js +++ b/web/app/assets/javascripts/web/downloads.js @@ -13,7 +13,7 @@ var platformName; // mac, windows, linux var platformDisplay; // Mac, Windows, Linux var platform = selectedPlatform; //MacOSX, Win32, Unix - var platformName1, platformName2, platform1, platform2; + var platformName1, platformName2, platform1, platform2, platformDisplay1, platformDisplay2; var uri = downloadUris[selectedPlatform]; // prepare template varaibles @@ -21,21 +21,27 @@ platformName = "linux"; platformDisplay = "Linux" platformName1 = "mac"; + platformDisplay1 = "Mac"; platformName2 = "windows"; + platformDisplay2 = "Windows"; platform1 = "MacOSX"; platform2 = "Win32" } else if(selectedPlatform == "Win32") { platformName = "windows"; platformDisplay = "Windows"; platformName1 = "mac"; + platformDisplay1 = "Mac"; platformName2 = "linux" + platformDisplay2 = "Linux"; platform1 = "MacOSX"; platform2 = "Unix"; } else if(selectedPlatform == "MacOSX") { platformName = "mac"; platformDisplay = "Mac"; platformName1 = "windows"; + platformDisplay1 = "Windows"; platformName2 = "linux"; + platformDisplay2 = "Linux"; platform1 = "Win32"; platform2 = "Unix"; } @@ -48,7 +54,9 @@ platformName : platformName, platformDisplay : platformDisplay, platformName1 : platformName1, + platformDisplay1 : platformDisplay1, platformName2 : platformName2, + platformDisplay2 : platformDisplay2, platform1: platform1, platform2: platform2, uri : uri ? uri : '#', @@ -105,9 +113,20 @@ function removeSpinner() { $('body.web .spinner-large').remove(); } + + function flashCongratulations() { + context.JK.flash('Congratulations!
    Your account is ready.', {hide:10}) + } function listClients(congratulations) { isCongratulations = congratulations; + if(isCongratulations) { + flashCongratulations(); + } + else { + //flashCongratulations(); + } + var rest = context.JK.Rest(); var currentOS = context.JK.detectOS(); var downloads = $('.downloads'); diff --git a/web/app/assets/javascripts/web/home.js b/web/app/assets/javascripts/web/home.js new file mode 100644 index 000000000..5eb7c1ed1 --- /dev/null +++ b/web/app/assets/javascripts/web/home.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + + function initialize() { + if(gon.signed_in) { + window.location = "/client#/home" + } + } + context.JK.HomePage = initialize; + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js new file mode 100644 index 000000000..0de4ae766 --- /dev/null +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -0,0 +1,62 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.IndividualJamTrack = function (app) { + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $page = null; + var $jamtrack_name = null; + var $previews = null; + var $jamTracksButton = null; + var $genericHeader = null; + var $individualizedHeader = null; + + function fetchJamTrack() { + rest.getJamTrack({plan_code: gon.jam_track_plan_code}) + .done(function (jam_track) { + logger.debug("jam_track", jam_track) + + if(!gon.just_previews) { + if (gon.generic) { + $genericHeader.removeClass('hidden'); + } + else { + $individualizedHeader.removeClass('hidden') + $jamtrack_name.text(jam_track.name); + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + } + } + + context._.each(jam_track.tracks, function (track) { + + var $element = $('
    ') + + $previews.append($element); + + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + }) + + $previews.append('
    ') + }) + .fail(function () { + app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) + }) + + } + function initialize() { + + $page = $('body') + $jamtrack_name = $page.find('.jamtrack_name') + $previews = $page.find('.previews') + $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') + $genericHeader = $page.find('h1.generic') + $individualizedHeader = $page.find('h1.individualized') + fetchJamTrack(); + } + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band.js new file mode 100644 index 000000000..d0a48dcb5 --- /dev/null +++ b/web/app/assets/javascripts/web/individual_jamtrack_band.js @@ -0,0 +1,60 @@ + +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.IndividualJamTrackBand = function (app) { + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $page = null; + var $jamTrackBandInfo = null; + var $jamTrackNoun = null; + var $previews = null; + var $jamTracksButton = null; + var $checkItOut = null; + + function fetchJamTrack() { + rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) + .done(function (jam_track) { + logger.debug("jam_track", jam_track) + + $jamTrackBandInfo.text(jam_track.band_jam_track_count + ' ' + jam_track.original_artist); + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + + if(jam_track.band_jam_track_count == 1) { + $jamTrackNoun.text('JamTrack') + $checkItOut.text(', Check It Out!') + } + context._.each(jam_track.tracks, function (track) { + + var $element = $('
    ') + + $previews.append($element); + + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + }) + + $previews.append('
    ') + }) + .fail(function () { + app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) + }) + + } + function initialize() { + + $page = $('body') + $jamTrackBandInfo = $page.find('.jamtrack_band_info') + $previews = $page.find('.previews') + $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') + $jamTrackNoun = $page.find('.jamtrack_noun') + $checkItOut = $page.find('.check-it-out') + + fetchJamTrack(); + } + + this.initialize = initialize; + } +})(window, jQuery); diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 466533efd..7f128d092 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -20,6 +20,7 @@ //= require jquery.icheck //= require jquery.bt //= require jquery.exists +//= require howler.core.js //= require AAA_Log //= require AAC_underscore //= require alert @@ -52,6 +53,7 @@ //= require recording_utils //= require helpBubbleHelper //= require facebook_rest +//= require jam_track_preview //= require landing/init //= require landing/signup //= require web/downloads @@ -59,7 +61,9 @@ //= require web/sessions //= require web/session_info //= require web/recordings -//= require web/welcome +//= require web/home +//= require web/individual_jamtrack +//= require web/individual_jamtrack_band //= require fakeJamClient //= require fakeJamClientMessages //= require fakeJamClientRecordings diff --git a/web/app/assets/javascripts/wizard/gear/gear_wizard.js b/web/app/assets/javascripts/wizard/gear/gear_wizard.js index f889aaffd..0293fa44c 100644 --- a/web/app/assets/javascripts/wizard/gear/gear_wizard.js +++ b/web/app/assets/javascripts/wizard/gear/gear_wizard.js @@ -179,6 +179,10 @@ wizard.setBackState(enabled); } + function moveToNext() { + wizard.moveToNext(); + } + function setChosenInputs(_inputs) { inputs = _inputs; } @@ -226,6 +230,7 @@ this.getChosenInputs = getChosenInputs; this.setNextState = setNextState; this.setBackState = setBackState; + this.moveToNext = moveToNext; this.initialize = initialize; this.createFTUEProfile = createFTUEProfile; this.getWizard = function() {return wizard; } diff --git a/web/app/assets/javascripts/wizard/gear/step_network_test.js b/web/app/assets/javascripts/wizard/gear/step_network_test.js index 008c2409a..f2b019b14 100644 --- a/web/app/assets/javascripts/wizard/gear/step_network_test.js +++ b/web/app/assets/javascripts/wizard/gear/step_network_test.js @@ -9,7 +9,8 @@ var logger = context.JK.logger; var networkTest = new context.JK.NetworkTest(app); var $step = null; - + // if not null and with in say 5 seconds, then the user is 'NEXT'ing too quickly. slow them down + var clickFastTime = null; function getLastNetworkFailAnalytics() { return networkTest.getLastNetworkFailure(); @@ -36,13 +37,29 @@ initializeBackButtonState(); } function initializeNextButtonState() { - dialog.setNextState(networkTest.hasScoredNetworkSuccessfully() && !networkTest.isScoring()); + dialog.setNextState(!networkTest.isScoring()); } function initializeBackButtonState() { dialog.setBackState(!networkTest.isScoring()); } + function handleNext() { + // if we don't have a valid score, and if it's been less than 5 seconds since we've shown this step, slow the user down + if (context.jamClient.GetNetworkTestScore() < 1 && userIsFastNexting()) { + context.JK.Banner.showYesNo({ + html: "By clicking NEXT and skipping the test, you won't be able to play online in real-time sessions with others. Is this OK?", + yes: function() { + dialog.moveToNext(); + }}); + + return false; + } + else { + return true; + } + } + function handleHelp() { return "https://jamkazam.desk.com/customer/portal/articles/1716139-what-to-do-if-you-cannot-pass-the-network-test" //return "https://jamkazam.desk.com/customer/portal/articles/1599969-first-time-setup---step-6---test-your-network"; @@ -57,6 +74,16 @@ networkTest.haltScoring(); networkTest.cancel(); updateButtons(); + watchForFastNexting(); + } + + // fast nexting is a someone hitting next very quickly + function watchForFastNexting() { + clickFastTime = new Date(); + } + + function userIsFastNexting() { + return new Date().getTime() - clickFastTime.getTime() < 5000 } function beforeHide() { @@ -77,6 +104,7 @@ this.handleHelp = handleHelp; this.newSession = newSession; this.beforeHide = beforeHide; + this.handleNext = handleNext; this.beforeShow = beforeShow; this.initialize = initialize; this.getLastNetworkFailAnalytics = getLastNetworkFailAnalytics; diff --git a/web/app/assets/javascripts/wizard/gear/step_select_gear.js b/web/app/assets/javascripts/wizard/gear/step_select_gear.js index 9479f86c9..bd67be896 100644 --- a/web/app/assets/javascripts/wizard/gear/step_select_gear.js +++ b/web/app/assets/javascripts/wizard/gear/step_select_gear.js @@ -46,6 +46,8 @@ var $templateDeviceNotValid = null; var $resyncStatus = null; var $resyncStatusText = null; + var $latencyScoreBox = null; + var $highLatencyNotice = null; var operatingSystem = null; @@ -579,6 +581,11 @@ function initializeResync() { $resyncBtn.unbind('click').click(function () { + if($highLatencyNotice) { + $highLatencyNotice.btOff() + $highLatencyNotice = null; + } + scheduleRescanSystem(function() { if (getSelectedInputs().length > 0 && getSelectedOutputs().length == 2) { logger.debug("after rescan, ready to attempt score") @@ -946,6 +953,21 @@ queueUpdateDeviceList = false; updateDeviceList(); } + + if(!data.validLatencyScore) { + if (selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) { + prodUserAboutHighLatency($latencyScoreBox, 'asio') + } + else if (selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) { + prodUserAboutHighLatency($latencyScoreBox, 'asio') + } + else if (selectedDeviceInfo.input.info.type == 'MacOSX_builtin' || selectedDeviceInfo.output.info.type == 'MacOSX_builtin') { + prodUserAboutHighLatency($latencyScoreBox, 'macosx-builtin') + } + else { + prodUserAboutHighLatency($latencyScoreBox, 'generic') + } + } } function getLastAudioTestFailAnalytics() { @@ -962,6 +984,13 @@ } } + function prodUserAboutHighLatency($btn, additional) { + + setTimeout(function() { + $highLatencyNotice = context.JK.prodBubble($btn, 'high-latency-notice', {additional: additional}, {duration: 20000, width:'400px', positions:['top']}); + }, 300) + } + function prodUserToTweakASIOSettings($btn) { setTimeout(function() { context.JK.prodBubble($btn, 'tweak-asio-settings', {}, {positions:['top']}); @@ -972,19 +1001,11 @@ renderScoringStopped(); gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true); - if(data.reason == "latency") { + if(data.reason == "io") { - console.log("selectedDeviceInfo", selectedDeviceInfo) - if(selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) { - prodUserToTweakASIOSettings($asioInputControlBtn) - } - else if(selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) { - prodUserToTweakASIOSettings($asioOutputControlBtn) - } - storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore); - } - else if(data.reason = "io") { + //storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore); + if(data.ioTarget == 'bad') { storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioTarget, data.ioTargetScore); } @@ -1210,6 +1231,7 @@ $instructions = $step.find('.instructions'); $resyncStatus = $step.find('.resync-status'); $resyncStatusText = $step.find('.resynctext'); + $latencyScoreBox = $step.find('.latency-score-section') operatingSystem = context.JK.GetOSAsString(); frameBuffers.initialize($knobs); $(frameBuffers) diff --git a/web/app/assets/javascripts/wizard/gear_test.js b/web/app/assets/javascripts/wizard/gear_test.js index fc397c2f9..7e7f2b352 100644 --- a/web/app/assets/javascripts/wizard/gear_test.js +++ b/web/app/assets/javascripts/wizard/gear_test.js @@ -52,7 +52,7 @@ var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid function isGoodFtue() { - return validLatencyScore && validIOScore && !asynchronousInvalidDevice; + return validIOScore && !asynchronousInvalidDevice; } function processIOScore(io) { @@ -90,7 +90,7 @@ // now base the overall IO score based on both values. - $self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass}) + $self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass, validLatencyScore: validLatencyScore}) //renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); if(aggregrateIOClass == "bad") { @@ -103,10 +103,10 @@ scoring = false; if(isGoodFtue()) { - $self.triggerHandler(GEAR_TEST_DONE) + $self.triggerHandler(GEAR_TEST_DONE, {validLatencyScore: validLatencyScore}) } else { - $self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std}); + $self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std, validLatencyScore: validLatencyScore}); } } @@ -182,11 +182,10 @@ updateScoreReport(latency, refocused); - // if there was a valid latency score, go on to the next step - if (validLatencyScore) { + if (true || validLatencyScore) { $self.triggerHandler(GEAR_TEST_IO_START); // reuse valid IO score if this is on refocus - if(refocused && validIOScore) { + if(false && (refocused && validIOScore)) { processIOScore(ioScore); } else { @@ -215,12 +214,12 @@ } else { scoring = false; - $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', latencyScore: latencyScore.latency}) + $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', validLatencyScore: validLatencyScore, latencyScore: latencyScore.latency}) } }) .fail(function(ftueSaveResult) { scoring = false; - $self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', data: ftueSaveResult}) + $self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', validLatencyScore: validLatencyScore, data: ftueSaveResult}) }) }, 250); } diff --git a/web/app/assets/javascripts/wizard/gear_utils.js b/web/app/assets/javascripts/wizard/gear_utils.js index c9d67f533..051ca1949 100644 --- a/web/app/assets/javascripts/wizard/gear_utils.js +++ b/web/app/assets/javascripts/wizard/gear_utils.js @@ -14,6 +14,8 @@ var VOICE_CHAT = context.JK.VOICE_CHAT; var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; var EVENTS = context.JK.EVENTS; + var SYSTEM_DEFAULT_PLAYBACK_ONLY = 'System Default (Playback Only)'; + context.JK.GearUtilsInstance = gearUtils; @@ -28,12 +30,42 @@ return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0; } + // to play with others, you have to have inputs, + // as well have a score below 20 ms + gearUtils.canPlayWithOthers = function(profile) { - gearUtils.createProfileName = function(deviceInfo, chatName) { + var isNoInputProfile = gearUtils.isNoInputProfile(profile); + var expectedLatency = context.jamClient.FTUEGetExpectedLatency(); + var audioLatency = expectedLatency ? expectedLatency.latency : null; + var highLatency = audioLatency > 20; + var networkScore = context.jamClient.GetNetworkTestScore(); + var badNetworkScore = networkScore < 2; + + return { + canPlay: !isNoInputProfile && !highLatency, + isNoInputProfile: isNoInputProfile, + badNetworkScore: badNetworkScore, + highLatency: highLatency, + audioLatency: audioLatency, + networkScore: networkScore, + } + } + + gearUtils.isNoInputProfile = function(profile) { + if (profile === undefined) { + profile = context.jamClient.FTUEGetMusicProfileName(); + } + + if(profile == SYSTEM_DEFAULT_PLAYBACK_ONLY) { + return true; + } + } + + gearUtils.createProfileName = function (deviceInfo, chatName) { var isSameInOut = deviceInfo.input.id == deviceInfo.output.id; var name = null; - if(isSameInOut) { + if (isSameInOut) { name = "In/Out: " + deviceInfo.input.info.displayName; } else { @@ -45,19 +77,19 @@ } - gearUtils.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) { + gearUtils.selectedDeviceInfo = function (audioInputDeviceId, audioOutputDeviceId, deviceInformation) { - if(!audioInputDeviceId) { + if (!audioInputDeviceId) { logger.debug("gearUtils.selectedDeviceInfo: no active input device"); return null; } - if(!audioOutputDeviceId) { + if (!audioOutputDeviceId) { logger.debug("gearUtils.selectedDeviceInfo: no active output device"); return null; } - if(!deviceInformation) { + if (!deviceInformation) { deviceInformation = gearUtils.loadDeviceInfo(); } @@ -81,7 +113,7 @@ } } - gearUtils.loadDeviceInfo = function() { + gearUtils.loadDeviceInfo = function () { var operatingSystem = context.JK.GetOSAsString(); // should return one of: @@ -128,6 +160,10 @@ return; } + if (device.name == "JamKazam Virtual Input") { + return; + } + var deviceInfo = {}; deviceInfo.id = device.guid; @@ -145,22 +181,22 @@ return loadedDevices; } - gearUtils.updateDefaultBuffers = function(selectedDeviceInfo, frameBuffers) { + gearUtils.updateDefaultBuffers = function (selectedDeviceInfo, frameBuffers) { function hasWDMAssociated() { - return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') + return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') } function hasASIOAssociated() { - return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_asio' || selectedDeviceInfo.output.info.type == 'Win32_asio') + return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_asio' || selectedDeviceInfo.output.info.type == 'Win32_asio') } // handle specific framesize settings - if(hasWDMAssociated() || hasASIOAssociated()) { + if (hasWDMAssociated() || hasASIOAssociated()) { var framesize = frameBuffers.selectedFramesize(); - if(framesize == 2.5) { + if (framesize == 2.5) { // if there is a WDM device, start off at 1/1 due to empirically observed issues with 0/0 - if(hasWDMAssociated()) { + if (hasWDMAssociated()) { logger.debug("setting default buffers to 1/1"); frameBuffers.setBufferIn('1'); frameBuffers.setBufferOut('1'); @@ -172,15 +208,15 @@ frameBuffers.setBufferOut('0'); } } - else if(framesize == 5) { + else if (framesize == 5) { logger.debug("setting default buffers to 3/2"); frameBuffers.setBufferIn('3'); frameBuffers.setBufferOut('2'); } else { - logger.debug("setting default buffers to 6/5"); - frameBuffers.setBufferIn('6'); - frameBuffers.setBufferOut('5'); + logger.debug("setting default buffers to 2/2"); + frameBuffers.setBufferIn('2'); + frameBuffers.setBufferOut('2'); } } else { @@ -193,7 +229,7 @@ context.jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut()); } - gearUtils.ftueSummary = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) { + gearUtils.ftueSummary = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) { return { os: operatingSystem, version: context.jamClient.ClientUpdateVersion(), @@ -203,7 +239,7 @@ validLatencyScore: gearTest.isValidLatencyScore(), validIOScore: gearTest.isValidIOScore(), latencyScore: gearTest.getLatencyScore(), - ioScore : gearTest.getIOScore(), + ioScore: gearTest.getIOScore(), }, audioParameters: { frameSize: frameBuffers.selectedFramesize(), @@ -221,21 +257,21 @@ * This is to provide a unified view of FTUEGetAllAudioConfigurations & FTUEGetGoodAudioConfigurations * @returns an array of profiles, where each profile is: {id: profile-name, good: boolean, class: 'bad' | 'good', current: boolean } */ - gearUtils.getProfiles = function() { + gearUtils.getProfiles = function () { var all = context.jamClient.FTUEGetAllAudioConfigurations(); var good = context.jamClient.FTUEGetGoodAudioConfigurations(); var current = context.jamClient.LastUsedProfileName(); var profiles = []; - context._.each(all, function(item) { + context._.each(all, function (item) { - profiles.push({id: item, good: false, class:'bad', current: current == item}) + profiles.push({id: item, good: false, class: 'bad', current: current == item}) }); - if(good) { - for(var i = 0; i < good.length; i++) { - for(var j = 0; j < profiles.length; j++) { - if(good[i] == profiles[j].id) { + if (good) { + for (var i = 0; i < good.length; i++) { + for (var j = 0; j < profiles.length; j++) { + if (good[i] == profiles[j].id) { profiles[j].good = true; profiles[j].class = 'good'; break; @@ -246,21 +282,21 @@ return profiles; } - gearUtils.postDiagnostic = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) { + gearUtils.postDiagnostic = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) { rest.createDiagnostic({ type: 'GEAR_SELECTION', data: { client_type: context.JK.clientType(), - client_id: - context.JK.JamServer.clientID, - summary:gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated)} + client_id: context.JK.JamServer.clientID, + summary: gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) + } }); } // complete list of possibly chatInputs, whether currently assigned as the chat channel or not // each item should be {id: channelId, name: channelName, assignment: channel assignment} - gearUtils.getChatInputs = function(){ + gearUtils.getChatInputs = function () { var musicPorts = jamClient.FTUEGetChannels(); //var chatsOnCurrentDevice = context.jamClient.FTUEGetChatInputs(true); @@ -273,11 +309,11 @@ var deDupper = {}; - context._.each(musicPorts.inputs, function(input) { + context._.each(musicPorts.inputs, function (input) { - var chatInput = {id: input.id, name: input.name, assignment:input.assignment}; - if(!deDupper[input.id]) { - if(input.assignment <= 0) { + var chatInput = {id: input.id, name: input.name, assignment: input.assignment}; + if (!deDupper[input.id]) { + if (input.assignment <= 0) { chatInputs.push(chatInput); deDupper[input.id] = chatInput; } @@ -295,9 +331,9 @@ } })*/ - context._.each(chatsOnOtherDevices, function(chatChannelName, chatChannelId) { + context._.each(chatsOnOtherDevices, function (chatChannelName, chatChannelId) { var chatInput = {id: chatChannelId, name: chatChannelName, assignment: null}; - if(!deDupper[chatInput.id]) { + if (!deDupper[chatInput.id]) { var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true); chatInput.assignment = assignment; @@ -309,11 +345,11 @@ return chatInputs; } - gearUtils.isChannelAvailableForChat = function(chatChannelId, musicPorts) { + gearUtils.isChannelAvailableForChat = function (chatChannelId, musicPorts) { var result = true; - context._.each(musicPorts.inputs, function(inputChannel) { + context._.each(musicPorts.inputs, function (inputChannel) { // if the channel is currently assigned to a track, it not unassigned - if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) { + if (inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) { result = false; return false; // break } @@ -324,13 +360,13 @@ // if the user has a good user network score, immediately returns with a resolved deferred object. // if not, the user will have the network test dialog prompted... once it's closed, then you'll be told reject() if score is still bad, or resolve() if now good - gearUtils.guardAgainstBadNetworkScore = function(app) { + gearUtils.guardAgainstBadNetworkScore = function (app) { var deferred = new $.Deferred(); if (!gearUtils.validNetworkScore()) { // invalid network test score. They have to score to move on - app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function() { - if(gearUtils.validNetworkScore()) { + app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function () { + if (gearUtils.validNetworkScore()) { deferred.resolve(); } else { @@ -346,19 +382,19 @@ // XXX this isn't quite right... it needs to check if a good device is *active* // but seen too many problems so far with the backend not reporting any profile active - gearUtils.hasGoodActiveProfile = function(verifyTracks) { + gearUtils.hasGoodActiveProfile = function (verifyTracks) { var hasOneConfigureDevice = context.JK.hasOneConfiguredDevice(); logger.debug("hasGoodActiveProfile: " + hasOneConfigureDevice ? "devices='has at least one configured device' " : "devices='has no configured device' ") return hasOneConfigureDevice; } // if the user does not have any profiles, show the FTUE - gearUtils.guardAgainstInvalidGearConfiguration = function(app) { + gearUtils.guardAgainstInvalidGearConfiguration = function (app) { var deferred = new $.Deferred(); if (context.jamClient.FTUEGetAllAudioConfigurations().length == 0) { - app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function() { - if(gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) { + app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function () { + if (gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) { deferred.resolve(); } else { @@ -373,27 +409,27 @@ return deferred; } - gearUtils.guardAgainstActiveProfileMissing = function(app, backendInfo) { + gearUtils.guardAgainstActiveProfileMissing = function (app, backendInfo) { var deferred = new $.Deferred(); logger.debug("guardAgainstActiveProfileMissing: backendInfo %o", backendInfo); - if(backendInfo.error && backendInfo['reason'] == 'no_profile' && context.jamClient.FTUEGetAllAudioConfigurations().length > 0) { + if (backendInfo.error && backendInfo['reason'] == 'no_profile' && context.jamClient.FTUEGetAllAudioConfigurations().length > 0) { // if the backend says we have no_profile, but we have profiles , send them to the audio profile screen // this should be a very rare path - deferred.reject({reason:'handled', nav: '/client#/account/audio'}); + deferred.reject({reason: 'handled', nav: '/client#/account/audio'}); context.JK.Banner.showAlert('No Active Profile', 'We\'ve sent you to the audio profile screen to remedy the fact that you have no active audio profile. Please select ACTIVATE on an existing profile, or select ADD NEW GEAR to add a new profile.'); } else if (backendInfo.error && backendInfo['reason'] == 'device_failure') { app.layout.showDialog('audio-profile-invalid-dialog') - .one(EVENTS.DIALOG_CLOSED, function(e, data) { - if(!data.result || data.result == 'cancel') { - deferred.reject({reason:'handled', nav: 'BACK'}); + .one(EVENTS.DIALOG_CLOSED, function (e, data) { + if (!data.result || data.result == 'cancel') { + deferred.reject({reason: 'handled', nav: 'BACK'}); } - else if(data.result == 'configure_gear'){ - deferred.reject({reason:'handled', nav: '/client#/account/audio'}); + else if (data.result == 'configure_gear') { + deferred.reject({reason: 'handled', nav: '/client#/account/audio'}); } - else if(data.result == 'session') { + else if (data.result == 'session') { deferred.resolve(); } else { @@ -409,43 +445,49 @@ } // tests both device config, and network score - gearUtils.guardAgainstInvalidConfiguration = function(app) { + gearUtils.guardAgainstInvalidConfiguration = function (app, verifyNetworkScore) { var deferred = new $.Deferred(); gearUtils.guardAgainstInvalidGearConfiguration(app) - .fail(function() { + .fail(function () { deferred.reject(); }) - .done(function() { - gearUtils.guardAgainstBadNetworkScore(app) - .fail(function() { - deferred.reject(); - }) - .done(function() { - deferred.resolve(); - }) + .done(function () { + if(verifyNetworkScore) { + gearUtils.guardAgainstBadNetworkScore(app) + .fail(function () { + deferred.reject(); + }) + .done(function () { + deferred.resolve(); + }) + } + else { + deferred.resolve(); + } + }) return deferred; } - gearUtils.skipNetworkTest = function() { + gearUtils.skipNetworkTest = function () { context.jamClient.SetNetworkTestScore(gearUtils.SKIPPED_NETWORK_TEST); gearUtils.skippedNetworkTest = true; } - gearUtils.isNetworkTestSkipped = function() { + gearUtils.isNetworkTestSkipped = function () { return gearUtils.skippedNetworkTest; } - gearUtils.validNetworkScore = function() { + gearUtils.validNetworkScore = function () { return gearUtils.skippedNetworkTest || context.jamClient.GetNetworkTestScore() >= 2; } - gearUtils.isRestartingAudio = function() { + gearUtils.isRestartingAudio = function () { return !!reloadAudioTimeout; } - gearUtils.scheduleAudioRestart = function(location, initial_delay, beforeScan, afterScan, cancelScan) { + gearUtils.scheduleAudioRestart = function (location, initial_delay, beforeScan, afterScan, cancelScan) { logger.debug("scheduleAudioRestart: (from " + location + ")") @@ -453,40 +495,42 @@ function clearAudioReloadTimer() { - if(!cancellable) {return;} + if (!cancellable) { + return; + } - if(cancelScan) { + if (cancelScan) { cancelScan(); } - else if(afterScan) { + else if (afterScan) { afterScan(true); } clearTimeout(reloadAudioTimeout); reloadAudioTimeout = null; - currentAudioRestartLocation = null; + currentAudioRestartLocation = null; cancellable = false; } // refresh timer if outstanding - if(reloadAudioTimeout) { + if (reloadAudioTimeout) { logger.debug("scheduleAudioRestart: clearing timeout (from " + location + ")") clearTimeout(reloadAudioTimeout); } currentAudioRestartLocation = location; - if(beforeScan) { + if (beforeScan) { beforeScan(); } - reloadAudioTimeout = setTimeout(function() { + reloadAudioTimeout = setTimeout(function () { logger.debug("scheduleAudioRestart: rescan beginning (from " + location + ")") reloadAudioTimeout = null; currentAudioRestartLocation = null; cancellable = false; - if(afterScan) { + if (afterScan) { afterScan(false); } }, initial_delay ? initial_delay : 5000); @@ -494,4 +538,45 @@ return clearAudioReloadTimer; } + gearUtils.bootstrapDefaultPlaybackProfile = function () { + + var profiles = gearUtils.getProfiles(); + + var foundSystemDefaultPlaybackOnly = false + context._.each(profiles, function (profile) { + if (profile.id == SYSTEM_DEFAULT_PLAYBACK_ONLY) { + foundSystemDefaultPlaybackOnly = true + return false; + } + }) + + if (!foundSystemDefaultPlaybackOnly) { + logger.debug("creating system default profile (playback only") + if(!gearUtils.createDefaultPlaybackOnlyProfile()) { + logger.error("unable to create the default playback profile!"); + } + } + } + gearUtils.createDefaultPlaybackOnlyProfile = function () { + + var eMixerInputSampleRate = { + JAMKAZAM_AUTO_SR: 0, + USE_DEVICE_DEFAULT_SR: 1, + PREFER_44: 2, + PREFER_48: 3, + PREFER_96: 4 + } + + // null//upgrade protect + if(context.jamClient.FTUECreateUpdatePlayBackProfile) { + return context.jamClient.FTUECreateUpdatePlayBackProfile(SYSTEM_DEFAULT_PLAYBACK_ONLY, + eMixerInputSampleRate.JAMKAZAM_AUTO_SR, + 0, // buffering + false); // start audio + } + else { + return false; + } + } + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/wizard.js b/web/app/assets/javascripts/wizard/wizard.js index 6d8600e3e..d0b1d45fc 100644 --- a/web/app/assets/javascripts/wizard/wizard.js +++ b/web/app/assets/javascripts/wizard/wizard.js @@ -69,11 +69,15 @@ if(result === false) {return false;} } + moveToNext(); + return false; + } + + function moveToNext() { previousStep = step; step = step + 1; moveToStep(); - return false; } function help() { @@ -238,6 +242,7 @@ this.getNextButton = getNextButton; this.setNextState = setNextState; this.setBackState = setBackState; + this.moveToNext = moveToNext; this.getCurrentStep = getCurrentStep; this.getCurrentWizardStep = getCurrentWizardStep; this.onCloseDialog = onCloseDialog; diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index a894e43de..e8db2e0eb 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -487,8 +487,8 @@ } /** account sessions */ - .account-sessions { - div#account-scheduled-sessions { + .account-sessions, .account-jamtracks { + div#account-scheduled-sessions, #account-my-jamtracks { position: relative; display: block; overflow: auto; diff --git a/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss b/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss new file mode 100644 index 000000000..f0a7c1fdf --- /dev/null +++ b/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss @@ -0,0 +1,51 @@ +@import 'common.css.scss'; + +#account-payment-history { + + .content-body-scroller { + padding:20px; + @include border_box_sizing; + } + + table td.loading { + text-align:center; + } + + .end-of-list { + margin-top:20px; + } + td { + + &.amount { + } + + &.voided { + + text-decoration:line-through; + } + } + + .account-left { + float: left; + min-width: 165px; + width: 20%; + } + + .account-left h2 { + color: #FFFFFF; + font-size: 23px; + font-weight: 400; + margin-bottom: 20px; + } + + .input-aligner { + margin: 10px 14px 20px 0; + text-align:right; + + .back { + margin-right:22px; + } + } + + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout.css.scss b/web/app/assets/stylesheets/client/checkout.css.scss index 6f9bc6a40..7d5190ad8 100644 --- a/web/app/assets/stylesheets/client/checkout.css.scss +++ b/web/app/assets/stylesheets/client/checkout.css.scss @@ -1,25 +1,24 @@ +@import "client/common.css.scss"; + .checkout-navigation { padding: 20px 0px; .nav-signin, .nav-payment-info, .nav-place-order { - width: 30%; + margin-right:50px; float: left; } .nav-signin { - margin-left: 5%; - } - - .nav-place-order { - margin-right: 5%; + margin-left: 30px; } .nav-text { font-size: 17px; float: left; + color:$ColorTextDisabled; } .nav-text.selected { - font-weight: bold; + color:$ColorTextHighlight; } .nav-arrow { @@ -28,229 +27,40 @@ } } -.checkout-signin, .checkout-payment-info, .checkout-place-order { - padding: 30px; - - .signin-form { - padding: 10px; - - strong { - font-weight: bold; - } - - label { - display: inline; - } - - .signin-password { - margin-left: 33px; - } - - .login-error { - background-color: #330000; - border: 1px solid #990000; - padding:4px; - - div.actions { - margin-top:10px; - } - } - - .login-error-msg { - display:none; - margin-top:10px; - text-align:center; - color:#F00; - font-size:11px; - } - - .login-error .login-error-msg { - display:block; - } - } - - form.payment-info { - width: 100%; - - input[type="text"] { - width: 90%; - } - - .billing-address { - float: left; - width: 50%; - - h2.billing-caption { - margin: 20px 5px; - font-size: 16px; - } - - .billing-label { - padding-top: 8px; - width: 30%; - float: left; - text-align: right; - margin-right: 5px; - } - - .billing-value { - width: 65%; - text-align: left; - float: left; - } - } - - .payment-method { - float: left; - width: 50%; - - h2.payment-method-caption { - margin: 20px 5px; - font-size: 16px; - } - - .card-label { - padding-top: 8px; - width: 35%; - float: left; - text-align: right; - margin-right: 5px; - } - - .card-value { - width: 60%; - text-align: left; - float: left; - } - - .save-card-checkbox { - float:left; - display:block; - margin-right:5px; - } - } - - .shipping-address { - float: left; - width: 50%; - - h2.shipping-address-label { - margin: 20px 5px; - font-size: 16px; - } - - .shipping-as-billing { - float:left; - display:block; - margin-right:5px; - } - - .divBillingHelper { - padding-top: 2px; - } - - .shipping-label { - padding-top: 8px; - width: 30%; - float: left; - text-align: right; - margin-right: 5px; - } - - .shipping-value { - width: 65%; - text-align: left; - float: left; - } - } - } -} - .thanks-panel { padding: 30px; + + span.notice { + font-style:italic; + font-size:12px; + } + + br.purchase-downloads { + clear:both; + margin-bottom:20px; + } + + .thanks-detail.purchased-jam-track { + + margin-top:20px; + + .purchased-jam-track-header { + font-size: 15px; + margin-bottom:10px; + } + + ul.purchased-list { + float:left; + margin:20px 100px 0 20px; + + li { + margin:0; + } + } + + .download-jamtrack { + width:auto; + vertical-align: middle; // to make bullets mid-align when error shows + } + } } - -.order-panel { - padding: 30px; - - .order-header { - h2 { - font-size: 16px; - } - } - - .order-content { - margin-top: 20px; - } - - .order-left-page { - float: left; - width: 60%; - - .payment-info-page { - padding: 5px; - - .info-caption-link { - .caption-text { - float: left; - } - .caption-link { - float: left; - margin-left: 5px; - } - } - - .address-info { - width: 50%; - float: left; - } - - .payment-method-info { - width: 50%; - float: left; - } - } - .order-items-page { - padding: 5px; - - .cart-item-caption { - width: 50%; - text-align: left; - float: left; - } - - .cart-item-caption#header { - font-weight: bold; - } - - .cart-item-price { - width: 25%; - text-align: right; - float: left; - } - - .cart-item-quantity { - width: 25%; - text-align: right; - float: left; - } - - .cart-items { - margin-top: 10px; - } - - .cart-item { - margin-top: 10px; - } - } - } - .order-right-page { - float: right; - width: 35%; - text-align: center; - - .order-total { - color: #ed3618; - } - } -} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout_order.css.scss b/web/app/assets/stylesheets/client/checkout_order.css.scss new file mode 100644 index 000000000..22a208ce0 --- /dev/null +++ b/web/app/assets/stylesheets/client/checkout_order.css.scss @@ -0,0 +1,201 @@ +@import "client/common.css.scss"; +#checkoutOrderScreen { + + p { + font-size:12px; + margin:0; + } + + .order-prompt { + color:white; + line-height:125%; + } + + h2 { + color:white; + background-color:#4d4d4d; + font-weight:normal; + margin: 0 0 10px 0; + font-size:14px; + padding: 3px 0 3px 10px; + height: 14px; + line-height: 14px; + vertical-align: middle; + text-align:left; + } + + .action-bar { + margin-top:20px; + } + + .line { + margin:10px 0 10px; + border-width:0 0 1px 0; + border-color:#ccc; + border-style:solid; + } + + #checkout-info-help { + margin-right:1px; + } + + .billing-info-item { + margin-bottom:3px; + } + + .country { + margin-left:15px; + } + .billing-address { + margin-bottom:20px; + } + .order-panel { + padding: 30px; + min-width:730px; + + .place-order { + font-size: 14px; + padding: 1px 3px; + line-height: 15px; + } + + .place-order-center { + text-align:center; + margin:20px 0 20px; + } + + .change-payment-info { + position:absolute; + font-size:12px; + left:180px; + } + + .billing-caption { + margin-bottom:5px; + float:left; + position:relative; + } + .order-header { + h2 { + font-size: 16px; + } + } + + .shipping-address { + display:none; + } + + .order-help { + margin:20px 0 30px; + } + .order-summary { + padding:0 20px; + + .billing-caption { + float:none; + margin-bottom:10px; + } + } + .order-items-header { + float:left; + margin-bottom:5px; + } + + .order-items-value { + float:right; + } + + .order-content { + margin-top: 20px; + background-color:#262626; + } + + .order-left-page { + float: left; + width: 65%; + background-color:#262626; + border-width:0 1px 0 0; + border-style:solid; + border-color:#333; + @include border_box_sizing; + + .payment-info-page { + + .info-caption-link { + .caption-text { + float: left; + } + .caption-link { + float: left; + margin-left: 5px; + } + } + + .address-info { + width: 50%; + float: left; + padding:0 10px; + @include border_box_sizing; + margin-bottom:30px; + } + + .payment-method-info { + width: 50%; + float: left; + padding:0 10px; + @include border_box_sizing; + } + } + .order-items-page { + .cart-item-caption { + width: 50%; + text-align: left; + float: left; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-item-price { + width: 25%; + text-align: right; + float: left; + padding:0 10px; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-item-quantity { + width: 10%; + text-align: right; + float: left; + padding:0 10px; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-items { + margin-top: 10px; + padding-left:10px; + } + + .cart-item { + margin-top: 10px; + } + + .no-cart-items { + } + } + } + .order-right-page { + float: left; + width: 35%; + text-align: left; + background-color:#262626; + @include border_box_sizing; + + .grand-total { + color: #ed3618; + } + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout_payment.css.scss b/web/app/assets/stylesheets/client/checkout_payment.css.scss new file mode 100644 index 000000000..c9b29bfd6 --- /dev/null +++ b/web/app/assets/stylesheets/client/checkout_payment.css.scss @@ -0,0 +1,249 @@ +@import "client/common.css.scss"; +#checkoutPaymentScreen { + + .payment-wrapper { + padding:10px 30px; + min-width:600px; + } + + p { + font-size:12px; + margin:0; + } + + .payment-prompt { + color:white; + line-height:125%; + } + + + .change-payment-info-holder { + display:block; + text-align:center; + margin:40px 0; + } + + .field.error { + background-color: transparent !important; + padding: 0 !important; + border-width:0 !important; + + li { + list-style:none; + } + } + + h2 { + color:white; + background-color:#4d4d4d; + font-weight:normal; + margin: 0 0 30px 0; + font-size:14px; + padding-left:10px; + + &.shipping-address-label { + //margin-top:10px; + } + } + + .field { + margin-bottom:5px; + } + + #payment_error { + @include border_box_sizing; + background-color: #300; + padding: 5px; + border: solid 1px #900; + } + + form.payment-info { + width: 100%; + display:block; + background-color:#262626; + padding-bottom: 10px; + margin-bottom: 20px; + + &.hidden { + display:none; + } + input[type="text"], input[type="password"] { + width: 90%; + @include border_box_sizing; + } + + select#billing-country { + width:90%; + @include border_box_sizing; + } + + &.signed-in { + .row.second { + .left-side { + width:100%; + } + .right-side { + display:none; + } + } + } + &.not-signed-in { + .row.second { + .left-side { + display:none; + } + .right-side { + width:100%; + } + } + } + + #divShippingFirstName, #divShippingLastName { + display:none; + } + + .row { + + margin-top:20px; + width:100%; + clear:both; + } + .left-side, .right-side { + float: left; + width: 50%; + @include border_box_sizing; + } + + .left-side { + border-width:0 1px 0 0; + border-style:solid; + border-color:#333; + } + + .jamkazam-account-signup { + .account-label { + padding-top: 4px; + width: 35%; + float: left; + text-align: left; + padding-left: 5px; + @include border_box_sizing; + } + + .account-value { + width: 65%; + text-align: left; + float: left; + @include border_box_sizing; + } + + div.terms-of-service.ichecbuttons { + margin-left:5px; + .icheckbox_minimal { + + float: left; + display: block; + margin: 5px 5px 0 0; + } + } + .terms-of-service-label-holder { + font-size:12px; + line-height:18px; + top:4px; + position:relative; + float:left; + } + } + + .hint { + font-size:12px; + } + .billing-address { + + .billing-label { + padding-top: 4px; + width: 35%; + float: left; + text-align: left; + padding-left: 5px; + @include border_box_sizing; + } + + .billing-value { + width: 65%; + text-align: left; + float: left; + @include border_box_sizing; + } + } + + .payment-method { + + + .card-label { + padding-top: 4px; + width: 35%; + float: left; + text-align: left; + padding-left: 5px; + @include border_box_sizing; + position:relative; + } + + .card-value { + width: 65%; + text-align: left; + float: left; + @include border_box_sizing; + } + + .save-card-checkbox, .reuse-existing-card-checkbox { + float:left; + display:block; + margin-right:5px; + } + + label[for="reuse-existing-card"], label[for="save-card"] { + line-height: 18px; + vertical-align: middle; + } + + .reuse-existing-card-helper { + margin-bottom:10px; + } + } + + .shipping-address { + + .shipping-as-billing { + float:left; + display:block; + margin: 0 5px 10px; + } + + .divBillingHelper { + padding-top: 2px; + + label { + + } + } + + .shipping-label { + padding-top: 4px; + width: 35%; + float: left; + text-align: left; + padding-left: 5px; + @include border_box_sizing; + } + + .shipping-value { + width: 65%; + text-align: left; + float: left; + @include border_box_sizing; + } + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout_signin.css.scss b/web/app/assets/stylesheets/client/checkout_signin.css.scss new file mode 100644 index 000000000..d436c5c58 --- /dev/null +++ b/web/app/assets/stylesheets/client/checkout_signin.css.scss @@ -0,0 +1,167 @@ +@import "client/common.css.scss"; + +#checkoutSignInScreen { + + .content-holder { + + margin-top:40px; + color:$ColorTextTypical; + + &.signed-in { + .left-side, .right-side { + display:none; + } + } + &.not-signed-in { + .already-signed-in { + display:none; + } + } + } + .already-signed-in { + text-align:center; + } + + a.forgot-password { + position:absolute; + top:0; + left:0; + font-size:10px; + } + + .signin-submit { + margin-right:2px; + } + + + .signup-later-prompt { + margin:30px 0 30px 0; + text-align:center; + } + + .signin-prompt { + float:right; + text-align:left; + margin:30px 0 15px 0; + width:60%; + max-width:300px; + @include border_box_sizing; + + span { + text-align:left; + } + } + + p { + font-size:14px; + line-height:125%; + } + + label { + display:inline-block; + width:80px; + text-align:left; + } + + .login-error { + + div.actions { + margin-top:10px; + } + + .field { + background-color: #330000; + border: 1px solid #990000; + padding:8px; + + } + } + + .field { + display:inline; + @include border_box_sizing; + } + + .login-error-msg { + display:none; + margin-top:10px; + text-align:center; + color:#F00; + font-size:11px; + width:60%; + max-width:300px; + @include border_box_sizing; + float:right; + margin-bottom:10px; + } + + + .login-error .login-error-msg { + display:block; + } + + h3 + { + color:$ColorTextHighlight; + text-align:center; + } + + input { + width:60%; + max-width:300px; + @include border_box_sizing; + } + + input[name="email"] { + margin-bottom:13px; + } + + .btnNext { + margin:0 auto; + } + + .left-side { + float:left; + width:50%; + @include border_box_sizing; + border-width:0 1px 0 0; + border-color:white; + border-style:solid; + text-align:right; + padding: 0 50px; + margin-bottom:20px; + + .actions { + width:60%; + max-width:300px; + @include border_box_sizing; + position:relative; + float:right; + } + } + + .left-side-content { + @include border_box_sizing; + width:100%; + } + + .right-side { + float:left; + width:50%; + @include border_box_sizing; + padding: 0 50px; + + .actions { + width:100%; + text-align:center; + } + } + + .facebook-prompt { + margin:40px 0 10px 0; + float:right; + text-align:left; + width:249px; + @include border_box_sizing; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 8403cf75a..5bb85acaa 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -31,6 +31,7 @@ *= require ./findSession *= require ./session *= require ./account + *= require ./accountPaymentHistory *= require ./search *= require ./ftue *= require ./jamServer @@ -46,12 +47,16 @@ *= require dialogs/dialog *= require ./iconInstrumentSelect *= require ./muteSelect + *= require ./metronomePlaybackModeSelect *= require ./terms *= require ./createSession *= require ./feed *= require ./jamtrack *= require ./shoppingCart *= require ./checkout + *= require ./checkout_signin + *= require ./checkout_payment + *= require ./checkout_order *= require ./genreSelector *= require ./sessionList *= require ./searchResults @@ -66,4 +71,6 @@ *= require jquery.Jcrop *= require icheck/minimal/minimal *= require users/syncViewer + *= require ./downloadJamTrack + *= require ./jamTrackPreview */ \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index e2f174fe0..75edd1bc1 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -12,8 +12,13 @@ $ColorLinkHover: #82AEAF; $ColorSidebarText: #a0b9bd; $ColorScreenBackground: lighten($ColorUIBackground, 10%); $ColorTextBoxBackground: #c5c5c5; +$ColorTextBoxDisabledBackground: #999; $ColorRecordingBackground: #471f18; +$ColorTextHighlight: white; +$ColorTextTypical: #CCCCCC; +$ColorTextDisabled: #AAAAAA; + $color1: #006AB6; /* mid blue */ $color2: #9A9084; /* warm gray */ $color3: #B11254; /* magenta */ @@ -208,6 +213,10 @@ $fair: #cc9900; background-color: $error; } + &.not_mine { + background-color: $good; + } + &.discarded { background-color: $unknown; } @@ -252,6 +261,10 @@ $fair: #cc9900; background-color: $good; } + &.not_mine { + background-color: $good; + } + .retry { display:none; position:absolute; @@ -284,4 +297,41 @@ $fair: #cc9900; } } +.badge-number { + font-size:25px; + color:white; + background-color:$ColorScreenPrimary; + width:30px; + height:30px; + -webkit-border-radius:50%; + -moz-border-radius:50%; + border-radius:50%; + text-align:center; + display:inline-block; + border:2px solid white; + margin-right:10px; + + &.disabled { + color:$ColorTextDisabled; + border-color:$ColorTextDisabled; + background-color:transparent; + } +} + +.white-bordered-button { + font-size:20px; + font-weight:bold; + background-color:white; + color:$ColorScreenPrimary; + border:3px solid $ColorScreenPrimary; + padding:18px; + -webkit-border-radius:8px; + -moz-border-radius:8px; + border-radius:8px; +} + + +.capitalize { + text-transform: capitalize +} diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index 9bee28a72..f5bad0e14 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -222,6 +222,10 @@ border:none; -webkit-box-shadow: inset 2px 2px 3px 0px #888; box-shadow: inset 2px 2px 3px 0px #888; + + &:disabled { + background-color: $ColorTextBoxDisabledBackground; + } } } @@ -383,6 +387,10 @@ a.arrow-down { select { font-size:11px; margin-top:4px; + + } + .dropdown-wrapper { + margin-right: 4px; } } diff --git a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss index 1cfe42004..1cae87032 100644 --- a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss +++ b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss @@ -1,5 +1,78 @@ @import "client/common"; .download-jamtrack { + display:inline-block; + width:100%; + .retry-button { + margin-top:20px; + } + + .retry { + margin-top:10px; + white-space: normal; + } + + .msg { + white-space: normal; + } + + .spinner-large { + margin:20px auto 0; + text-align:center; + } + + .small { + .state { + text-align:left; + } + font-size:inherit; + .msg { + line-height: 32px; + height: 32px; + display: inline-block; + vertical-align: middle; + } + .spinner-small { + display:inline-block; + vertical-align:middle; + } + } + + .large { + .state { + text-align:center; + } + } + + &.active { + + .small { + margin-bottom:5px; + + .msg { + font-weight:bold; + color:white; + display:inline; + } + .errormsg { + display:block; + font-size:14px; + } + .retry { + display:block; + margin:3px 0 0 0; + font-size:14px; + } + .retry-button { + float:right; + margin:5px 0 5px 20px; + } + + .msg-holder { + display:block; + } + } + + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/flash.css.scss b/web/app/assets/stylesheets/client/flash.css.scss new file mode 100644 index 000000000..918f3b014 --- /dev/null +++ b/web/app/assets/stylesheets/client/flash.css.scss @@ -0,0 +1,24 @@ +body > .flash-notice { + + background-color:#006673; + position:absolute; + width:300px; + height:100px; + line-height: 100px; + margin:0 auto; + border-width:0 1px 1px; + border-style:solid; + border-color:white; + text-align:center; + left:50%; + margin-left:-150px; + + .flash-content { + color:white; + font-size:20px; + display: inline-block; + vertical-align: middle; + line-height: normal; + } + +} diff --git a/web/app/assets/stylesheets/client/help.css.scss b/web/app/assets/stylesheets/client/help.css.scss index 4755d711f..6dbfa7149 100644 --- a/web/app/assets/stylesheets/client/help.css.scss +++ b/web/app/assets/stylesheets/client/help.css.scss @@ -45,7 +45,11 @@ body.jam, body.web, .dialog{ } } - .help-hover-recorded-tracks, .help-hover-stream-mix { + .help-high-latency-notice { + width:400px; + } + + .help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks { font-size:12px; padding:5px; diff --git a/web/app/assets/stylesheets/client/home.css.scss b/web/app/assets/stylesheets/client/home.css.scss index d9b7d9d45..c4e7c07f7 100644 --- a/web/app/assets/stylesheets/client/home.css.scss +++ b/web/app/assets/stylesheets/client/home.css.scss @@ -8,6 +8,10 @@ background-repeat: no-repeat; background-position: bottom left; border: 1px solid $translucent1; + + &.not-logged-in { + opacity:0.6; + } } .homecard.createsession { background-image: url(/assets/content/bkg_home_create.jpg); @@ -38,7 +42,7 @@ background-image: url(/assets/content/bkg_home_bands.jpg); } .homecard.musicians { - background-image: url(/assets/content/bkg_home_musicians.jpg); + background-image: url(/assets/content/bkg_home_guitar.jpg); } .homecard.jamtrack { background-image: url(/assets/content/bkg_home_jamtracks.jpg); @@ -94,7 +98,7 @@ background-image: url(/assets/content/bkg_home_bands_x.jpg); } .homecard.musicians.hover { - background-image: url(/assets/content/bkg_home_musicians_x.jpg); + background-image: url(/assets/content/bkg_home_guitar_x.jpg); } .homecard.jamtrack.hover { background-image: url(/assets/content/bkg_home_jamtracks_x.jpg); diff --git a/web/app/assets/stylesheets/client/jamTrackPreview.css.scss b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss new file mode 100644 index 000000000..28213d8b4 --- /dev/null +++ b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss @@ -0,0 +1,35 @@ +@import "client/common"; + +.jam-track-preview { + + display:inline-block; + line-height:24px; + vertical-align: middle; + @include border_box_sizing; + color:$ColorTextTypical; + font-size:14px; + + .actions { + display:inline; + vertical-align: middle; + } + + img.instrument-icon { + display:inline; + vertical-align: middle; + margin-left:10px; + } + + .instrument-name { + display:inline; + vertical-align: middle; + margin-left:10px; + + } + + .part { + display:inline; + vertical-align: middle; + margin-left:4px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 8ccd7a022..d1e2b4192 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -322,6 +322,10 @@ input[type="text"], input[type="password"]{ border:none; padding:3px; font-size:15px; + + &:disabled { + background-color: $ColorTextBoxDisabledBackground; + } } textarea { @@ -388,6 +392,24 @@ textarea { padding:5px; border: solid 1px #900; + .error-text { + display:block; + font-size:11px; + color:#F00; + margin:10px 0 0; + } + + &.transparent { + background-color:transparent; + padding:0; + border-width:0px; + + .error-text { + margin:5px 0 0; + font-size:14px; + font-weight:bold; + } + } } .error input { @@ -399,12 +421,6 @@ textarea { display:none; } -.error .error-text { - display:block; - font-size:11px; - color:#F00; - margin:10px 0 0; -} .grey { color:#999; } @@ -593,5 +609,4 @@ body.jam .icheckbox_minimal { display:inline-block; } - } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamtrack.css.scss b/web/app/assets/stylesheets/client/jamtrack.css.scss index 7d69b1b6f..979ac5377 100644 --- a/web/app/assets/stylesheets/client/jamtrack.css.scss +++ b/web/app/assets/stylesheets/client/jamtrack.css.scss @@ -1,4 +1,99 @@ +@import 'common'; + +#jamtrackLanding { + font-family: verdana; + .two-column-list-container { + -moz-column-count: 2; + -moz-column-gap: 20px; + -webkit-column-count: 2; + -webkit-column-gap: 20px; + column-count: 2; + column-gap: 20px; + margin-left: 0px; + ul { + list-style-type: none; + li { + margin: 1px 4px 1px 0px; + font-size:11px; + } + } + } + + ul { + li { + margin: 1px 4px 1px 4px; + font-size:11px; + } + } + + .browse-header { + padding: 4px 0 4px 0; + } + + .list-columns { + padding: 4px; + h2 { + font-size: 16pt; + font-weight:600; + font-style: bolder; + font-family: verdana; + text-transform: lowercase; + margin-bottom: 2em; + } + + .free-jamtrack { + font-size: 11pt; + padding: 3px; + @include border-radius(7px); + background-color:$ColorScreenPrimary; + color: white !important; + text-align: center; + vertical-align: center; + } + + .what, .howto { + margin-bottom: 2em; + * { + font-family: arial; + font-size: 11pt; + font-weight: 200; + line-height: 17px; + color: #cccccc; + } + } + + .details { + margin-bottom: 4px !important; + } + + .about { + @include border_box_sizing; + float: left; + width: 50%; + > * { + margin: 4px; + } + } + .browse { + @include border_box_sizing; + float: left; + width: 50%; + > * { + margin: 4px; + } + } + } +} + #jamtrackScreen { + .jamtrack-header { + background-color: #4c4c4c; + font-weight: bold; + text-transform: uppercase; + height: 2em; + padding: 4px; + } + a.jamtrack_help { color: #fff; text-decoration: none; @@ -11,6 +106,8 @@ .jamtrack-content { text-align: center; + border: 1px solid #222222; + padding: 2px } .no-jamtracks-msg { @@ -18,19 +115,21 @@ } .jamtrack-record { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; text-align: left; } .jamtrack-detail { - float: left; - width: 50%; + width: 35%; + @include border_box_sizing; + width: 30%; padding: 10px 0px; - .detail-label { width: 40%; float: left; margin-top: 5px; + font-weight: 400; + font-size: 11pt; } .detail-value { @@ -53,20 +152,21 @@ .jamtrack-description { display: none; } + + } - .jamtrack-detail-btn { - cursor: pointer; - margin-top: 5px; - margin-right: 5px; - padding-top: 5px; - } + .jamtrack-detail-btn { + cursor: pointer; + margin-top: 7px; + margin-right: 5px; + padding-top: 5px; + vertical-align: bottom; } .jamtrack-tracks { - float: left; - width: 25%; - padding: 10px 0px; - + @include border_box_sizing; + width: 35%; + padding: 10px 0px; .tracks-caption { margin-top: 5px; margin-bottom: 10px; @@ -78,6 +178,7 @@ .instrument-image { float: left; + margin-right: 2px; } .instrument-desc { @@ -86,13 +187,17 @@ margin-left: 10px; } } + + .jamtrack-track { + margin-left: 7px; + } .jamtrack-action { - float: left; - width: 25%; - padding: 10px 0px; + @include border_box_sizing; + width: 35%; + // padding: 0px 0px; text-align: center; - + // vertical-align: top; .play-button { margin-top: 5px; } @@ -113,4 +218,26 @@ width: 60%; } } +} + +#jamtrack-license-dialog { + .dialog-inner { + height: auto; + .content-body { + max-height: auto; + .content-body-scroller { + height: 350px; + .paragraph { + margin-bottom: 1em; + } + overflow: hidden; + } + border: 1px solid #222; + margin: 4px 4px 8px 4px; + } + } +} + +.jamtrack_buttons { + margin: 8px 4px 12px 4px; } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss new file mode 100644 index 000000000..7700a57be --- /dev/null +++ b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss @@ -0,0 +1,66 @@ +@import "client/common"; + +.metronome-playback-mode-selector-popup { + .bt-content { + width:180px; + background-color:#333; + overflow:auto; + border:1px solid #ED3618; + text-align:left; + font-family: Raleway, Arial, Helvetica, sans-serif; + ul { + height:100%; + margin-left:20px; + } + li { + font-size:12px; + margin-left:0; + list-style-type: none; + + margin-bottom:5px; + } + + p.please-select { + font-size:14px; + text-align:left; + margin-bottom:10px; + } + } +} + +#metronome-playback-select { + + margin-top:-10px; + span.metronome-state { + position:relative; + } + a { + color:#ffcc00 !important; + position:absolute; + top:4px; + right:-35px; + } + .down-arrow { + cursor:pointer; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid #fff; + position:absolute; + top:4px; + right:-20px; + } + + .up-arrow { + cursor:pointer; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #fff; + position:absolute; + top:2px; + right:-20px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 8c37af233..fbf72fc30 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -13,16 +13,30 @@ background-color:#4c4c4c; min-height:20px; position:relative; + min-width:690px; } + .pending-metronome { + .spinner-large { + margin:20px auto 0; + text-align:center; + } + p { + text-align:center; + font-size:14px; + } + } .track { width:70px; - height:290px; + height:300px; display:inline-block; margin-right:8px; position:relative; background-color:#242323; + -webkit-border-radius:4px; + -moz-border-radius:4px; + border-radius:4px; .disabled-track-overlay { width:100%; @@ -42,7 +56,11 @@ .recording-position { display:inline-block; - width:80%; + width:calc(100% - 27px - 47px); // 27 accounts for play arrow, 47px acconts for the total time (0:00) + + position:absolute; + left:0; + margin-left:27px; font-family:Arial, Helvetica, sans-serif; font-size:11px; @@ -50,6 +68,9 @@ vertical-align:top; } + .recording { + top:310px; // // this is to prevent scroll bars from pushing this element up + } .recording-controls { display:none; @@ -63,6 +84,32 @@ .recording-current { top:3px ! important; } + + .jam-track-get-ready { + display:none; + position:absolute; + top:-29px; + margin-left:-50px; + width:100px; + vertical-align:middle; + height:32px; + line-height:32px; + left:50%; + + &[data-mode="JAMTRACK"] { + &[data-current-time="0"] { + display:block; + } + } + .spinner-small { + vertical-align:middle; + display:inline-block; + } + + span { + vertical-align:middle; + } + } } .playback-mode-buttons { @@ -74,17 +121,23 @@ left:5px; } - .open-media-file-header { + .open-media-file-header, .use-metronome-header { font-size:16px; line-height:100%; margin:0; - float:left; img { position:relative; top:3px; } } + .open-media-file-header { + float: left; + } + + .use-metronome-header { + clear: both; + } .open-media-file-options { font-size:16px; @@ -110,11 +163,25 @@ .session-recording-name-wrapper{ position:relative; - white-space:nowrap; - display:none; + white-space:normal; + display:none; + + .session-recording-name { + position:relative; + margin-top:9px; + margin-bottom:8px; + font-size:16px; + height: 22px; + min-height: 22px; + max-height: 22px; + display: inline-block; + width:60%; + text-overflow:ellipsis; + } .session-add { margin-top:9px; + margin-right:10px; } .session-add a { vertical-align:top; @@ -133,6 +200,185 @@ margin-bottom:8px; font-size:16px; } + + .download-jamtrack { + margin-top:50px; + } + + .metronome-playback-options { + height:100%; + line-height:100%; + vertical-align:middle; + + span.metronome-state { + cursor:pointer; + height:100%; + line-height:100%; + vertical-align:middle; + } + } + + #metronome-playback-help { + position:absolute; + right:5px; + top:5px; + color:#ffcc00; + cursor:help; + } + + + .leave { + margin-right:25px; + } + + $mytracks-minwidth: 180px; + $livetracks-minwidth:180px; + $otheraudio-minwidth:195px; + $otheraudio-open-minwidth:230px; + + #session-mytracks-notracks { + display:none; + + p { + font-size:14px; + white-space:normal; + margin:10px 10px 0 0; + line-height:125%; + } + + } + + .session-mytracks { + padding-left:15px; + float:left; + display:inline-block; + width:$mytracks-minwidth; + border-right:solid 1px #666; + @include border_box_sizing; + } + + .session-fluidtracks { + position:relative; + float:left; + @include border_box_sizing; + + &.showing { + width:calc(100% - #{$mytracks-minwidth}); // VRFS-3034 + } + } + + .session-livetracks { + padding-left:15px; + padding-right:15px; + float:left; + display:inline-block; + min-width:$livetracks-minwidth; + max-width:calc(100% - #{$otheraudio-minwidth}); + width:calc(100% - #{$otheraudio-minwidth}); + border-right:solid 1px #666; + @include border_box_sizing; + + &[media-state="open"] { + width:calc(100% - #{$otheraudio-open-minwidth}); + max-width:calc(100% - #{$otheraudio-open-minwidth}); + min-width:#{$livetracks-minwidth}; + } + + .recording { + left: 50%; + margin-left: -67px; + } + + .recording-controls { + min-width:230px; + } + + #recording-start-stop { + @include border-radius(4px); + padding-left:5px; + padding-right:5px; + } + } + + .session-recordings { + padding-left:15px; + padding-right:15px; + display:inline-block; + max-width:calc(100% - #{$livetracks-minwidth}); + min-width:#{$otheraudio-minwidth}; + width:#{$otheraudio-minwidth}; + float:left; + @include border_box_sizing; + + &[media-state="open"] { + width:$otheraudio-open-minwidth; + min-width:$otheraudio-open-minwidth; + max-width:calc(100% - #{$livetracks-minwidth}); + } + + + .recording.metronome-mode { + margin-left:-100px; + } + } + + #tracks .when-empty.livetracks { + margin: 0px; + padding:0px; + display:block; + padding-top: 125px; + vertical-align:middle; + text-align:center; + } + #tracks .when-empty.recordings { + //padding-top: 137px; + text-align:left; + padding-top:6px; + margin:0; + } + + #tracks .when-empty a { + text-decoration: underline; + color: inherit; + } + + .session-add { + margin-top:9px; + margin-bottom:8px; + font-size:16px; + height: 22px; + min-height: 22px; + max-height: 22px; + } + + .session-add a { + color:#ccc; + text-decoration: none; + } + + .session-add a img { + vertical-align:bottom; + } + + .session-tracks-scroller { + position:relative; + overflow-x:auto; + overflow-y:hidden; + width:100%; + height:370px; + float:left; + white-space:nowrap; + } + + .play-controls-holder { + width:100%; + text-align:center; + + .recording { + left:50%; + margin-left:-46.5%; + } + } } @@ -159,6 +405,25 @@ #tracks { margin-top:12px; overflow:auto; + + &.no-local-tracks { + + #session-mytracks-notracks { + display:block; + } + + #session-mytracks-container { + display:none; + } + + #recording-start-stop { + display:none; + } + + #session-invite-musicians { + display:none; + } + } } .track-empty a { @@ -201,6 +466,9 @@ table.vu td { position: absolute; text-align:center; width: 55px; + height: 15px; + min-height: 11px; + max-height: 33px; max-width: 55px; white-space:normal; top: 3px; @@ -208,6 +476,7 @@ table.vu td { font-family: Arial, Helvetica, sans-serif; font-size: 11px; font-weight: bold; + text-overflow:ellipsis; } .track-close { @@ -270,86 +539,6 @@ table.vu td { margin-left:15px; } -.leave { - margin-right:25px; -} - -.session-mytracks { - margin-left:15px; - float:left; - display:inline-block; - width:19%; - min-width:165px; - border-right:solid 1px #666; -} - -.session-livetracks { - margin-left:15px; - float:left; - display:inline-block; - width:35%; - min-width:220px; - border-right:solid 1px #666; -} - -.session-recordings { - margin-left:15px; - display:inline-block; - width:35%; - min-width:220px; -} - -#tracks .when-empty.livetracks { - margin: 0px; - padding:0px; - display:block; - padding-top: 125px; - vertical-align:middle; - text-align:center; -} -#tracks .when-empty.recordings { - //padding-top: 137px; - text-align:left; - padding-top:6px; - margin:0; -} - -#tracks .when-empty a { - text-decoration: underline; - color: inherit; -} - - - -.session-add { - margin-top:9px; - margin-bottom:8px; - font-size:16px; - height: 22px; - min-height: 22px; - max-height: 22px; -} - -.session-add a { - color:#ccc; - text-decoration: none; -} - -.session-add a img { - vertical-align:bottom; -} - - -.session-tracks-scroller { - position:relative; - overflow-x:auto; - overflow-y:hidden; - width:100%; - height:340px; - float:left; - white-space:nowrap; -} - @@ -482,12 +671,9 @@ table.vu td { .track-gain { position:absolute; width:28px; - height:83px; + height:63px; top:138px; left:23px; - background-image:url('/assets/content/bkg_gain_slider.png'); - background-repeat:repeat-y; - background-position:bottom; } .track-gain-wrapper { @@ -514,6 +700,45 @@ table.vu td { height: 18px; background-image:url('/assets/content/icon_mute.png'); background-repeat:no-repeat; + text-align: center; +} + +.track-icon-loop { + cursor: pointer; + position:absolute; + top:250px; + left:11px; + width: 20px; + height: 18px; + text-align: center; + font-size: 8pt; + font-weight: bold; + + .icheckbox_minimal { + top:5px; + margin-right:5px; + } +} + +.metronome-selects { + position: absolute; + width: 52px; + top:252px; + left: 10px; + + height: 18px; + text-align: center; + //display: block; + //padding: 4px; + + select.metronome-select { + position: relative; + padding: 4px 0px 4px 0px; + margin: 0; + width: 100% !important; + font-size: 10px; + font-weight: normal; + } } .track-icon-mute.muted { @@ -524,12 +749,12 @@ table.vu td { } .session-livetracks .track-icon-mute, .session-recordings .track-icon-mute { - top:245px; + top:225px; } .track-icon-settings { position:absolute; - top:255px; + top:235px; left:28px; } @@ -652,7 +877,6 @@ table.vu td { margin-top:15px; padding:3px; height:19px; - width:95%; background-color:#242323; position:absolute; text-align:center; diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index 16d208a59..ce326ef9c 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -1,7 +1,7 @@ @import "client/common"; -table.findsession-table, table.local-recordings, table.open-jam-tracks, #account-session-detail { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, #account-session-detail, table.payment-table { .latency-unacceptable { width: 50px; @@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, #account text-align:center; } } -table.findsession-table, table.local-recordings, table.open-jam-tracks { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, table.payment-table { width:98%; height:10%; font-size:11px; diff --git a/web/app/assets/stylesheets/client/shoppingCart.css.scss b/web/app/assets/stylesheets/client/shoppingCart.css.scss index f8a8c6fdb..ba3b31146 100644 --- a/web/app/assets/stylesheets/client/shoppingCart.css.scss +++ b/web/app/assets/stylesheets/client/shoppingCart.css.scss @@ -29,9 +29,7 @@ } .cart-item-caption { - width: 50%; - text-align: left; - float: left; + text-align: left; } .cart-item-caption#header { @@ -39,21 +37,15 @@ } .cart-item-price { - width: 15%; - text-align: right; - float: left; + text-align: right; } .cart-item-quantity { - width: 15%; - text-align: right; - float: left; + text-align: right; } .cart-item-actions { - width: 20%; - text-align: center; - float: left; + //text-align: center; } .cart-items { diff --git a/web/app/assets/stylesheets/client/sidebar.css.scss b/web/app/assets/stylesheets/client/sidebar.css.scss index 17c975616..59a3bdfb5 100644 --- a/web/app/assets/stylesheets/client/sidebar.css.scss +++ b/web/app/assets/stylesheets/client/sidebar.css.scss @@ -5,6 +5,10 @@ background-color: $ColorElementPrimary; + &.not-logged-in { + opacity:0.6; + } + .panel-header { margin:0px; padding:0px; diff --git a/web/app/assets/stylesheets/client/site_validator.css.scss b/web/app/assets/stylesheets/client/site_validator.css.scss new file mode 100644 index 000000000..4534ebd8c --- /dev/null +++ b/web/app/assets/stylesheets/client/site_validator.css.scss @@ -0,0 +1,38 @@ +@import "client/common"; + +.site_validator { + + .validator-input { + float: left; + } + .validator-add-rec { + float: left; + } + + input { + width: 100%; + padding: 5px 5px 5px 30px; + float: left; + } + .validate-checkmark { + background-image: url('/assets/content/icon_checkmark_circle.png'); + background-repeat:no-repeat; + background-position:center; + width:32px; + height:32px; + background-size: 50% 50%; + display:inline-block; + vertical-align: middle; + position: relative; + margin-top: -40px; + left: 0px; + } + .error { + } + span.spinner-small { + display:inline-block; + vertical-align: middle; + position: relative; + margin-top: -40px; + } +} diff --git a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index d79750a7c..3951f7619 100644 --- a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -381,6 +381,11 @@ margin-top: 20px; } + a.forever-network-test { + margin-top:20px; + display:none; + } + .network-test-score { height: 24px; padding: 10px; @@ -463,8 +468,12 @@ } + .instructions { + height: 228px !important; + } + .network-test-results { - height: 248px !important; + height: 228px !important; @include border_box_sizing; &.testing { diff --git a/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss b/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss index a3ada5d95..401eea8a9 100644 --- a/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss @@ -135,6 +135,25 @@ } + .get-a-free-jamtrack-section { + + &.has-free-jamtrack { + h2.get-a-free-jamtrack { + display:block; + } + + .action-button { + margin-top:-7px; + } + } + + &.no-free-jamtrack { + h2.browse-jamtracks { + display:block; + } + } + } + .ftue-inner table a { text-decoration:none; } diff --git a/web/app/assets/stylesheets/dialogs/loginRequiredDialog.css.scss b/web/app/assets/stylesheets/dialogs/loginRequiredDialog.css.scss new file mode 100644 index 000000000..bc27a60ed --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/loginRequiredDialog.css.scss @@ -0,0 +1,12 @@ +#login-required-dialog { + + width:455px; + + p { + margin:0 0 20px 0; + } + + .buttons { + margin-top:20px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss b/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss new file mode 100644 index 000000000..06b4ffb2e --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss @@ -0,0 +1,40 @@ +@import "client/common"; + +#open-backing-track-dialog { + table.open-backing-tracks { + tbody { + tr:hover { + background-color: #777; + cursor:pointer; + } + + tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] { + background-color:#777; + color:#aaa; + } + } + } + + .right { + margin-right:10px; + } + + .help-links { + text-align: left; + position: absolute; + margin: 0 auto; + width: 70%; + //left: 15%; + font-size: 12px; + padding-top:5px; + + a { + margin:0 10px; + } + } + + .paginator-holder { + padding-top:3px; + } +} + diff --git a/web/app/assets/stylesheets/dialogs/openJamTrackDialog.css.scss b/web/app/assets/stylesheets/dialogs/openJamTrackDialog.css.scss index 4ff94bb71..b13355afe 100644 --- a/web/app/assets/stylesheets/dialogs/openJamTrackDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/openJamTrackDialog.css.scss @@ -31,6 +31,7 @@ left: 15%; font-size: 12px; padding-top:5px; + z-index:-1; a { margin:0 10px; @@ -40,5 +41,11 @@ .paginator-holder { padding-top:3px; } + + .recording-wrapper { + height:290px; + overflow:auto; + + } } diff --git a/web/app/assets/stylesheets/dialogs/singlePlayerProfileGuard.css.scss b/web/app/assets/stylesheets/dialogs/singlePlayerProfileGuard.css.scss new file mode 100644 index 000000000..89eedac9a --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/singlePlayerProfileGuard.css.scss @@ -0,0 +1,29 @@ +#single-player-profile-dialog { + + .dialog-inner { + + &.high-latency { + .high-latency { + display:block + } + } + + &.has-no-inputs { + .has-no-inputs { + display:block + } + } + } + + .audio-latency { + font-weight:bold; + } + + .action-buttons { + margin:20px 0; + } + + p { + line-height:125%; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/easydropdown_jk.css.scss b/web/app/assets/stylesheets/easydropdown_jk.css.scss index 47e43c108..d3601b002 100644 --- a/web/app/assets/stylesheets/easydropdown_jk.css.scss +++ b/web/app/assets/stylesheets/easydropdown_jk.css.scss @@ -11,6 +11,54 @@ body.jam { } } + .dropdown-wrapper.black-flat { + li { + color:white; + + &.focus { + background-color: #ed3618; + color:white; + } + } + + ul { + background-color:#242323; + } + + div.dropdown-container { + border-width:0; + border-radius:0; + } + + div.dropdown { + background-color: #242323; + box-shadow: none; + border-radius:0; + border-width:0; + + .selected { + color:white; + } + + .carat { + border-top: 8px solid white; + } + li { + color:white; + + &.focus { + background-color: #ed3618; + color:white; + } + } + + div.after { + box-shadow:none; + } + } + + } + .dropdown-wrappper div.dropdown-container { width:auto; } @@ -174,4 +222,11 @@ body.jam div.dropdown { border-style:solid; border-width: 1px 0 0 1px; } + + .black-flat { + ul { + border-width:0; + background-color:black; + } + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss new file mode 100644 index 000000000..6111876ee --- /dev/null +++ b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss @@ -0,0 +1,32 @@ +body.web.landing_jamtrack.individual_jamtrack { + + .previews { + margin-top:10px; + } + .jamtrack-reasons { + margin: 10px 0 0 20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .browse-jamtracks-wrapper { + text-align:center; + width:90%; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss new file mode 100644 index 000000000..db25181b1 --- /dev/null +++ b/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss @@ -0,0 +1,32 @@ +body.web.landing_jamtrack.individual_jamtrack_band { + + .previews { + margin-top:10px; + } + .jamtrack-reasons { + margin: 10px 0 0 20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .browse-jamtracks-wrapper { + text-align:center; + width:90%; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/landing_page.css.scss b/web/app/assets/stylesheets/landings/landing_page.css.scss index 51491d12a..56d1ad837 100644 --- a/web/app/assets/stylesheets/landings/landing_page.css.scss +++ b/web/app/assets/stylesheets/landings/landing_page.css.scss @@ -9,10 +9,394 @@ body.web.landing_page { display:none; } - &.wo_1 { - .landing-content h1 { - margin-left:45px; + &.kick { + + h1 { + } + p { + margin-bottom:15px; + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:100%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .column:nth-child(1) { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column:nth-child(2) { + width:50% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + } + + &.kick_4 { + + h1 { + + } + + .column h2 { + position:absolute; + font-size:12px; + margin-left:45px; + top:45px; + } + p { + margin-bottom:15px; + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:100%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .column:nth-child(1) { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column:nth-child(2) { + width:50% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container { + + width:80%; + + a { + width:90%; + text-align:center; + + img { + width:65%; + margin-top:20px; + } + } + } + } + + } + + + &.kick_2 { + + .linker { + -webkit-border-radius:6px; + -moz-border-radius:6px; + border-radius:6px; + border-width:1px; + border-style:solid; + border-color:$ColorScreenPrimary; + position:absolute; + width:92%; + left:-1%; + top:60px; + height:270px; + } + h1 { + //padding-left:20px; + } + + .youtube-time-tip { + font-style:italic; + } + + p { + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:85%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .back-us { + margin-top:15px; + width:75%; + margin-left:30px; + img { + width:100%; + } + } + .column.one { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column.two { + width:45% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + .testimonial { + margin-top:70px; + p { + font-size:18px !important; + } + } + + .signature { + margin-top:15px; + margin-left:30%; + font-size:18px; + position:relative; + + .dash { + position:absolute; + width:20px; + left:-10px; + top:0; + } + } + + .signature-details { + margin-top:5px; + font-style:italic; + margin-left:30%; + } + + .signup-holder { + position:absolute; + top:350px; + width:45%; + } + .signup-info { + + } + + .signup-wrapper { + width:90%; + } + + } + + &.kick_3 { + + .linker { + -webkit-border-radius:6px; + -moz-border-radius:6px; + border-radius:6px; + border-width:1px; + border-style:solid; + border-color:$ColorScreenPrimary; + position:absolute; + width:92%; + left:-1%; + top:56px; + height:270px; + } + + .column h1 { + font-size:16px !important; + } + + .youtube-time-tip { + font-style:italic; + } + + p { + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:85%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .back-us { + margin-top:15px; + width:75%; + margin-left:30px; + + img { + width:100%; + } + } + .column.one { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column.two { + width:45% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + .testimonial { + margin-top:70px; + p { + font-size:18px !important; + } + } + + .signature { + margin-top:15px; + margin-left:30%; + font-size:18px; + position:relative; + + .dash { + position:absolute; + width:20px; + left:-10px; + top:0; + } + } + + .signature-details { + margin-top:5px; + font-style:italic; + margin-left:30%; + } + + .signup-holder { + position:absolute; + top:350px; + width:45%; + } + .signup-info { + + } + + .signup-wrapper { + width:90%; + } + + } + + + &.wo_1 { + + .landing-tag { + left: 50%; + } + + .cta-container { + width:75%; + text-align:center; + margin-left:0% !important; + + h2 { + margin-left:0px !important; + } + } + + .column:nth-child(1) { + width:50% !important; + } + + .column:nth-child(2) { + width:50% !important; + h1 { + + } + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + } &.wo_3 { .landing-content h1 { diff --git a/web/app/assets/stylesheets/landings/landing_page_new.css.scss b/web/app/assets/stylesheets/landings/landing_page_new.css.scss new file mode 100644 index 000000000..897152536 --- /dev/null +++ b/web/app/assets/stylesheets/landings/landing_page_new.css.scss @@ -0,0 +1,56 @@ +@import "client/common.css.scss"; + +body.web.landing_page { + + .two_by_two { + + h1 { + margin:0 0 5px; + padding:7px 0; + display:inline-block; + + &.hidden { + display:none; + } + } + .row { + @include border_box_sizing; + + &:nth-of-type(1) { + padding:20px 0 0 0; + } + + + .column { + width:50%; + float:left; + @include border_box_sizing; + } + } + } + + &.landing_jamtrack, &.landing_product { + + .landing-tag { + left:50%; + text-align:center; + } + p, ul, li { + font-size:14px; + line-height:125%; + color:$ColorTextTypical; + } + p { + + } + ul { + list-style-type: disc; + } + li { + margin-left:20px; + } + .video-container { + margin-top:0; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/landing_product.css.scss b/web/app/assets/stylesheets/landings/landing_product.css.scss new file mode 100644 index 000000000..99739aa57 --- /dev/null +++ b/web/app/assets/stylesheets/landings/landing_product.css.scss @@ -0,0 +1,53 @@ +@import "client/common.css.scss"; + +body.web.landing_product { + + h1.product-headline { + color:white; + background-color:$ColorScreenPrimary; + margin-bottom:20px; + padding:7px; + border-radius:4px; + } + + .product-description { + margin-bottom:20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .cta-big-button { + text-align:center; + width:90%; + } + + .linked-video-holder { + text-align:center; + width:90%; + margin-top:20px; + } + + .previews { + margin-top:10px; + } + + .jamtrack-reasons { + margin-top:20px; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/playbackControls.css.scss b/web/app/assets/stylesheets/playbackControls.css.scss index f0a0ab29a..a1e20c77f 100644 --- a/web/app/assets/stylesheets/playbackControls.css.scss +++ b/web/app/assets/stylesheets/playbackControls.css.scss @@ -1,5 +1,4 @@ .recording-controls { - .playback-mode-buttons { - display:none; - } + + } \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/audioWidgets.css.scss b/web/app/assets/stylesheets/web/audioWidgets.css.scss index 55871e7ba..4c044f8b7 100644 --- a/web/app/assets/stylesheets/web/audioWidgets.css.scss +++ b/web/app/assets/stylesheets/web/audioWidgets.css.scss @@ -28,6 +28,21 @@ display:inline-block; white-space:nowrap; + &.metronome-mode { + width:200px; + margin-left:-100px; + left:50%; + .recording-position, .recording-current, .playback-mode-buttons { + display:none; + } + } + + &.jamtrack-mode, &.mediafile-mode { + .metronome-playback-options { + display:none; + } + } + .recording-status { font-size:15px; } diff --git a/web/app/assets/stylesheets/web/downloads.css.scss b/web/app/assets/stylesheets/web/downloads.css.scss new file mode 100644 index 000000000..4634d2f04 --- /dev/null +++ b/web/app/assets/stylesheets/web/downloads.css.scss @@ -0,0 +1,131 @@ +@import "client/common.css.scss"; + +body.downloads { + + + h2 { + font-size:22px; + color:white !important; + } + + ul { + width:auto !important; + } + + ul li { + color:#CCC; + font-size:14px; + line-height:125%; + text-indent:-5px; + margin-left:40px; + + &:before + { content:"-"; + position:relative; + left:-5px; + } + } + + p { + margin:20px 0 10px; + line-height:125% !important; + width:100% !important; + } + + .download-app { + + padding-top:20px; + width:55%; + float:left; + @include border_box_sizing; + } + + .jamtracks { + + padding-top:20px; + float:left; + width:45%; + @include border_box_sizing; + padding-left:5%; + + .shop-jamtracks { + + } + + .special-value { + font-size:16px; + } + } + + .video-container { + position:relative; + width:320px; + height:180px; + } + + .video-container iframe, + .video-container object, + .video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .download-content { + border-style:solid; + border-width:0 1px 0 0; + border-color:#ccc; + } + .download-entreaty { + + + p.click-to-download { + margin-bottom:5px; + } + + } + + .downloads-container { + position:relative; + display:inline-block; + height:92px; + top:-25px; + left:20px; + } + .downloads-blurb { + + h5 { + color:#ccc; + margin-bottom:5px; + margin-top:-15px; + } + + ul li { + font-size:12px; + margin-left:15px; + } + + .choose-other-platform {line-height:125%;} + + } + + .downloads { + position:relative; + } + + .download-box { + color:#fff; + text-align:center; + } + + .go-jamtrack-shopping { + line-height: 125%; + text-align: center; + display: block; + position: absolute; + right: 0; + bottom: 90px; + } +} diff --git a/web/app/assets/stylesheets/web/home.css.scss b/web/app/assets/stylesheets/web/home.css.scss new file mode 100644 index 000000000..4a82efe37 --- /dev/null +++ b/web/app/assets/stylesheets/web/home.css.scss @@ -0,0 +1,135 @@ +@charset "UTF-8"; + +@import "client/common.css.scss"; + +body.web.home { + + .landing-tag { + margin-top:20px; + } + + .logo-home { + margin:20px 0 15px; + } + + .home-column { + margin-top: 20px; + width: 345px; + float: left; + margin-right: 30px; + text-align: center; + margin-bottom:35px; + + &.last{ + margin-right:0; + } + + h3 { + text-align:left; + margin-top:12px; + margin-bottom:6px; + font-size:18px; + font-weight:700; + } + p { + color:white; + text-align:left; + line-height:120%; + font-size:13px; + margin-bottom:20px; + } + + .extra-links { + width:234px; + display:inline-block; + } + .learn-more { + font-size: 12px; + margin-top: 5px; + + &.shared { + float:left; + margin-left:10px; + } + } + + .sign-in-holder { + font-size: 12px; + margin-top: 5px; + + &.shared { + float:right; + margin-right:10px; + } + } + + } + + .latest-promo { + float:left; + } + + .endorsement-promo { + float:right; + } + + .home-buzz { + + h2 { + width:100%; + text-align:center; + margin:20px 0; + } + width: 300px; + position:relative; + margin-right:20px; + .buzz-items { + .buzz-item { + padding: 12px 0; + &:last-child { + padding-bottom:0; + } + } + .buzz-item-text { + padding-left: 78px; // 58px width for image + 20px margin + } + } + } + + .latest { + width: 750px; + position:relative; + top:-45px; + + .home-session-list { + top:5px; // XXX remove post release + width:100%; + height:400px; + border: solid 1px #ed3618; + background-color:#353535; + float:left; + overflow:hidden; + position:relative; + + } + .latest-head { + position: absolute; + padding:20px 20px 12px; + height: 53px; + width:inherit; + } + + .latest-body { + width:100%; + top:65px; + bottom:0; + position:absolute; + overflow-y:scroll; + @include border_box_sizing; + + .session-list-wrapper { + padding: 0 20px; + } + } + } +} diff --git a/web/app/assets/stylesheets/web/main.css.scss b/web/app/assets/stylesheets/web/main.css.scss index 291f61967..2a2891d60 100644 --- a/web/app/assets/stylesheets/web/main.css.scss +++ b/web/app/assets/stylesheets/web/main.css.scss @@ -48,6 +48,16 @@ body.web { margin-left:70px; width:400px; + &.no-user-dropdown { + margin-left:0; + margin-top:30px; + position:absolute; + top:0; + right:0; + text-align:center; + width:350px; + } + h1 { color:#ed3618; font-size:26px; @@ -64,10 +74,16 @@ body.web { } } + &.register { + .landing-content { + min-height:460px; + } + } + .landing-content { background-color:black; width:100%; - min-height: 460px; + min-height: 366px; position:relative; padding-bottom:30px; @@ -304,15 +320,29 @@ body.web { .register-page { .register-container { - padding:10px; + padding:0 10px 10px; } input.register-musician { } + .create-account-header { + padding-top:10px; + margin-left:10px; + } + .actions { margin-top:20px; + input, a { + font-size:18px; + } + input { + margin-left:2px; + } + a { + margin-left:10px; + } } .error { @@ -321,6 +351,10 @@ body.web { margin-right:-12px; } + div.register-as { + margin-left:45px; + } + input.register-fan { margin-left:20px; } @@ -409,39 +443,11 @@ body.web { font-weight:normal; } - .download-box { - padding:15px; - color:#fff; - text-align:center; - font-size:20px; - -webkit-border-radius: 8px; - border-radius: 8px; - background:#383838; - margin-top:100px; - } - - .system-requirements { - margin-top:35px; - display:none; - - ul { - display:none; - list-style:disc outside none; - padding-left:40px; - } - } div.proceed { margin:40px 0 250px 5px; } - .downloads { - ul { - list-style:disc outside none; - padding-left:40px; - } - } - .congratulations { padding-top:1px; } diff --git a/web/app/assets/stylesheets/web/recordings.css.scss b/web/app/assets/stylesheets/web/recordings.css.scss index 753a3e160..20b9aca35 100644 --- a/web/app/assets/stylesheets/web/recordings.css.scss +++ b/web/app/assets/stylesheets/web/recordings.css.scss @@ -19,6 +19,10 @@ float:left; margin-left:20px; } + + .jam-track-get-ready { + display:none; + } } .recording-position { diff --git a/web/app/assets/stylesheets/web/web.css b/web/app/assets/stylesheets/web/web.css index d91e79de2..5761892c5 100644 --- a/web/app/assets/stylesheets/web/web.css +++ b/web/app/assets/stylesheets/web/web.css @@ -2,6 +2,7 @@ *= require client/jamServer *= require client/ie *= require client/jamkazam +*= require jquery.bt *= require easydropdown *= require easydropdown_jk *= require client/screen_common @@ -13,15 +14,20 @@ *= require client/hoverBubble *= require client/help *= require client/listenBroadcast +*= require client/flash +*= require client/jamTrackPreview *= require web/main *= require web/footer *= require web/recordings *= require web/welcome +*= require web/home #= require web/sessions *= require web/events *= require web/session_info +*= require web/downloads *= require users/signinCommon *= require dialogs/dialog -*= require landings/landing_page +*= require client/help +*= require_directory ../landings *= require icheck/minimal/minimal */ \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/welcome.css.scss b/web/app/assets/stylesheets/web/welcome.css.scss index c4bd44786..8417ca9a2 100644 --- a/web/app/assets/stylesheets/web/welcome.css.scss +++ b/web/app/assets/stylesheets/web/welcome.css.scss @@ -2,7 +2,7 @@ @import "client/common.css.scss"; -body.web { +body.web.welcome { .signin-common { height:auto; @@ -19,6 +19,15 @@ body.web { } } + .jamfest { + top:-70px; + position:relative; + + .jamblaster { + font-weight:bold; + } + } + .follow-links { position: absolute; right: 0; diff --git a/web/app/controllers/api_backing_tracks_controller.rb b/web/app/controllers/api_backing_tracks_controller.rb new file mode 100644 index 000000000..9ed591422 --- /dev/null +++ b/web/app/controllers/api_backing_tracks_controller.rb @@ -0,0 +1,32 @@ +class ApiBackingTracksController < ApiController + + # have to be signed in currently to see this screen + before_filter :api_signed_in_user + + before_filter :lookup_recorded_backing_track, :only => [ :backing_track_silent ] + + respond_to :json + + def index + tracks = [ + {:name=>'foo',:path=>"foobar.mp3", :length=>4283}, + {:name=>'bar',:path=>"foo.mp3",:length=>3257} + ] + @backing_tracks, @next = tracks, nil + render "api_backing_tracks/index", :layout => nil + end + + def backing_track_silent + @recorded_backing_track.mark_silent + + render :json => {}, :status => 200 + end + + private + + def lookup_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user) + end + +end # class ApiBackingTracksController diff --git a/web/app/controllers/api_genres_controller.rb b/web/app/controllers/api_genres_controller.rb index c413bf1f2..293552737 100644 --- a/web/app/controllers/api_genres_controller.rb +++ b/web/app/controllers/api_genres_controller.rb @@ -1,8 +1,5 @@ class ApiGenresController < ApiController - # have to be signed in currently to see this screen - before_filter :api_signed_in_user - respond_to :json def index diff --git a/web/app/controllers/api_instruments_controller.rb b/web/app/controllers/api_instruments_controller.rb index fc2c44fd9..c5f10bd49 100644 --- a/web/app/controllers/api_instruments_controller.rb +++ b/web/app/controllers/api_instruments_controller.rb @@ -1,8 +1,5 @@ class ApiInstrumentsController < ApiController - # have to be signed in currently to see this screen - before_filter :api_signed_in_user - respond_to :json def index diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb index cb257be16..fd8019462 100644 --- a/web/app/controllers/api_jam_tracks_controller.rb +++ b/web/app/controllers/api_jam_tracks_controller.rb @@ -1,18 +1,49 @@ class ApiJamTracksController < ApiController # have to be signed in currently to see this screen - before_filter :api_signed_in_user + before_filter :api_signed_in_user, :except => [:index, :show, :show_with_artist_info] + before_filter :api_any_user, :only => [:index, :show, :show_with_artist_info] before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right] respond_to :json + def show + @jam_track = JamTrack.find_by_plan_code!(params[:plan_code]) + end + + def show_with_artist_info + @jam_track = JamTrack.find_by_plan_code!(params[:plan_code]) + end + def index - data = JamTrack.index(params, current_user) + data = JamTrack.index(params, any_user) @jam_tracks, @next = data[0], data[1] render "api_jam_tracks/index", :layout => nil end + def played + if params[:id].blank? + render(:json => { :message => "JamTrack ID required" }, :status => 400) and return + end + play = PlayablePlay.new + play.player_id = current_user.id + play.ip_address = request.remote_ip + + # VRFS-2916 jam_tracks.id is varchar: REMOVE + # play.jam_track = JamTrack.where(id: params[:id].to_i).first + # VRFS-2916 jam_tracks.id is varchar: ADD + play.playable = JamTrack.where(id: params[:id]).first + + play.save + + if play.errors.any? + render :json => { :message => "Unexpected error occurred" }, :status => 500 + else + render :json => {}, :status => 201 + end + end + def purchased params[:show_purchased_only] = true data = JamTrack.index(params, current_user) @@ -23,23 +54,17 @@ class ApiJamTracksController < ApiController render "api_jam_tracks/purchased", :layout => nil end - def downloads - begin - render :json => JamTrack.list_downloads(current_user, params[:limit], params[:since]), :status => 200 - rescue - render :json => { :message => "could not produce list of files" }, :status => 403 - end - end - def download - if @jam_track_right.valid? - if (@jam_track_right && @jam_track_right.signed && @jam_track_right.url.present? &&@jam_track_right.url.file.exists?) + def download + if @jam_track_right.valid? + sample_rate = params[:sample_rate].nil? ? nil : params[:sample_rate].to_i + if (@jam_track_right && @jam_track_right.ready?(sample_rate)) @jam_track_right.update_download_count @jam_track_right.last_downloaded_at = Time.now @jam_track_right.save! - redirect_to @jam_track_right.sign_url + redirect_to @jam_track_right.sign_url(120, sample_rate) else - @jam_track_right.enqueue_if_needed + @jam_track_right.enqueue_if_needed(sample_rate) render :json => { :message => "not available, digitally signing Jam Track offline." }, :status => 202 end else @@ -48,8 +73,8 @@ class ApiJamTracksController < ApiController end def enqueue - @jam_track_right.enqueue_if_needed - + sample_rate = params[:sample_rate].nil? ? nil : params[:sample_rate].to_i + @jam_track_right.enqueue_if_needed(sample_rate) render :json => { :message => "enqueued" }, :status => 200 end @@ -58,16 +83,21 @@ class ApiJamTracksController < ApiController end def keys - jamtrack_ids = params[:jamtracks] + jamtrack_holder = params[:jamtracks] - unless jamtrack_ids.kind_of?(Array) - render :json => {message: 'jamtracks parameter must be an array'}, :status => 200 + unless jamtrack_holder.kind_of?(Hash) + render :json => {message: 'jamtracks parameter must be an hash'}, :status => 422 return end - @jam_tracks = JamTrackRight.list_keys(current_user, params[:jamtracks]) + jamtrack_ids = jamtrack_holder[:tracks] - render "api_jam_tracks/list_keys", :layout => nil + unless jamtrack_ids.kind_of?(Array) + render :json => {message: 'jamtracks:tracks parameter must be an array'}, :status => 422 + return + end + + @jam_tracks = JamTrackRight.list_keys(current_user, jamtrack_ids) end private diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index b85f265d0..6fb5df5cf 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController # have to be signed in currently to see this screen before_filter :api_signed_in_user, :except => [ :add_like, :show, :show_history, :add_session_info_comment ] - before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close] + before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close, :backing_track_open, :backing_track_close, :metronome_open, :metronome_close] skip_before_filter :api_signed_in_user, only: [:perf_upload] respond_to :json @@ -165,7 +165,8 @@ class ApiMusicSessionsController < ApiController def show unless @music_session.can_see? current_user - raise ActiveRecord::RecordNotFound + # render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 403 + raise JamRuby::PermissionError end end @@ -357,7 +358,7 @@ class ApiMusicSessionsController < ApiController end def track_sync - @tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks]) + @tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_tracks], params[:metronome_open]) unless @tracks.kind_of? Array # we have to do this because api_session_detail_url will fail with a bad @tracks @@ -597,8 +598,44 @@ class ApiMusicSessionsController < ApiController respond_with_model(@music_session) end + def backing_track_open + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end - private + @backing_track_path = params[:backing_track_path] + @music_session.open_backing_track(current_user, @backing_track_path) + respond_with_model(@music_session) + end + + def backing_track_close + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.close_backing_track() + respond_with_model(@music_session) + end + + def metronome_open + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.open_metronome(current_user) + respond_with_model(@music_session) + end + + def metronome_close + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.close_metronome() + respond_with_model(@music_session) + end + +private def lookup_session @music_session = ActiveMusicSession.find(params[:id]) diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 6244e1e17..27eb6c856 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -1,8 +1,9 @@ class ApiRecordingsController < ApiController before_filter :api_signed_in_user, :except => [ :add_like ] - before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ] + before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim, :add_timeline ] before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] + before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ] before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ] before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ] @@ -43,7 +44,11 @@ class ApiRecordingsController < ApiController @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) end - def download + def show_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) + end + + def download # track raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user) @recorded_track.current_user = current_user @@ -52,7 +57,22 @@ class ApiRecordingsController < ApiController @recorded_track.valid? if !@recorded_track.errors.any? @recorded_track.save! - redirect_to @recorded_track.sign_url + redirect_to @recorded_track.sign_url() + else + render :json => { :message => "download limit surpassed" }, :status => 404 + end + end + + def backing_track_download + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user) + + @recorded_backing_track.current_user = current_user + @recorded_backing_track.update_download_count + + @recorded_backing_track.valid? + if !@recorded_backing_track.errors.any? + @recorded_backing_track.save! + redirect_to @recorded_backing_track.sign_url else render :json => { :message => "download limit surpassed" }, :status => 404 end @@ -227,6 +247,61 @@ class ApiRecordingsController < ApiController end end + def backing_track_upload_next_part + length = params[:length] + md5 = params[:md5] + + @recorded_backing_track.upload_next_part(length, md5) + + if @recorded_backing_track.errors.any? + + response.status = :unprocessable_entity + # this is not typical, but please don't change this line unless you are sure it won't break anything + # this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's + # state doesn't cause errors to shoot out like normal. + render :json => { :errors => @recorded_backing_track.errors }, :status => 422 + else + result = { + :part => @recorded_backing_track.next_part_to_upload, + :offset => @recorded_backing_track.file_offset.to_s + } + + render :json => result, :status => 200 + end + + end + + def backing_track_upload_sign + render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200 + end + + def backing_track_upload_part_complete + part = params[:part] + offset = params[:offset] + + @recorded_backing_track.upload_part_complete(part, offset) + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + else + render :json => {}, :status => 200 + end + end + + def backing_track_upload_complete + @recorded_backing_track.upload_complete + @recorded_backing_track.recording.upload_complete + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + return + else + render :json => {}, :status => 200 + end + end + # POST /api/recordings/:id/videos/:video_id/upload_sign def video_upload_sign length = params[:length] @@ -302,6 +377,13 @@ class ApiRecordingsController < ApiController end end + # metadata + def add_timeline + @recording.add_timeline(params) + + render :json => {}, :status => 200 + end + private def lookup_recording @@ -314,6 +396,11 @@ class ApiRecordingsController < ApiController raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user) end + def lookup_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user) + end + def lookup_stream_mix @quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id) raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user) diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index 575366d55..d69b7b3cf 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -1,98 +1,171 @@ -require 'recurly_client' +require 'jam_ruby/recurly_client' class ApiRecurlyController < ApiController - before_filter :api_signed_in_user + before_filter :api_signed_in_user, :except => [:create_account] before_filter :create_client respond_to :json # create Recurly account def create_account - @account = @client.find_or_create_account(current_user, params[:billing_info]) - render :json=>account_json(@account) - rescue RecurlyClientError => x - render json: { :message => x.inspect, errors: x.errors }, :status => 404 + + billing_info = params[:billing_info] + shipping_info = params[:shipping_info] + # should we let the user reuse this card next time? + reuse_card_next_time = params[:reuse_card_next_time] == "true" + # should we update the card info, or use what's on file this time? + reuse_card_this_time = params[:reuse_card_this_time] == "true" + # terms of service accepted? + terms_of_service = params[:terms_of_service] == "true" + + + if current_user + # keep reuse card up-to-date + User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time]) + else + options = { + remote_ip: request.remote_ip, + first_name: billing_info[:first_name], + last_name: billing_info[:last_name], + email: params[:email], + password: params[:password], + password_confirmation: params[:password], + terms_of_service: terms_of_service, + instruments: [{:instrument_id => 'other', :proficiency_level => 1, :priority => 1}], + birth_date: nil, + location: {:country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]}, + musician: true, + skip_recaptcha: true, + invited_user: nil, + fb_signup: nil, + signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm", + any_user: any_user, + reuse_card: reuse_card_next_time + } + + user = UserManager.new.signup(options) + + if user.errors.any? + # render any @user.errors on error + respond_with_model(user) + return + else + sign_in user + end + end + + begin + billing_info[:ip_address] = request.remote_ip if billing_info + if reuse_card_this_time + # do not attempt to update any billing/shipping info unless the user re-inputs their info! + @account = @client.get_account(current_user) + else + @account = @client.find_or_create_account(current_user, billing_info) + end + + render :json => account_json(@account) + rescue RecurlyClientError => x + render json: {:message => x.inspect, errors: x.errors}, :status => 404 + end end def delete_account @client.delete_account(current_user) render json: {}, status: 200 rescue RecurlyClientError => x - render json: { :message => x.inspect, errors: x.errors}, :status => 404 + render json: {:message => x.inspect, errors: x.errors}, :status => 404 end # get Recurly account def get_account - @account=@client.get_account(current_user) + @account = @client.get_account(current_user) - render :json=>account_json(@account) + render :json => account_json(@account) rescue RecurlyClientError => e - render json: { message: x.inspect, errors: x.errors}, :status => 404 + render json: {message: x.inspect, errors: x.errors}, :status => 404 + end + + # get Recurly payment history + def payment_history + @payments=@client.payment_history(current_user) + render :json => {payments: @payments} + rescue RecurlyClientError => x + render json: {message: x.inspect, errors: x.errors}, :status => 404 end # update Recurly account def update_account - @account=@client.update_account(current_user, params[:billing_info]) - render :json=>account_json(@account) + @account=@client.update_account(current_user, params[:billing_info]) + render :json => account_json(@account) rescue RecurlyClientError => x - render json: { message: x.inspect, errors: x.errors}, :status => 404 + render json: {message: x.inspect, errors: x.errors}, :status => 404 end # get Billing Information def billing_info @account = @client.get_account(current_user) - # @billing = @account.billing_info - # @billing ||= @account - render :json=> account_json(@account) + if @account + render :json => account_json(@account) + else + render :json => {}, :status => 404 + end rescue RecurlyClientError => x - render json: { message: x.inspect, errors: x.errors}, :status => 404 + render json: {message: x.inspect, errors: x.errors}, :status => 404 end # update Billing Information def update_billing_info - @account=@client.update_billing_info(current_user, params[:billing_info]) - render :json=> account_json(@account) + @account = @client.update_billing_info(current_user, params[:billing_info]) + render :json => account_json(@account) rescue RecurlyClientError => x - render json: { message: x.inspect, errors: x.errors}, :status => 404 + render json: {message: x.inspect, errors: x.errors}, :status => 404 end def place_order error=nil - puts "PLACING ORDER #{params.inspect}" - params[:jam_tracks].each do |jam_track_id| - jam_track = JamTrack.where("id=?", jam_track_id).first - if jam_track - @client.place_order(current_user, jam_track) - else - error="JamTrack not found for '#{jam_track_id}'" - break + response = {jam_tracks: []} + + sales = Sale.place_order(current_user, current_user.shopping_carts) + + sales.each do |sale| + if sale.is_jam_track_sale? + sale.sale_line_items.each do |line_item| + jam_track = line_item.product + jam_track_right = jam_track.right_for_user(current_user) + response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version} + end end end if error - render json: { errors: {message:error}}, :status => 404 + render json: {errors: {message: error}}, :status => 404 else - render :json=>{}, :status=>200 + render :json => response, :status => 200 end rescue RecurlyClientError => x - render json: { message: x.inspect, errors: x.errors}, :status => 404 + render json: {message: x.inspect, errors: x.errors}, :status => 404 end -private + private def create_client @client = RecurlyClient.new end def account_json(account) + + billing_info = account.billing_info.nil? ? nil : { + :first_name => account.billing_info.first_name, + :last_name => account.billing_info.last_name, + :address1 => account.billing_info.address1, + :address2 => account.billing_info.address2, + :city => account.billing_info.city, + :state => account.billing_info.state, + :zip => account.billing_info.zip, + :country => account.billing_info.country, + :last_four => account.billing_info.last_four + } + { - :first_name => account.first_name, - :last_name => account.last_name, - :email => account.email, - :address1 => account.billing_info ? account.billing_info.address1 : nil, - :address2 => account.billing_info ? account.billing_info.address2 : nil, - :city => account.billing_info ? account.billing_info.city : nil, - :state => account.billing_info ? account.billing_info.state : nil, - :zip => account.billing_info ? account.billing_info.zip : nil, - :country => account.billing_info ? account.billing_info.country : nil + billing_info: billing_info } end - + end # class \ No newline at end of file diff --git a/web/app/controllers/api_recurly_web_hook_controller.rb b/web/app/controllers/api_recurly_web_hook_controller.rb new file mode 100644 index 000000000..a809800b1 --- /dev/null +++ b/web/app/controllers/api_recurly_web_hook_controller.rb @@ -0,0 +1,30 @@ +class ApiRecurlyWebHookController < ApiController + + http_basic_authenticate_with name: Rails.application.config.recurly_webhook_user, password: Rails.application.config.recurly_webhook_pass + + before_filter :api_signed_in_user, only: [] + #respond_to :xml + + + def on_hook + begin + + document = Nokogiri::XML(request.body) + + if RecurlyTransactionWebHook.is_transaction_web_hook?(document) + transaction = RecurlyTransactionWebHook.create_from_xml(document) + end + + rescue Exception => e + Stats.write('web.recurly.webhook.transaction.error', {message: e.to_s, value: 1}) + + log.error("unable to process webhook: #{e.to_s}") + + raise JamArgumentError.new("unable to parse webhook #{e.to_s}") + end + + Stats.write('web.recurly.webhook.transaction.success', {value: 1}) + + render xml: { success: true }, :status => 200 + end +end diff --git a/web/app/controllers/api_sales_controller.rb b/web/app/controllers/api_sales_controller.rb new file mode 100644 index 000000000..8886ba6bd --- /dev/null +++ b/web/app/controllers/api_sales_controller.rb @@ -0,0 +1,15 @@ +class ApiSalesController < ApiController + + respond_to :json + + def index + data = Sale.index(current_user, + page: params[:page], + per_page: params[:per_page]) + + + @sales = data[:query] + @next = data[:next_page] + render "api_sales/index", :layout => nil + end +end \ No newline at end of file diff --git a/web/app/controllers/api_shopping_carts_controller.rb b/web/app/controllers/api_shopping_carts_controller.rb index 5cff85b97..761c3dc81 100644 --- a/web/app/controllers/api_shopping_carts_controller.rb +++ b/web/app/controllers/api_shopping_carts_controller.rb @@ -1,11 +1,11 @@ class ApiShoppingCartsController < ApiController - before_filter :api_signed_in_user + before_filter :api_any_user respond_to :json def index - @carts = current_user.shopping_carts + @carts = any_user.shopping_carts end def add_jamtrack @@ -16,7 +16,7 @@ class ApiShoppingCartsController < ApiController raise StateError, "Invalid JamTrack." end - @cart = ShoppingCart.create current_user, jam_track + @cart = ShoppingCart.add_jam_track_to_cart(any_user, jam_track) if @cart.errors.any? response.status = :unprocessable_entity @@ -43,15 +43,16 @@ class ApiShoppingCartsController < ApiController end def remove_cart - @cart = current_user.shopping_carts.find_by_id(params[:id]) + @cart = any_user.shopping_carts.find_by_id(params[:id]) raise StateError, "Invalid Cart." if @cart.nil? - @cart.destroy + ShoppingCart.remove_jam_track_from_cart(any_user, @cart) + respond_with responder: ApiResponder, :status => 204 end def clear_all - ShoppingCart.where("user_id=?", current_user).destroy_all + any_user.destroy_all_shopping_carts render :json=>{}, :status=>200 end diff --git a/web/app/controllers/api_signup_hints_controller.rb b/web/app/controllers/api_signup_hints_controller.rb new file mode 100644 index 000000000..8d9e6ef31 --- /dev/null +++ b/web/app/controllers/api_signup_hints_controller.rb @@ -0,0 +1,19 @@ +class ApiSignupHintsController < ApiController + + before_filter :api_anonymous_user + + respond_to :json + + def show + @signup_hint = SignupHint.find(params[:id]) + respond_with_model(@signup_hint) + end + + def create + @signup_hint = SignupHint.refresh_by_anoymous_user(anonymous_user, params) + + respond_with_model(@signup_hint, new: true, location: lambda { return api_signup_hint_detail_url(@signup_hint.id) }) + end +end + + diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 7d8d40f94..8065a504d 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -1,7 +1,7 @@ require 'sanitize' class ApiUsersController < ApiController - before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump] + before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :liking_create, :liking_destroy, # likes :following_create, :following_show, :following_destroy, # followings @@ -26,7 +26,7 @@ class ApiUsersController < ApiController @user = User.includes([{musician_instruments: :instrument}, {band_musicians: :user}, {genre_players: :genre}, - :bands, :instruments, :genres]) + :bands, :instruments, :genres, :jam_track_rights]) .find(params[:id]) respond_with @user, responder: ApiResponder, :status => 200 @@ -46,8 +46,10 @@ class ApiUsersController < ApiController @user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments) @user.update_genres(params[:genres].nil? ? [] : params[:genres]) if params.has_key?(:genres) @user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next) + @user.show_whats_next_count = params[:show_whats_next_count] if params.has_key?(:show_whats_next_count) @user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email) @user.biography = params[:biography] if params.has_key?(:biography) + @user.want_jamblaster = params[:want_jamblaster] if params.has_key?(:want_jamblaster) @user.mod_merge(params[:mods]) if params[:mods] # allow keyword of 'LATEST' to mean set the notification_seen_at to the most recent notification for this user @@ -699,6 +701,37 @@ class ApiUsersController < ApiController end end + def validate_data + unless (data = params[:data]).present? + render(json: { message: "blank data #{data}" }, status: :unprocessable_entity) && return + end + url = nil + site = params[:sitetype] + if site.blank? || 'url'==site + url = data + elsif Utils.recording_source?(site) + rec_id = Utils.extract_recording_id(site, data) + if rec_id + render json: { message: 'Valid Site', recording_id: rec_id, data: data }, status: 200 + return + else + render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200 + return + end + else + url = Utils.username_url(data, site) + end + unless url.blank? + if errmsg = Utils.site_validator(url, site) + render json: { message: 'Invalid Site', data: data, errors: { site: [errmsg] } }, status: 200 + else + render json: { message: 'Valid Site', data: data }, status: 200 + end + else + render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity + end + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/app/controllers/application_controller.rb b/web/app/controllers/application_controller.rb index 29e294ac5..4a2ae2397 100644 --- a/web/app/controllers/application_controller.rb +++ b/web/app/controllers/application_controller.rb @@ -12,6 +12,8 @@ class ApplicationController < ActionController::Base gon_setup end + before_filter :set_tracking_cookie + before_filter do if params[AffiliatePartner::PARAM_REFERRAL].present? && current_user.nil? if cookies[AffiliatePartner::PARAM_COOKIE].blank? @@ -25,6 +27,12 @@ class ApplicationController < ActionController::Base cookies[AffiliatePartner::PARAM_COOKIE] end + + # http://stackoverflow.com/questions/15807214/where-to-set-a-tracking-permanent-cookie-in-rails + def set_tracking_cookie + cookies.permanent[:user_uuid] = SecureRandom.uuid unless cookies[:user_uuid] + end + private def add_user_info_to_bugsnag(notif) # Add some app-specific data which will be displayed on a custom diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 928715adf..51813a8d3 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -16,6 +16,7 @@ class ClientsController < ApplicationController return end + gon.recurly_tax_estimate_jam_track_plan = Rails.application.config.recurly_tax_estimate_jam_track_plan render :layout => 'client' end diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index 0249897d7..e7e7150ca 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -22,6 +22,22 @@ class LandingsController < ApplicationController end end + def watch_overview_kick + render 'watch_kick', layout: 'web' + end + + def watch_overview_kick2 + render 'watch_kick_2', layout: 'web' + end + + def watch_overview_kick3 + render 'watch_kick_3', layout: 'web' + end + + def watch_overview_kick4 + render 'watch_kick_4', layout: 'web' + end + def watch_overview @promo_buzz = PromoBuzz.active @@ -49,5 +65,39 @@ class LandingsController < ApplicationController def watch_overview_tight render 'watch_overview_tight', layout: 'web' end + + def individual_jamtrack + @jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code]) + gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil + gon.generic = params[:generic] + render 'individual_jamtrack', layout: 'web' + end + + def individual_jamtrack_band + @jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code]) + gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil + + render 'individual_jamtrack_band', layout: 'web' + end + + def product_jamblaster + render 'product_jamblaster', layout: 'web' + end + + def product_platform + render 'product_platform', layout: 'web' + end + + def product_jamtracks + gon.generic = true + gon.just_previews = true + jam_track = JamTrack.select('plan_code').where(plan_code: Rails.application.config.nominated_jam_track).first + unless jam_track + jam_track = JamTrack.first + end + + gon.jam_track_plan_code = jam_track.plan_code if jam_track + render 'product_jamtracks', layout: 'web' + end end diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index b07987b4d..6807ef6bd 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -33,8 +33,38 @@ class SpikesController < ApplicationController def subscription + #Notification.send_reload(MessageFactory::ALL_NATIVE_CLIENTS) + Notification.send_subscription_message('test', '1', '{"msg": "oh hai 1"}') Notification.send_subscription_message('test', '2', '{"msg": "oh hai 2"}') render text: 'oh hai' end + + def download_jam_track + + jamTrack = JamTrack.find(params[:jam_track_id]) + jamTrackRight = jamTrack.right_for_user(current_user) + + gon.jamTrackId = jamTrack.id + gon.jamTrackRightId = jamTrackRight.id + gon.size = params[:size] ? params[:size] : 'large' + gon.switchState = params[:state] + + render :layout => 'web' + end + + def jam_track_preview + gon.jamTrackPlanCode = params[:plan_code] + + + render :layout => 'web' + end + + def site_validate + render :layout => 'web' + end + + def recording_source + render :layout => 'web' + end end diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 6cd34cabd..b9a3592ea 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -37,6 +37,7 @@ class UsersController < ApplicationController end def new + @no_user_dropdown = true if current_user redirect_to client_url return @@ -103,6 +104,8 @@ class UsersController < ApplicationController return end + signup_hint = SignupHint.where(anonymous_user_id: anonymous_user.id).where('expires_at > ?', Time.now).first if anonymous_user + @fb_signup = load_facebook_signup(params) # check if the email specified by @fb_signup already exists in the database--if so, log them in and redirect @@ -112,7 +115,8 @@ class UsersController < ApplicationController # update user_authorization for user because this is fresher user.update_fb_authorization(@fb_signup) sign_in(user) - redirect_to client_url + redirect_url = handle_signup_hint(user, signup_hint, client_url) + redirect_to redirect_url return end end @@ -124,10 +128,11 @@ class UsersController < ApplicationController if user_authorization user_authorization.user.update_fb_authorization(@fb_signup) sign_in(user_authorization.user) - redirect_to client_url + + redirect_url = handle_signup_hint(user_authorization.user, signup_hint, client_url) + redirect_to redirect_url return end - end @invited_user = load_invited_user(params) @@ -168,36 +173,49 @@ class UsersController < ApplicationController else sign_in @user + new_user(@user, signup_hint) # sets a cookie used for GA analytics (one-time new user stuff in JavaScript) destination = @user.musician ? :congratulations_musician : :congratulations_fan - - redirect_to :action => destination, :type => @user.user_authorization('facebook') ? 'Facebook' : 'Native' + redirect_url = handle_signup_hint(@user, signup_hint, {:action => destination, :type => @user.user_authorization('facebook') ? 'Facebook' : 'Native'}) + redirect_to redirect_url end end + # given the current user, and any signup hint (can be nil) + # handle the final destination of the user + def handle_signup_hint(user, signup_hint, default_redirect) + redirect_url = default_redirect + if signup_hint + if signup_hint.want_jamblaster + User.where(id: user.id).update_all(want_jamblaster: true) + end + + if signup_hint.redirect_location + redirect_url = signup_hint.redirect_location + end + end + + redirect_url + end + def congratulations_fan + @no_user_dropdown = true render :layout => "web" end def congratulations_musician + @no_user_dropdown = true render :layout => "web" end def downloads + @no_user_dropdown = true render :layout => "web" end # DO NOT USE CURRENT_USER IN THIS ROUTINE. IT'S CACHED FOR THE WHOLE SITE - def welcome - - @slides = [ - Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), - Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"), - Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1"), - Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), - Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"), - Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1") - ] + def home + @no_user_dropdown = true @promo_buzz = PromoBuzz.active if Rails.application.config.use_promos_on_homepage @@ -206,12 +224,8 @@ class UsersController < ApplicationController @promo_latest, start = Feed.index(nil, limit: 10) end - # temporary--will go away soon - @jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID - @jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID - # temporary--end - @welcome_page = true + gon.signed_in = !current_user.nil? render :layout => "web" end @@ -392,6 +406,19 @@ JS end end + def unsubscribe + unless @user = User.read_access_token(params[:user_token]) + redirect_to '/' + end if params[:user_token].present? + + if request.get? + + elsif request.post? + @user.subscribe_email = false + @user.save! + end + end + private def is_native_client diff --git a/web/app/helpers/application_helper.rb b/web/app/helpers/application_helper.rb index 827379344..160a1b3cf 100644 --- a/web/app/helpers/application_helper.rb +++ b/web/app/helpers/application_helper.rb @@ -10,6 +10,14 @@ module ApplicationHelper end end + def meta_description(description) + if description + description + else + 'JamKazam' + end + end + def self.base_uri(request) (request.ssl? ? "https://" : "http://") + request.host_with_port end diff --git a/web/app/helpers/recording_helper.rb b/web/app/helpers/recording_helper.rb index 3d8a6eeec..8eb09f158 100644 --- a/web/app/helpers/recording_helper.rb +++ b/web/app/helpers/recording_helper.rb @@ -49,8 +49,8 @@ module RecordingHelper end end - def description_for_claimed_recording(claimed_recording) - truncate(claimed_recording.name, length:250) + def description_for_claimed_recording(claimed_recording, length = 250) + truncate(claimed_recording.name, length:length) end def listen_mix_url(recording) diff --git a/web/app/helpers/sessions_helper.rb b/web/app/helpers/sessions_helper.rb index 98b0c9774..332db70c3 100644 --- a/web/app/helpers/sessions_helper.rb +++ b/web/app/helpers/sessions_helper.rb @@ -26,6 +26,10 @@ module SessionsHelper !current_user.nil? end + def has_anonymous_user? + !anonymous_user.nil? + end + def current_user=(user) @current_user = user end @@ -34,6 +38,39 @@ module SessionsHelper @current_user ||= User.find_by_remember_token(cookies[:remember_token]) end + def anonymous_user=(anonymous_user) + @anonymous_user = anonymous_user + end + + def anonymous_user + if anon_cookie + @anonymous_user ||= AnonymousUser.new(anon_cookie) + else + nil + end + end + + # tries current_user over anonymous_user + def any_user + current_user || anonymous_user + end + + def anon_cookie + @anon_cookie ||= cookies[:user_uuid] + end + + def new_user(user, signup_hint) + + want_jamblaster = false + redirect_location = nil + if signup_hint + want_jamblaster = signup_hint.want_jamblaster + redirect_location = signup_hint.redirect_location + end + + cookies[:new_user] = { musician: user.musician, registrationType: user.user_authorization('facebook') ? 'Facebook' : 'Native', want_jamblaster: want_jamblaster, redirect_location: redirect_location }.to_json + end + def current_user?(user) user == current_user end @@ -45,13 +82,25 @@ module SessionsHelper end end - def api_signed_in_user unless signed_in? render :json => { :message => "not logged in"}, :status => 403 end end + # take either the signed in user, or if that fails, try the anonymous user + def api_any_user + unless signed_in? || has_anonymous_user? + render :json => { :message => "not logged in"}, :status => 403 + end + end + + def api_anonymous_user + unless has_anonymous_user? + render :json => { :errors => {:user_uuid => ['not specified']}}, :status => 422 + end + + end def sign_out current_user = nil cookies.delete(:remember_token, domain: Rails.application.config.session_cookie_domain) @@ -91,4 +140,15 @@ module SessionsHelper current_user.musician? ? 'Musician' : 'Fan' end end + + def logged_in_not_logged_in_class + signed_in? ? "logged-in" : "not-logged-in" + end + + + def metronome_tempos + [ + 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 + ] + end end diff --git a/web/app/views/api_backing_tracks/index.rabl b/web/app/views/api_backing_tracks/index.rabl new file mode 100644 index 000000000..a5b9b4b9b --- /dev/null +++ b/web/app/views/api_backing_tracks/index.rabl @@ -0,0 +1,7 @@ +node :next do |page| + @next +end + +node :backing_tracks do |page| + @backing_tracks +end diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index dbc1c7fd9..c22a49ad3 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -2,6 +2,8 @@ # I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata # depictable. +# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState + object @claimed_recording attributes :id, :name, :description, :is_public, :genre_id, :discarded @@ -18,8 +20,20 @@ node :mix do |claimed_recording| end child(:recording => :recording) { - attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count - + attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count, :jam_track_id, :jam_track_initiator_id + + node :timeline do |recording| + recording.timeline ? JSON.parse(recording.timeline) : {} + end + + child(:jam_track => :jam_track) { + attributes :id + + node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil + end + } + child(:band => :band) { attributes :id, :name, :location, :photo_url } @@ -36,6 +50,27 @@ child(:recording => :recording) { } } + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url + } + + node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + } + + child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) { + node do |recorded_jam_track_track| + { + id: recorded_jam_track_track.jam_track_track.id, + timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : [] + } + end + } + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_jam_tracks/list_keys.rabl b/web/app/views/api_jam_tracks/keys.rabl similarity index 85% rename from web/app/views/api_jam_tracks/list_keys.rabl rename to web/app/views/api_jam_tracks/keys.rabl index 43a33b24e..f82aa5650 100644 --- a/web/app/views/api_jam_tracks/list_keys.rabl +++ b/web/app/views/api_jam_tracks/keys.rabl @@ -2,7 +2,7 @@ object @jam_tracks node do |jam_track| { - id: jam_track['id'], + id: jam_track['id'].to_s, private: jam_track['private_key'], error: jam_track['private_key'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' ) } diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl index f9ea856db..65d887ea8 100644 --- a/web/app/views/api_jam_tracks/show.rabl +++ b/web/app/views/api_jam_tracks/show.rabl @@ -1,17 +1,28 @@ object @jam_track -attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price +attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price, :version node :genres do |item| [item.genre.description] # XXX: need to return single genre; not array end node :added_cart do |item| - current_user.shopping_carts.where("cart_id='?'",item.id).count != 0 + any_user.shopping_carts.where(cart_id: item.id).count != 0 +end + +node :purchased do |item| + !!item.right_for_user(current_user) end child(:jam_track_tracks => :tracks) { - attributes :id, :part, :instrument + attributes :id, :part, :instrument, :track_type + + node do |track| + { + preview_mp3_url: track.preview_public_url('mp3'), + preview_ogg_url: track.preview_public_url('ogg') + } + end } child(:licensor => :licensor) { diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl index 4e708a4ac..bc212f46d 100644 --- a/web/app/views/api_jam_tracks/show_for_client.rabl +++ b/web/app/views/api_jam_tracks/show_for_client.rabl @@ -1,9 +1,21 @@ object @jam_track -attributes :id, :name, :description, :initial_play_silence, :original_artist +attributes :id, :name, :description, :initial_play_silence, :original_artist, :version, :genre + +node :genre do |jam_track| + jam_track.genre.present? ? jam_track.genre.id : nil +end + +node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil +end + +node :jam_track_right_id do |jam_track| + jam_track.right_for_user(current_user).id +end child(:jam_track_tracks => :tracks) { - attributes :id, :part, :instrument + attributes :id, :part, :instrument, :track_type } child(:jam_track_tap_ins => :tap_ins) { diff --git a/web/app/views/api_jam_tracks/show_with_artist_info.rabl b/web/app/views/api_jam_tracks/show_with_artist_info.rabl new file mode 100644 index 000000000..76dc3b68b --- /dev/null +++ b/web/app/views/api_jam_tracks/show_with_artist_info.rabl @@ -0,0 +1,7 @@ +object @jam_track + +attributes :band_jam_track_count + +node do |jam_track| + partial "api_jam_tracks/show", object: @jam_track +end diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 124f9ba56..771933fbd 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -13,7 +13,7 @@ if !current_user } else - attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score + attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id node :can_join do |session| session.can_join?(current_user, true) @@ -45,7 +45,7 @@ else child(:connections => :participants) { collection @music_sessions, :object_root => false - attributes :ip_address, :client_id, :joined_session_at, :audio_latency, :id + attributes :ip_address, :client_id, :joined_session_at, :audio_latency, :id, :metronome_open node :user do |connection| { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state } @@ -54,6 +54,10 @@ else child(:tracks => :tracks) { attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at } + + child(:backing_tracks => :backing_tracks) { + attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at + } } child({:invitations => :invitations}) { @@ -70,14 +74,10 @@ else # only show currently open jam track info if the current user is in the session child({:jam_track => :jam_track}, :if => lambda { |music_session| music_session.users.exists?(current_user) }) { - attributes :id, :name, :description, :initial_play_silence + attributes :id, :name, :description child(:jam_track_tracks => :tracks) { - attributes :id, :part, :instrument - } - - child(:jam_track_tap_ins => :tap_ins) { - attributes :offset_time, :bpm, :tap_in_count + attributes :id, :part, :instrument, :track_type } } @@ -103,6 +103,10 @@ else end } + child({:jam_track => :jam_track}) { + attributes :id, :name, :description + } + child(:recorded_tracks => :recorded_tracks) { attributes :id, :fully_uploaded, :client_track_id, :client_id, :instrument_id @@ -114,6 +118,27 @@ else attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url } } + + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + } + } + + child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) { + node do |recorded_jam_track_track| + { + id: recorded_jam_track_track.jam_track_track.id, + user_id: recorded_jam_track_track.user_id, + part: recorded_jam_track_track.jam_track_track.part, + instrument: recorded_jam_track_track.jam_track_track.instrument, + track_type: recorded_jam_track_track.jam_track_track.track_type, + timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : [] + } + end + } } } diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index 1fe574026..d59732cb3 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -1,6 +1,10 @@ object @recording -attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded? +attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session + +node :fan_access do |recording| + recording.non_active_music_session.fan_access +end node :mix do |recording| if recording.mix @@ -12,6 +16,17 @@ node :mix do |recording| end end +node :timeline do |recording| + recording.timeline ? JSON.parse(recording.timeline) : {} +end + +child(:jam_track => :jam_track) { + attributes :id + + node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil + end +} child(:band => :band) { attributes :id, :name, :location, :photo_url @@ -27,6 +42,21 @@ child(:recorded_tracks => :recorded_tracks) { end } +child(:recorded_backing_tracks => :recorded_backing_tracks) { + node do |recorded_backing_track| + partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track) + end +} + +child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) { + node do |recorded_jam_track_track| + { + id: recorded_jam_track_track.jam_track_track.id, + timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : [] + } + end +} + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_recordings/show_recorded_backing_track.rabl b/web/app/views/api_recordings/show_recorded_backing_track.rabl new file mode 100644 index 000000000..6487b9db6 --- /dev/null +++ b/web/app/views/api_recordings/show_recorded_backing_track.rabl @@ -0,0 +1,11 @@ +object @recorded_backing_track + +attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename + +node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + +child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url +} \ No newline at end of file diff --git a/web/app/views/api_sales/index.rabl b/web/app/views/api_sales/index.rabl new file mode 100644 index 000000000..1b61714f5 --- /dev/null +++ b/web/app/views/api_sales/index.rabl @@ -0,0 +1,7 @@ +node :next_page do |page| + @next +end + +node :entries do |page| + partial "api_sales/show", object: @sales +end \ No newline at end of file diff --git a/web/app/views/api_sales/show.rabl b/web/app/views/api_sales/show.rabl new file mode 100644 index 000000000..d6d7129a6 --- /dev/null +++ b/web/app/views/api_sales/show.rabl @@ -0,0 +1,11 @@ +object @sale + +attributes :id, :recurly_invoice_id, :recurly_subtotal_in_cents, :recurly_tax_in_cents, :recurly_total_in_cents, :recurly_currency, :sale_type, :recurly_invoice_number, :state, :created_at + +child(:recurly_transactions => :recurly_transactions) { + attributes :transaction_type, :amount_in_cents +} + +child(:sale_line_items => :line_items) { + attributes :id, :product_info +} diff --git a/web/app/views/api_signup_hints/show.rabl b/web/app/views/api_signup_hints/show.rabl new file mode 100644 index 000000000..6c72b2273 --- /dev/null +++ b/web/app/views/api_signup_hints/show.rabl @@ -0,0 +1,3 @@ +object @signup_hint + +attributes :id, :redirect_location, :want_jamblaster \ No newline at end of file diff --git a/web/app/views/api_user_syncs/show.rabl b/web/app/views/api_user_syncs/show.rabl index 0c63ba010..730120276 100644 --- a/web/app/views/api_user_syncs/show.rabl +++ b/web/app/views/api_user_syncs/show.rabl @@ -18,7 +18,6 @@ glue :recorded_track do partial("api_recordings/show", :object => recorded_track.recording) end - node :upload do |recorded_track| { should_upload: true, @@ -35,6 +34,45 @@ glue :recorded_track do end + +glue :recorded_backing_track do + + @object.current_user = current_user + + node :type do |i| + 'recorded_backing_track' + end + + attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename + + node :user do |recorded_backing_track| + partial("api_users/show_minimal", :object => recorded_backing_track.user) + end + + node :recording do |recorded_backing_track| + partial("api_recordings/show", :object => recorded_backing_track.recording) + end + + node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + + node :upload do |recorded_backing_track| + { + should_upload: true, + too_many_upload_failures: recorded_backing_track.too_many_upload_failures? + } + end + + node :download do |recorded_backing_track| + { + should_download: recorded_backing_track.can_download?(current_user), + too_many_downloads: recorded_backing_track.too_many_downloads? + } + end + +end + glue :mix do @object.current_user = current_user diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index b005b0fa8..32c79fee0 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -10,13 +10,17 @@ end # give back more info if the user being fetched is yourself if @user == current_user - attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter, :new_notifications + attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count node :geoiplocation do |user| geoiplocation = current_user.geoiplocation geoiplocation.info if geoiplocation end + node :free_jamtrack do |user| + Rails.application.config.one_free_jamtrack_per_user && user.has_redeemable_jamtrack + end + node :mods do |user| user.mods_json end diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index f699e9717..daef51e5d 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -114,18 +114,19 @@
    - + UPDATE

    @@ -137,14 +138,17 @@
    - + VIEW

    diff --git a/web/app/views/clients/_account_jamtracks.html.slim b/web/app/views/clients/_account_jamtracks.html.slim new file mode 100644 index 000000000..3ae1a39d0 --- /dev/null +++ b/web/app/views/clients/_account_jamtracks.html.slim @@ -0,0 +1,44 @@ +/! Account jamtracks Dialog +#account-jamtracks.screen.secondary layout='screen' layout-id='account/jamtracks' + .content-head + .content-icon + = image_tag "content/icon_account.png", :width => 27, :height => 20 + h1 my account + = render "screen_navigation" + + /! jamtracks scrolling area + .content-body + .content-body-scroller.account-content-scroller#account-jamtracks-content-scroller + .content-wrapper.account-jamtracks + .jamtracks-header + .left.jamtracks-caption + h2 my jamtracks: + .clearall + + #account-my-jamtracks + table.generaltable + thead + th TITLE + th ORIGINAL ARTIST + th ACTIONS + tbody + tr.no-jamtracks-found.hidden + td colspan="3" + | You don't currently own any JamTracks. + a.orange href="/client#/jamtrackBrowse" Browse JamTracks Now + .right + a.button-orange href="/client#/jamtrackBrowse" JAMTRACKS + a.button-grey href="javascript:history.go(-1)" BACK + +script#template-account-jamtrack type='text/template' + tbody + = "{% _.each(data.jamtracks, function(jamtrack) { %}" + tr data-id="{{jamtrack.id}}" data-genre="{{jamtrack.genre}}" + td + | {{jamtrack.name}} + td + | {{jamtrack.original_artist}} + td + .table-link: a.jamtrack-solo-session href= '#' jamtrack-id="{{jamtrack.id}}" Get into solo session + .table-link: a.jamtrack-group-session href= '#' jamtrack-id="{{jamtrack.id}}" Get into session others can join + = "{% }); %}" diff --git a/web/app/views/clients/_account_payment_history.html.slim b/web/app/views/clients/_account_payment_history.html.slim new file mode 100644 index 000000000..5a31dffaf --- /dev/null +++ b/web/app/views/clients/_account_payment_history.html.slim @@ -0,0 +1,44 @@ +.screen.secondary layout="screen" layout-id="account/paymentHistory" class="screen secondary" id="account-payment-history" + + .content + .content-head + .content-icon=image_tag("content/icon_account.png", height:20, width:27 ) + h1 my account + =render "screen_navigation" + .content-body + .content-body-scroller + + .account-left + h2 payment history: + table.payment-table + thead + tr + th DATE + th METHOD + th DESCRIPTION + th STATUS + th AMOUNT + tbody + a.btn-next-pager href="/api/sales?page=1" Next + .end-of-payments-list.end-of-list="No more payment history" + + + .input-aligner + a.back href="" class="button-grey" BACK + br clear="all" + + + + +script#template-payment-history-row type="text/template" + tr + td + | {{data.date}} + td.capitalize + | {{data.payment_method}} + td + | {{data.description}} + td.capitalize + | {{data.status}} + td.amount class="{{data.status}}" + | ${{data.amount}} diff --git a/web/app/views/clients/_checkout_order.html.slim b/web/app/views/clients/_checkout_order.html.slim new file mode 100644 index 000000000..83b88a268 --- /dev/null +++ b/web/app/views/clients/_checkout_order.html.slim @@ -0,0 +1,155 @@ +div layout="screen" layout-id="checkoutOrder" id="checkoutOrderScreen" class="screen secondary" + .content + .content-head + .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) + h1 check out + = render "screen_navigation" + .content-body + .content-body-scroller + .content-wrapper + .checkout-navigation-bar + .order-panel + .payment-wrapper + p.order-prompt.hidden + | Please review your order, and if everything looks correct, click the PLACE YOUR ORDER button. Thank you! + p.empty-cart-prompt.hidden + | You have nothing in your cart. You can go browse for JamTracks  + a href="/client#/jamtrackBrowse" here + | . + p.no-account-info-prompt.hidden + | You have no billing info. Please go back to the  + a href="/client#/checkoutPayment" Payment + |  page to enter your billing info. + .order-content + + #order_error.error.hidden + .clearall + .action-bar + + .right + a.button-grey href="#" id="checkout-info-help" HELP + a.button-grey.back href="#" BACK + a.button-orange.place-order href="#" PLACE YOUR ORDER + .clearall + .thanks-panel + h2 Thank you for your order! + br + .thanks-detail We'll send you an email confirming your order shortly. + br + .thanks-detail.jam-tracks-in-browser.hidden + | To play your purchased JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .thanks-detail.purchased-jam-track.hidden + h2.purchased-jam-track-header Downloading Your Purchased JamTracks + span Each JamTrack will be downloaded sequentially. + br + span.notice Note that you do not have to wait for this to complete in order to use your JamTrack later. + br.clear + ul.purchased-list + + + +script type='text/template' id='template-order-content' + .order-left-page + .payment-info-page + h2 ADDRESS & PAYMENT + .address-info + .billing-address + .billing-caption + | BILLING ADDRESS: + a.change-payment-info href="#" change + .clearall + .billing-info-item= "{{data.billing_info.address1}}" + .billing-info-item= "{{data.billing_info.address2}}" + .billing-info-item + | {{data.billing_info.city}}, {{data.billing_info.state}} {{data.billing_info.zip}} + span.country= "{{data.billing_info.country}}" + .shipping-address + .billing-caption + | SHIPPING ADDRESS: + a.change-payment-info href="#" change + .clearall + = "{% if (data.shipping_as_billing) { %}" + .billing-info-item Same as billing address + = "{% } else { %}" + .billing-info-item= "{{data.shipping_info.address1}}" + .billing-info-item= "{{data.shipping_info.address2}}" + .billing-info-item + | {{data.shipping_info.city}}, {{data.shipping_info.state}} {{data.shipping_info.zip}} + span.country= "{{data.shipping_info.country}}" + = "{% } %}" + br + .payment-method-info + .billing-caption + | PAYMENT METHOD: + a.change-payment-info href="#" change + .clearall + + /= image_tag '' + ="Credit card ending {{data.billing_info.last_four}}" + + .clearall + .order-items-page + h2 ORDER DETAILS + .cart-items + .cart-item-caption + span YOUR ORDER INCLUDES: + .cart-item-price + span PRICE + .cart-item-quantity + span QUANTITY + .clearall + = "{% if (data.carts.length == 0) { %}" + .no-cart-items You have no orders now. + = "{% } %}" + = "{% _.each(data.carts, function(cart) { %}" + .cart-item cart-id="{{cart.id}}" + .cart-item-caption + = "{{cart.cart_type}}: {{cart.product_info.name}}" + .cart-item-price + = "$ {{Number(cart.product_info.total_price).toFixed(2)}}" + .cart-item-quantity + = "{{cart.quantity}}" + .clearall + = "{% }); %}" + .clearall + .order-right-page + h2 PLACE ORDER + .recurly-data.hidden + .plan.jamtrack data-plan-code="{{gon.recurly_tax_estimate_jam_track_plan}}" + input data-recurly="plan" type="text" value="{{gon.recurly_tax_estimate_jam_track_plan}}" + .order-summary + .place-order-center + a.button-orange.place-order href="#" PLACE YOUR ORDER + .clearall + + .billing-caption ORDER SUMMARY: + .order-items-header.order-total Order items: + .order-items-value.order-total= "{{data.sub_total}}" + .clearall + .order-items-header.shipping-handling Shipping & handling: + .order-items-value.shipping-handling= "{{data.shipping_handling}}" + .clearall + .line + .order-items-header.sub-total Total before tax: + .order-items-value.sub-total= "{{data.sub_total}}" + .clearall + .order-items-header.taxes Taxes: + .order-items-value.taxes= "{{data.taxes}}" + .clearall + .line + .order-items-header.grand-total Order total: + .order-items-value.grand-total= "{{data.grand_total}}" + .clearall + .order-help + span By placing your order, you agree to JamKazam's + ' + a href="http://www.jamkazam.com/corp/terms" rel="external" terms of service + ' + span and + ' + a href="http://www.jamkazam.com/corp/returns" rel="external" returns policy + span . + .clearall + +script type='text/template' id='template-purchased-jam-track' + li data-jam-track-id="{{data.jam_track_id}}" \ No newline at end of file diff --git a/web/app/views/clients/_checkout_payment.html.slim b/web/app/views/clients/_checkout_payment.html.slim new file mode 100644 index 000000000..1bf5794a4 --- /dev/null +++ b/web/app/views/clients/_checkout_payment.html.slim @@ -0,0 +1,217 @@ +div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class="screen secondary no-login-required" + .content + .content-head + .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) + h1 check out + = render "screen_navigation" + .content-body + .content-body-scroller + .content-wrapper + .checkout-navigation-bar + .payment-wrapper + p.payment-prompt.free-jamtrack.hidden + | Please enter your billing address and payment information below. You will not be billed for your first JamTrack, which is 100% free.  + | But we need this data to prevent fraud/abuse of those who would create multiple accounts to collect multiple free JamTracks.  + | You will not be billed for any charges of any kind without your explicit authorization.  + | There are no "hidden" charges or fees, thank you! + p.payment-prompt.no-free-jamtrack.hidden + | Please enter your billing address and payment information below.  + p.payment-prompt.already-entered.hidden + | You recently entered payment info successfully. If you want to change your payment info, click the CHANGE PAYMENT INFO button. Otherwise, click the NEXT button to checkout. + span.change-payment-info-holder + a.button-orange.change-payment-info href='#' CHANGE PAYMENT INFO + form.hidden class="payment-info" id="checkout-payment-info" + .row.first + .left-side + .billing-address + h2.billing-caption BILLING ADDRESS + #divBillingFirstName.field + .billing-label + label for="billing-first-name" First Name: * + .billing-value + input type="text" id="billing-first-name" + .clearall + #divBillingLastName.field + .billing-label + label for="billing-last-name" Last Name: * + .billing-value + input type="text" id="billing-last-name" + .clearall + #divBillingAddress1.field + .billing-label + label for="billing-address1" Address 1: * + .billing-value + input type="text" id="billing-address1" + .clearall + #divBillingAddress2.field + .billing-label + label for="billing-address2" Address 2: + .billing-value + input type="text" id="billing-address2" + .clearall + #divBillingCity.field + .billing-label + label for="billing-city" City: * + .billing-value + input type="text" id="billing-city" + .clearall + #divBillingState.field + .billing-label + label for="billing-state" State/Region: * + .billing-value + input type="text" id="billing-state" + .clearall + #divBillingZip.field + .billing-label + label for="billing-zip" Zip: * + .billing-value + input type="text" id="billing-zip" + .clearall + #divBillingCountry.field + .billing-label + label for="billing-country" Country: * + .billing-value + select id="billing-country" + option value="US" US + .clearall + .right-side + .payment-method + h2.payment-method-caption PAYMENT METHOD + .new-card-info + #divCardName.field.hidden + .card-label + label for="card-name" Name of Card: * + .card-value + input type="text" id="card-name" + .clearall + #divCardNumber.field + .card-label + label for="card-number" Card Number: * + .card-value + input type="text" id="card-number" + .clearall + #divCardExpiry.field + .card-label Expiration Date: * + .card-value + =date_select("card", "expire-date", use_two_digit_numbers: true, discard_day: true, :start_year => Time.now.year, :end_year => Time.now.year + 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html => {:class => "account-profile-birthdate", :id => "card-expiry"}) + .clearall + #divCardVerify.field + .card-label + label for="card-verify" + | CVV Code: * + .hint.cvv + | (back of card) + .card-value + input type="text" id="card-verify" + .clearall + .reuse-existing-card + .card-label + .card-value + .reuse-existing-card-checkbox.ichecbuttons + input type="checkbox" id="reuse-existing-card" name="reuse-existing-card" checked="checked" + .reuse-existing-card-helper + label for="reuse-existing-card" + | Use card ending in  + span.existing-card-ends-with + .clearall + .card-label + .card-value + .save-card-checkbox.ichecbuttons + input type="checkbox" id="save-card" name="save-card" checked="checked" + .divSaveCardHelper + label for="save-card" Save card for future use + .clearall + .clearall + .clearall + .row.second + left-side.hidden + .shipping-address + h2.shipping-address-label SHIPPING ADDRESS + .shipping-as-billing.ichecbuttons + input type="checkbox" id="shipping-as-billing" name="shipping-as-billing" checked="checked" + .divBillingHelper + label for="shipping-as-billing" Same as billing address + .clearall + .shipping-address-detail.hidden + #divShippingFirstName.field + .shipping-label + label for="shipping-first-name" First Name: + .shipping-value + input type="text" id="shipping-first-name" + .clearall + #divShippingLastName.field + .shipping-label + label for="shipping-last-name" Last Name: + .shipping-value + input type="text" id="shipping-last-name" + .clearall + #divShippingAddress1.field + .shipping-label + label for="shipping-address1" Address 1: + .shipping-value + input type="text" id="shipping-address1" + .clearall + #divShippingAddress2.field + .shipping-label + label for="shipping-address2" Address 2: + .shipping-value + input type="text" id="shipping-address2" + .clearall + #divShippingCity.field + .shipping-label + label for="shipping-city" City: + .shipping-value + input type="text" id="shipping-city" + .clearall + #divShippingState.field + .shipping-label + label for="shipping-state" State/Region: + .shipping-value + input type="text" id="shipping-state" + .clearall + #divShippingZip.field + .shipping-label + label for="shipping-zip" Zip: + .shipping-value + input type="text" id="shipping-zip" + .clearall + #divShippingCountry.field + .shipping-label + label for="shipping-country" Country: + .shipping-value + input type="text" id="shipping-country" + .clearall + .right-side + .jamkazam-account-signup + h2.jamkazam-account-caption JAMKAZAM ACCOUNT + #divJamKazamEmail.field + .account-label + label for="email" Email Address: * + .account-value + input name="email" id="checkout-signup-email" type="text" + .clearall + #divJamKazamPassword.field + .account-label + label for="password" Password: * + .account-value + input name="password" id="checkout-signup-password" type="password" + .clearall + #divJamKazamTos.field + .terms-of-service.ichecbuttons + input type="checkbox" name="terms-of-service" + .terms-of-service-label-holder + label for="terms-of-service" + | I have read and agree to the JamKazam  + a rel="external" href=corp_terms_path terms of service + .clearall + .clearall + .row.third.hidden#payment_error + + + .clearall + .action-bar + + .right + a href="#" id="payment-info-help" class="button-grey" HELP + a href="#" id="payment-info-next" class="button-orange" NEXT + .clearall diff --git a/web/app/views/clients/_checkout_signin.html.slim b/web/app/views/clients/_checkout_signin.html.slim new file mode 100644 index 000000000..d6dd4ccdb --- /dev/null +++ b/web/app/views/clients/_checkout_signin.html.slim @@ -0,0 +1,92 @@ +div layout="screen" layout-id="checkoutSignin" id="checkoutSignInScreen" class="screen secondary no-login-required" + .content + .content-head + .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) + h1 check out + = render "screen_navigation" + .content-body + .content-body-scroller + .checkout-signin + .checkout-navigation-bar + + .content-holder + .already-signed-in + h3 YOU ARE ALREADY LOGGED IN + p.carry-on-prompt + | You can move on to the next step of checkout. + .actions + a.btnNext.button-orange NEXT + + .left-side + h3 ALREADY A MEMBER OF THE JAMKAZAM COMMUNITY? + .left-side-content + .signin-form + .signin-prompt Sign in using your email address: + .clearall + form.signin-form + .input-elements + .out + label.inline Email: + .email.field + input name='email' autofocus="true" type="text" + .out + label.inline Password: + .password.field + input name='password' autofocus="true" type="password" + .login-error-msg Invalid login + + + br clear='all' + + .actions + small + a.forgot-password href='/request_reset_password' Forgot Password? + = link_to "SIGN IN", '#', class: 'button-orange signin-submit' + + + p.facebook-prompt Or sign in using Facebook: + = link_to image_tag("content/button_facebook_signin.png", {:width => 249, :height => 46}), '/auth/facebook', class: "signin-facebook" + .right-side + h3 NOT A MEMBER YET? + + p.signup-later-prompt + | Thousands of musicians are now registered members of JamKazam. Click the NEXT button below to join us, and welcome! + + .actions + a.btnNext.button-orange NEXT + +script type='text/template' id='template-checkout-navigation' + .checkout-navigation + .nav-signin + = "{% if (data.current == 1) { %}" + .nav-text.selected + .badge-number 1 + | Sign In + = "{% } else { %}" + .nav-text + .badge-number.disabled 1 + | Sign In + = "{% } %}" + .clearall + .nav-payment-info + = "{% if (data.current == 2) { %}" + .nav-text.selected + .badge-number 2 + | Address & Payment + = "{% } else { %}" + .nav-text + .badge-number.disabled 2 + | Address & Payment + = "{% } %}" + .clearall + .nav-place-order + = "{% if (data.current == 3) { %}" + .nav-text.selected + .badge-number 3 + | Place Order + = "{% } else { %}" + .nav-text + .badge-number.disabled 3 + | Place Order + = "{% } %}" + .clearall diff --git a/web/app/views/clients/_download_jamtrack_templates.html.slim b/web/app/views/clients/_download_jamtrack_templates.html.slim index bf5831cb4..fef7f2cd8 100644 --- a/web/app/views/clients/_download_jamtrack_templates.html.slim +++ b/web/app/views/clients/_download_jamtrack_templates.html.slim @@ -1,2 +1,86 @@ script type="text/template" id='template-download-jamtrack' .download-jamtrack + .state + +script type="text/template" id="template-download-jamtrack-state-no-client" + .state-no-client + .large.hidden + .msg + | To play your JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .small.hidden + .msg + | {{data.name}} (launch client) + +script type="text/template" id="template-download-jamtrack-state-synchronized" + .state-synchronized + .large.hidden + .msg + | Your JamTrack is on your system and ready to play. + .small.hidden + .msg + | {{data.name}} (done) + +script type="text/template" id="template-download-jamtrack-state-packaging" + .state-packaging + .large.hidden + .msg + | Your JamTrack is currently being created on the JamKazam server. + .spinner-large + .small.hidden + .msg + | {{data.name}} (packaging) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-downloading" + .state-downloading + .large.hidden + .msg + | Your JamTrack is currently being downloaded. + .spinner-large + .small.hidden + .msg + | {{data.name}} (downloading) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-keying" + .state-keying + .large.hidden + .msg + | Your JamTrack is being authenticated. + .spinner-large + .small.hidden + .msg + | {{data.name}} (keying) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-initial" + .state-initial + .large.hidden + .msg + | Initializing JamTrack... + .spinner-large + .small.hidden + .msg + | {{data.name}} (initializing) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-quiet" + .state-quiet + .large.hidden + .msg + .small.hidden + .msg + | {{data.name}} (pending) + +script type="text/template" id="template-download-jamtrack-state-errored" + .state-errored + .large.hidden + .msg + .retry + a.button-orange.retry-button RETRY + .small.hidden + .msg-holder + .msg + a.button-orange.retry-button RETRY + .errormsg + .retry diff --git a/web/app/views/clients/_flash.html.slim b/web/app/views/clients/_flash.html.slim new file mode 100644 index 000000000..ec6b293ce --- /dev/null +++ b/web/app/views/clients/_flash.html.slim @@ -0,0 +1,3 @@ +script type="text/template" id="template-flash-notice" + .flash-notice + .flash-content \ No newline at end of file diff --git a/web/app/views/clients/_header.html.erb b/web/app/views/clients/_header.html.erb index 855f0e988..de9015e66 100644 --- a/web/app/views/clients/_header.html.erb +++ b/web/app/views/clients/_header.html.erb @@ -13,8 +13,8 @@ <% if Rails.application.config.jam_tracks_available %> - - + <% end %> diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 1f0efda08..235c23381 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -30,6 +30,29 @@ script type="text/template" id="template-help-can-move-on" script type="text/template" id="template-help-tweak-asio-settings" | Click here to try faster ASIO settings. +script type="text/template" id="template-help-high-latency-notice" + .help-high-latency-notice + | {% if(data.additional == 'asio') { %} + p.gear-specific-latency-notice Tip: click the ASIO SETTINGS button to try faster ASIO settings. + p + | If you are unable to get your audio gear latency below 20 milliseconds, you can click NEXT to proceed through setup with a high-latency audio profile. This will allow you to play with JamTracks and backing tracks, but not play with others.  + p + a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" Click here + |  for more troubleshooting tips to speed up your audio gear setup. + | {% } else if(data.additional == 'macosx-builtin') { %} + p.gear-specific-latency-notice Tip: Insert your headphones on a Mac to bring your latency down, and click the RESYNC button to try again. + p + | If you are unable to get your audio gear latency below 20 milliseconds, you can click NEXT to proceed through setup with a high-latency audio profile. This will allow you to play with JamTracks and backing tracks, but not play with others.  + p + a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" Click here + |  for more troubleshooting tips to speed up your audio gear setup. + | {% } else { %} + p.general-info + | Your computer and interface are processing audio too slowly to play online in real-time sessions with other musicians over the Internet. You may click NEXT to proceed through setup to play alone in sessions with JamTracks or backing tracks, or if you want to improve your speed score to play online,  + a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" click here + |  for a troubleshooting article. + | {% } %} + script type="text/template" id="template-help-session-plus-musicians" | Plus any interested JamKazam musicians that I approve. @@ -199,8 +222,31 @@ script type="text/template" id="template-help-media-controls-disabled" | Only the person who opened the recording can control the volume levels. | {% } %} +script type="text/template" id="template-help-jamtrack-controls-disabled" + | During a recording, volume and mute controls for JamTracks are disabled. So, get the session volume levels right before starting the recording. + + +script type="text/template" id="template-help-volume-media-mixers" + | Audio files only expose both master and personal mix controls, so any change here will also affect everyone in the session. + script type="text/template" id="template-help-downloaded-jamtrack" .downloaded-jamtrack p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client. p However, in some cases, you may need to download the JamTrack again (if you change machines, for instance). p If you do not currently have it and try to open it now, we will try to download it immediately. + +script type="text/template" id="template-help-metronome-unstable" + .metronome-unstable + span.definition Background + p The metronome feature requires that every user's computer in the session must agree on the current time. + span.definition The Problem + p The computers of {{data.names}} have not successfully synchronized to the current time. + span.definition Solution + p The JamKazam service is trying to automatically correct this error condition. Please close this message, wait about 10 seconds, and then try opening the metronome again. If this problem persists after a couple of attempts, we recommend that the unsynchronized users restart the JamKazam application. + p If this error persists after a restart, please have the users with the issue contact support@jamkazam.com. + +script type="text/template" id="template-help-metromone-playback-modes" + .metromone-playback-modes + p The metronome plays a local metronome back to each musician locally, with the local metronomes all synchronized to a global clock with high precision. + + p The cluster test mixes playback of your local metronome with the streamed audio of all other musician metronomes. This will give you a good sense of the audible latency in your session. If all the metronome sounds are tightly clustered, there is low latency. If not, it will be more difficult to play in sync. diff --git a/web/app/views/clients/_home.html.slim b/web/app/views/clients/_home.html.slim index 5492f904c..9247d18f0 100644 --- a/web/app/views/clients/_home.html.slim +++ b/web/app/views/clients/_home.html.slim @@ -1,6 +1,6 @@ -.screen layout="screen" layout-id="home" +.screen.no-login-required layout="screen" layout-id="home" / Layout is different if jam_tracks tile available: - -jamtracks=Rails.configuration.jam_tracks_available + -jamtracks=Rails.configuration.jam_tracks_available || (current_user && current_user.admin) -if (jamtracks) -small_tile_size="2.4" -column_positions=["0.0,1", "2.4,1", "4.8,1", "7.2,1", "9.6,1"] @@ -11,71 +11,71 @@ / individual spells span those spaces -if @nativeClient .grid layout-grid="2x12" - .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" + .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" h2 create session .homebox-info /! 4 friends online, 2 currently in sessions - .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" + .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" class="#{logged_in_not_logged_in_class}" h2 find session .homebox-info /! 1 session invitation, 19 public sessions active - .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" + .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" class="#{logged_in_not_logged_in_class}" h2 feed .homebox-info /! 4 friends online, 2 currently in sessions - .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" + .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" h2 musicians .homebox-info /! 5 followers, 3 following - .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" + .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" h2 bands .homebox-info /! 1 session invitation, 19 public sessions active -if jamtracks - .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrack" + .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrackLanding" h2 jamtracks .homebox-info /! 5 followers, 3 following - .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" + .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" h2 profile .homebox-info /! 5 followers, 3 following - .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" + .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" h2 account .homebox-info /! free service level -else .grid layout-grid="2x12" - .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" + .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" h2 create session .homebox-info /! 4 friends online, 2 currently in sessions - .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" + .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" class="#{logged_in_not_logged_in_class}" h2 find session .homebox-info /! 1 session invitation, 19 public sessions active - .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" + .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" class="#{logged_in_not_logged_in_class}" h2 feed .homebox-info /! 4 friends online, 2 currently in sessions - .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" + .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" h2 musicians .homebox-info /! 5 followers, 3 following - .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" + .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" h2 bands .homebox-info -if jamtracks /! 1 session invitation, 19 public sessions active - .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrack" + .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrackLanding" h2 jamtracks .homebox-info /! 5 followers, 3 following - .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" + .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" h2 profile .homebox-info /! 5 followers, 3 following - .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" + .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" h2 account .homebox-info /! free service level diff --git a/web/app/views/clients/_jam_track_preview.html.slim b/web/app/views/clients/_jam_track_preview.html.slim new file mode 100644 index 000000000..f10de55c7 --- /dev/null +++ b/web/app/views/clients/_jam_track_preview.html.slim @@ -0,0 +1,10 @@ +script type="text/template" id='template-jam-track-preview' + .jam-track-preview + .actions + a.play-button href="#" + | Play + a.stop-button.hidden href="#" + | Stop + img.instrument-icon hoveraction="instrument" data-instrument-id="" width="24" height="24" + .instrument-name + .part \ No newline at end of file diff --git a/web/app/views/clients/_jamtrack.html.haml b/web/app/views/clients/_jamtrack.html.haml deleted file mode 100644 index a45e932fe..000000000 --- a/web/app/views/clients/_jamtrack.html.haml +++ /dev/null @@ -1,81 +0,0 @@ -%div{ layout: 'screen', :'layout-id' => 'jamtrack', id: 'jamtrackScreen', :class => 'screen secondary'} - .content - .content-head - .content-icon= image_tag("content/icon_jamtracks.png", {:height => 19, :width => 19}) - %h1 jamtracks - %a{href: "#", class: "jamtrack_help"} What is a JamTrack? - = render "screen_navigation" - .content-body - = form_tag('', {:id => 'jamtrack-find-form', :class => 'inner-content'}) do - = render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_JAMTRACK}) - .filter-body - .content-body-scroller - .profile-wrapper - .jamtrack-content - %a{href: "/api/jamtracks?page=1", class: "btn-next-pager"}= 'Next' - %div{id: 'end-of-jamtrack-list', class: 'end-of-list'}= 'No more Jamtracks' - -%script{type: 'text/template', id: 'template-jamtrack'} - .jamtrack-record{"jamtrack-id" => "{{data.jamtrack.id}}"} - .jamtrack-detail - .detail-label - Name: - .detail-value - {{data.jamtrack.name}} - .clearall.detail-label - Type: - .detail-value - {{data.jamtrack.recording_type}} - .clearall.detail-label - Original Artist: - .detail-value - {{data.jamtrack.original_artist}} - .clearall.detail-label - Genre: - .detail-value - {{data.jamtrack.genres[0]}} - .clearall.detail-label - Writer/Composer: - .detail-value - {{[data.jamtrack.songwriter, data.jamtrack.publisher].join(", ")}} - .clearall.detail-label - Copyright: - .copyright-value - = "{% if (data.jamtrack.licensor != null) { %}" - {{data.jamtrack.licensor.name}} - ="{% }; %}" - .detail-arrow - = image_tag 'down_arrow.png', class: 'jamtrack-detail-btn' - .clearall.jamtrack-description - .detail-label - Description - .detail-value - {{data.jamtrack.description}} - .clearall - .jamtrack-tracks - .tracks-caption - Tracks in This Recording: - = "{% _.each(data.jamtrack.tracks, function(track) { %}" - .track-instrument - .instrument-image - %img{src: "{{track.instrument_url}}", width: 24, height: 24} - .instrument-desc - {{track.instrument_desc}} - .clearall - = "{% }); %}" - .jamtrack-action - %a{href: "#", class: 'play-button', "data-jamtrack-id" => "{{data.jamtrack.id}}"} - = image_tag 'shared/play_button.png' - .jamtrack-price - {{"$ " + data.jamtrack.price}} - = "{% if (data.jamtrack.added_cart) { %}" - %a.jamtrack-add-cart-disabled.button-grey.button-disabled{href: "javascript:void(0)"} Added to Cart - = "{% } else { %}" - %a.jamtrack-add-cart.button-orange{href: "#", "data-jamtrack-id" => "{{data.jamtrack.id}}"} Add to Cart - = "{% }; %}" - = "{% if (data.jamtrack.sales_region == 'United States') { %}" - .jamtrack-license - This JamTrack available only to US customers. - %a{href: "#", class: 'license-us-why'} why? - = "{% }; %}" - .clearall \ No newline at end of file diff --git a/web/app/views/clients/_jamtrack.html.slim b/web/app/views/clients/_jamtrack.html.slim new file mode 100644 index 000000000..629c0daea --- /dev/null +++ b/web/app/views/clients/_jamtrack.html.slim @@ -0,0 +1,88 @@ +#jamtrackScreen.screen.secondary.no-login-required layout='screen' layout-id='jamtrackBrowse' + .content + .content-head + .content-icon=image_tag("content/icon_jamtracks.png", height:19, width:19 ) + h1 jamtracks + =render "screen_navigation" + .content-body + =form_tag('', {:id => 'jamtrack-find-form', :class => 'inner-content'}) do + =render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_JAMTRACK}) + .filter-body + .content-body-scroller + .profile-wrapper + table.generaltable + thead + tr + th JAMTRACK + th TRACKS INCLUDED/PREVIEW + th SHOP + tbody.jamtrack-content + a.btn-next-pager href="/api/jamtracks?page=1" Next + .end-of-jamtrack-list.end-of-list="No more Jamtracks" + +script#template-jamtrack type='text/template' + tr.jamtrack-record jamtrack-id="{{data.jamtrack.id}}" + td.jamtrack-detail + .detail-label + | Title: + .detail-value + | {{data.jamtrack.name}} + .clearall.detail-label + | Original Artist: + .detail-value + | {{data.jamtrack.original_artist}} + .clearall.detail-label + | Genre: + .detail-value + | {{data.jamtrack.genres[0]}} + ="{% if (data.expanded) { %}" + .clearall.detail-label + | Writer(s): + .detail-value + | {{data.jamtrack.songwriter}} + .clearall.detail-label + | Publisher: + .detail-value + | {{data.jamtrack.publisher}} + .clearall.detail-label + | Description: + .detail-value + | {{data.jamtrack.description}} + ="{% } %}" + td.jamtrack-tracks + .detail-arrow + .jamtrack-detail-btn.orange + ="{% if (data.expanded) { %}" + | hide tracks ({{data.jamtrack.tracks.length}}) + a.details-arrow.arrow-up-orange + ="{% } else { %}" + | show tracks ({{data.jamtrack.tracks.length}}) + a.details-arrow.arrow-down-orange + ="{% } %}" + ="{% if (data.expanded) { %}" + ="{% _.each(data.jamtrack.tracks, function(track) { %}" + .jamtrack-track jamtrack-track-id="{{track.id}}" + / .instrument-desc + / | {{track.instrument_desc}} + /.track-instrument + .jamtrack-preview + .clearall + ="{% }); %}" + ="{% } %}" + td.jamtrack-action + / a.play-button href="#" data-jamtrack-id="{{data.jamtrack.id}}" + / =image_tag "shared/play_button.png" + .jamtrack-price + | {{"$ " + data.jamtrack.price}} + ="{% if (data.jamtrack.purchased) { %}" + a.jamtrack-add-cart-disabled.button-grey.button-disabled href="javascript:void(0)" Purchased + ="{% } else if (data.jamtrack.added_cart) { %}" + a.jamtrack-add-cart-disabled.button-grey.button-disabled href="client#/shoppingCart" Already In Cart + ="{% } else { %}" + a.jamtrack-add-cart.button-orange href="#" data-jamtrack-id="{{data.jamtrack.id}}" Add to Cart + ="{% }; %}" + ="{% if (data.jamtrack.sales_region==JK.AVAILABILITY_US) { %}" + .jamtrack-license + | This JamTrack available only to US customers.      + a.license-us-why.orange href="#" why? + ="{% }; %}" diff --git a/web/app/views/clients/_jamtrack_landing.html.slim b/web/app/views/clients/_jamtrack_landing.html.slim new file mode 100644 index 000000000..69e92422c --- /dev/null +++ b/web/app/views/clients/_jamtrack_landing.html.slim @@ -0,0 +1,35 @@ +#jamtrackLanding.screen.secondary.no-login-required layout='screen' layout-id='jamtrackLanding' + .content + .content-head + .content-icon=image_tag("content/icon_jamtracks.png", height:19, width:19) + h1 jamtracks + = render "screen_navigation" + .content-body + .list-columns + .about + h2 what are jamtracks? + .what + .details 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 and every part of the master mix. Used with the free JamKazam app & Internet service, you can: + ul + li Solo just the part you want to play in order to hear and learn it + li Mute just the part you want to play and play along with the rest + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube + li And even go online to play with others in real time -- for example, you can play the electric guitar lead, while someone else plays the bass, and all other parts play from the recorded tracks in your session + / TODO: put in video thumbnail when available: + .browse + h2 my jamtracks + .howto + .details + span="To play with your JamTracks, open a JamTrack while in a session in the JamKazam app. Or " + a href="client#/jamtrackBrowse" visit the JamTracks Section of your account. + .free-jamtrack.orange-fill.details + | For a limited time, get one JamTrack free. Browse JamTracks below, add one to your shopping cart, and we'll make it free during the checkout process. + h2 browse jamtracks + .browse-header + | browse by band    + a href="client#/jamtrackBrowse" or browse all jamtracks + .band-browse.two-column-list-container + ul#band_list + li#no_bands_found.hidden No bands found + diff --git a/web/app/views/clients/_metronome_playback_mode.slim b/web/app/views/clients/_metronome_playback_mode.slim new file mode 100644 index 000000000..2af251789 --- /dev/null +++ b/web/app/views/clients/_metronome_playback_mode.slim @@ -0,0 +1,11 @@ +script type='text/template' id='template-metronome-playback-mode' + p.please-select Please select one: + ul + li data-playback-option="self" + a href='#' - Play metronome + + li data-playback-option="cricket" + a href='#' - Play cluster test + + li data-ui-option="show-metronome-window" + a href='#' - Show visual metronome diff --git a/web/app/views/clients/_network_test.html.haml b/web/app/views/clients/_network_test.html.haml index a61e4e081..db61e567a 100644 --- a/web/app/views/clients/_network_test.html.haml +++ b/web/app/views/clients/_network_test.html.haml @@ -1,5 +1,5 @@ .network-test - .help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection. + .help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection. If you don't want to play online in real-time sessions, you can click NEXT to skip this step. .wizard-step-content .wizard-step-column %h2 Instructions @@ -16,6 +16,8 @@ %p Then click on the Start Network Test button below. .center %a.button-orange.start-network-test{href:'#'} START NETWORK TEST + %br + %a.button-orange.forever-network-test{href:'#'} THE FOREVER TEST (ADMIN ONLY) .wizard-step-column %h2 Test Results .network-test-results.ftue-box diff --git a/web/app/views/clients/_order.html.slim b/web/app/views/clients/_order.html.slim index 241af7217..f5ea0cc91 100644 --- a/web/app/views/clients/_order.html.slim +++ b/web/app/views/clients/_order.html.slim @@ -71,19 +71,20 @@ div layout="screen" layout-id="order" id="orderScreen" class="screen secondary" input type="text" id="card-name" .clearall #divCardNumber - .card-label + .card-label.mt10 label for="card-number" Card Number: - .card-value + .card-value.mt10 input type="text" id="card-number" .clearall - .card-label Expiration Date: - .card-value - = date_select("card", "expire-date", use_two_digit_numbers: true, discard_day: true, :start_year => Time.now.year, :end_year => Time.now.year + 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html=>{:class => "account-profile-birthdate"} ) - .clearall + #divCardExpiry + .card-label.mt10 Expiration Date: + .card-value.mt10 + =date_select("card", "expire-date", use_two_digit_numbers: true, discard_day: true, :start_year => Time.now.year, :end_year => Time.now.year + 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html=>{:class => "account-profile-birthdate", :id=>"card-expiry"} ) + .clearall #divCardVerify - .card-label + .card-label.mt10 label for="card-verify" Verification Value: - .card-value + .card-value.mt10 input type="text" id="card-verify" .clearall .card-label.mt15 @@ -178,8 +179,15 @@ div layout="screen" layout-id="order" id="orderScreen" class="screen secondary" br .thanks-detail We'll send you an email confirming your order shortly. br - .thanks-detail If you purchased any JamTracks, the next time you run the JamKazam application, your JamTracks will automatically be downloaded to the app, and you will receive a notification when the download is complete. - + .thanks-detail.jam-tracks-in-browser.hidden + | To play your purchased JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .thanks-detail.purchased-jam-track.hidden + h2.purchased-jam-track-header Downloading Your Purchased JamTracks + span Each JamTrack will be downloaded sequentially. + br + span.notice Note that you do not have to wait for this to complete in order to use your JamTrack later. + br.clear + ul.purchased-list @@ -276,4 +284,7 @@ script type='text/template' id='template-order-content' span and ' a href="http://www.jamkazam.com/corp/returns" returns policy - span . \ No newline at end of file + span . + +script type='text/template' id='template-purchased-jam-track' + li data-jam-track-id="{{data.jam_track_id}}" \ No newline at end of file diff --git a/web/app/views/clients/_play_controls.html.erb b/web/app/views/clients/_play_controls.html.erb index 88e088018..671b1701f 100644 --- a/web/app/views/clients/_play_controls.html.erb +++ b/web/app/views/clients/_play_controls.html.erb @@ -1,6 +1,10 @@ +
    + + ? +
    \ No newline at end of file diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb deleted file mode 100644 index 377ec043d..000000000 --- a/web/app/views/clients/_session.html.erb +++ /dev/null @@ -1,199 +0,0 @@ - -
    -
    -
    - <%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %> -
    -

    session

    -
    - -
    - - - - - -
    -
    - - -
    - - -
    -

    my tracks

    -
    - <%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %> - Settings -
    - -
    -
    - -
    -
    - - - - - -
    -

    other audio

    - -
    -
    -
    - <%= image_tag "content/icon_folder.png", {width:22, height:20} %> Open: -
      -
    • Recording
    • - <% if Rails.application.config.jam_tracks_available %> -
    • JamTrack
    • - <% end %> - -
    -
    -
    -
    - - <%= render "play_controls" %> - -
    - - - -
    - -
    -
    -
    -
    -
    - - -<%= render "configureTrack" %> -<%= render "addTrack" %> -<%= render "addNewGear" %> -<%= render "error" %> -<%= render "sessionSettings" %> - - - - - - - - - diff --git a/web/app/views/clients/_session.html.slim b/web/app/views/clients/_session.html.slim new file mode 100644 index 000000000..3ffaa3b68 --- /dev/null +++ b/web/app/views/clients/_session.html.slim @@ -0,0 +1,159 @@ +#session-screen.screen.secondary[layout="screen" layout-id="session" layout-arg="id"] + .content-head + .content-icon + = image_tag "shared/icon_session.png", {:height => 19, :width => 19} + h1 + | session + .content-body + #session-controls + a#session-resync.button-grey.resync.left + = image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} + | RESYNC + a#session-settings-button.button-grey.left[layout-link="session-settings"] + = image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} + | SETTINGS + a.button-grey.left[layout-link="share-dialog"] + = image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} + | SHARE + .block + .label + | VOLUME: + #volume.fader.lohi[mixer-id=""] + .block.monitor-mode-holder + .label + | MIX: + select.monitor-mode.easydropdown + option.label[value="personal"] + | Personal + option[value="master"] + | Master + a#session-leave.button-grey.right.leave[href="/client#/home"] + | X  LEAVE + #tracks + .content-scroller + .content-wrapper + .session-mytracks + h2 + | my tracks + #track-settings.session-add[style="display:block;" layout-link="configure-tracks"] + = image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} + span + | Settings + .session-tracks-scroller + #session-mytracks-notracks + p.notice + | You have not set up any inputs for your instrument or vocals.  + | If you want to hear yourself play through the JamKazam app,  + | and let the app mix your live playing with JamTracks, or with other musicians in online sessions,  + a.open-ftue-no-tracks href='#' click here now. + #session-mytracks-container + #voice-chat.voicechat[style="display:none;" mixer-id=""] + .voicechat-label + | CHAT + .voicechat-gain + .voicechat-mute.enabled[control="mute" mixer-id=""] + .session-fluidtracks + .session-livetracks + h2 + | live tracks + .session-add[layout-link="select-invites"] + a#session-invite-musicians[href="#"] + = image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} + |   Invite Musicians + .session-tracks-scroller + #session-livetracks-container + .when-empty.livetracks + | No other musicians + br + | are in your session + br[clear="all"] + #recording-start-stop.recording + a + = image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} + |    + span#recording-status + | Make a Recording + .session-recordings + h2 + | other audio + .session-recording-name-wrapper + .session-recording-name.left + | (No audio loaded) + .session-add.right + a#close-playback-recording[href="#"] + = image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} + |   Close + .session-tracks-scroller + #session-recordedtracks-container + .when-empty.recordings + span.open-media-file-header + = image_tag "content/icon_folder.png", {width:22, height:20} + | Open: + ul.open-media-file-options + li + a#open-a-recording[href="#"] + | Recording + - if Rails.application.config.jam_tracks_available || (current_user && current_user.admin) + li + a#open-a-jamtrack[href="#"] + | JamTrack + - if Rails.application.config.backing_tracks_available + li + a#open-a-backingtrack[href="#"] + | Audio File + .when-empty.use-metronome-header + - if Rails.application.config.metronome_available + = image_tag "content/icon_metronome.png", {width:22, height:20} + a#open-a-metronome[href="#"] + | Use Metronome + br[clear="all"] + .play-controls-holder + = render "play_controls" += render "configureTrack" += render "addTrack" += render "addNewGear" += render "error" += render "sessionSettings" +script#template-session-track[type="text/template"] + .session-track.track client-id="{clientId}" track-id="{trackId}" + .track-vu-left.mixer-id="{vuMixerId}_vul" + .track-vu-right.mixer-id="{vuMixerId}_vur" + .track-label[title="{name}"] + span.name-text="{name}" + #div-track-close.track-close.op30 track-id="{trackId}" + =image_tag("content/icon_closetrack.png", {width: 12, height: 12}) + div class="{avatarClass}" + img src="{avatar}" + .track-instrument class="{preMasteredClass}" + img height="45" src="{instrumentIcon}" width="45" + .track-gain mixer-id="{mixerId}" + .track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}" + .track-icon-loop.hidden control="loop" + input#loop-button type="checkbox" value="loop" Loop + .track-connection.grey mixer-id="{mixerId}_connection" + CONNECTION + .disabled-track-overlay + .metronome-selects.hidden + select.metronome-select.metro-sound title="Metronome Sound" + option.label value="Beep" Knock + option.label value="Click" Tap + option.label value="Snare" Snare + option.label value="Kick" Kick + br + select.metronome-select.metro-tempo title="Metronome Tempo" + - metronome_tempos.each do |t| + option.label value=t + =t + +script#template-option type="text/template" + option value="{value}" title="{label}" selected="{selected}" + ="{label}" + +script#template-genre-option type="text/template" + option value="{value}" + ="{label}" + +script#template-pending-metronome type="text/template" + .pending-metronome + .spinner-large + p Your metronome is synchronizing. diff --git a/web/app/views/clients/_shopping_cart.html.haml b/web/app/views/clients/_shopping_cart.html.haml index df3be13f9..f34be6584 100644 --- a/web/app/views/clients/_shopping_cart.html.haml +++ b/web/app/views/clients/_shopping_cart.html.haml @@ -1,4 +1,4 @@ -%div{ layout: 'screen', :'layout-id' => 'shoppingCart', id: 'shoppingCartScreen', :class => 'screen secondary'} +%div{ layout: 'screen', :'layout-id' => 'shoppingCart', id: 'shoppingCartScreen', :class => 'screen secondary no-login-required'} .content .content-head .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) @@ -19,35 +19,37 @@ .clearall %script{type: 'text/template', id: 'template-shopping-cart-body'} - .cart-items - .cart-item-caption#header - Your shopping cart now contains: - .cart-item-price - %span{style: "text-decoration: underline;"} Price - .cart-item-quantity - %span{style: "text-decoration: underline;"} Quantity - .clearall + %table.cart-items + %tr + %th.cart-item-caption + YOUR SHOPPING CART NOW CONTAINS: + %th.cart-item-price + Price + %th.cart-item-quantity + Quantity + %th = "{% if (data.carts.length == 0) { %}" - .no-cart-items Nothing in cart + %tr + %td.no-cart-items colspan=4 Nothing in cart = "{% } %}" + = "{% _.each(data.carts, function(cart) { %}" - .cart-item{"cart-id" => "{{cart.id}}"} - .cart-item-caption + %tr.cart-item{"cart-id" => "{{cart.id}}"} + %td.cart-item-caption {{cart.cart_type}}: {{cart.product_info.name}} - .cart-item-price + %td.cart-item-price $ {{cart.product_info.price}} - .cart-item-quantity + %td.cart-item-quantity {{cart.quantity}} - .cart-item-actions - %a.button-grey.remove-cart{href: "#", "cart-id" => "{{cart.id}}"} DELETE - .clearall + %td.cart-item-actions + %a.button-grey.remove-cart{href: "#", "cart-id" => "{{cart.id}}"} + DELETE = "{% }); %}" .shopping-sub-total Subtotal: $ {{data.sub_total}} .clearall - .left - %a.button-grey{href: "#"} HELP .right - %a.button-orange{href: "/client#/jamtrack"} CONTINUE SHOPPING + %a.button-grey{href: "#"} HELP + %a.button-orange{href: "/client#/jamtrackBrowse"} CONTINUE SHOPPING %a.button-orange.proceed-checkout{href: "#"} PROCEED TO CHECKOUT .clearall \ No newline at end of file diff --git a/web/app/views/clients/_sidebar.html.erb b/web/app/views/clients/_sidebar.html.erb index dbb92c66a..b8d44dc68 100644 --- a/web/app/views/clients/_sidebar.html.erb +++ b/web/app/views/clients/_sidebar.html.erb @@ -210,7 +210,7 @@ <% end %> + <%= include_gon %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb index 2c4238803..518c775c6 100644 --- a/web/app/views/layouts/client.html.erb +++ b/web/app/views/layouts/client.html.erb @@ -2,6 +2,7 @@ <%= full_title(yield(:title)) %> + @@ -25,6 +26,7 @@ <%= include_gon %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> + <% if content_for?(:social_meta) %> <%= yield(:social_meta) %> <% else %> @@ -35,5 +37,7 @@ <%= yield %> <%= render "shared/ga" %> + <%= render "shared/recurly" %> + diff --git a/web/app/views/layouts/corporate.html.erb b/web/app/views/layouts/corporate.html.erb index b852f8341..0e7e8fb1a 100644 --- a/web/app/views/layouts/corporate.html.erb +++ b/web/app/views/layouts/corporate.html.erb @@ -23,6 +23,7 @@ <%= include_gon(:init => true) %> <%= javascript_include_tag "corp/corporate" %> <%= csrf_meta_tags %> + "> <% if content_for?(:social_meta) %> <%= yield(:social_meta) %> <% else %> diff --git a/web/app/views/layouts/landing.html.erb b/web/app/views/layouts/landing.html.erb index 7ae5a821c..70010c915 100644 --- a/web/app/views/layouts/landing.html.erb +++ b/web/app/views/layouts/landing.html.erb @@ -25,6 +25,7 @@ <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> + <% if content_for?(:social_meta) %> <%= yield(:social_meta) %> <% else %> diff --git a/web/app/views/layouts/minimal.html.erb b/web/app/views/layouts/minimal.html.erb index f29412ae4..7c021b2b0 100644 --- a/web/app/views/layouts/minimal.html.erb +++ b/web/app/views/layouts/minimal.html.erb @@ -25,6 +25,7 @@ <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> + <% if content_for?(:social_meta) %> <%= yield(:social_meta) %> <% else %> diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb index e3e828bc4..708e33124 100644 --- a/web/app/views/layouts/web.html.erb +++ b/web/app/views/layouts/web.html.erb @@ -25,6 +25,7 @@ <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> + <% if content_for?(:social_meta) %> <%= yield(:social_meta) %> <% else %> @@ -37,10 +38,13 @@
    - <% unless @welcome_page %> - <%= render "users/user_dropdown" %> + <% if @no_user_dropdown %> <% else %> - <%= render "users/video_carousel" %> + <% unless @welcome_page %> + <%= render "users/user_dropdown" %> + <% else %> + <%= render "users/video_carousel" %> + <% end %> <% end %>
    @@ -50,8 +54,10 @@
    <% unless @welcome_page %> - <%= content_tag(:div, content_tag(:h1,'Play music together over the Internet as if in the same room'), :class => "landing-tag") %> - <%= content_tag(:div,'',:class => "clearall") %> +
    +

    Live music platform &
    social network for musicians

    +
    +
    <% end %>
    @@ -79,6 +85,8 @@ <%= render "clients/hoverRecording" %> <%= render "clients/help" %> <%= render "clients/listenBroadcast" %> + <%= render "clients/flash" %> + <%= render "clients/jam_track_preview" %> <%= render 'dialogs/dialogs' %> diff --git a/web/app/views/music_sessions/session_info.html.haml b/web/app/views/music_sessions/session_info.html.haml index 7c7e359d7..e49bc10f7 100644 --- a/web/app/views/music_sessions/session_info.html.haml +++ b/web/app/views/music_sessions/session_info.html.haml @@ -1,4 +1,19 @@ - provide(:page_name, 'session_info') +- provide(:description, description_for_music_session(@music_session)) + +- content_for :social_meta do + %meta {property: "fb:app_id", content: Rails.application.config.facebook_app_id} + %meta {property: "og:title", content: title_for_music_session(@music_session)} + %meta {property: "og:url", content: request.original_url} + %meta {property: "og:description", content: description_for_music_session(@music_session)} + %meta {property: "og:image", content: facebook_image_for_music_session(@music_session)} + %meta {property: "og:image:width", content: facebook_image_size_for_music_session(@music_session)} + %meta {property: "og:image:height", content: facebook_image_size_for_music_session(@music_session)} + %meta {property: "og:type", content: 'website'} + %meta {property: "twitter:card", content: 'summary'} + %meta {property: "twitter:site", content: '@jamkazam'} + %meta {property: "twitter:title", content: title_for_music_session(@music_session)} + %meta {property: "twitter:description", content: description_for_music_session(@music_session)} - unless @music_session.nil? - provide(:title, @music_session.name) diff --git a/web/app/views/music_sessions/show.html.erb b/web/app/views/music_sessions/show.html.erb index 8256193fe..c0ed420ee 100644 --- a/web/app/views/music_sessions/show.html.erb +++ b/web/app/views/music_sessions/show.html.erb @@ -1,4 +1,5 @@ <% provide(:title, "#{@music_session.description}") %> +<% provide(:description, description_for_music_session(@music_session)) %> <% content_for :social_meta do %> diff --git a/web/app/views/recordings/show.html.erb b/web/app/views/recordings/show.html.erb index 5bdda20f9..34eccd73e 100644 --- a/web/app/views/recordings/show.html.erb +++ b/web/app/views/recordings/show.html.erb @@ -1,4 +1,5 @@ <% provide(:title, "#{@claimed_recording.name}") %> +<% provide(:description, description_for_claimed_recording(@claimed_recording)) %> <% content_for :social_meta do %> diff --git a/web/app/views/sessions/oauth_complete.erb b/web/app/views/sessions/oauth_complete.erb index a6c22d587..e8c16289b 100644 --- a/web/app/views/sessions/oauth_complete.erb +++ b/web/app/views/sessions/oauth_complete.erb @@ -1,3 +1,5 @@ +<% provide(:description, 'Finished authorization') %> + diff --git a/web/app/views/sessions/signin.html.haml b/web/app/views/sessions/signin.html.haml index e20fa396e..a326d9148 100644 --- a/web/app/views/sessions/signin.html.haml +++ b/web/app/views/sessions/signin.html.haml @@ -1,5 +1,6 @@ - provide(:title, 'Sign in') - provide(:page_name, 'signin') +- provide(:description, 'Sign in to your JamKazam account to connect and play with other musicians.') .signin-overlay.signin-page .content-head diff --git a/web/app/views/sessions/twitter_oauth_failure.html.erb b/web/app/views/sessions/twitter_oauth_failure.html.erb index 9ec6255b6..2eb8f45e6 100644 --- a/web/app/views/sessions/twitter_oauth_failure.html.erb +++ b/web/app/views/sessions/twitter_oauth_failure.html.erb @@ -1,3 +1,5 @@ +<% provide(:description, 'Unable to authorize twitter') %> + Unable to authorize application. Reasons:
      diff --git a/web/app/views/shared/_recurly.html.slim b/web/app/views/shared/_recurly.html.slim new file mode 100644 index 000000000..d22bdabbd --- /dev/null +++ b/web/app/views/shared/_recurly.html.slim @@ -0,0 +1,4 @@ +script src="https://js.recurly.com/v3/recurly.js" + +javascript: + recurly.configure(gon.global.recurly_public_api_key) \ No newline at end of file diff --git a/web/app/views/spikes/download_jam_track.html.slim b/web/app/views/spikes/download_jam_track.html.slim new file mode 100644 index 000000000..ade3744b5 --- /dev/null +++ b/web/app/views/spikes/download_jam_track.html.slim @@ -0,0 +1,44 @@ += javascript_include_tag "download_jamtrack" += render "clients/download_jamtrack_templates" += stylesheet_link_tag "client/downloadJamTrack" + +- provide(:title, 'Download Jam Track Widget') + +.content-wrapper + h2 Jam Track State Widget + + h3 Possible States + ul + li synchronized + li no_client + li packaging + li downloading + li keying + li initial + li errored + #widget + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { + if(initialized) { + return; + } + initialized = true + + setTimeout(function() { + window.downloadJamTrack = new JK.DownloadJamTrack(data.app, {id: gon.jamTrackId, jam_track_right_id: gon.jamTrackRightId, name: 'Back in Black'}, gon.size) + downloadJamTrack.init() + $('#widget').append(window.downloadJamTrack.root) + + if (gon.switchState == 'errored') { + downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + } + else if (gon.switchState) { + downloadJamTrack.transition(downloadJamTrack.states[gon.switchState]); + } + }, 1) + + }) + }) \ No newline at end of file diff --git a/web/app/views/spikes/jam_track_preview.html.slim b/web/app/views/spikes/jam_track_preview.html.slim new file mode 100644 index 000000000..3ea46c7d3 --- /dev/null +++ b/web/app/views/spikes/jam_track_preview.html.slim @@ -0,0 +1,40 @@ + +- provide(:title, 'Jam Track Preview') + +.content-wrapper + h2 Jam Track Preview + + #players + + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + + var rest = JK.Rest(); + + if(gon.jamTrackPlanCode) { + rest.getJamTrack({plan_code: gon.jamTrackPlanCode}) + .done(function(jamTrack) { + var $players = $('#players') + + _.each(jamTrack.tracks, function(track) { + + var $element = $('
      ') + + $players.append($element); + + new JK.JamTrackPreview(data.app, $element, jamTrack, track, {master_shows_duration: true}) + }) + }) + .fail(function() { + alert("couldn't fetch jam track") + }) + + } + else { + alert("You need to add ?jam_track_plan_code=jamtracks-acdc-backinblack for this to work (or any jamtrack 'plancode')") + } + + + }) diff --git a/web/app/views/spikes/recording_source.html.slim b/web/app/views/spikes/recording_source.html.slim new file mode 100644 index 000000000..ba7c0fb5b --- /dev/null +++ b/web/app/views/spikes/recording_source.html.slim @@ -0,0 +1,19 @@ += javascript_include_tag "site_validator" +div style="width:50%" + = render "clients/site_validator", site_type: params[:site_type] += stylesheet_link_tag "client/site_validator" +
      += select_tag "site_type", options_for_select(Utils::RECORDING_SOURCES, params[:site_type]) + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + setTimeout(function() { + window.site_validator = new JK.SiteValidator('#{params[:site_type] || 'rec_youtube'}'); + site_validator.init(); + $('#validate_input_'+'#{params[:site_type] || 'url'}').val('jonathankolyer'); + }, 1) + }); + $('#site_type').change(function(){ + location.href = 'recording_source?site_type='+$(this).val(); + }); diff --git a/web/app/views/spikes/site_validate.html.slim b/web/app/views/spikes/site_validate.html.slim new file mode 100644 index 000000000..ccd9d8749 --- /dev/null +++ b/web/app/views/spikes/site_validate.html.slim @@ -0,0 +1,19 @@ += javascript_include_tag "site_validator" +div style="width:50%" + = render "clients/site_validator", site_type: params[:site_type] || 'url' += stylesheet_link_tag "client/site_validator" +
      += select_tag "site_type", options_for_select(Utils::SITE_TYPES, params[:site_type] || 'url') + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + setTimeout(function() { + window.site_validator = new JK.SiteValidator('#{params[:site_type] || 'url'}'); + site_validator.init(); + $('#validate_input_'+'#{params[:site_type] || 'url'}').val('jonathankolyer'); + }, 1) + }); + $('#site_type').change(function(){ + location.href = 'site_validate?site_type='+$(this).val(); + }); diff --git a/web/app/views/users/_download_templates.html.erb b/web/app/views/users/_download_templates.html.erb deleted file mode 100644 index 6b8fad6e4..000000000 --- a/web/app/views/users/_download_templates.html.erb +++ /dev/null @@ -1,46 +0,0 @@ - - - - \ No newline at end of file diff --git a/web/app/views/users/_download_templates.html.slim b/web/app/views/users/_download_templates.html.slim new file mode 100644 index 000000000..21d886117 --- /dev/null +++ b/web/app/views/users/_download_templates.html.slim @@ -0,0 +1,46 @@ + +script type="text/template" id="client-download-blurb-contents" + .downloads + + a href="{{data.uri}}" class="current-os-download" data-platform="{{data.platform}}" + + .downloads-container + + h5 SYSTEM REQUIREMENTS: + | {% if(data.platform == "Win32") { %} + ul.windows-requirements + li Windows 7 or 8, 64-bit (32-bit not supported) + li Dual core processor or higher + li 75MB hard disk space for app + li External audio interface recommended (but you can start with built-in mic and & headphone jack) + li Ethernet port for real-time online sessions (WiFi not recommended) + li Broadband Internet service with 1Mbps uplink bandwidth for real-time online sessions + | {% } else if(data.platform == "MacOSX") { %} + ul.mac-requirements + li Mac OS X 10.7 or higher, 64-bit + li Dual-core processor or higher + li 75MB hard disk space for app + li External audio interface recommended (but you can start with built-in mic and & headphone jack) + li Ethernet port for real-time online sessions (WiFi not recommended) + li Broadband Internet service with 1Mbps uplink bandwidth for real-time online sessions + | {% } else { %} + ul.linux-requirements + li Linux is not yet supported + | {% } %} + + + + .hidden.hidden-images + = image_tag("content/button_download_mac.png", :alt => "download mac", :size => "348x92", "data-purpose" => "mac") + = image_tag("content/button_download_windows.png", :alt => "download windows", :size => "348x92", "data-purpose" => "windows") + = image_tag("content/button_download_linux.png", :alt => "download linux", :size => "348x92", "data-purpose" => "linux") + +script type="text/template" id="client-download-select-others" + .download-box + .download-others + a.choose-other-platform href="#" data-order="1" data-platform="{{data.platform1}}" + | Need a different version? + br + | Click here for to get JamKazam + br + | for {{data.platformDisplay1}} \ No newline at end of file diff --git a/web/app/views/users/_downloads.html.erb b/web/app/views/users/_downloads.html.erb deleted file mode 100644 index 0fe47521a..000000000 --- a/web/app/views/users/_downloads.html.erb +++ /dev/null @@ -1,43 +0,0 @@ - - -
      -
      -
      - -
      -
      - -
      - -
      - -
      -<% content_for :after_black_bar do %> -
      -

      SYSTEM REQUIREMENTS:


      -

      A short summary of requirements follows. For a more detailed explanation of system requirements, please review our Minimum System Requirements knowledgebase article.

      -
        -
      • Windows 64-bit operating system (Win 7 & 8 tested, Win XP and Vista like to work but not officially supported
      • -
      • Dual-core processor or higher
      • -
      • Ethernet port for Internet (we strongly advise that you not use Wi-Fi)
      • -
      • 74MB hard disk space for app, plus any space needed for recordings
      • -
      • Audio interface (best to use an audio interface device that gets your music into your computer, else can use built-in mic & headphones on your computer to get started)
      • -
      • Broadband Internet service with 1Mbps uplink bandwidth
      • -
      -
        -
      • Mac OS X 64-bit operating system 10.7 or higher
      • -
      • Dual-core processor or higher
      • -
      • Ethernet port for Internet (we strongly advise that you not use Wi-Fi)
      • -
      • 74MB hard disk space for app, plus any space needed for recordings
      • -
      • Audio interface (best to use an audio interface device that gets your music into your computer, else can use built-in mic & headphones on your computer to get started)
      • -
      • Broadband Internet service with 1Mbps uplink bandwidth
      • -
      -
        -
      • Linux is not yet supported
      • -
      -
      -<%end%> - -<%= render "users/download_templates" %> - - diff --git a/web/app/views/users/_downloads.html.slim b/web/app/views/users/_downloads.html.slim new file mode 100644 index 000000000..6c3977e40 --- /dev/null +++ b/web/app/views/users/_downloads.html.slim @@ -0,0 +1,64 @@ +// used by congrats_musician, and downloads +- provide(:page_name, 'downloads') +- provide(:title, 'Download') + +.w100 + .download-app + .spinner-large + + h2.create-account-header + .badge-number 2 + | Download the free JamKazam app + + .download-content + .download-entreaty + + p You need the JamKazam application to: + ul + li Play music with others in real time on the JamKazam platform + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube or URL + li Live broadcast your sessions to family, friends, and fans + li Have full control over your JamTracks multi-track recordings + + p.click-to-download Click the button below to download the JamKazam application installer. + .downloads-blurb + + .jamtracks + + h2.shop-jamtracks + .badge-number 3 + | Get your free JamTrack + span.special-value + |   ($1.99 value) + + + .jamtrack-content + .jamtrack-entreaty + + p JamTracks are multi-track pro recordings you can use to: + ul + li Solo any part to hear and learn it + li Mute the part you want to play, and play along with the rest + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube or URL + li Go online to play real time sessions, with others playing parts + p + | Watch the video below to learn more. Then click the button to shop + |  for your first JamTrack - free! Add it to your shopping cart, and we'll + |  make it free during the checkout process. Free offer good for 1 week only! + .video-container + iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen + + a.go-jamtrack-shopping href="/client#/jamtrackBrowse" rel="external" + | Shop for free + br + | JamTrack now! + + br clear="all" + + + + = render "users/download_templates" + + diff --git a/web/app/views/users/already_signed_up.html.erb b/web/app/views/users/already_signed_up.html.erb index e0cf7635a..cff7f1479 100644 --- a/web/app/views/users/already_signed_up.html.erb +++ b/web/app/views/users/already_signed_up.html.erb @@ -1,4 +1,5 @@ <% provide(:title, 'Already Signed Up') %> +<% provide(:description, 'You have already signed up with JamKazam') %>
      diff --git a/web/app/views/users/congratulations_fan.html.erb b/web/app/views/users/congratulations_fan.html.erb index 7c0735c9d..77a714e10 100644 --- a/web/app/views/users/congratulations_fan.html.erb +++ b/web/app/views/users/congratulations_fan.html.erb @@ -1,13 +1,7 @@ <% provide(:title, 'Congratulations') %> +<% provide(:description, 'Congratulations on becoming a new JamKazam member!') %> +<%= render "users/downloads" %> -
      -
      Congratulations!
      - -

      You have successfully registered as a JamKazam fan.

      - -
      <%= link_to "PROCEED TO JAMKAZAM SITE", client_path, :class =>"button-orange m0" %>
      - -
      \ No newline at end of file + $(function() { window.congratulations.initialize(true, jQuery.QueryString["type"]) }) + diff --git a/web/app/views/users/congratulations_musician.html.erb b/web/app/views/users/congratulations_musician.html.erb index f2684fff2..77a714e10 100644 --- a/web/app/views/users/congratulations_musician.html.erb +++ b/web/app/views/users/congratulations_musician.html.erb @@ -1,16 +1,7 @@ <% provide(:title, 'Congratulations') %> - -<% if @nativeClient %> -
      -
      Congratulations!
      -

      You have successfully registered as a musician.

      -
      <%= link_to "PROCEED TO JAMKAZAM SITE", client_path, :class =>"button-orange m0" %>
      -
      -<% else %> - <%= render "users/downloads" %> -<% end %> - +<% provide(:description, 'Congratulations on becoming a new JamKazam member!') %> +<%= render "users/downloads" %> diff --git a/web/app/views/users/downloads.html.erb b/web/app/views/users/downloads.html.erb index ddd7d5f7c..021927b78 100644 --- a/web/app/views/users/downloads.html.erb +++ b/web/app/views/users/downloads.html.erb @@ -1,7 +1,8 @@ <% provide(:title, 'Downloads') %> +<% provide(:description, 'Download the JamKazam app for Windows or Mac to play music online with others.') %> <%= render "users/downloads" %> \ No newline at end of file diff --git a/web/app/views/users/finalize_update_email.html.erb b/web/app/views/users/finalize_update_email.html.erb index df7f19506..98ac7add7 100644 --- a/web/app/views/users/finalize_update_email.html.erb +++ b/web/app/views/users/finalize_update_email.html.erb @@ -1,4 +1,6 @@ <% provide(:title, 'Email Change Confirmation') %> +<% provide(:description, 'Your email has been changed successfully') %> +