* merged develop

This commit is contained in:
Seth Call 2015-03-24 11:27:26 -05:00
commit ccd60aa631
63 changed files with 1028 additions and 248 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,3 @@
ActiveAdmin.register JamRuby::Event, :as => 'Event' do
menu :parent => 'Events'
menu :parent => 'Misc'
end

View File

@ -1,3 +1,3 @@
ActiveAdmin.register JamRuby::EventSession, :as => 'Event Session' do
menu :parent => 'Events'
menu :parent => 'Misc'
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -25,7 +25,6 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
column :original_artist
column :name
column :status
column :preview do |jam_track| jam_track.has_preview? ? (link_to "Download", jam_track.sign_url(3600)) : 'None' end
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 :genre

View File

@ -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

View File

@ -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 = ''

View File

@ -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
end
=end

View File

@ -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

View File

@ -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

View File

@ -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;
}

362
admin/app/models/cohort.rb Normal file
View File

@ -0,0 +1,362 @@
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 =<<SQL
SELECT played.user_id FROM
(SELECT user_id, COUNT(*) cnt FROM music_sessions_user_history msuh1
WHERE
msuh1.created_at >= '#{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 =<<SQL
SELECT played.player_id FROM
(SELECT player_id, COUNT(*) cnt FROM playable_plays pp
WHERE
pp.created_at >= '#{start_date}' AND
pp.created_at <= '#{end_date}' AND
pp.playable_type = 'JamRuby::JamTrack'
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 =<<SQL
SELECT #{assoc.foreign_key} FROM #{assoc.class_name.constantize.table_name} tt
WHERE
tt.created_at >= '#{start_date}' AND
tt.created_at <= '#{end_date}'
SQL
yield(sql) if block_given?
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.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.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 _join_user_all_time(assoc_ref)
assoc_ref.active_record
.joins("INNER JOIN users AS uu ON uu.id = #{assoc_ref.foreign_key}")
.where(created_at: self.group_start..self.group_end)
.where(['uu.created_at >= ? AND uu.created_at <= ?', self.group_start, self.group_end])
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)
_put_data_set(assoc_key, count, num_user)
count = _subquery(assoc_key = :jam_tracks_played, num_user) do |subsql|
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

View File

@ -4,7 +4,6 @@
= f.input :name, :input_html => { :rows=>1, :maxlength=>200 }
= f.input :description, :input_html => { :rows=>5, :maxlength=>1000 }
= f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly'
= f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the media in the JamTrack. Changing JMEP does not count as a version change; changing anything about a track (audio, instrument, part) does.'
//= 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'
@ -23,15 +22,11 @@
= 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 :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.sign_url(3600) style='padding:0 0 0 20px'
| Download
//= 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

View File

@ -4,24 +4,29 @@
= 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
= 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_sign_url(3600) style='padding:0 0 0 20px'
| Download Preview
// 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_48, :as => :file, :label => 'Track file (48kHz)'
// = 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)'
// = 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'
| Download
| #{File.basename(f.object[:url_44])}
= link_to_remove_association "Delete Track", f, class: 'button', style: 'margin-left:10px'

View File

@ -0,0 +1,102 @@
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')
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 #{start} #{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
@@log.debug("uploading preview to #{self.preview_filename}")
s3_manager.upload(self.preview_filename, output)
self.skip_uploader = true
# and finally update the JamTrackTrack with the new info
self["preview_url"] = self.preview_filename
self["preview_md5"] = ::Digest::MD5.file(output).hexdigest
self["preview_length"] = File.new(output).size
self.save!
end
end
rescue Exception => e
@@log.error("error in sox command #{e.to_s}")
@preview_generate_error = e.to_s
end
end
end

View File

@ -6,7 +6,6 @@ class JamRuby::JamTrack
before_save :jmep_json_generate
validate :jmep_text_validate
validate :preview
def jmep_text_validate
begin
@ -28,98 +27,4 @@ class JamRuby::JamTrack
#errors.add(:jmep_text, err.to_s)
end
end
def preview
if preview_generate_error
errors.add(:preview_url, 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')
start = self.preview_start_time.to_f / 1000
stop = start + 20
master_track = self.master_track
raise 'no master track' unless master_track
s3_manager.download(master_track.url_by_sample_rate(44), input)
command = "sox \"#{input}\" \"#{output}\" trim #{start} #{stop}"
@@log.debug("trimming using: " + command)
sox_output = `#{command}`
result_code = $?.to_i
if result_code != 0
@preview_generate_error = "unable to execute cut command #{sox_output}"
else
@@log.debug("uploading preview to #{self.preview_filename}")
s3_manager.upload(self.preview_filename, output)
# and finally update the JamTrackTrack with the new info
self["preview_url"] = self.preview_filename
self["preview_md5"] = ::Digest::MD5.file(output).hexdigest
self["preview_length"] = File.new(output).size
self.save!
end
end
rescue Exception => e
@preview_generate_error = e.to_s
end
end
end

View File

@ -41,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

View File

@ -261,5 +261,9 @@ 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
shopping_cart_anonymous.sql
user_reuse_card_and_reedem.sql
user_reuse_card_and_reedem.sql

19
db/up/cohorts.sql Normal file
View File

@ -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);

View File

@ -0,0 +1 @@
ALTER TABLE connections ADD COLUMN metronome_open BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -0,0 +1 @@
ALTER TABLE jam_track_rights ADD COLUMN is_test_purchase BOOLEAN DEFAULT FALSE NOT NULL;

View File

@ -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;

View File

@ -89,7 +89,7 @@ module JamRuby
udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable
sql =<<SQL
UPDATE connections SET (channel_id, aasm_state, updated_at, music_session_id, joined_session_at, stale_time, expire_time, udp_reachable, gateway, is_network_testing) = ('#{channel_id}', '#{Connection::CONNECT_STATE.to_s}', NOW(), #{music_session_id_expression}, #{joined_session_at_expression}, #{connection_stale_time}, #{connection_expire_time}, #{udp_reachable_value}, '#{gateway}', FALSE)
UPDATE connections SET (channel_id, aasm_state, updated_at, music_session_id, joined_session_at, stale_time, expire_time, udp_reachable, gateway, is_network_testing, metronome_open) = ('#{channel_id}', '#{Connection::CONNECT_STATE.to_s}', NOW(), #{music_session_id_expression}, #{joined_session_at_expression}, #{connection_stale_time}, #{connection_expire_time}, #{udp_reachable_value}, '#{gateway}', FALSE, FALSE)
WHERE
client_id = '#{conn.client_id}'
RETURNING music_session_id

View File

@ -35,9 +35,9 @@ module JamRuby
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(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}'`
@ -47,9 +47,9 @@ module JamRuby
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}' -V '#{version}'"
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

View File

@ -21,6 +21,7 @@ module JamRuby
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

View File

@ -17,7 +17,7 @@ module JamRuby
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :preview_start_time_raw, as: :admin
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
@ -31,7 +31,6 @@ module JamRuby
validates :sales_region, inclusion: {in: [nil] + SALES_REGION}
validates_format_of :price, with: /^\d+\.*\d{0,2}$/
validates :version, presence: true
validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true
validates :pro_ascap, inclusion: {in: [true, false]}
validates :pro_bmi, inclusion: {in: [true, false]}
validates :pro_sesac, inclusion: {in: [true, false]}
@ -60,15 +59,27 @@ module JamRuby
class << self
def index(options, user)
limit = options[:limit]
limit ||= 20
limit = limit.to_i
if options[:page]
page = options[:page].to_i
per_page = options[:per_page].to_i
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)
@ -92,21 +103,6 @@ module JamRuby
end
end
# create name of the file
def preview_filename
"jam_track_previews/#{self.original_artist}/#{self.name}/preview-44100.ogg"
end
def has_preview?
!self["preview_url"].nil?
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[:preview_url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def master_track
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first

View File

@ -11,6 +11,8 @@ module JamRuby
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

View File

@ -7,17 +7,22 @@ module JamRuby
# there should only be one Master per JamTrack, but there can be N Track per JamTrack
TRACK_TYPE = %w{Track Master}
mount_uploader :url_48, JamTrackTrackUploader
mount_uploader :url_44, JamTrackTrackUploader
@@log = Logging.logger[JamTrackTrack]
# 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, 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: 25}
validates :track_type, inclusion: {in: TRACK_TYPE }
validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true
validates_uniqueness_of :position, scope: :jam_track_id
validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id]
# validates :jam_track, presence: true
@ -37,6 +42,24 @@ module JamRuby
"#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
end
# create name of the file
def preview_filename
filename("#{File.basename(self["url_44"], ".ogg")}-preview.ogg")
end
def has_preview?
!self["preview_url"].nil?
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 preview_sign_url(expiration_time = 120)
s3_manager.sign_url(self[:preview_url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def manually_uploaded_filename(mounted_as)
if track_type == 'Master'
filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg")

View File

@ -109,7 +109,7 @@ module JamRuby
# 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, backing_tracks = [])
def self.sync(clientId, tracks, backing_tracks = [], metronome_open = false)
result = {}
backing_tracks = [] unless backing_tracks
@ -117,6 +117,11 @@ module JamRuby
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 = []

View File

@ -58,15 +58,16 @@ 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_48)
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_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_48]).should be_true
s3.length(jam_track_track[:url_48]).should == File.size?(ogg_path)

View File

@ -46,9 +46,11 @@ 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_48)
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
@ -56,7 +58,6 @@ describe JamTrackTrack do
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_48]).should be_true
s3.length(jam_track_track[:url_48]).should == 'abc'.length

View File

@ -172,5 +172,21 @@ describe Track do
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

View File

@ -27,8 +27,8 @@ describe JamTracksBuilder do
jam_track_track = FactoryGirl.create(:jam_track_track)
jam_track = jam_track_track.jam_track
#uploader = JamTrackTrackUploader.new(jam_track_track, :url_48)
jam_track_track.url_48.store!(File.open(ogg_path, 'rb'))
@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)
@ -36,7 +36,7 @@ describe JamTracksBuilder do
# 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)
@s3.exists?(jam_track_track[:url_44]).should be_false
jam_track_track[:url_44].should be_nil
# Check right
jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track)
@ -45,7 +45,7 @@ describe JamTracksBuilder do
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
@s3.exists?(jam_track_track[:url_44]).should be_false
jam_track_track[:url_44].should be_nil
end
describe "with bitrate 44" do
@ -59,7 +59,8 @@ describe JamTracksBuilder do
# uploader = JamTrackTrackUploader.new(jam_track_track, :url_44)
# uploader.store!(File.open(ogg_path, 'rb'))
jam_track_track.url_44.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)
@ -67,7 +68,7 @@ describe JamTracksBuilder do
# 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)
@s3.exists?(jam_track_track[:url_48]).should be_false
jam_track_track[:url_48].should be_nil
# Check right
jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track)
@ -77,7 +78,7 @@ describe JamTracksBuilder do
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
@s3.exists?(jam_track_track[:url_48]).should be_false
jam_track_track[:url_48].should be_nil
end
end
end

View File

@ -1,3 +1,4 @@
Jasmine Javascript Unit Tests
=============================

View File

@ -58,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);

View File

@ -42,9 +42,16 @@
var $li = $(this);
var playbackMode = $li.attr('data-playback-option');
value = playbackMode;
close();
$parent.triggerHandler(context.JK.EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, {playbackMode: playbackMode});
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;
};
@ -69,7 +76,7 @@
cssClass: 'metronome-playback-mode-selector-popup',
spikeGirth:0,
spikeLength:0,
width:160,
width:180,
closeWhenOthersOpen: true,
offsetParent: $parent.offsetParent(),
positions:['top'],

View File

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

View File

@ -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();

View File

@ -115,8 +115,9 @@
var $openBackingTrack = null;
var $metronomePlaybackSelect = null;
var $metronomePlaybackHelp = null;
var $templatePendingMetronome = null;
var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup];
var muteBothMasterAndPersonalGroups = [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.
@ -235,12 +236,8 @@
promptLeave = false;
window.location = '/client#/home'
});
})
})
}
function notifyWithUserInfo(title , text, clientId) {
@ -496,7 +493,7 @@
}
function checkJamTrackTransition(currentSession) {
// handle jam tracks
// handle jam tracks
if (jamTrack == null && (currentSession && currentSession.jam_track != null)) {
playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK);
}
@ -507,7 +504,7 @@
}
function checkBackingTrackTransition(currentSession) {
// handle backing tracks
// handle backing tracks
if (backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) {
playbackControls.startMonitor();
}
@ -519,7 +516,7 @@
function checkRecordingTransition(currentSession) {
// handle claimed recordings
// 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
@ -603,10 +600,11 @@
}
function resetOtherAudioContent() {
if ($('.session-recordings .track').length === 0 && $('.session-recordings .download-jamtrack').length === 0) {
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)')
}
}
@ -637,6 +635,7 @@
if ($('.session-livetracks .track').length === 0) {
$('.session-livetracks .when-empty').show();
}
checkPendingMetronome();
resetOtherAudioContent();
/**
@ -1211,12 +1210,12 @@
});
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) {
@ -1347,6 +1346,7 @@
setFormFromMetronome()
metroCricket = context.jamClient.getMetronomeCricketTestState();
setMetronomePlaybackMode()
$closePlaybackRecording.show();
}
@ -2451,6 +2451,30 @@
.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')) {
@ -2950,6 +2974,7 @@
$openBackingTrack = $('#open-a-backingtrack');
$metronomePlaybackSelect = $('#metronome-playback-select')
$metronomePlaybackHelp = $('#metronome-playback-help')
$templatePendingMetronome = $('#template-pending-metronome');
events();

View File

@ -34,7 +34,7 @@
var sessionPageEnterTimeout = null;
var startTime = null;
var joinDeferred = null;
var previousBackingTracks = [];
var previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []};
var openBackingTrack = null;
var shownAudioMediaMixerHelp = false;
var controlsLockedForJamTrackRecording = false;
@ -68,6 +68,19 @@
}
}
// 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
@ -358,7 +371,7 @@
}
currentSessionId = null;
currentParticipants = {}
previousBackingTracks = []
previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
openBackingTrack = null
shownAudioMediaMixerHelp = false
controlsLockedForJamTrackRecording = false;
@ -603,26 +616,27 @@
return mixerMode;
}
function syncTracks(backingTracks) {
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;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// backingTracks can be passed in as an optimization, so that we don't hit the backend excessively
if(backingTracks === undefined ) {
backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
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)
@ -793,17 +807,27 @@
}
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
var backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
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(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);
@ -840,6 +864,7 @@
this.isPersonalMixMode = isPersonalMixMode;
this.getMixMode = getMixMode;
this.selfOpenedJamTracks = selfOpenedJamTracks;
this.isMetronomeOpen = isMetronomeOpen;
this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording;
this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording;
this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording;

View File

@ -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,8 +49,9 @@
return tracks;
},
getBackingTracks: function(jamClient) {
var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4);
// 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) {
@ -56,11 +76,11 @@
* 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 = [];

View File

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

View File

@ -2,7 +2,7 @@
.metronome-playback-mode-selector-popup {
.bt-content {
width:160px;
width:180px;
background-color:#333;
overflow:auto;
border:1px solid #ED3618;

View File

@ -15,6 +15,16 @@
position:relative;
}
.pending-metronome {
.spinner-large {
margin:20px auto 0;
text-align:center;
}
p {
text-align:center;
font-size:14px;
}
}
.track {
width:70px;

View File

@ -342,6 +342,11 @@
margin-top: 20px;
}
a.forever-network-test {
margin-top:20px;
display:none;
}
.network-test-score {
height: 24px;
padding: 10px;

View File

@ -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;
}
}

View File

@ -357,7 +357,7 @@ class ApiMusicSessionsController < ApiController
end
def track_sync
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_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

View File

@ -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 }

View File

@ -24,19 +24,19 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
h2.billing-caption BILLING ADDRESS
#divBillingFirstName.field
.billing-label
label for="billing-first-name" First Name:
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:
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:
label for="billing-address1" Address 1: *
.billing-value
input type="text" id="billing-address1"
.clearall
@ -48,25 +48,25 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
.clearall
#divBillingCity.field
.billing-label
label for="billing-city" City:
label for="billing-city" City: *
.billing-value
input type="text" id="billing-city"
.clearall
#divBillingState.field
.billing-label
label for="billing-state" State/Region:
label for="billing-state" State/Region: *
.billing-value
input type="text" id="billing-state"
.clearall
#divBillingZip.field
.billing-label
label for="billing-zip" Zip:
label for="billing-zip" Zip: *
.billing-value
input type="text" id="billing-zip"
.clearall
#divBillingCountry.field
.billing-label
label for="billing-country" Country:
label for="billing-country" Country: *
.billing-value
select id="billing-country"
option value="US" US
@ -77,25 +77,25 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
.new-card-info
#divCardName.field.hidden
.card-label
label for="card-name" Name of Card:
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:
label for="card-number" Card Number: *
.card-value
input type="text" id="card-number"
.clearall
#divCardExpiry.field
.card-label Expiration Date:
.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:
| CVV Code: *
.hint.cvv
| (back of card)
.card-value
@ -183,13 +183,13 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
h2.jamkazam-account-caption JAMKAZAM ACCOUNT
#divJamKazamEmail.field
.account-label
label for="email" Email Address:
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:
label for="password" Password: *
.account-value
input name="password" id="checkout-signup-password" type="password"
.clearall

View File

@ -1,8 +1,4 @@
.screen.no-login-required layout="screen" layout-id="home"
-if Rails.configuration.show_jamblaster_notice
#jamblaster-notice
a href='https://www.youtube.com/watch?v=gAJAIHMyois' rel="external"
span Check out the amazing new JamBlaster, and learn how it can improve your sessions!
/ Layout is different if jam_tracks tile available:
-jamtracks=Rails.configuration.jam_tracks_available
-if (jamtracks)

View File

@ -6,3 +6,6 @@ script type='text/template' id='template-metronome-playback-mode'
li data-playback-option="cricket"
a href='#' - Play cluster test
li data-ui-option="show-metronome-window"
a href='#' - Show visual metronome

View File

@ -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

View File

@ -143,4 +143,9 @@ script#template-option type="text/template"
script#template-genre-option type="text/template"
option value="{value}"
="{label}"
="{label}"
script#template-pending-metronome type="text/template"
.pending-metronome
.spinner-large
p Your metronome is synchronizing.

View File

@ -314,7 +314,6 @@ if defined?(Bundler)
config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development']
config.allow_spikes = false
config.show_jamblaster_notice = true
config.show_jamblaster_kickstarter_link = true
config.metronome_available = true
config.backing_tracks_available = true

View File

@ -140,10 +140,10 @@ class MusicSessionManager < BaseManager
Notification.send_session_depart(active_music_session, connection.client_id, user, recordingId)
end
def sync_tracks(active_music_session, client_id, new_tracks, backing_tracks)
def sync_tracks(active_music_session, client_id, new_tracks, backing_tracks, metronome_open)
tracks = nil
active_music_session.with_lock do # VRFS-1297
tracks = Track.sync(client_id, new_tracks, backing_tracks)
tracks = Track.sync(client_id, new_tracks, backing_tracks, metronome_open)
active_music_session.tick_track_changes
end
Notification.send_tracks_changed(active_music_session)