Merge and fix

This commit is contained in:
Steven Miers 2015-04-16 12:16:09 -05:00
parent e71b82b15b
commit 766568f8cb
471 changed files with 23836 additions and 3387 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-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 'jquery-ui-rails', '4.2.1'
gem 'rails3-jquery-autocomplete' gem 'rails3-jquery-autocomplete'
gem 'activeadmin', '0.6.2' gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable'
gem 'mime-types', '1.25' gem 'mime-types', '1.25'
gem 'meta_search' gem 'meta_search'
gem 'fog', "~> 1.18.0" gem 'fog', "~> 1.18.0"
@ -49,7 +49,7 @@ gem 'unf', '0.1.3' #optional fog dependency
gem 'country-select' gem 'country-select'
gem 'aasm', '3.0.16' gem 'aasm', '3.0.16'
gem 'postgres-copy', '0.6.0' gem 'postgres-copy', '0.6.0'
gem 'aws-sdk' #, '1.29.1' gem 'aws-sdk', '~> 1'
gem 'bugsnag' gem 'bugsnag'
gem 'gon' gem 'gon'
gem 'cocoon' gem 'cocoon'
@ -74,6 +74,7 @@ gem 'sanitize'
gem 'slim' gem 'slim'
gem 'influxdb', '0.1.8' gem 'influxdb', '0.1.8'
gem 'influxdb-rails', '0.1.10' gem 'influxdb-rails', '0.1.10'
gem 'recurly'
group :libv8 do group :libv8 do
gem 'libv8', "~> 3.11.8" gem 'libv8', "~> 3.11.8"

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 :timestamp
filter :user_email, :as => :string filter :user_email, :as => :string
filter :client_id filter :client_id
menu :parent => 'Debug' menu :parent => 'Misc'
index do index do
column "Timestamp" do |post| column "Timestamp" do |post|

View File

@ -1,6 +1,6 @@
ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do 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.sort_order = 'updated_at DESC'
config.batch_actions = false config.batch_actions = false

View File

@ -1,6 +1,6 @@
ActiveAdmin.register JamRuby::EmailBatchScheduledSessions, :as => 'Daily Sessions' do 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.sort_order = 'updated_at DESC'
config.filters = false config.filters = false

View File

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

View File

@ -1,3 +1,3 @@
ActiveAdmin.register JamRuby::EventSession, :as => 'Event Session' do ActiveAdmin.register JamRuby::EventSession, :as => 'Event Session' do
menu :parent => 'Events' menu :parent => 'Misc'
end 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' config.sort_order = 'created_at_desc'
menu :parent => 'Debug' menu :parent => 'Misc'
end end

View File

@ -1,5 +1,5 @@
ActiveAdmin.register JamRuby::ArtifactUpdate, :as => 'Artifacts' do ActiveAdmin.register JamRuby::ArtifactUpdate, :as => 'Artifacts' do
menu :label => 'Artifacts' menu :label => 'Artifacts', :parent => 'Operations'
config.sort_order = 'product,environment' config.sort_order = 'product,environment'
#config.batch_actions = false #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]}%"]) 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 end
def create def create
@jam_ruby_user = JamRuby::User.new(params[:jam_ruby_user]) @jam_ruby_user = JamRuby::User.new(params[:jam_ruby_user])
@jam_ruby_user.administratively_created = true @jam_ruby_user.administratively_created = true

View File

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

View File

@ -22,29 +22,19 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
links links
end end
column :id
column :name
column :description
column :initial_play_silence
column :time_signature
column :status
column :recording_type
column :original_artist column :original_artist
column :songwriter column :name
column :publisher 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 :licensor
column :pro
column :genre column :genre
column :sales_region
column :price column :price
column :reproduction_royalty column :reproduction_royalty
column :public_performance_royalty column :public_performance_royalty
column :reproduction_royalty_amount column :reproduction_royalty_amount
column :licensor_royalty_amount column :licensor_royalty_amount
column :pro_royalty_amount column :id
column :url
column :created_at
column :jam_track_tracks do |jam_track| column :jam_track_tracks do |jam_track|
table_for jam_track.jam_track_tracks.order('position ASC') do table_for jam_track.jam_track_tracks.order('position ASC') do

View File

@ -1,6 +1,6 @@
ActiveAdmin.register JamRuby::PromoBuzz, :as => 'Buzz' do 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.sort_order = 'position ASC aasm_state DESC updated_at DESC'
config.batch_actions = false config.batch_actions = false

View File

@ -1,6 +1,6 @@
ActiveAdmin.register JamRuby::PromoLatest, :as => 'Latest' do ActiveAdmin.register JamRuby::PromoLatest, :as => 'Latest' do
menu :label => 'Latest', :parent => 'Home Page' menu :label => 'Promo Latest', :parent => 'Misc'
config.batch_actions = false config.batch_actions = false
config.sort_order = '' config.sort_order = ''

View File

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

View File

@ -1,5 +1,6 @@
ActiveAdmin.register_page "Download CSV" do =begin
menu :parent => 'Score' ActiveAdmin.register_page "Download Score CSV" do
menu :parent => 'Misc'
page_action :create_csv, :method => :post do page_action :create_csv, :method => :post do
@ -95,4 +96,5 @@ ActiveAdmin.register_page "Download CSV" do
#end #end
end end
end end
=end

View File

@ -1,3 +1,4 @@
=begin
ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do
menu :parent => 'Score' menu :parent => 'Score'
@ -80,3 +81,4 @@ ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do
column "To Client", :to_client_id column "To Client", :to_client_id
end end
end end
=end

View File

@ -1,5 +1,5 @@
ActiveAdmin.register_page "Current Scoring Load" do ActiveAdmin.register_page "Current Scoring Load" do
menu :parent => 'Score' menu :parent => 'Misc'
content :title => "Current Scoring Load" do content :title => "Current Scoring Load" do
table_for GetWork.summary do table_for GetWork.summary do

View File

@ -31,3 +31,7 @@
// .active_admin applies to any Active Admin namespace // .active_admin applies to any Active Admin namespace
// .admin_namespace applies to the admin namespace (eg: /admin) // .admin_namespace applies to the admin namespace (eg: /admin)
// .other_namespace applies to a custom namespace named other (eg: /other) // .other_namespace applies to a custom namespace named other (eg: /other)
.cohort_col {
text-align: center;
}

View File

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

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

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

View File

@ -2,37 +2,36 @@
= f.semantic_errors *f.object.errors.keys = f.semantic_errors *f.object.errors.keys
= f.inputs name: 'JamTrack fields' do = f.inputs name: 'JamTrack fields' do
= f.input :name, :input_html => { :rows=>1, :maxlength=>200 } = 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 :description, :input_html => { :rows=>5, :maxlength=>1000 }
= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly'
= f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: false //= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)'
= f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false = 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 :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE, include_blank: false
= f.input :original_artist, :input_html => { :rows=>2, :maxlength=>200 } = f.input :original_artist, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :songwriter, :input_html => { :rows=>5, :maxlength=>1000 } = f.input :songwriter, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :publisher, :input_html => { :rows=>5, :maxlength=>1000 } = f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: false = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true
= f.input :pro, collection: JamRuby::JamTrack::PRO, include_blank: false
= f.input :genre, collection: JamRuby::Genre.all, include_blank: false = 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 :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 :reproduction_royalty, :label => 'Reproduction Royalty'
= f.input :public_performance_royalty, :label => 'Public Performance Royalty' = f.input :public_performance_royalty, :label => 'Public Performance Royalty'
= f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'} = f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :licensor_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| = f.semantic_fields_for :jam_track_tracks do |track|
= render 'jam_track_track_fields', f: 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 .links
= link_to_add_association 'Add Track', f, :jam_track_tracks, class: 'button', style: 'margin:20px;padding:10px 20px' = 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 = f.actions

View File

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

View File

@ -1,21 +1,35 @@
= f.inputs name: 'Track fields' do = f.inputs name: 'Track fields' do
ol.nested-fields 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 :instrument, collection: Instrument.all, include_blank: false
= f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' } = f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' }
= f.input :position = 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? - if f.object.new_record?
p style='margin-left:10px' //p style='margin-left:10px'
i //i
| before you can upload, you must select 'Update JamTrack' // | before you can upload, you must select 'Update JamTrack'
- else - else
= f.input :url, :as => :file, :label => 'Track file' // = f.input :url_48, :as => :file, :label => 'Track file (48kHz)'
- unless f.object.nil? || f.object[:url].nil? - unless f.object.nil? || f.object[:url_48].nil?
.current_file_holder style='margin-bottom:10px' .current_file_holder style='margin-bottom:10px'
a href=f.object.sign_url(3600) style='padding:0 0 0 20px' 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' = link_to_remove_association "Delete Track", f, class: 'button', style: 'margin-left:10px'

View File

@ -147,5 +147,8 @@ module JamAdmin
config.influxdb_hosts = ["localhost"] config.influxdb_hosts = ["localhost"]
config.influxdb_port = 8086 config.influxdb_port = 8086
config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development'] 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
end end

View File

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

View File

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

View File

@ -32,6 +32,7 @@ JamAdmin::Application.routes.draw do
match '/api/artifacts' => 'artifacts#update_artifacts', :via => :post match '/api/artifacts' => 'artifacts#update_artifacts', :via => :post
match '/api/mix/:id/enqueue' => 'admin/mixes#mix_again', :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" mount Resque::Server.new, :at => "/resque"

View File

@ -11,6 +11,7 @@ FactoryGirl.define do
state "NC" state "NC"
country "US" country "US"
terms_of_service true terms_of_service true
reuse_card true
factory :admin do factory :admin do
@ -40,6 +41,7 @@ FactoryGirl.define do
scoring_timeout Time.now scoring_timeout Time.now
sequence(:channel_id) { |n| "Channel#{n}"} sequence(:channel_id) { |n| "Channel#{n}"}
association :user, factory: :user association :user, factory: :user
metronome_open false
end end
factory :artifact_update, :class => JamRuby::ArtifactUpdate do factory :artifact_update, :class => JamRuby::ArtifactUpdate do
@ -223,7 +225,6 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" } sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" } sequence(:description) { |n| "description-#{n}" }
bpm 100.1
time_signature '4/4' time_signature '4/4'
status 'Production' status 'Production'
recording_type 'Cover' recording_type 'Cover'

2
build
View File

@ -66,7 +66,7 @@ DEB_SERVER=http://localhost:9010/apt-`uname -p`
GEM_SERVER=http://localhost:9000/gems GEM_SERVER=http://localhost:9000/gems
# if still going, then push all debs up # 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 ""
echo "PUSHING DB ARTIFACTS" echo "PUSHING DB ARTIFACTS"

View File

@ -246,4 +246,34 @@ text_message_migration.sql
user_model_about_changes.sql user_model_about_changes.sql
performance_samples.sql performance_samples.sql
user_presences.sql user_presences.sql
discard_scores_optimized.sql 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

View File

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

2
db/up/backing_tracks.sql Normal file
View File

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

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 @@
DROP INDEX jam_track_tracks_position_uniqkey;

View File

@ -0,0 +1 @@
ALTER TABLE jam_tracks ADD COLUMN duration INTEGER;

View File

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

View File

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

View File

@ -0,0 +1,2 @@
ALTER TABLE jam_tracks ADD COLUMN jmep_text VARCHAR;
ALTER TABLE jam_tracks ADD COLUMN jmep_json JSON;

View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE jam_track_rights ADD COLUMN redeemed 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 @@
ALTER TABLE jam_tracks ADD COLUMN version VARCHAR NOT NULL DEFAULT 0;

2
db/up/metronome.sql Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE recordings ADD COLUMN timeline JSON;

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE jam_tracks DROP COLUMN bpm;

49
db/up/sales.sql Normal file
View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN show_whats_next_count INTEGER NOT NULL DEFAULT 0;

12
db/up/signup_hints.sql Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
alter table user_authorizations ALTER COLUMN token TYPE character varying(2000);

View File

@ -50,6 +50,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
end end
it "is possible for #{user3} to sign in and not get disconnected within 30 seconds" do 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 as_monitor(user3) do
sign_in_poltergeist(user3) sign_in_poltergeist(user3)
repeat_for(30.seconds) do repeat_for(30.seconds) do

View File

@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[])
users.each do |user| users.each do |user|
in_client(user) do in_client(user) do
users.reject {|u| u==user}.each do |other| 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" #puts user.name + " is able to see " + other.name + "\'s track"
end end
end end

View File

@ -28,7 +28,7 @@ gem 'amqp', '1.0.2'
gem 'will_paginate' gem 'will_paginate'
gem 'actionmailer', '3.2.13' gem 'actionmailer', '3.2.13'
gem 'sendgrid', '1.2.0' gem 'sendgrid', '1.2.0'
gem 'aws-sdk' #, '1.29.1' gem 'aws-sdk', '~> 1'
gem 'carrierwave', '0.9.0' gem 'carrierwave', '0.9.0'
gem 'aasm', '3.0.16' gem 'aasm', '3.0.16'
gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError) gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError)
@ -49,6 +49,7 @@ gem 'iso-639'
gem 'rubyzip' gem 'rubyzip'
gem 'sanitize' gem 'sanitize'
gem 'influxdb', '0.1.8' gem 'influxdb', '0.1.8'
gem 'recurly'
group :test do group :test do
gem 'simplecov', '~> 0.7.1' gem 'simplecov', '~> 0.7.1'

View File

@ -7,5 +7,3 @@ Create development database 'jam_ruby'
Once you've created your database, migrate it: Once you've created your database, migrate it:
`bundle exec jam_ruby up` `bundle exec jam_ruby up`

View File

@ -30,6 +30,7 @@ require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/conflict_error" require "jam_ruby/errors/conflict_error"
require "jam_ruby/lib/app_config" require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin" 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/module_overrides"
require "jam_ruby/lib/s3_util" require "jam_ruby/lib/s3_util"
require "jam_ruby/lib/s3_manager" 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/google_analytics_event"
require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/batch_email_job"
require "jam_ruby/mq_router" require "jam_ruby/mq_router"
require "jam_ruby/recurly_client"
require "jam_ruby/base_manager" require "jam_ruby/base_manager"
require "jam_ruby/connection_manager" require "jam_ruby/connection_manager"
require "jam_ruby/version" require "jam_ruby/version"
require "jam_ruby/environment" require "jam_ruby/environment"
require "jam_ruby/init" require "jam_ruby/init"
require "jam_ruby/app/mailers/admin_mailer"
require "jam_ruby/app/mailers/user_mailer" require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer" require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/mailers/corp_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/amqp/amqp_connection_manager"
require "jam_ruby/database" require "jam_ruby/database"
require "jam_ruby/message_factory" require "jam_ruby/message_factory"
require "jam_ruby/models/backing_track"
require "jam_ruby/models/feedback" require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer" require "jam_ruby/models/feedback_observer"
#require "jam_ruby/models/max_mind_geo" #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_player"
require "jam_ruby/models/genre" require "jam_ruby/models/genre"
require "jam_ruby/models/user" 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_request"
require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_slot"
require "jam_ruby/models/rsvp_request_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"
require "jam_ruby/models/recording_comment" require "jam_ruby/models/recording_comment"
require "jam_ruby/models/recording_liker" 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"
require "jam_ruby/models/recorded_track_observer" 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"
require "jam_ruby/models/quick_mix_observer" require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token" 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/jam_company"
require "jam_ruby/models/user_sync" require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source" require "jam_ruby/models/video_source"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/text_message" 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_tracks_manager"
require "jam_ruby/jam_track_importer"
require "jam_ruby/jmep_manager"
include Jampb include Jampb

View File

@ -0,0 +1,22 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
include SendGrid
DEFAULT_SENDER = "JamKazam <noreply@jamkazam.com>"
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

View File

@ -11,7 +11,7 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded. # Add a white list of extensions which are allowed to be uploaded.
def extension_white_list def extension_white_list
%w(ogg) %w(ogg wav)
end end
def store_dir def store_dir
@ -23,6 +23,8 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base
end end
def filename 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
end end

View File

@ -11,7 +11,7 @@ class JamTrackUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded. # Add a white list of extensions which are allowed to be uploaded.
def extension_white_list def extension_white_list
%w(jkz) %w(ogg)
end end
def store_dir def store_dir
@ -23,6 +23,6 @@ class JamTrackUploader < CarrierWave::Uploader::Base
end end
def filename def filename
"#{model.store_dir}/#{model.filename}" if model.id "#{model.preview_filename}" if model.id && model.uploading_preview
end end
end end

View File

@ -1,45 +1,64 @@
<% provide(:title, 'Welcome to JamKazam!') %> <% provide(:title, 'Welcome to JamKazam!') %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- <p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p> </p>
<p> We're delighted that you have decided to try the JamKazam service, <p> 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. and we hope that you will enjoy using JamKazam to play
Following are links to some resources that can help to get you up and running quickly. music with others.
Following are some resources that can help you get oriented and get the most out of JamKazam.
</p>
<p>
<p><b style="color: white;">Getting Started</b><br/>
There are basically three kinds of setups you can use to play on JamKazam.<br/>
<ul>
<li><b style="color: white;">Built-In Audio on Your Computer</b> - 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.</li>
<li><b style="color: white;">Computer with External Audio Interface</b> - 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 <a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements">Minimum System Requirements</a>
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
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=DBo--aj_P1w">Getting Started Video</a> to learn more about your options here.</li>
<li><b style="color: white;">The JamBlaster</b> - 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
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=gAJAIHMyois">JamBlaster Video</a> to learn more about this amazing new product.</li>
</ul>
</p>
<p><b style="color: white;">JamKazam Features</b><br/>
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:<br/>
<ul>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=EZZuGcDUoWk">Creating a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=xWponSJo-GU">Finding a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=zJ68hA8-fLA">Playing in a Session</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=4KWklSZZxRc">Connecting with Other Musicians</a></li>
<li><a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=Gn-dOqnNLoY">Working with Recordings</a></li>
</ul>
</p>
<p><b style="color: white;">Getting Help</b><br/>
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
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/">Support Portal</a>
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 <a style="color: #ffcc00;" href="http://forums.jamkazam.com/">Community Forum</a>
.
</p> </p>
<p> <p>
Getting Started Video<br/> Again, welcome to JamKazam, and we look forward to seeing and hearing you online soon!
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=DBo--aj_P1w">https://www.youtube.com/watch?v=DBo--aj_P1w</a>
</p> </p>
<p> <p>Best Regards,<br/>
Other Great Tutorial Videos<br /> Team JamKazam</p>
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:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles">https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles</a>
</p>
<p>
Knowledge Base Articles<br />
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:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles">https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles</a>
</p>
<p>
JamKazam Support Portal<br />
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:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/">https://jamkazam.desk.com/</a>
</p>
<p>
JamKazam Community Forum<br />
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:
<a style="color: #ffcc00;" href="http://forums.jamkazam.com/">http://forums.jamkazam.com/</a>
</p>
<p>
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!
</p>
&nbsp;&nbsp;-- Team JamKazam

View File

@ -1,27 +1,43 @@
Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- 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 Getting Started
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 There are basically three kinds of setups you can use to play on JamKazam.
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 * 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.
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 * 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.
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! * 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

View File

@ -39,7 +39,7 @@
<td align="left"> <td align="left">
<!-- CALL OUT BOX --> <!-- CALL OUT BOX -->
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/account/profile">here to unsubscribe</a> and update your profile settings. <p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/unsubscribe/#{@user.unsubscribe_token}">here to unsubscribe</a> and update your profile settings.
</font></p> </font></p>
</td></tr></table> </td></tr></table>

View File

@ -4,8 +4,8 @@
<%= yield %> <%= yield %>
<% end %> <% end %>
<% unless @suppress_user_has_account_footer == true %> <% 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. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile. 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 %> <% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -89,7 +89,7 @@ module JamRuby
udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable
sql =<<SQL 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 WHERE
client_id = '#{conn.client_id}' client_id = '#{conn.client_id}'
RETURNING music_session_id RETURNING music_session_id
@ -311,14 +311,19 @@ SQL
end end
end end
else else
# there are still people in the session
conn.exec("UPDATE active_music_sessions set backing_track_initiator_id = NULL, backing_track_path = NULL where backing_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set metronome_initiator_id = NULL, metronome_active = FALSE where metronome_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
#ensure that there is no active claimed recording if the owner of that recording left the session #ensure that there is no active claimed recording if the owner of that recording left the session
conn.exec("UPDATE active_music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2", conn.exec("UPDATE active_music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id]) [user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2", conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id]) [user_id, previous_music_session_id])
end end
end end

View File

@ -47,5 +47,6 @@ module NotificationTypes
# Jam Tracks: # Jam Tracks:
JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE" JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE"
JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED"
end end

View File

@ -82,6 +82,8 @@ module ValidationMessages
MUST_BE_KNOWN_TIMEZONE = "not valid" MUST_BE_KNOWN_TIMEZONE = "not valid"
JAM_TRACK_ALREADY_OPEN = 'another jam track already open' JAM_TRACK_ALREADY_OPEN = 'another jam track already open'
RECORDING_ALREADY_IN_PROGRESS = "recording being made" RECORDING_ALREADY_IN_PROGRESS = "recording being made"
METRONOME_ALREADY_OPEN = 'another metronome already open'
BACKING_TRACK_ALREADY_OPEN = 'another audio file already open'
# notification # notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender' DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'

View File

@ -1,3 +1,18 @@
# initialize actionmailer # initialize actionmailer
ActionMailer::Base.raise_delivery_errors = true ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__) ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__)
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
case JamRuby::Environment.mode
when 'production'
Recurly.api_key = "7d623daabfc2434fa2a893bb008eb3e6"
Recurly.subdomain = 'jamkazam'
when 'development'
Recurly.api_key = "7d623daabfc2434fa2a893bb008eb3e6"
Recurly.subdomain = 'jamkazam-development'
else
Recurly.api_key = "4631527f203b41848523125b3ae51341"
Recurly.subdomain = 'jamkazam-test'
end
Recurly.default_currency = 'USD'

View File

@ -0,0 +1,904 @@
require 'json'
require 'tempfile'
require 'open3'
require 'fileutils'
require 'open-uri'
require 'yaml'
module JamRuby
class JamTrackImporter
@@log = Logging.logger[JamTrackImporter]
attr_accessor :name
attr_accessor :reason
attr_accessor :detail
def jamkazam_s3_manager
@s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
def public_jamkazam_s3_manager
@public_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_public, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
def finish(reason, detail)
self.reason = reason
self.detail = detail
end
def dry_run(metadata, metalocation)
metadata ||= {}
parsed_metalocation = parse_metalocation(metalocation)
return unless parsed_metalocation
original_artist = parsed_metalocation[1]
name = parsed_metalocation[2]
success = dry_run_metadata(metadata, original_artist, name)
return unless success
dry_run_audio(metadata, "audio/#{original_artist}/#{name}")
finish("success", nil)
end
def parse_metalocation(metalocation)
bits = metalocation.split('/')
if bits.length != 4
finish("invalid_metalocation", "metalocation not valid #{metalocation}")
return nil
end
if bits[0] != "audio"
finish("invalid_metalocation", "first bit is not 'audio' #{metalocation}")
return nil
end
if bits[3] != 'meta.yml'
finish('invalid_metalocation', "last bit is not 'meta.yml' #{metalocation}")
return nil
end
bits
end
# if you change this, it will (at least without some work )break development usage of jamtracks
def gen_plan_code(original_artist, name)
# remove all non-alphanumeric chars from artist as well as name
artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase
name_code = name.gsub(/[^0-9a-z]/i, '').downcase
"jamtrack-#{artist_code[0...20]}-#{name_code}"[0...50] # make sure it's a max of 50 long
end
def dry_run_metadata(metadata, original_artist, name)
self.name = metadata["name"] || name
original_artist = metadata["original_artist"] || original_artist
plan_code = metadata["plan_code"] || gen_plan_code(original_artist, self.name)
description = metadata["description"]
@@log.debug("#{self.name} original_artist=#{original_artist}")
@@log.debug("#{self.name} plan_code=#{plan_code}")
true
end
def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name)
metadata ||= {}
self.name = metadata["name"] || name
if jam_track.new_record?
jam_track.id = "#{JamTrack.count + 1}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that
jam_track.status = 'Staging'
jam_track.metalocation = metalocation
jam_track.original_artist = metadata["original_artist"] || original_artist
jam_track.name = self.name
jam_track.genre_id = 'rock'
jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name)
jam_track.price = 1.99
jam_track.reproduction_royalty_amount = 0
jam_track.licensor_royalty_amount = 0
jam_track.sales_region = 'United States'
jam_track.recording_type = 'Cover'
jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"."
else
#@@log.debug("#{self.name} skipped because it already exists in database")
finish("jam_track_exists", "")
return false
end
saved = jam_track.save
if !saved
finish("invalid_definition", jam_track.errors.inspect)
end
saved
end
# oddballs - Guitar Solo.wav
# Rocket Man Stem - Vocal Back Up
# Rocket Man Stem - Vocal Lead Double
# Rock and Roll Stem - Electric Guitar - Main - Solo
def determine_instrument(potential_instrument_original, potential_part_original = nil)
potential_instrument = potential_instrument_original.downcase
potential_part = potential_part_original.downcase if potential_part_original
instrument = nil
used_helper = false
part = nil
if potential_instrument == 'guitar'
if potential_part
if potential_part == 'acoustic'
instrument = 'acoustic guitar'
used_helper = true
elsif potential_part == 'electric'
instrument = 'electric guitar'
used_helper = true
elsif potential_part == 'acoustic solo'
instrument = 'acoustic guitar'
used_helper = true
part = 'Solo'
elsif potential_part.include?('acoustic')
used_helper = true # ambiguous
else
instrument = 'electric guitar'
used_helper = false
end
else
instrument = 'electric guitar'
end
elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat'
instrument = 'electric guitar'
elsif potential_instrument == 'keys'
instrument = 'keyboard'
elsif potential_instrument == 'vocal' || potential_instrument == 'vocals'
instrument = 'voice'
elsif potential_instrument == 'bass'
instrument = 'bass guitar'
elsif potential_instrument == 'drum'
instrument = 'drums'
elsif potential_instrument == 'sound effects' || potential_instrument == 'sound efx' || potential_instrument == 'effects'
instrument = 'computer'
if potential_part_original
part = "Sound FX (#{potential_part_original})"
else
part = 'Sound FX'
end
elsif potential_instrument == "sax"
instrument = 'saxophone'
elsif potential_instrument == "vocal back up"
instrument = "voice"
part = "Back Up"
elsif potential_instrument == "vocal lead double"
instrument = "voice"
part = "Lead Double"
elsif potential_instrument == "guitar solo"
instrument = "electric guitar"
part = "Solo"
elsif potential_instrument == 'stadium crowd'
instrument = 'computer'
part = 'Crowd Noise'
elsif potential_instrument == 'cannons'
instrument = 'computer'
part = 'Cannons'
elsif potential_instrument == 'bells'
instrument = 'computer'
part = 'Bells'
elsif potential_instrument == 'percussion'
instrument = 'drums'
part = 'Percussion'
elsif potential_instrument == 'fretless bass'
instrument = 'bass guitar'
part = 'Fretless'
elsif potential_instrument == 'clock percussion'
instrument = 'computer'
part = 'Clock'
elsif potential_instrument == 'horns'
instrument = 'other'
part = 'Horns'
elsif potential_instrument == 'strings'
instrument = 'other'
part = 'Strings'
elsif potential_instrument == 'orchestration'
instrument = 'computer'
part = 'Orchestration'
elsif potential_instrument == 'claps' || potential_instrument == 'hand claps'
instrument = 'computer'
part = 'Claps'
else
found_instrument = Instrument.find_by_id(potential_instrument)
if found_instrument
instrument = found_instrument.id
end
end
if !used_helper && !part
part = potential_part_original
end
part = potential_instrument_original if !part
{instrument: instrument,
part: part}
end
def parse_wav(file)
bits = file.split('/')
filename = bits[bits.length - 1] # remove all but just the filename
filename_no_ext = filename[0..-5]
comparable_filename = filename_no_ext.downcase # remove .wav
master = false
instrument = nil
part = nil
if comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix")
master = true
else
stem_location = comparable_filename.index('stem -')
unless stem_location
stem_location = comparable_filename.index('stems -')
end
unless stem_location
stem_location = comparable_filename.index('stem-')
end
unless stem_location
stem_location = comparable_filename.index('stems-')
end
if stem_location
bits = filename_no_ext[stem_location..-1].split('-')
bits.collect! { |bit| bit.strip }
possible_instrument = nil
possible_part = nil
if bits.length == 2
# second bit is instrument
possible_instrument = bits[1]
elsif bits.length == 3
# second bit is instrument, third bit is part
possible_instrument = bits[1]
possible_part = bits[2]
elsif bits.length == 4
possible_instrument = bits[1]
possible_part = "#{bits[2]} #{bits[3]}"
end
result = determine_instrument(possible_instrument, possible_part)
instrument = result[:instrument]
part = result[:part]
end
end
{filename: filename, master: master, instrument: instrument, part: part}
end
def dry_run_audio(metadata, s3_path)
all_files = fetch_wav_files(s3_path)
all_files.each do |file|
if file.end_with?('.wav')
parsed_wav = parse_wav(file)
if parsed_wav[: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
end
else
@@log.debug("#{self.name} ignoring non-wav file #{file}")
end
end
end
def sort_tracks(tracks)
def set_custom_weight(track)
weight = 5
case track.instrument_id
when 'electric guitar'
weight = 1
when 'acoustic guitar'
weight = 2
when 'drums'
weight = 3
when 'keys'
weight = 4
when 'computer'
weight = 10
else
weight = 5
end
if track.track_type == 'Master'
weight = 1000
end
weight
end
sorted_tracks = tracks.sort do |a, b|
a_weight = set_custom_weight(a)
b_weight = set_custom_weight(b)
a_weight <=> 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

View File

@ -11,27 +11,33 @@ module JamRuby
@@log = Logging.logger[JamTracksManager] @@log = Logging.logger[JamTracksManager]
include JamRuby::S3ManagerMixin
class << self 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) jam_track_right = jam_track.right_for_user(user)
raise ArgumentError if jam_track_right.nil? 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 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 jam_track = jam_track_right.jam_track
#py_root = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "jamtracks"))
py_root = APP_CONFIG.jamtracks_dir py_root = APP_CONFIG.jamtracks_dir
Dir.mktmpdir do |tmp_dir| Dir.mktmpdir do |tmp_dir|
jam_file_opts="" jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track| 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 # 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_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, track_filename)
copy_url_to_file(track_url, File.join(".", nm)) jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}"
jam_file_opts << " -i '#{track_filename}+#{jam_track_track.part}'"
end end
#puts "LS + " + `ls -la '#{tmp_dir}'` #puts "LS + " + `ls -la '#{tmp_dir}'`
@ -39,10 +45,11 @@ module JamRuby
title=jam_track.name title=jam_track.name
output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz") output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz")
py_file = File.join(py_root, "jkcreate.py") 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})" @@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: # 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| Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
pid = wait_thr.pid pid = wait_thr.pid
exit_status = wait_thr.value exit_status = wait_thr.value
@ -51,11 +58,14 @@ module JamRuby
#puts "stdout: #{out}, stderr: #{err}" #puts "stdout: #{out}, stderr: #{err}"
raise ArgumentError, "Error calling python script: #{err}" if err.present? 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")) 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) #raise ArgumentError, "output_jkz is empty #{output_jkz}" unless File.exists?(output_jkz)
if sample_rate==48
jam_track_right.url.store!(File.open(output_jkz, "rb")) 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.signed=true
jam_track_right.downloaded_since_sign=false jam_track_right.downloaded_since_sign=false
jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem") jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem")
@ -73,7 +83,7 @@ module JamRuby
http.request request do |response| http.request request do |response|
response_code = response.code.to_i response_code = response.code.to_i
unless response_code >= 200 && response_code <= 299 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}" raise "bad status code: #{response_code}. body: #{response.body}"
end end
response.read_body do |chunk| response.read_body do |chunk|

View File

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

View File

@ -44,6 +44,10 @@ module JamRuby
s3_bucket.objects[key].url_for(operation, options).to_s s3_bucket.objects[key].url_for(operation, options).to_s
end 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) def presigned_post(key, options = @@def_opts)
s3_bucket.objects[key].presigned_post(options) s3_bucket.objects[key].presigned_post(options)
end end
@ -72,8 +76,15 @@ module JamRuby
s3_bucket.objects[filename].delete s3_bucket.objects[filename].delete
end end
def upload(key, filename) def upload(key, filename, options={})
s3_bucket.objects[key].write(:file => filename) 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 end
def delete_folder(folder) def delete_folder(folder)
@ -88,6 +99,24 @@ module JamRuby
end end
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) def exists?(filename)
s3_bucket.objects[filename].exists? s3_bucket.objects[filename].exists?
end end

View File

@ -10,8 +10,8 @@ module JamRuby
end end
def s3_manager(options={:public => false}) def s3_manager(options={:bucket => nil, :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) @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 end
end end

View File

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

View File

@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions' 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, :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" 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, :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 :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 :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' 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 belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id
@ -27,6 +30,10 @@ module JamRuby
validate :creator_is_musician validate :creator_is_musician
validate :validate_opening_recording, :if => :opening_recording validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track 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 after_create :started_session
@ -73,22 +80,47 @@ module JamRuby
if is_jam_track_open? if is_jam_track_open?
errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN) errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end 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 end
def validate_opening_jam_track 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 # validate that there is no jam track already open in this session
unless jam_track_id_was.nil? if jam_track_id_was.present?
errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN) errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end end
# validate that there is no recording being made # validate that there is no recording being made
if is_recording? if is_recording?
errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
end end
# validate that there is no recording being played back to the session # validate that there is no recording being played back to the session
if is_playing_recording? 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
end end
@ -593,6 +625,14 @@ module JamRuby
!self.jam_track.nil? !self.jam_track.nil?
end 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? # is this music session currently recording?
def is_recording? def is_recording?
recordings.where(:duration => nil).count > 0 recordings.where(:duration => nil).count > 0
@ -742,6 +782,35 @@ module JamRuby
self.save self.save
end 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) def self.sync(session_history)
music_session = MusicSession.find_by_id(session_history.id) music_session = MusicSession.find_by_id(session_history.id)

View File

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

View File

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

View File

@ -18,15 +18,18 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id 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_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 :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 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 :as_musician, :inclusion => {:in => [true, false, nil]}
validates :client_type, :inclusion => {:in => CLIENT_TYPES} 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 :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present 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_create :did_create
after_save :report_add_participant after_save :report_add_participant
@ -60,11 +63,11 @@ module JamRuby
def state_message def state_message
case self.aasm_state.to_sym case self.aasm_state.to_sym
when CONNECT_STATE when CONNECT_STATE
'Connected' 'Connected'
when STALE_STATE when STALE_STATE
'Stale' 'Stale'
else else
'Idle' 'Idle'
end end
end end
@ -83,7 +86,7 @@ module JamRuby
def joining_session? def joining_session?
joining_session joining_session
end end
def can_join_music_session def can_join_music_session
# puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed? # 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 end
def associate_tracks(tracks) def associate_tracks(tracks)
self.tracks.clear()
unless tracks.nil? unless tracks.nil?
self.tracks.clear()
tracks.each do |track| tracks.each do |track|
t = Track.new t = Track.new
t.instrument = Instrument.find(track["instrument_id"]) t.instrument = Instrument.find(track["instrument_id"])

View File

@ -10,73 +10,175 @@ module JamRuby
PRODUCT_TYPE = 'JamTrack' 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, attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type,
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, :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 :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
validates :description, length: {maximum: 1000} validates :description, length: {maximum: 1000}
validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/ validates :time_signature, inclusion: {in: [nil] + [''] + TIME_SIGNATURES} # the empty string is needed because of activeadmin
validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES}
validates :status, inclusion: {in: [nil] + STATUS} validates :status, inclusion: {in: [nil] + STATUS}
validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE} validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE}
validates :original_artist, length: {maximum: 200} validates :original_artist, length: {maximum: 200}
validates :songwriter, length: {maximum: 1000} validates :songwriter, length: {maximum: 1000}
validates :publisher, length: {maximum: 1000} validates :publisher, length: {maximum: 1000}
validates :pro, inclusion: {in: [nil] + PRO}
validates :sales_region, inclusion: {in: [nil] + SALES_REGION} validates :sales_region, inclusion: {in: [nil] + SALES_REGION}
validates_format_of :price, with: /^\d+\.*\d{0,2}$/ 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 :reproduction_royalty, inclusion: {in: [nil, true, false]}
validates :public_performance_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 :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/
validates_format_of :licensor_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 :genre, class_name: "JamRuby::Genre"
belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' 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_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 :owners, :through => :jam_track_rights, :class_name => "JamRuby::User", :source => :user
has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" 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_tracks, allow_destroy: true
accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true
class << self def duplicate_positions?
def index(options, user) counter = {}
limit = options[:limit] jam_track_tracks.each do |track|
limit ||= 20 count = counter[track.position]
limit = limit.to_i 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) 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] if options[:show_purchased_only]
query = query.joins(:jam_track_rights) query = query.joins(:jam_track_rights)
query = query.where("jam_track_rights.user_id = ?", user.id) query = query.where("jam_track_rights.user_id = ?", user.id)
end 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_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_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.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
query = query.group("jam_tracks.id")
query = query.order('jam_tracks.name')
if query.length == 0 if query.length == 0
[query, nil] [query, nil]
@ -87,69 +189,19 @@ module JamRuby
end end
end 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. def master_track
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first
# 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})
end end
def can_download?(user) def can_download?(user)
owners.include?(user) owners.include?(user)
end end
def right_for_user(user) def right_for_user(user)
jam_track_rights.where("user_id=?", user).first 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 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
end end

View File

@ -3,19 +3,24 @@ module JamRuby
# describes what users have rights to which tracks # describes what users have rights to which tracks
class JamTrackRight < ActiveRecord::Base class JamTrackRight < ActiveRecord::Base
include JamRuby::S3ManagerMixin 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 :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack" belongs_to :jam_track, class_name: "JamRuby::JamTrack"
validates :user, presence:true validates :user, presence:true
validates :jam_track, presence:true validates :jam_track, presence:true
validates :is_test_purchase, inclusion: {in: [true, false]}
validate :verify_download_count validate :verify_download_count
after_save :after_save after_save :after_save
validates_uniqueness_of :user_id, scope: :jam_track_id validates_uniqueness_of :user_id, scope: :jam_track_id
# Uploads the JKZ: # Uploads the JKZ:
mount_uploader :url, JamTrackRightUploader mount_uploader :url_48, JamTrackRightUploader
mount_uploader :url_44, JamTrackRightUploader
before_destroy :delete_s3_files before_destroy :delete_s3_files
MAX_JAM_TRACK_DOWNLOADS = 1000 MAX_JAM_TRACK_DOWNLOADS = 1000
@ -30,7 +35,7 @@ module JamRuby
end end
def store_dir def store_dir
"#{jam_track.store_dir}/rights" "jam_track_rights/#{created_at.strftime('%m-%d-%Y')}/#{user_id}-#{id}"
end end
# create name of the file # create name of the file
@ -59,12 +64,17 @@ module JamRuby
else else
raise "Error sending notification #{self.errors}" raise "Error sending notification #{self.errors}"
end end
end end
def finish_sign(length, md5)
def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now self.last_signed_at = Time.now
self.length = length if bitrate==48
self.md5 = md5 self.length_48 = length
self.md5_48 = md5
else
self.length_44 = length
self.md5_44 = md5
end
self.signed = true self.signed = true
self.error_count = 0 self.error_count = 0
self.error_reason = nil 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 # 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 # 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 # but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120) def sign_url(expiration_time = 120, bitrate=48)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :secure => false}) field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => false})
end end
def delete_s3_files def delete_s3_files
remove_url! remove_url_48!
remove_url_44!
end end
def enqueue
def enqueue(sample_rate=48)
begin begin
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil) 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 true
rescue Exception => e rescue Exception => e
puts "e: #{e}"
# implies redis is down. we don't update started_at by bailing out here # implies redis is down. we don't update started_at by bailing out here
false false
end end
end end
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off # 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 state = signing_state
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false false
else else
enqueue enqueue(sample_rate)
true true
end end
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 # returns easy to digest state field
# SIGNED - the package is ready to be downloaded # SIGNED - the package is ready to be downloaded
# ERROR - the package was built unsuccessfully # ERROR - the package was built unsuccessfully
@ -123,8 +147,6 @@ module JamRuby
state = nil state = nil
if signed if signed
state = 'SIGNED' state = 'SIGNED'
elsif error_count > 0
state = 'ERROR'
elsif signing_started_at elsif signing_started_at
if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time
state = 'SIGNING_TIMEOUT' state = 'SIGNING_TIMEOUT'
@ -137,6 +159,8 @@ module JamRuby
else else
state = 'QUEUED' state = 'QUEUED'
end end
elsif error_count > 0
state = 'ERROR'
else else
state = 'QUIET' # needs to be poked to go build state = 'QUIET' # needs to be poked to go build
end end

View File

@ -35,7 +35,6 @@ module JamRuby
else else
raise "format of offset time must be MM:SS:MLS" raise "format of offset time must be MM:SS:MLS"
end end
end end
end end
end end

View File

@ -3,42 +3,90 @@ module JamRuby
# describes an audio track (like the drums, or guitar) that comprises a JamTrack # describes an audio track (like the drums, or guitar) that comprises a JamTrack
class JamTrackTrack < ActiveRecord::Base class JamTrackTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin include JamRuby::S3ManagerMixin
include JamRuby::S3PublicManagerMixin
# there should only be one Master per JamTrack, but there can be N Track per JamTrack # 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 :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 :track_type, inclusion: {in: TRACK_TYPE }
validates_uniqueness_of :position, 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 validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id]
# validates :jam_track, presence: true # validates :jam_track, presence: true
belongs_to :instrument, class_name: "JamRuby::Instrument" belongs_to :instrument, class_name: "JamRuby::Instrument"
belongs_to :jam_track, class_name: "JamRuby::JamTrack" 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 # create storage directory that will house this jam_track, as well as
def store_dir def store_dir
"#{jam_track.store_dir}/tracks" "jam_track_tracks"
end end
# create name of the file # create name of the file
def filename def filename(original_name)
track_type == 'Master' ? 'master.ogg' : "#{part}.ogg" "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
end 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. # 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 # 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 # 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 # but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120) def sign_url(expiration_time = 120, sample_rate=48)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end end
def can_download?(user) 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? # 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) jam_track.owners.include?(user)

View File

@ -136,11 +136,62 @@ module JamRuby
def manifest def manifest
one_day = 60 * 60 * 24 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" => [] } manifest = { "files" => [], "timeline" => [] }
mix_params = [] 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| recording.recorded_tracks.each do |recorded_track|
manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0, limiter:pick_limiter }
mix_params << { "level" => 100, "balance" => 0 } 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 end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }

View File

@ -307,7 +307,7 @@ module JamRuby
filter_approved = only_approved ? 'AND rrrs.chosen = true' : '' filter_approved = only_approved ? 'AND rrrs.chosen = true' : ''
MusicSession.where(%Q{music_sessions.canceled = FALSE AND 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.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND
music_sessions.id in ( music_sessions.id in (
select distinct(rs.music_session_id) select distinct(rs.music_session_id)

View File

@ -43,7 +43,7 @@ module JamRuby
session_user_history.music_session_id = music_session_id session_user_history.music_session_id = music_session_id
session_user_history.user_id = user_id session_user_history.user_id = user_id
session_user_history.client_id = client_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 session_user_history.save
end end

View File

@ -2,9 +2,27 @@ module JamRuby
class PlayablePlay < ActiveRecord::Base class PlayablePlay < ActiveRecord::Base
self.table_name = "playable_plays" 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 :user, :class_name => "JamRuby::User", :foreign_key => "player_id"
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_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
end end

View File

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

View File

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

View File

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

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