merged from master

This commit is contained in:
Seth Call 2015-12-28 07:19:40 -06:00
commit fb24f3beae
439 changed files with 20231 additions and 3465 deletions

View File

@ -44,7 +44,7 @@ gem 'rails3-jquery-autocomplete'
gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable'
gem 'mime-types', '1.25'
gem 'meta_search'
gem 'fog', "~> 1.32.0"
gem 'fog'
gem 'unf', '0.1.3' #optional fog dependency
gem 'country-select'
gem 'aasm', '3.0.16'

View File

@ -0,0 +1,37 @@
ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quarterly Payments' do
menu :label => 'Quarterly Reports', :parent => 'Affiliates'
config.sort_order = 'due_amount_in_cents DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = true
config.per_page = 50
config.paginate = true
filter :affiliate_partner
filter :year
filter :quarter
filter :closed
filter :paid
form :partial => 'form'
index do
# default_actions # use this for all view/edit/delete links
column 'Year' do |oo| oo.year end
column 'Quarter' do |oo| oo.quarter end
column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end
column "Due (\u00A2)" do |oo| oo.due_amount_in_cents end
column 'JamTracks Sold' do |oo| oo.jamtracks_sold end
column 'Paid' do |oo| oo.paid end
column 'Closed' do |oo| oo.paid end
end
controller do
end
end

View File

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

View File

@ -2,15 +2,18 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do
menu :label => 'Referrals', :parent => 'Affiliates'
config.sort_order = 'created_at DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = false
config.filters = true
filter :affiliate_referral
index do
column 'User' do |oo| link_to(oo.name, "http://www.jamkazam.com/client#/profile/#{oo.id}", {:title => oo.name}) end
column 'User' do |oo| link_to(oo.name, oo.admin_url, {:title => oo.name}) end
column 'Email' do |oo| oo.email end
column 'Created' do |oo| oo.created_at end
column 'Partner' do |oo| oo.affiliate_referral.partner_name end
column 'Partner' do |oo| oo.affiliate_referral.display_name end
end
controller do

View File

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

View File

@ -1,28 +1,27 @@
ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do
# Note: a lame thing is it's not obvious how to make it search on email instead of user_id.
filter :timestamp
filter :user_email, :as => :string
filter :client_id
filter :user_id
menu :parent => 'Misc'
config.sort_order = 'created_at DESC'
index do
column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
column "Client Version", :client_version
column "Client Type", :client_type
column "Download" do |post|
link_to 'Link', post.sign_url
end
column "Timestamp" do |post|
(post.timestamp || post.created_at).strftime('%b %d %Y, %H:%M')
end
column "Client Type", :client_type
column "Dump URL" do |post|
link_to post.uri, post.uri
column "Description" do |post|
post.description
end
column "User ID", :user_id
# FIXME (?): This isn't performant (though it likely doesn't matter). Could probably do a join.
column "User Email" do |post|
unless post.user_id.nil?
post.user_email
end
end
column "Client ID", :client_id
actions
end
end

View File

@ -0,0 +1,35 @@
ActiveAdmin.register JamRuby::DownloadTracker, :as => 'DownloadTrackers' do
menu :label => 'Download Trackers', :parent => 'JamTracks'
config.batch_actions = false
config.filters = true
config.per_page = 50
filter :remote_ip
index do
column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
column 'Created' do |oo| oo.created_at end
column 'JamTrack' do |oo| oo.jam_track end
column 'Paid' do |oo| oo.paid end
column 'Blacklisted?' do |oo| IpBlacklist.listed(oo.remote_ip) ? 'Yes' : 'No' end
column 'Remote IP' do |oo| oo.remote_ip end
column "" do |oo|
link_to 'Blacklist This IP', "download_trackers/#{oo.id}/blacklist_by_ip"
end
end
member_action :blacklist_by_ip, :method => :get do
tracker = DownloadTracker.find(params[:id])
if !IpBlacklist.listed(tracker.remote_ip)
ip = IpBlacklist.new
ip.remote_ip = tracker.remote_ip
ip.save!
end
redirect_to admin_download_trackers_path, :notice => "IP address #{tracker.remote_ip} blacklisted."
end
end

View File

@ -32,6 +32,7 @@ ActiveAdmin.register_page "Fake Purchaser" do
jam_track_right.user = user
jam_track_right.jam_track = jam_track
jam_track_right.is_test_purchase = true
jam_track_right.version = jam_track.version
jam_track_right.save!
count = count + 1
end

View File

@ -0,0 +1,41 @@
ActiveAdmin.register_page "Giftcarduploads" do
menu :label => 'Gift Cards Upload', :parent => 'JamTracks'
page_action :upload_giftcards, :method => :post do
GiftCard.transaction do
puts params
file = params[:jam_ruby_gift_card][:csv]
array_of_arrays = CSV.read(file.tempfile.path)
array_of_arrays.each do |row|
if row.length != 1
raise "UKNONWN CSV FORMAT! Must be 1 column"
end
code = row[0]
gift_card = GiftCard.new
gift_card.code = code
gift_card.card_type = params[:jam_ruby_gift_card][:card_type]
gift_card.origin = file .original_filename
gift_card.save!
end
redirect_to admin_giftcarduploads_path, :notice => "Created #{array_of_arrays.length} gift cards!"
end
end
content do
semantic_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "Upload Gift Cards" do
f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)"
f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES
end
f.actions
end
end
end

View File

@ -0,0 +1,24 @@
ActiveAdmin.register JamRuby::GiftCard, :as => 'GiftCards' do
menu :label => 'Gift Cards', :parent => 'JamTracks'
config.batch_actions = false
config.filters = true
config.per_page = 50
scope("Redeemed Most Recently", default: true) { |scope| scope.where('user_id IS NOT NULL').order('updated_at DESC') }
scope("Available") { |scope| scope.where('user_id is NULL') }
filter :card_type
filter :origin
filter :code
index do
column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
column 'Code' do |oo| oo.code end
column 'Card Type' do |oo| oo.card_type end
column 'Origin' do |oo| oo.origin end
column 'Created' do |oo| oo.created_at end
end
end

View File

@ -0,0 +1,13 @@
ActiveAdmin.register JamRuby::IpBlacklist, :as => 'IP Blacklist' do
menu :label => 'IP Blacklist', :parent => 'Operations'
config.sort_order = 'created_at desc'
config.batch_actions = false
index do
column :remote_ip
column :notes
column :created_at
end
end

View File

@ -5,13 +5,16 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
config.sort_order = 'name_asc'
config.batch_actions = false
filter :name
filter :original_artist
filter :genres
filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS
scope("Default", default: true) { |scope| scope }
scope("Onboarding TODO") { |scope| scope.where('onboarding_exceptions is not null') }
scope("Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'") }
scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') }
scope("TimTracks Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tim Waurick'") }
# scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') }
form :partial => 'form'

View File

@ -0,0 +1,40 @@
ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do
menu :label => 'Line Items', :parent => 'Purchases'
config.sort_order = 'created_at DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = true
config.per_page = 50
config.paginate = true
filter :affiliate_referral_id
filter :free
form :partial => 'form'
scope("Non Free", default: true) { |scope| scope.where(free: false).order('created_at desc') }
scope("Free") { |scope| scope.where(free: true).order('created_at desc') }
index do
# default_actions # use this for all view/edit/delete links
column 'Product' do |oo| oo.product end
column "Partner" do |oo|
link_to("#{oo.affiliate_referral.display_name} #{oo.affiliate_referral_fee_in_cents ? "#{oo.affiliate_referral_fee_in_cents}\u00A2" : ''}", oo.affiliate_referral.admin_url, {:title => oo.affiliate_referral.display_name}) if oo.affiliate_referral
end
column 'User' do |oo|
link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name})
end
column 'When' do |oo|
oo.created_at
end
end
controller do
end
end

View File

@ -0,0 +1,13 @@
ActiveAdmin.register JamRuby::UserBlacklist, :as => 'User Blacklist' do
menu :label => 'User Blacklist', :parent => 'Operations'
config.sort_order = 'created_at desc'
config.batch_actions = false
index do
column :user
column :notes
column :created_at
end
end

View File

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

View File

@ -15,5 +15,10 @@ class EmailController < ApplicationController
headers['Content-Type'] ||= 'text/csv'
@users = User.where(subscribe_email: true)
# if specified, return only users that have redeemed or bought a JamTrack
if params[:any_jam_track]
@users = @users.select('DISTINCT users.id, email, first_name, last_name').joins(:sales => :sale_line_items).where("sale_line_items.product_type = 'JamTrack'")
end
end
end

View File

@ -2,6 +2,8 @@
<%= f.semantic_errors *f.object.errors.keys %>
<%= f.inputs do %>
<%= f.input(:partner_name, :input_html => {:maxlength => 128}) %>
<%= f.input(:entity_type, :as => :select, :collection => AffiliatePartner::ENTITY_TYPES) %>
<%= f.input(:rate) %>
<% end %>
<%= f.actions %>
<% end %>

View File

@ -13,6 +13,7 @@
= f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true
= f.input :genres
= f.input :year
= f.input :duration, hint: 'this should rarely need editing because it comes from the import process'
= f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false
= f.input :price, :required => true, :input_html => {type: 'numeric'}

View File

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

View File

@ -110,6 +110,7 @@ module JamAdmin
config.redis_host = "localhost:6379"
config.email_social_alias = 'social@jamkazam.com'
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'
@ -153,5 +154,12 @@ module JamAdmin
config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep"))
config.email_dump_code = 'rcAUyC3TZCbgGx4YQpznBRbNnQMXW5iKTzf9NSBfzMLsnw9dRQ'
config.admin_port = ENV['ADMIN_PORT'] || 3333
config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}"
config.download_tracker_day_range = 30
config.max_user_ip_address = 10
config.max_multiple_users_same_ip = 2
end
end

View File

@ -46,4 +46,5 @@ JamAdmin::Application.configure do
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'
config.email_social_alias = 'social-dev@jamkazam.com'
end

View File

@ -0,0 +1,9 @@
class JamRuby::GiftCard
attr_accessor :csv
def process_csv
end
end

View File

@ -2,28 +2,5 @@ class JamRuby::JamTrack
# add a custom validation
attr_accessor :preview_generate_error
before_save :jmep_json_generate
validate :jmep_text_validate
def jmep_text_validate
begin
JmepManager.execute(self.jmep_text)
rescue ArgumentError => err
errors.add(:jmep_text, err.to_s)
end
end
def jmep_json_generate
self.licensor_id = nil if self.licensor_id == ''
self.jmep_json = nil if self.jmep_json == ''
self.time_signature = nil if self.time_signature == ''
begin
self[:jmep_json] = JmepManager.execute(self.jmep_text)
rescue ArgumentError => err
#errors.add(:jmep_text, err.to_s)
end
end
end

View File

@ -16,6 +16,3 @@ PLATFORMS
DEPENDENCIES
pg_migrate (= 0.1.13)!
BUNDLED WITH
1.10.5

View File

@ -298,11 +298,25 @@ musician_search.sql
enhance_band_profile.sql
alter_band_profile_rate_defaults.sql
repair_band_profile.sql
profile_teacher.sql
jam_track_onboarding_enhancements.sql
jam_track_name_drop_unique.sql
populate_languages.sql
populate_subjects.sql
jam_track_searchability.sql
harry_fox_agency.sql
jam_track_slug.sql
mixdown.sql
aac_master.sql
video_recording.sql
web_playable_jamtracks.sql
affiliate_partner_rate.sql
track_downloads.sql
jam_track_lang_idx.sql
giftcard.sql
add_description_to_crash_dumps.sql
acappella.sql
purchasable_gift_cards.sql
versionable_jamtracks.sql
session_controller.sql
jam_tracks_bpm.sql
profile_teacher.sql
populate_languages.sql
populate_subjects.sql

3
db/up/aac_master.sql Normal file
View File

@ -0,0 +1,3 @@
ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint;

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

@ -0,0 +1,2 @@
INSERT INTO genres (id, description) values ('acapella', 'A Capella');
ALTER TABLE jam_track_licensors ADD COLUMN slug VARCHAR UNIQUE;

View File

@ -0,0 +1 @@
ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(20000);

View File

@ -0,0 +1 @@
ALTER TABLE affiliate_partners ADD COLUMN rate NUMERIC(8,2) DEFAULT 0.10;

6
db/up/crash_dumps_2.sql Normal file
View File

@ -0,0 +1,6 @@
ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255);
ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000);
ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100);
ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100);
ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey;

13
db/up/giftcard.sql Normal file
View File

@ -0,0 +1,13 @@
CREATE TABLE gift_cards (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(64) UNIQUE NOT NULL,
user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE,
card_type VARCHAR(64) NOT NULL,
origin VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id);
ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0;

View File

@ -0,0 +1 @@
CREATE INDEX ON jam_tracks(language);

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

@ -0,0 +1,2 @@
ALTER TABLE jam_tracks ADD COLUMN bpm numeric(8,3);
INSERT INTO instruments (id, description) VALUES ('percussion', 'Percussion');

61
db/up/mixdown.sql Normal file
View File

@ -0,0 +1,61 @@
CREATE TABLE jam_track_mixdowns (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
settings JSON NOT NULL,
name VARCHAR(1000) NOT NULL,
description VARCHAR(1000),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE jam_track_mixdown_packages (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE CASCADE,
file_type VARCHAR NOT NULL ,
sample_rate INTEGER NOT NULL,
url VARCHAR(2048),
md5 VARCHAR,
length INTEGER,
downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE,
last_step_at TIMESTAMP,
last_signed_at TIMESTAMP,
download_count INTEGER NOT NULL DEFAULT 0,
signed_at TIMESTAMP,
downloaded_at TIMESTAMP,
signing_queued_at TIMESTAMP,
error_count INTEGER NOT NULL DEFAULT 0,
error_reason VARCHAR,
error_detail VARCHAR,
should_retry BOOLEAN NOT NULL DEFAULT FALSE,
packaging_steps INTEGER,
current_packaging_step INTEGER,
private_key VARCHAR,
signed BOOLEAN,
signing_started_at TIMESTAMP,
first_downloaded TIMESTAMP,
signing BOOLEAN NOT NULL DEFAULT FALSE,
encrypt_type VARCHAR,
first_downloaded_at TIMESTAMP,
last_downloaded_at TIMESTAMP,
version VARCHAR NOT NULL DEFAULT '1',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL;
ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE;
ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued);
CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at);
CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at);
CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued);
CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at);
CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at);

View File

@ -0,0 +1,24 @@
CREATE TABLE gift_card_types (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
card_type VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5');
INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10');
CREATE TABLE gift_card_purchases (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
gift_card_type_id VARCHAR(64) REFERENCES gift_card_types(id) ON DELETE SET NULL,
recurly_adjustment_uuid VARCHAR(500),
recurly_adjustment_credit_uuid VARCHAR(500),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE sale_line_items ADD COLUMN gift_card_purchase_id VARCHAR(64) REFERENCES gift_card_purchases(id);

View File

@ -0,0 +1 @@
ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id);

30
db/up/track_downloads.sql Normal file
View File

@ -0,0 +1,30 @@
CREATE TABLE download_trackers (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
remote_ip VARCHAR(400) NOT NULL,
jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
paid BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id);
CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip);
CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid);
CREATE TABLE ip_blacklists (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
remote_ip VARCHAR(400) UNIQUE NOT NULL,
notes VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_blacklists (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
notes VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1 @@
ALTER TABLE jam_track_rights ADD COLUMN version VARCHAR NOT NULL DEFAULT '0';

View File

@ -0,0 +1,3 @@
ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE user_authorizations ADD refresh_token VARCHAR;
ALTER TABLE recordings ADD external_video_id VARCHAR;

View File

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

View File

@ -82,6 +82,10 @@ message ClientMessage {
JAM_TRACK_SIGN_COMPLETE = 260;
JAM_TRACK_SIGN_FAILED = 261;
// jamtracks mixdown notifications
MIXDOWN_SIGN_COMPLETE = 270;
MIXDOWN_SIGN_FAILED = 271;
TEST_SESSION_MESSAGE = 295;
PING_REQUEST = 300;
@ -188,6 +192,10 @@ message ClientMessage {
optional JamTrackSignComplete jam_track_sign_complete = 260;
optional JamTrackSignFailed jam_track_sign_failed = 261;
// jamtrack mixdown notification
optional MixdownSignComplete mixdown_sign_complete = 270;
optional MixdownSignFailed mixdown_sign_failed = 271;
// Client-Session messages (to/from)
optional TestSessionMessage test_session_message = 295;
@ -612,6 +620,15 @@ message JamTrackSignFailed {
required int32 jam_track_right_id = 1; // jam track right id
}
message MixdownSignComplete {
required string mixdown_package_id = 1; // jam track mixdown package id
}
message MixdownSignFailed {
required string mixdown_package_id = 1; // jam track mixdown package id
}
message SubscriptionMessage {
optional string type = 1; // the type of the subscription
optional string id = 2; // data about what to subscribe to, specifically

View File

@ -64,6 +64,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner"
require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/scheduled/tally_affiliates"
require "jam_ruby/resque/jam_tracks_builder"
require "jam_ruby/resque/jam_track_mixdown_packager"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/resque/long_running"
@ -105,10 +106,14 @@ require "jam_ruby/models/max_mind_release"
require "jam_ruby/models/genre_player"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
require "jam_ruby/models/user_event"
require "jam_ruby/models/anonymous_user"
require "jam_ruby/models/signup_hint"
require "jam_ruby/models/machine_fingerprint"
require "jam_ruby/models/machine_extra"
require "jam_ruby/models/download_tracker"
require "jam_ruby/models/ip_blacklist"
require "jam_ruby/models/user_blacklist"
require "jam_ruby/models/fraud_alert"
require "jam_ruby/models/fingerprint_whitelist"
require "jam_ruby/models/rsvp_request"
@ -209,6 +214,8 @@ require "jam_ruby/models/jam_track_track"
require "jam_ruby/models/jam_track_right"
require "jam_ruby/models/jam_track_tap_in"
require "jam_ruby/models/jam_track_file"
require "jam_ruby/models/jam_track_mixdown"
require "jam_ruby/models/jam_track_mixdown_package"
require "jam_ruby/models/genre_jam_track"
require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"
@ -249,6 +256,10 @@ require "jam_ruby/models/language"
require "jam_ruby/models/subject"
require "jam_ruby/models/band_search"
require "jam_ruby/import/tency_stem_mapping"
require "jam_ruby/models/jam_track_search"
require "jam_ruby/models/gift_card"
require "jam_ruby/models/gift_card_purchase"
require "jam_ruby/models/gift_card_type"
include Jampb

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
class AdminMailer < ActionMailer::Base
include SendGrid
@ -20,6 +20,14 @@ module JamRuby
subject: options[:subject])
end
def social(options)
mail(to: APP_CONFIG.email_social_alias,
from: APP_CONFIG.email_generic_from,
body: options[:body],
content_type: "text/plain",
subject: options[:subject])
end
def recurly_alerts(user, options)
body = options[:body]

View File

@ -1,5 +1,9 @@
<% provide(:title, 'Confirm Email') %>
<% provide(:title, 'Welcome to JamKazam!') %>
<p>Welcome to JamKazam, <%= @user.first_name %>!</p>
<p>Were delighted you have joined our community of 20,000+ musicians. Wed like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please <a style="color: #ffcc00;" href="<%= @signup_confirm_url %>">click here to confirm this email</a> has reached you successfully and we will then send the orientation email.</p>
<p>To confirm this email address, please go to the <a style="color: #ffcc00;" href="<%= @signup_confirm_url %>">signup confirmation page</a>.</p>
<p>If you have received this email but arent familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.</p>
<p>Best Regards,<br/>
Team JamKazam
</p>

View File

@ -1,3 +1,8 @@
Welcome to JamKazam, <%= @user.first_name %>!
Welcome to JamKazam!
To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.
Were delighted you have joined our community of 20,000+ musicians. Wed like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click <%= @signup_confirm_url %> to confirm this email has reached you successfully and we will then send the orientation email.
If you have received this email but arent familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.
Best Regards,
Team JamKazam

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,63 +1,118 @@
<% provide(:title, 'Welcome to JamKazam!') %>
<% if !@user.anonymous? %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p>
<% end %>
<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.
Following are some resources that can help you get oriented and get the most out of JamKazam.
<p> We're delighted you have decided to join the JamKazam community of musicians, and we hope
you will enjoy using JamKazam and JamTracks to play more music. Following are some
resources and some things you can do to get the most out of JamKazam.
</p>
<p><b style="color: white;">Playing With JamTracks</b><br/>
JamTracks are the best way to play along with your favorite songs. Far better and different than
traditional backing tracks, our JamTracks are complete multi-track professional recordings, with
fully isolated tracks for each part of the music. And our free app and Internet service are packed
with features that give you unmatched creative freedom to learn, practice, record, play with
others, and share your performances. Here are some great JamTracks resources:
</p>
<ul>
<li><a style="color:#fc0" href="https://www.youtube.com/watch?v=07zJC7C2ICA">JamTracks Overview Video</a> - See all the great things you can do with JamTracks.</li>
<li><a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/2124663-playing-with-jamtracks">JamTracks User Guide</a> - A set of articles that explain how to use all the JamTracks
features.</li>
<li><a style="color:#fc0" href="https://www.jamkazam.com/client#/jamtrack">Get a JamTrack Free</a> - A web page you can visit to search our catalog of JamTracks.
When you find a song you like, click the Get It Free button, and your first one is free! If
you already redeemed a free JamTrack or purchased JamTracks, you can also access
them on this page from your web browser.</li>
<li><a style="color:#fc0" href="https://www.jamkazam.com/downloads">JamKazam Application</a> - A web page where you can download our free Mac or Windows
app. The app lets you do a lot more with JamTracks than you can do in a browser.</li>
</ul>
<p><b style="color: white;">Play Live With Others from Different Locations on JamKazam</b><br/>
JamKazams free app lets musicians play together live and in sync from different locations over
the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio
quality. You can set up online sessions that are public or private, for you alone or for others to
join. You can find and join others sessions, use backing tracks and loops in sessions, make
audio and video recordings of session performances, and more. <a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/673198-tutorials-on-major-features/articles">Click here for a set of tutorial
videos that show how to use these features</a>.
</p>
<p><b style="color: white;">Teach or Take Online Music Lessons</b><br/>
If you teach music lessons and have tried to give lessons using Skype, youll know how
unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and
student playing together at all. JamKazam is a terrific service for teaching and taking online
music lessons. If you want to use JamKazam for lessons, well be happy to support both you and
your students in getting set up and ready to go.
</p>
<p><b style="color: white;">Complete Your Profile</b><br/>
Every member of our community has a profile. Its a great way to share a little bit about who
you are as a musician, as well as your musical interests. For example, what instruments do you
play? What musical genres do you like best? Are you interested in getting into a virtual/online
or a real-world band? And so on. Filling out your profile will help you connect with others with
common interests. To do this, go to <a style="color:#fc0" href="https://www.jamkazam.com/client">www.jamkazam.com</a> or launch the JamKazam app. Then
click on the Profile tile, and click the Edit Profile button.
</p>
<p><b style="color: white;">Invite Your Friends</b><br/>
Have friends who are musicians? Invite them to join you to play together on JamKazam. To do
this, go to <a style="color:#fc0" href="https://www.jamkazam.com/client">www.jamkazam.com</a> or launch the JamKazam app. Then move your mouse over the
user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in
this menu, and you can then use the options to invite your friends using their email addresses,
or via Facebook, or using your Google contacts.
<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;">Get Help</b><br/>
<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>
If you run into trouble and need help, please reach out to us. We will be glad to do everything
<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>
we can to answer your questions and get you up and running. You can visit our <a style="color:#fc0" 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 <a style="color:#fc0" href="mailto:support@jamkazam.com">support@jamkazam.com</a>. 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:#fc0" href="http://forums.jamkazam.com/">Community Forum</a>.
<p>
<p>
Again, welcome to JamKazam, and we look forward to seeing and hearing you online soon!
<br/>
<br/>
Again, welcome to JamKazam, and we hope you have a great time here!
</p>
<p>Best Regards,<br/>

View File

@ -1,4 +1,4 @@
Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %>
We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam.

View File

@ -28,13 +28,13 @@ module JamRuby
##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb
def gather_friends(connection, user_id)
friend_ids = []
connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
friend_ids = []
connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
return friend_ids
end
return friend_ids
end
# this simulates music_session destroy callbacks with activerecord
@ -42,7 +42,7 @@ module JamRuby
music_session = ActiveMusicSession.find_by_id(music_session_id)
music_session.before_destroy if music_session
end
# reclaim the existing connection, if ip_address is not nil then perhaps a new address as well
def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time, udp_reachable, gateway)
music_session_id = nil
@ -65,11 +65,19 @@ module JamRuby
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
if isp.nil? then ispid = 0 else ispid = isp.coid end
if isp.nil? then
ispid = 0
else
ispid = isp.coid
end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
if block.nil? then locid = 0 else locid = block.locid end
if block.nil? then
locid = 0
else
locid = block.locid
end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@ -183,11 +191,19 @@ SQL
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
if isp.nil? then ispid = 0 else ispid = isp.coid end
if isp.nil? then
ispid = 0
else
ispid = isp.coid
end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
if block.nil? then locid = 0 else locid = block.locid end
if block.nil? then
locid = 0
else
locid = block.locid
end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@ -199,11 +215,11 @@ SQL
lock_connections(conn)
conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
[user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
[user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
# we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends
conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result|
count = result.getvalue(0, 0) .to_i
count = result.getvalue(0, 0).to_i
# we're passing all this stuff so that the user record might be updated as well...
blk.call(conn, count) unless blk.nil?
end
@ -291,7 +307,7 @@ SQL
# destroy the music_session if it's empty
num_participants = nil
conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
[previous_music_session_id]) do |result|
num_participants = result.getvalue(0, 0).to_i
end
@ -324,11 +340,65 @@ SQL
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
update_session_controller(previous_music_session_id)
end
end
end
def update_session_controller(music_session_id)
active_music_session = ActiveMusicSession.find(music_session_id)
if active_music_session
music_session = active_music_session.music_session
if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller)
# find next in line, because the current 'session controller' is not part of the session
next_in_line(music_session, active_music_session)
end
end
end
# determine who should be session controller after someone leaves
def next_in_line(music_session, active_music_session)
session_users = active_music_session.users
# check friends 1st
session_friends = music_session.creator.friends && session_users
if session_friends.length > 0
music_session.session_controller = session_friends[0]
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
# check invited 2nd
invited = music_session.invited_musicians && session_users
if invited.length > 0
music_session.session_controller = invited[0]
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
# go by who joined earliest
earliest = active_music_session.connections.order(:joined_session_at).first
if earliest
music_session.session_controller = earliest
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
music_session.creator
end
def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil)
connection = nil
@ -349,7 +419,10 @@ SQL
if connection.errors.any?
raise ActiveRecord::Rollback
else
update_session_controller(music_session.id)
end
end
connection
@ -383,6 +456,8 @@ SQL
if result.cmd_tuples == 1
@log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}")
update_session_controller(music_session.id)
JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id)
session_checks(conn, previous_music_session_id, user_id)
blk.call() unless blk.nil?

View File

@ -51,4 +51,7 @@ module NotificationTypes
JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE"
JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED"
MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE"
MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED"
end

View File

@ -19,6 +19,8 @@ module ValidationMessages
# sessions
SESSION_NOT_FOUND = "Session not found."
NOT_FOUND = 'not found'
# genres
RECORDING_GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed."
BAND_GENRE_LIMIT_EXCEEDED = "No more than 3 genres are allowed."

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ module JamRuby
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ
next if jam_track_track.track_type == "Master" # master mixes do not go into the JKZ
# use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata
nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate))
@ -52,7 +52,8 @@ module JamRuby
step = bump_step(jam_track_right, step)
copy_url_to_file(track_url, track_filename)
jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}"
part = jam_track_track.track_type == 'Click' ? 'ClickTrack' : jam_track_track.part
jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{part}")}"
end
#puts "LS + " + `ls -la '#{tmp_dir}'`

View File

@ -121,6 +121,10 @@ module JamRuby
s3_bucket.objects[filename].exists?
end
def object(filename)
s3_bucket.objects[filename]
end
def length(filename)
s3_bucket.objects[filename].content_length
end

View File

@ -14,15 +14,29 @@ module JamRuby
end
def self.mount_source_up_requested(mount)
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json)
end
def self.mount_source_down_requested(mount)
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json)
end
def self.jam_track_signing_job_change(jam_track_right)
Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json )
Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s,
{signing_state: jam_track_right.signing_state,
current_packaging_step: jam_track_right.current_packaging_step,
packaging_steps: jam_track_right.packaging_steps}.to_json)
end
def self.mixdown_signing_job_change(jam_track_mixdown_package)
Notification.send_subscription_message('mixdown', jam_track_mixdown_package.id.to_s,
{signing_state: jam_track_mixdown_package.signing_state,
current_packaging_step: jam_track_mixdown_package.current_packaging_step,
packaging_steps: jam_track_mixdown_package.packaging_steps}.to_json)
end
def self.test
Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json)
end
end
end

View File

@ -736,6 +736,30 @@ module JamRuby
)
end
def mixdown_sign_complete(receiver_id, mixdown_package_id)
signed = Jampb::MixdownSignComplete.new(
:mixdown_package_id => mixdown_package_id
)
Jampb::ClientMessage.new(
:type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE,
:route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET,
:mixdown_sign_complete => signed
)
end
def mixdown_sign_failed(receiver_id, mixdown_package_id)
signed = Jampb::MixdownSignFailed.new(
:mixdown_package_id => mixdown_package_id
)
Jampb::ClientMessage.new(
:type => ClientMessage::Type::MIXDOWN_SIGN_FAILED,
:route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET,
:mixdown_sign_failed=> signed
)
end
def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at)
recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new(

View File

@ -9,7 +9,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
attr_accessible :partner_name, :partner_code, :partner_user_id
attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin
ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other }
@ -50,6 +50,14 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
record.entity_type ||= ENTITY_TYPES.first
end
def display_name
partner_name || (partner_user ? partner_user.name : 'abandoned')
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/affiliates/#{id}"
end
# used by admin
def self.create_with_params(params={})
raise 'not supported'
@ -111,18 +119,16 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
end
def should_attribute_sale?(shopping_cart)
if shopping_cart.is_jam_track?
if created_within_affiliate_window(shopping_cart.user, Time.now)
product_info = shopping_cart.product_info
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
{fee_in_cents: real_quantity * 20}
else
false
end
if created_within_affiliate_window(shopping_cart.user, Time.now)
product_info = shopping_cart.product_info
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
{fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round}
else
raise 'shopping cart type not implemented yet'
false
end
end
def cumulative_earnings_in_dollars
@ -469,4 +475,8 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
def affiliate_query_params
AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s
end
def to_s
display_name
end
end

View File

@ -15,20 +15,47 @@ module JamRuby
ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC')
end
def destroy_all_shopping_carts
ShoppingCart.destroy_all(anonymous_user_id: @id)
end
def destroy_jam_track_shopping_carts
ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE)
end
def admin
false
end
def has_redeemable_jamtrack
raise "not a cookied anonymous user" if @cookies.nil?
APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack]
end
def gifted_jamtracks
0
end
def free_jamtracks
if has_redeemable_jamtrack
1
else
0
end
end
def show_free_jamtrack?
ShoppingCart.user_has_redeemable_jam_track?(self)
end
def signup_hint
SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first
end
def reload
end
end
end

View File

@ -102,11 +102,19 @@ module JamRuby
def self.search_target_class
end
def self.genre_ids
@@genre_ids ||= Hash[ *Genre.pluck(:id).collect { |v| [ v, v ] }.flatten ]
end
def self.instrument_ids
@@instrument_ids ||= Hash[ *Instrument.pluck(:id).collect { |v| [ v, v ] }.flatten ]
end
def _genres(rel, query_data=json)
gids = query_data[KEY_GENRES]
unless gids.blank?
allgids = Genre.order(:id).pluck(:id)
gids = gids.select { |gg| allgids.index(gg).present? }
allgids = self.class.genre_ids
gids = gids.select { |gg| allgids.has_key?(gg) }
unless gids.blank?
gidsql = gids.join("','")
@ -119,8 +127,8 @@ module JamRuby
def _instruments(rel, query_data=json)
unless (instruments = query_data[KEY_INSTRUMENTS]).blank?
instrids = Instrument.order(:id).pluck(:id)
instruments = instruments.select { |ii| instrids.index(ii['instrument_id']).present? }
instrids = self.class.instrument_ids
instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) }
unless instruments.blank?
instsql = "SELECT player_id FROM musicians_instruments WHERE (("

View File

@ -1,6 +1,8 @@
module JamRuby
class CrashDump < ActiveRecord::Base
include JamRuby::S3ManagerMixin
self.table_name = "crash_dumps"
self.primary_key = 'id'
@ -15,7 +17,7 @@ module JamRuby
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}"
end
def user_email
@ -23,5 +25,8 @@ module JamRuby
self.user.email
end
def sign_url(expiration_time = 3600 * 24 * 7, secure=true)
s3_manager.sign_url(self[:ri], {:expires => expiration_time, :secure => secure})
end
end
end

View File

@ -0,0 +1,121 @@
module JamRuby
class DownloadTracker < ActiveRecord::Base
@@log = Logging.logger[DownloadTracker]
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :mixdown, :class_name => "JamRuby::JamTrackMixdownPackage", foreign_key: 'mixdown_id'
belongs_to :stem, :class_name => "JamRuby::JamTrackTrack", foreign_key: 'stem_id'
belongs_to :jam_track, :class_name => "JamRuby::JamTrack"
# one of mixdown or stem need to be specified. could validate this?
validates :user, presence:true
validates :remote_ip, presence: true
#validates :paid, presence: true
validates :jam_track, presence: :true
def self.create(user, remote_ip, target, owned)
dt = DownloadTracker.new
dt.user = user
dt.remote_ip = remote_ip
dt.paid = owned
if target.is_a?(JamTrackTrack)
dt.jam_track_id = target.jam_track_id
elsif target.is_a?(JamTrackMixdownPackage)
dt.jam_track_id = target.jam_track_mixdown.jam_track_id
end
if !dt.save
@@log.error("unable to create Download Tracker: #{dt.errors.inspect}")
end
dt
end
def self.check(user, remote_ip, target, owned)
return unless APP_CONFIG.guard_against_browser_fraud
create(user, remote_ip, target, owned)
# let's check the following
alert_freebies_snarfer(remote_ip)
alert_user_sharer(user)
end
# somebody who has shared account info with a large number of people
# high number of downloads of the same user from different IP addresses that were or were not paid for
# raw query created by this code:
# SELECT distinct(user_id), count(user_id) FROM "download_trackers" WHERE (created_at > NOW() - '30 days'::interval) GROUP BY user_id HAVING count(distinct(remote_ip)) >= 2
def self.check_user_sharer(max, user_id = nil)
query = DownloadTracker.select('distinct(user_id), count(user_id)')
query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
if !user_id.nil?
query = query.where('user_id = ?', user_id)
end
query.group(:user_id).having("count(distinct(remote_ip)) >= #{max}")
end
# somebody who has figured out how to bypass cookie based method of identity checking, and is getting lots of free JamTracks
# high number of downloads of different jam tracks from different users for the same IP address that weren't paid for
# raw query created by this code:
# SELECT distinct(remote_ip), count(remote_ip) FROM "download_trackers" WHERE (paid = false) AND (created_at > NOW() - '30 days'::interval) GROUP BY remote_ip HAVING count(distinct(jam_track_id)) >= 2
def self.check_freebie_snarfer(max, remote_ip = nil)
query = DownloadTracker.select('distinct(remote_ip), count(remote_ip)').where("paid = false")
query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
if !remote_ip.nil?
query = query.where('remote_ip = ?', remote_ip)
end
query.group(:remote_ip).having("count(distinct(jam_track_id)) >= #{max}")
end
def self.alert_user_sharer(user)
violation = check_user_sharer(APP_CONFIG.max_user_ip_address, user.id).first
if violation
body = "User has downloaded from too many IP addresses #{user.id}\n"
body << "Download Count: #{violation['count']}\n"
body << "User URL #{user.admin_url}\n"
body << "Add to blacklist: #{UserBlacklist.admin_url}"
AdminMailer.alerts({
subject:"Account IP Access Violation. USER: #{user.email}",
body:body
}).deliver
end
end
def self.alert_freebies_snarfer(remote_ip)
violation = check_freebie_snarfer(APP_CONFIG.max_multiple_users_same_ip, remote_ip).first
if violation
body = "IP Address: #{remote_ip}\n"
body << "Download Count: #{violation['count']}\n"
body << "Add to blacklist: #{IpBlacklist.admin_url}"
body << "Check Activity: #{IpBlacklist.admin_activity_url(remote_ip)}"
AdminMailer.alerts({
subject:"Single IP Access Violation. IP:#{remote_ip}",
body:body
}).deliver
end
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/download_trackers/" + id
end
def to_s
if stem?
"stem:#{stem} #{remote_ip} #{user}"
else
"mixdown:#{mixdown} #{remote_ip} #{user}"
end
end
end
end

View File

@ -25,5 +25,13 @@ module JamRuby
def to_s
description
end
def self.jam_track_list
sql = "SELECT DISTINCT genre_id FROM genres_jam_tracks WHERE genre_id IS NOT NULL"
Genre.select("DISTINCT(genres.id), genres.*")
.where("genres.id IN (#{sql})")
.order('genres.description ASC, genres.id')
end
end
end

View File

@ -2,7 +2,10 @@ module JamRuby
class GenreJamTrack < ActiveRecord::Base
self.table_name = 'genres_jam_tracks'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :genre, class_name: 'JamRuby::Genre'
attr_accessible :jam_track_id, :genre_id
belongs_to :jam_track, class_name: 'JamRuby::JamTrack', inverse_of: :genres_jam_tracks
belongs_to :genre, class_name: 'JamRuby::Genre', inverse_of: :genres_jam_tracks
end
end

View File

@ -0,0 +1,36 @@
# represents the gift card you hold in your hand
module JamRuby
class GiftCard < ActiveRecord::Base
@@log = Logging.logger[GiftCard]
JAM_TRACKS_5 = 'jam_tracks_5'
JAM_TRACKS_10 = 'jam_tracks_10'
CARD_TYPES =
[
JAM_TRACKS_5,
JAM_TRACKS_10
]
belongs_to :user, class_name: "JamRuby::User"
validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
validates :code, presence: true, uniqueness: true
after_save :check_gifted
def check_gifted
if user && user_id_changed?
if card_type == JAM_TRACKS_5
user.gifted_jamtracks += 5
elsif card_type == JAM_TRACKS_10
user.gifted_jamtracks += 10
else
raise "unknown card type #{card_type}"
end
user.save!
end
end
end
end

View File

@ -0,0 +1,17 @@
# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard)
module JamRuby
class GiftCardPurchase < ActiveRecord::Base
@@log = Logging.logger[GiftCardPurchase]
attr_accessible :user, :gift_card_type
def name
gift_card_type.sale_display
end
# who purchased the card?
belongs_to :user, class_name: "JamRuby::User"
belongs_to :gift_card_type, class_name: "JamRuby::GiftCardType"
end
end

View File

@ -0,0 +1,70 @@
# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard)
module JamRuby
class GiftCardType < ActiveRecord::Base
@@log = Logging.logger[GiftCardType]
PRODUCT_TYPE = 'GiftCardType'
JAM_TRACKS_5 = 'jam_tracks_5'
JAM_TRACKS_10 = 'jam_tracks_10'
CARD_TYPES =
[
JAM_TRACKS_5,
JAM_TRACKS_10
]
validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
def self.jam_track_5
GiftCardType.find(JAM_TRACKS_5)
end
def self.jam_track_10
GiftCardType.find(JAM_TRACKS_10)
end
def name
sale_display
end
def price
if card_type == JAM_TRACKS_5
10.00
elsif card_type == JAM_TRACKS_10
20.00
else
raise "unknown card type #{card_type}"
end
end
def sale_display
if card_type == JAM_TRACKS_5
'JamTracks Gift Card (5)'
elsif card_type == JAM_TRACKS_10
'JamTracks Gift Card (10)'
else
raise "unknown card type #{card_type}"
end
end
def plan_code
if card_type == JAM_TRACKS_5
"jamtrack-giftcard-5"
elsif card_type == JAM_TRACKS_10
"jamtrack-giftcard-10"
else
raise "unknown card type #{card_type}"
end
end
def sales_region
'Worldwide'
end
def to_s
sale_display
end
end
end

View File

@ -50,6 +50,12 @@ module JamRuby
return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC')
end
def self.jam_track_list
sql = "SELECT DISTINCT instrument_id FROM jam_track_tracks WHERE instrument_id IS NOT NULL"
Instrument.where("instruments.id IN (#{sql})")
.order('instruments.description ASC')
end
def icon_name
MAP_ICON_NAME[self.id]
end

View File

@ -0,0 +1,30 @@
module JamRuby
class IpBlacklist < ActiveRecord::Base
attr_accessible :remote_ip, :notes, as: :admin
@@log = Logging.logger[IpBlacklist]
validates :remote_ip, presence:true, uniqueness:true
def self.listed(remote_ip)
IpBlacklist.count(:conditions => "remote_ip = '#{remote_ip}'") == 1
end
def self.admin_url
APP_CONFIG.admin_root_url + "/admin/ip_blacklists/"
end
def self.admin_activity_url(remote_ip)
APP_CONFIG.admin_root_url + "/admin/download_trackers?q[remote_ip_equals]=#{URI.escape(remote_ip)}&commit=Filter&order=id_desc"
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + id
end
def to_s
remote_ip
end
end
end

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
module JamRuby
class JamTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin
@ -18,7 +19,7 @@ module JamRuby
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
:jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration,
:server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin
:server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin
validates :name, presence: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
@ -52,7 +53,7 @@ module JamRuby
belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks
has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id"
has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id", inverse_of: :jam_track
has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre
has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC'
@ -85,6 +86,10 @@ module JamRuby
after_save :sync_reproduction_royalty
after_save :sync_onboarding_exceptions
def increment_version!
self.version = version.to_i + 1
save!
end
def sync_reproduction_royalty
@ -154,6 +159,9 @@ module JamRuby
true
end
def sale_display
"JamTrack: " + name
end
def duplicate_positions?
counter = {}
jam_track_tracks.each do |track|
@ -255,12 +263,12 @@ module JamRuby
limit = options[:limit]
limit ||= 20
limit = limit.to_i
per_page = limit
else
limit = per_page
end
start = (page -1 )* per_page
limit = per_page
else
limit = options[:limit]
limit ||= 20
@ -337,9 +345,18 @@ module JamRuby
query = query.where('genre_id = ? ', options[:genre])
end
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank?
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type = 'Track'") unless options[:instrument].blank?
query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
# FIXME: n+1 queries for rights and genres
# query = query.includes([{ jam_track_tracks: :instrument },
# :jam_track_tap_ins,
# :jam_track_rights,
# :genres])
# { genres_jam_tracks: :genre },
# query = query.includes([{ jam_track_tracks: :instrument },
# { genres_jam_tracks: :genre }])
count = query.total_entries
if count == 0
@ -421,13 +438,43 @@ module JamRuby
end
end
def click_track_file
JamTrackFile.where(jam_track_id: self.id).where(file_type: 'ClickWav').first
end
def click_track
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Click').first
end
def has_count_in?
has_count_in = false
if jmep_json
jmep = JSON.parse(jmep_json)
if jmep["Events"]
events = jmep["Events"]
metronome = nil
events.each do |event|
if event.has_key?("metronome")
metronome = event["metronome"]
break
end
end
if metronome
has_count_in = true
end
end
end
has_count_in
end
def master_track
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first
end
def stem_tracks
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track')
JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'")
end
def can_download?(user)
@ -438,6 +485,10 @@ module JamRuby
jam_track_rights.where("user_id=?", user).first
end
def mixdowns_for_user(user)
JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id)
end
def short_plan_code
prefix = 'jamtrack-'
plan_code[prefix.length..-1]
@ -450,7 +501,80 @@ module JamRuby
def generate_slug
self.slug = sluggarize(original_artist) + '-' + sluggarize(name)
puts "Self.slug #{self.slug}"
if licensor && licensor.slug.present?
#raise "no slug on licensor #{licensor.id}" if licensor.slug.nil?
self.slug << "-" + licensor.slug
end
end
def gen_plan_code
# remove all non-alphanumeric chars from artist as well as name
artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase
name_code = name.gsub(/[^0-9a-z]/i, '').downcase
self.plan_code = "jamtrack-#{artist_code[0...20]}-#{name_code}"
if licensor && licensor.slug
raise "no slug on licensor #{licensor.id}" if licensor.slug.nil?
self.plan_code << "-" + licensor.slug
end
self.plan_code = self.plan_code[0...50] # make sure it's a max of 50 long
end
def to_s
"#{self.name} (#{self.original_artist})"
end
def self.latestPurchase(user_id)
jtx_created = JamTrackRight
.select('created_at')
.where(user_id: user_id)
.order('created_at DESC')
.limit(1)
jtx_created.first.created_at.to_i
end
attr_accessor :preview_generate_error
before_save :jmep_json_generate
validate :jmep_text_validate
def jmep_text_validate
begin
JmepManager.execute(self.jmep_text)
rescue ArgumentError => err
errors.add(:jmep_text, err.to_s)
end
end
def jmep_json_generate
self.licensor_id = nil if self.licensor_id == ''
self.jmep_json = nil if self.jmep_json == ''
self.time_signature = nil if self.time_signature == ''
begin
self[:jmep_json] = JmepManager.execute(self.jmep_text)
rescue ArgumentError => err
#errors.add(:jmep_text, err.to_s)
end
end
# used in mobile simulate purchase
def self.forsale(user)
sql =<<SQL
SELECT jt.* FROM jam_tracks jt
WHERE jt.id NOT IN (
SELECT jt.id
FROM jam_tracks jt
JOIN jam_track_rights AS jtr ON jtr.jam_track_id = jt.id
WHERE jtr.user_id = '#{user.id}'
)
LIMIT 1
SQL
self.find_by_sql(sql).first
end
end

View File

@ -27,9 +27,18 @@ module JamRuby
"jam_track_files"
end
def licensor_suffix
suffix = ''
if jam_track.licensor
raise "no licensor name" if jam_track.licensor.name.nil?
suffix = " - #{jam_track.licensor.name}"
end
suffix
end
# create name of the file
def filename(original_name)
"#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
"#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}/#{original_name}"
end
def manually_uploaded_filename

View File

@ -4,7 +4,7 @@ module JamRuby
table_name = 'jam_track_licensors'
attr_accessible :name, :description, :attention, :address_line_1, :address_line_2,
:city, :state, :zip_code, :contact, :email, :phone, as: :admin
:city, :state, :zip_code, :contact, :email, :phone, :slug, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :description, length: {maximum: 1000}

View File

@ -0,0 +1,137 @@
module JamRuby
# describes what users have rights to which tracks
class JamTrackMixdown < ActiveRecord::Base
@@log = Logging.logger[JamTrackMixdown]
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
has_many :jam_track_mixdown_packages, class_name: "JamRuby::JamTrackMixdownPackage", order: 'created_at DESC'
has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_mixdown_id', inverse_of: :last_mixdown
validates :name, presence: true, length: {maximum: 100}
validates :description, length: {maximum: 1000}
validates :user, presence: true
validates :jam_track, presence: true
validates :settings, presence: true
validates_uniqueness_of :name, scope: [:user_id, :jam_track_id]
validate :verify_settings
validate :verify_max_mixdowns
def self.index(params, user)
jam_track_id = params[:id]
limit = 20
query = JamTrackMixdown.where('jam_track_id = ?', jam_track_id).where('user_id = ?', user.id).order('created_at').paginate(page: 1, per_page: limit)
count = query.total_entries
if count == 0
[query, nil, count]
elsif query.length < limit
[query, nil, count]
else
[query, start + limit, count]
end
end
def verify_max_mixdowns
if self.jam_track && self.user && self.jam_track.mixdowns_for_user(self.user).length >= 5
errors.add(:jam_track, 'allowed 5 mixes')
end
end
def verify_settings
# the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do
parsed = JSON.parse(self.settings)
specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0
tweaked = false
all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track'
if parsed["speed"]
tweaked = true
end
if parsed["pitch"]
tweaked = true
end
if parsed["tracks"]
parsed["tracks"].each do |track|
if track["mute"]
tweaked = true
end
if track["vol"] && track["vol"] != 0
tweaked = true
end
if track["pan"] && track["pan"] != 0
tweaked = true
end
# there is at least one track with volume specified.
if !track["mute"] && track["vol"] != 0
all_quiet = false
end
end
end
if parsed["count-in"]
all_quiet = false
tweaked = true
end
if all_quiet
errors.add(:settings, 'are all muted')
end
if !tweaked && !parsed['full']
errors.add(:settings, 'have nothing specified')
end
if parsed["speed"] && !parsed["speed"].is_a?(Integer)
errors.add(:settings, 'has non-integer speed')
end
if parsed["pitch"] && !parsed["pitch"].is_a?(Integer)
errors.add(:settings, 'has non-integer pitch')
end
end
def self.create(name, description, user, jam_track, settings)
mixdown = JamTrackMixdown.new
mixdown.name = name
mixdown.description = description
mixdown.user = user
mixdown.jam_track = jam_track
mixdown.settings = settings.to_json # RAILS 4 CAN REMOVE .to_json
mixdown.save
mixdown
end
def will_pitch_shift?
self.settings["pitch"] != 0 || self.settings["speed"] != 0
end
def self.mixdownChecksum(user_id, jam_track_id)
dates = self
.select('created_at')
.where(user_id: user_id, jam_track_id: jam_track_id)
.order(:id)
dates = dates.map do |date|
date.created_at.to_i.to_s
end.join('')
Digest::MD5.hexdigest(dates)
end
end
end

View File

@ -0,0 +1,253 @@
module JamRuby
# describes what users have rights to which tracks
class JamTrackMixdownPackage < ActiveRecord::Base
include JamRuby::S3ManagerMixin
@@log = Logging.logger[JamTrackMixdownPackage]
# these are used as extensions for the files stored in s3
FILE_TYPE_MP3 = 'mp3'
FILE_TYPE_OGG = 'ogg'
FILE_TYPE_AAC = 'aac'
FILE_TYPES = [FILE_TYPE_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC]
SAMPLE_RATE_44 = 44
SAMPLE_RATE_48 = 48
SAMPLE_RATES = [SAMPLE_RATE_44, SAMPLE_RATE_48]
ENCRYPT_TYPE_JKZ = 'jkz'
ENCRYPT_TYPES = [ENCRYPT_TYPE_JKZ, nil]
default_scope { order('created_at desc') }
belongs_to :jam_track_mixdown, class_name: "JamRuby::JamTrackMixdown", dependent: :destroy
validates :jam_track_mixdown, presence: true
validates :file_type, inclusion: {in: FILE_TYPES}
validates :sample_rate, inclusion: {in: SAMPLE_RATES}
validates :encrypt_type, inclusion: {in: ENCRYPT_TYPES}
validates_uniqueness_of :file_type, scope: [:sample_rate, :encrypt_type, :jam_track_mixdown_id]
validates :signing, inclusion: {in: [true, false]}
validates :signed, inclusion: {in: [true, false]}
validate :verify_download_count
before_destroy :delete_s3_files
after_save :after_save
MAX_JAM_TRACK_DOWNLOADS = 1000
def self.estimated_queue_time
jam_track_signing_count = JamTrackRight.where(queued: true).count
mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).first
total_mixdowns = mixdowns['queue_count'].to_i
slow_mixdowns = mixdowns['speed_pitch_count'].to_i
fast_mixdowns = total_mixdowns - slow_mixdowns
guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns
Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns})
guess
end
def after_save
# try to catch major transitions:
# if just queue time changes, start time changes, or signed time changes, send out a notice
if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was
SubscriptionMessage.mixdown_signing_job_change(self)
end
end
def self.create(mixdown, file_type, sample_rate, encrypt_type)
package = JamTrackMixdownPackage.new
package.speed_pitched = mixdown.will_pitch_shift?
package.jam_track_mixdown = mixdown
package.file_type = file_type
package.sample_rate = sample_rate
package.signed = false
package.signing = false
package.encrypt_type = encrypt_type
package.save
package
end
def verify_download_count
if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin
errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}")
end
end
def is_pitch_speed_shifted?
mix_settings = JSON.parse(self.settings)
mix_settings["speed"] || mix_settings["pitch"]
end
def finish_errored(error_reason, error_detail)
self.last_errored_at = Time.now
self.last_signed_at = Time.now
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
self.should_retry = self.error_count < 5
self.signing = false
self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts
if save
Notification.send_mixdown_sign_failed(self)
else
raise "Error sending notification #{self.errors}"
end
end
def finish_sign(url, private_key, length, md5)
self.url = url
self.private_key = private_key
self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts
self.downloaded_since_sign = false
self.last_signed_at = Time.now
self.length = length
self.md5 = md5
self.signed = true
self.signing = false
self.error_count = 0
self.error_reason = nil
self.error_detail = nil
self.should_retry = false
save!
end
def store_dir
"jam_track_mixdowns/#{created_at.strftime('%m-%d-%Y')}/#{self.jam_track_mixdown.user_id}"
end
def filename
if encrypt_type
"#{id}.#{encrypt_type}"
else
"#{id}.#{file_type}"
end
end
# creates a short-lived URL that has access to the object.
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120, content_type = nil, response_content_disposition = nil)
options = {:expires => expiration_time, :secure => true}
options[:response_content_type] = content_type if content_type
options[:response_content_disposition] = response_content_disposition if response_content_disposition
s3_manager.sign_url(self['url'], options)
end
def enqueue
begin
self.signing_queued_at = Time.now
self.signing_started_at = nil
self.last_signed_at = nil
self.queued = true
self.save
queue_time = JamTrackMixdownPackage.estimated_queue_time
# is_pitch_speed_shifted?
Resque.enqueue(JamTrackMixdownPackager, self.id)
return queue_time
rescue Exception => e
puts "e: #{e}"
# implies redis is down. we don't update started_at by bailing out here
false
end
end
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
def enqueue_if_needed
state = signing_state
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
else
return enqueue
end
end
def ready?
self.signed && self.url.present?
end
# returns easy to digest state field
# SIGNED - the package is ready to be downloaded
# ERROR - the package was built unsuccessfully
# SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung
# SIGNING - the package is currently signing
# QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed
# QUEUED - the package is queued to sign
# QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued
def signing_state
state = nil
if signed
state = 'SIGNED'
elsif signing_started_at && signing
# the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long.
if Time.now - signing_started_at > APP_CONFIG.signing_job_signing_max_time
state = 'SIGNING_TIMEOUT'
elsif Time.now - last_step_at > APP_CONFIG.mixdown_step_max_time
state = 'SIGNING_TIMEOUT'
else
state = 'SIGNING'
end
elsif signing_queued_at
if Time.now - signing_queued_at > APP_CONFIG.mixdown_job_queue_max_time
state = 'QUEUED_TIMEOUT'
else
state = 'QUEUED'
end
elsif error_count > 0
state = 'ERROR'
else
if Time.now - created_at > 60 # it should not take more than a minute to get QUIET out
state = 'QUIET_TIMEOUT'
else
state = 'QUIET' # needs to be poked to go build
end
end
state
end
def signed?
signed
end
def update_download_count(count=1)
self.download_count = self.download_count + count
self.last_downloaded_at = Time.now
if self.signed
self.downloaded_since_sign = true
end
end
def self.stats
stats = {}
result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count')
stats['count'] = result[0]['total'].to_i
stats['signing_count'] = result[0]['signing_count'].to_i
stats
end
def delete_s3_files
s3_manager.delete(self.url) if self.url && s3_manager.exists?(self.url)
end
end
end

View File

@ -11,7 +11,10 @@ module JamRuby
attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right
belongs_to :last_stem, class_name: 'JamRuby::JamTrackTrack', foreign_key: 'last_stem_id', inverse_of: :jam_track_right
validates :version, presence: true
validates :user, presence: true
validates :jam_track, presence: true
validates :is_test_purchase, inclusion: {in: [true, false]}
@ -25,9 +28,16 @@ module JamRuby
mount_uploader :url_48, JamTrackRightUploader
mount_uploader :url_44, JamTrackRightUploader
before_destroy :delete_s3_files
before_create :create_private_keys
MAX_JAM_TRACK_DOWNLOADS = 1000
def create_private_keys
rsa_key = OpenSSL::PKey::RSA.new(1024)
key = rsa_key.to_pem()
self.private_key_44 = key
self.private_key_48 = key
end
def after_save
# try to catch major transitions:
@ -58,6 +68,7 @@ module JamRuby
def finish_errored(error_reason, error_detail, sample_rate)
self.last_signed_at = Time.now
self.queued = false
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
@ -77,6 +88,7 @@ module JamRuby
def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now
self.queued = false
if bitrate==48
self.length_48 = length
self.md5_48 = md5
@ -99,10 +111,10 @@ module JamRuby
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120, bitrate=48, secure=true)
field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
end
def sign_url(expiration_time = 120, bitrate=48, secure=true)
field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
end
def delete_s3_files
remove_url_48!
@ -112,7 +124,7 @@ module JamRuby
def enqueue(sample_rate=48)
begin
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil, :queued => true)
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e
@ -122,8 +134,33 @@ module JamRuby
end
end
def cleanup_old_package!
if self.jam_track.version != self.version
delete_s3_files
self[:url_48] = nil
self[:url_44] = nil
self.signing_queued_at = nil
self.signing_started_at_48 = nil
self.signing_started_at_44 = nil
self.last_signed_at = nil
self.current_packaging_step = nil
self.packaging_steps = nil
self.should_retry = false
self.signing_44 = false
self.signing_48 = false
self.signed_44 = false
self.signed_48 = false
self.queued = false
self.version = self.jam_track.version
self.save!
end
end
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
def enqueue_if_needed(sample_rate=48)
# delete any package that's out dated
cleanup_old_package!
state = signing_state(sample_rate)
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
@ -137,9 +174,9 @@ module JamRuby
# @return true if signed && file exists for the sample_rate specifed:
def ready?(sample_rate=48)
if sample_rate==48
self.signed_48 && self.url_48.present? && self.url_48.file.exists?
self.signed_48 && self.url_48.present? && self.url_48.file.exists? && self.version == self.jam_track.version
else
self.signed_44 && self.url_44.present? && self.url_44.file.exists?
self.signed_44 && self.url_44.present? && self.url_44.file.exists? && self.version == self.jam_track.version
end
end

View File

@ -0,0 +1,168 @@
module JamRuby
class JamTrackSearch < BaseSearch
cattr_accessor :jschema, :search_meta
attr_accessor :user_counters
KEY_QUERY = 'query'
KEY_SEARCH_STR = 'search_str'
KEY_RESULT_TYPES = 'result_types'
KEY_SONGS = 'songs'
KEY_ARTISTS = 'artists'
KEY_RESULTS = 'results'
KEY_RESULT_SETS = 'result_sets'
KEY_PAGE_NUM = 'page_num'
KEY_TOTAL_COUNT = 'total_count'
KEY_PAGE_COUNT = 'page_count'
KEY_PER_PAGE = 'per_page'
PER_PAGE = 'development'==Rails.env ? 8 : 20
KEY_GENRES = 'genres'
KEY_INSTRUMENTS = 'instruments'
KEY_LANGUAGE = 'language'
KEY_ORIGINAL_ARTIST = 'original_artist'
def self.json_schema
return @@jschema ||= {
KEY_QUERY => {
KEY_SEARCH_STR => '',
KEY_INSTRUMENTS => [],
KEY_GENRES => [],
KEY_LANGUAGE => '',
KEY_ORIGINAL_ARTIST => '',
KEY_RESULT_TYPES => [],
KEY_PAGE_NUM => 1,
KEY_PER_PAGE => PER_PAGE,
},
KEY_RESULT_SETS => {
KEY_SONGS => {
KEY_RESULTS => [],
KEY_PAGE_NUM => 1,
KEY_TOTAL_COUNT => 0,
KEY_PAGE_COUNT => 0,
},
KEY_ARTISTS => {
KEY_RESULTS => [],
KEY_PAGE_NUM => 1,
KEY_TOTAL_COUNT => 0,
KEY_PAGE_COUNT => 0,
},
},
}
end
def self.search_target_class
JamTrack
end
def do_search(query)
rel = JamTrack.unscoped
unless (gids = query[KEY_GENRES]).blank?
allgids = self.class.genre_ids
gids = gids.select { |gg| allgids.has_key?(gg) }
unless gids.blank?
sqlstr = "'#{gids.join("','")}'"
rel = rel.joins(:genres_jam_tracks)
rel = rel.where("genres_jam_tracks.genre_id IN (#{sqlstr})")
end
end
unless (instruments = query[KEY_INSTRUMENTS]).blank?
instrids = self.class.instrument_ids
instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) }
unless instruments.blank?
sqlstr = "'#{instruments.join("','")}'"
rel = rel.joins(:jam_track_tracks)
rel = rel.where("jam_track_tracks.instrument_id IN (#{sqlstr})")
rel = rel.where("jam_track_tracks.track_type = 'Track'")
end
end
unless (artist_name = query[KEY_ORIGINAL_ARTIST]).blank?
rel = rel.where(original_artist: artist_name)
end
rel
end
def search_results_page(query=nil)
filter = {
KEY_QUERY => query,
}
result_types = query[KEY_RESULT_TYPES]
if result_types
has_songs, has_artists = result_types.index(KEY_SONGS), result_types.index(KEY_ARTISTS)
else
has_songs, has_artists = true, true
end
result_sets = filter[KEY_RESULT_SETS] = self.class.json_schema[KEY_RESULT_SETS].clone
if has_songs
rel = do_search(query)
unless (val = query[KEY_SEARCH_STR]).blank?
tsquery = Search.create_tsquery(val)
rel = rel.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) if tsquery
end
rel = rel.order(:name).includes(:genres)
pgnum = [query[KEY_PAGE_NUM].to_i, 1].max
rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE])
results = rel.all.collect do |jt|
{
'id' => jt.id,
'name' => jt.name,
'artist' => jt.original_artist,
'genre' => jt.genres.map(&:description).join(', '),
'plan_code' => jt.plan_code,
'year' => jt.year
}
end
result_sets[KEY_SONGS] = {
KEY_RESULTS => results,
KEY_PAGE_NUM => pgnum,
KEY_TOTAL_COUNT => rel.total_entries,
KEY_PAGE_COUNT => rel.total_pages,
}
end
if has_artists
rel = do_search(query)
counter = rel.select("DISTINCT(jam_tracks.original_artist)")
rel = rel.select("DISTINCT ON(jam_tracks.original_artist) jam_tracks.id, jam_tracks.original_artist")
unless (val = query[KEY_SEARCH_STR]).blank?
rel = rel.where("original_artist ILIKE ?","%#{val}%")
counter = counter.where("original_artist ILIKE ?","%#{val}%")
end
rel = rel.order(:original_artist)
pgnum = [query[KEY_PAGE_NUM].to_i, 1].max
rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE])
results = rel.all.collect do |jt|
{ 'id' => jt.id, 'artist' => jt.original_artist }
end
artist_count = counter.count
result_sets[KEY_ARTISTS] = {
KEY_RESULTS => results,
KEY_PAGE_NUM => pgnum,
KEY_TOTAL_COUNT => artist_count,
KEY_PAGE_COUNT => (artist_count / query[KEY_PER_PAGE].to_f).ceil,
}
end
filter
end
def self.all_languages
JamTrack.select("SELECT DISTINCT(language)").order(:language).collect do |lang|
{ description: ISO_639.find_by_code(lang), id: lang }
end
end
end
end

View File

@ -6,7 +6,7 @@ module JamRuby
include JamRuby::S3PublicManagerMixin
# there should only be one Master per JamTrack, but there can be N Track per JamTrack
TRACK_TYPE = %w{Track Master}
TRACK_TYPE = %w{Track Master Click}
@@log = Logging.logger[JamTrackTrack]
@ -19,7 +19,7 @@ module JamRuby
attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin
attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin
attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error
attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error, :wav_file, :tmp_duration, :skip_inst_part_uniq
before_destroy :delete_s3_files
@ -27,29 +27,44 @@ module JamRuby
validates :part, length: {maximum: 35}
validates :track_type, inclusion: {in: TRACK_TYPE }
validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true
validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id]
validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id], unless: :skip_inst_part_uniq
# validates :jam_track, presence: true
belongs_to :instrument, class_name: "JamRuby::Instrument"
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy
has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_stem_id', inverse_of: :last_stem
# create storage directory that will house this jam_track, as well as
def store_dir
"jam_track_tracks"
end
def licensor_suffix
suffix = ''
if jam_track.licensor
raise "no licensor name" if jam_track.licensor.name.nil?
suffix = " - #{jam_track.licensor.name}"
end
suffix
end
# create name of the file
def filename(original_name)
"#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
"#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}/#{original_name}"
end
# create name of the preview file.
# md5-'ed because we cache forever
def preview_filename(md5, ext='ogg')
original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}"
"jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
"#{preview_directory}/#{original_name}"
end
def preview_directory
"jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}"
end
def has_preview?
@ -58,7 +73,16 @@ module JamRuby
# generates a URL that points to a public version of the preview
def preview_public_url(media_type='ogg')
url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url]
case media_type
when 'ogg'
url = self[:preview_url]
when 'mp3'
url = self[:preview_mp3_url]
when 'aac'
url = self[:preview_aac_url]
else
raise "unknown media_type #{media_type}"
end
if url
s3_public_manager.public_url(url,{ :secure => true})
else
@ -66,6 +90,18 @@ module JamRuby
end
end
def display_name
if track_type == 'Master'
'Master Mix'
else
display_part = ''
if part
display_part = "-(#{part})"
end
"#{instrument.description}#{display_part}"
end
end
def manually_uploaded_filename(mounted_as)
if track_type == 'Master'
filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg")
@ -89,6 +125,18 @@ module JamRuby
def sign_url(expiration_time = 120, sample_rate=48)
s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true})
end
def web_download_sign_url(expiration_time = 120, type='mp3', content_type = nil, response_content_disposition = nil)
options = {:expires => expiration_time, :secure => true}
options[:response_content_type] = content_type if content_type
options[:response_content_disposition] = response_content_disposition if response_content_disposition
url_field = self['url_' + type + '_48']
url_field = self['url_48'] if type == 'ogg' # ogg has different column format in database
s3_manager.sign_url(url_field, options)
end
def can_download?(user)
# I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download?
@ -157,6 +205,7 @@ module JamRuby
uuid = SecureRandom.uuid
output = File.join(tmp_dir, "#{uuid}.ogg")
output_mp3 = File.join(tmp_dir, "#{uuid}.mp3")
output_aac = File.join(tmp_dir, "#{uuid}.aac")
start = self.preview_start_time.to_f / 1000
stop = start + 20
@ -176,7 +225,6 @@ module JamRuby
# now create mp3 off of ogg preview
convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\""
@@log.debug("converting to mp3 using: " + convert_mp3_cmd)
convert_output = `#{convert_mp3_cmd}`
@ -187,35 +235,55 @@ module JamRuby
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute mp3 convert command #{convert_output}"
else
ogg_digest = ::Digest::MD5.file(output)
mp3_digest = ::Digest::MD5.file(output_mp3)
self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
@@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -c:a libfdk_aac -b:a 192k \"#{output_aac}\""
@@log.debug("converting to aac using: " + convert_aac_cmd)
self.skip_uploader = true
convert_output = `#{convert_aac_cmd}`
original_ogg_preview_url = self["preview_url"]
original_mp3_preview_url = self["preview_mp3_url"]
result_code = $?.to_i
# and finally update the JamTrackTrack with the new info
self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
self["preview_length"] = File.new(output).size
# and finally update the JamTrackTrack with the new info
self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
self["preview_mp3_length"] = File.new(output_mp3).size
self.save!
if result_code != 0
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute aac convert command #{convert_output}"
else
# if all that worked, now delete old previews, if present
begin
s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
ogg_digest = ::Digest::MD5.file(output)
mp3_digest = ::Digest::MD5.file(output_mp3)
aac_digest = ::Digest::MD5.file(output_aac)
self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
self["preview_aac_md5"] = aac_md5 = mp3_digest.hexdigest
@@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
@@log.debug("uploading aac preview to #{self.preview_filename('aac')}")
s3_public_manager.upload(self.preview_filename(aac_md5, 'aac'), output_aac, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
self.skip_uploader = true
original_ogg_preview_url = self["preview_url"]
original_mp3_preview_url = self["preview_mp3_url"]
original_aac_preview_url = self["preview_aac_url"]
self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
self["preview_length"] = File.new(output).size
self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
self["preview_mp3_length"] = File.new(output_mp3).size
self["preview_aac_url"] = self.preview_filename(aac_md5, 'aac')
self["preview_aac_length"] = File.new(output_aac).size
self.save!
# if all that worked, now delete old previews, if present
begin
s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
s3_public_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
end
end
end

View File

@ -36,6 +36,8 @@ module JamRuby
belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id
belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions
has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all
has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id"
has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id"
@ -116,6 +118,7 @@ module JamRuby
new_session.open_rsvps = self.open_rsvps
new_session.is_unstructured_rsvp = self.is_unstructured_rsvp
new_session.legal_terms = true
new_session.session_controller = self.session_controller
# copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots
RsvpSlot.find_each(:conditions => "music_session_id = '#{self.id}'") do |slot|
@ -255,6 +258,30 @@ module JamRuby
end
end
def set_session_controller(current_user, user)
# only allow update of session controller by the creator or the currently marked user
should_tick = false
if current_user != creator && current_user != self.session_controller
return should_tick
end
if active_music_session
if user
if active_music_session.users.exists?(user)
self.session_controller = user
should_tick = save
end
else
self.session_controller = nil
should_tick = save
end
end
should_tick
end
def self.index(current_user, user_id, band_id = nil, genre = nil)
hide_private = false
if current_user.id != user_id
@ -343,6 +370,7 @@ module JamRuby
ms.legal_terms = true
ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps]
ms.creator = user
ms.session_controller = user
ms.create_type = options[:create_type]
ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp]
ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone]

View File

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

View File

@ -14,6 +14,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id"
belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id"
belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id"
validates :target_user, :presence => true
validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message?
@ -1255,7 +1256,7 @@ module JamRuby
def send_jam_track_sign_complete(jam_track_right)
notification = Notification.new
notification.jam_track_right_id = jam_track_right.id
notification.jam_track_mixdown_package = jam_track_right.id
notification.description = NotificationTypes::JAM_TRACK_SIGN_COMPLETE
notification.target_user_id = jam_track_right.user_id
notification.save!
@ -1265,6 +1266,30 @@ module JamRuby
#@@mq_router.publish_to_all_clients(msg)
end
def send_mixdown_sign_failed(jam_track_mixdown_package)
notification = Notification.new
notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id
notification.description = NotificationTypes::MIXDOWN_SIGN_FAILED
notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id
notification.save!
msg = @@message_factory.mixdown_sign_failed(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id)
@@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg)
end
def send_mixdown_sign_complete(jam_track_mixdown_package)
notification = Notification.new
notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id
notification.description = NotificationTypes::MIXDOWN_SIGN_COMPLETE
notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id
notification.save!
msg = @@message_factory.mixdown_sign_complete(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id)
@@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg)
end
def send_client_update(product, version, uri, size)
msg = @@message_factory.client_update( product, version, uri, size)

View File

@ -205,7 +205,7 @@ module JamRuby
end
# Start recording a session.
def self.start(music_session, owner)
def self.start(music_session, owner, record_video: false)
recording = nil
# Use a transaction and lock to avoid races.
music_session.with_lock do
@ -213,6 +213,7 @@ module JamRuby
recording.music_session = music_session
recording.owner = owner
recording.band = music_session.band
recording.video = record_video
if recording.save
GoogleAnalyticsEvent.report_band_recording(recording.band)
@ -700,6 +701,10 @@ module JamRuby
self.save(:validate => false)
end
def add_video_data(data)
Recording.where(id: self.id).update_all(external_video_id: data[:video_id])
end
def add_timeline(timeline)
global = timeline["global"]
raise JamArgumentError, "global must be specified" unless global

View File

@ -92,53 +92,15 @@ module JamRuby
transaction.save!
# now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
if sale && sale.is_jam_track_sale?
if sale.sale_line_items.length == 1
if sale.recurly_total_in_cents == transaction.amount_in_cents
line_item = sale.sale_line_items[0]
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
if jam_track_right
line_item.affiliate_refunded = true
line_item.affiliate_refunded_at = Time.now
line_item.save!
if sale
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
jam_track_right.destroy
# associate which JamTrack we assume this is related to in this one success case
transaction.jam_track = jam_track
transaction.save!
AdminMailer.recurly_alerts(transaction.user, {
subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
body: "A #{transaction.transaction_type} event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
}).deliver
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
body: "We received a #{transaction.transaction_type} notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales",

View File

@ -69,27 +69,12 @@ module JamRuby
}
end
def self.preview_invoice(current_user, shopping_carts)
line_items = {jam_tracks: []}
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
def self.ios_purchase(current_user, jam_track, receipt)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = false
jam_track_right.version = jam_track.version
end
jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
line_items[:jam_tracks] = jam_track_items if jam_track_items
# TODO: process shopping_carts_subscriptions
line_items
end
# place_order will create one or more sales based on the contents of shopping_carts for the current user
@ -99,19 +84,14 @@ module JamRuby
def self.place_order(current_user, shopping_carts)
sales = []
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
if Sale.is_mixed(shopping_carts)
# the controller checks this too; this is just an extra-level of sanity checking
return sales
end
jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
jam_track_sale = order_jam_tracks(current_user, shopping_carts)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
@ -119,22 +99,52 @@ module JamRuby
sales
end
def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
### XXX TODO;
# we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
def self.is_only_freebie(shopping_carts)
free = true
shopping_carts.each do |cart|
free = cart.product_info[:free]
# if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
# just a pain to implement
if !free
break
end
end
free
end
def self.is_only_freebie(shopping_carts_jam_tracks)
shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free]
# we don't allow mixed shopping carts :/
def self.is_mixed(shopping_carts)
free = false
non_free = false
shopping_carts.each do |cart|
if cart.product_info[:free]
free = true
else
non_free = true
end
end
free && non_free
end
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
def self.order_jam_tracks(current_user, shopping_carts)
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts_gift_cards = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
elsif shopping_cart.is_gift_card?
shopping_carts_gift_cards << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
raise "unknown shopping cart type #{shopping_cart.cart_type}"
shopping_carts_subscriptions << shopping_cart
end
end
client = RecurlyClient.new
@ -143,8 +153,8 @@ module JamRuby
sale = create_jam_track_sale(current_user)
if sale.valid?
if is_only_freebie(shopping_carts_jam_tracks)
sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil)
if is_only_freebie(shopping_carts)
sale.process_shopping_carts(current_user, shopping_carts, nil)
sale.recurly_subtotal_in_cents = 0
sale.recurly_tax_in_cents = 0
@ -159,11 +169,13 @@ module JamRuby
return sale
end
sale_line_item = sale.sale_line_items[0]
sale_line_item.recurly_tax_in_cents = 0
sale_line_item.recurly_total_in_cents = 0
sale_line_item.recurly_currency = 'USD'
sale_line_item.recurly_discount_in_cents = 0
sale.sale_line_items.each do |sale_line_item|
sale_line_item = sale.sale_line_items[0]
sale_line_item.recurly_tax_in_cents = 0
sale_line_item.recurly_total_in_cents = 0
sale_line_item.recurly_currency = 'USD'
sale_line_item.recurly_discount_in_cents = 0
end
sale.save
else
@ -173,7 +185,7 @@ module JamRuby
purge_pending_adjustments(account)
created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
# now invoice the sale ... almost done
@ -229,13 +241,13 @@ module JamRuby
sale
end
def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
def process_shopping_carts(current_user, shopping_carts, account)
created_adjustments = []
begin
shopping_carts_jam_tracks.each do |shopping_cart|
process_jam_track(current_user, shopping_cart, account, created_adjustments)
shopping_carts.each do |shopping_cart|
process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
end
rescue Recurly::Error, NoMethodError => x
# rollback any adjustments created if error
@ -251,7 +263,7 @@ module JamRuby
end
def process_jam_track(current_user, shopping_cart, account, created_adjustments)
def process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
@ -259,15 +271,20 @@ module JamRuby
shopping_cart.reload
# get the JamTrack in this shopping cart
jam_track = shopping_cart.cart_product
cart_product = shopping_cart.cart_product
if jam_track.right_for_user(current_user)
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
return
if shopping_cart.is_jam_track?
jam_track = cart_product
if jam_track.right_for_user(current_user)
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
return
end
end
if account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
@ -300,38 +317,70 @@ module JamRuby
# if the sale line item is invalid, blow up the transaction
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
@@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
raise RecurlyClientError.new(sale_line_item.errors)
end
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = shopping_cart.free?
end
if shopping_cart.is_jam_track?
jam_track = cart_product
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
if shopping_cart.free?
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
current_user.has_redeemable_jamtrack = false # make sure model reflects the truth
end
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
unless jam_track_right.save
raise RecurlyClientError.new(jam_track_right.errors)
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = shopping_cart.free?
jam_track_right.version = jam_track.version
end
# also if the purchase was a free one, then:
# first, mark the free has_redeemable_jamtrack field if that's still true
# and if still they have more free things, then redeem the giftable_jamtracks
if shopping_cart.free?
if user.has_redeemable_jamtrack
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
current_user.has_redeemable_jamtrack = false
else
User.where(id: current_user.id).update_all(gifted_jamtracks: current_user.gifted_jamtracks - 1)
current_user.gifted_jamtracks = current_user.gifted_jamtracks - 1
end
end
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
unless jam_track_right.save
raise RecurlyClientError.new(jam_track_right.errors)
end
end
# blow up the transaction if the JamTrackRight did not get created
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
elsif shopping_cart.is_gift_card?
gift_card_type = cart_product
raise "gift card is null" if gift_card_type.nil?
raise if current_user.nil?
shopping_cart.quantity.times do |item|
gift_card_purchase = GiftCardPurchase.new(
{
user: current_user,
gift_card_type: gift_card_type
})
unless gift_card_purchase.save
raise RecurlyClientError.new(gift_card_purchase.errors)
end
end
else
raise 'unknown shopping cart type: ' + shopping_cart.cart_type
end
# delete the shopping cart; it's been dealt with
shopping_cart.destroy if shopping_cart
# blow up the transaction if the JamTrackRight did not get created
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
end
@ -361,7 +410,7 @@ module JamRuby
def self.create_jam_track_sale(user)
sale = Sale.new
sale.user = user
sale.sale_type = JAMTRACK_SALE
sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = 0
sale.save
sale

View File

@ -4,14 +4,16 @@ module JamRuby
JAMBLASTER = 'JamBlaster'
JAMCLOUD = 'JamCloud'
JAMTRACK = 'JamTrack'
GIFTCARD = 'GiftCardType'
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
belongs_to :gift_card, class_name: 'JamRuby::GiftCard'
belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid'
validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]}
validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD]}
validates :unit_price, numericality: {only_integer: false}
validates :quantity, numericality: {only_integer: true}
validates :free, numericality: {only_integer: true}
@ -21,9 +23,19 @@ module JamRuby
validates :recurly_plan_code, presence:true
validates :sale, presence:true
def is_jam_track?
product_type == JAMTRACK
end
def is_gift_card?
product_type == GIFTCARD
end
def product
if product_type == JAMTRACK
JamTrack.find_by_id(product_id)
elsif product_type == GIFTCARD
GiftCardType.find_by_id(product_id)
else
raise 'unsupported product type'
end

View File

@ -12,6 +12,8 @@ module JamRuby
attr_accessible :quantity, :cart_type, :product_info
attr_accessor :skip_mix_check
validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id]
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
@ -20,12 +22,13 @@ module JamRuby
validates :cart_type, presence: true
validates :cart_class_name, presence: true
validates :marked_for_redeem, numericality: {only_integer: true}
validate :not_mixed
default_scope order('created_at DESC')
def product_info
product = self.cart_product
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region} unless product.nil?
{type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil?
end
# multiply quantity by price
@ -38,6 +41,31 @@ module JamRuby
(quantity - marked_for_redeem) * product.price
end
def not_mixed
return if @skip_mix_check
existing_carts = []
this_user = any_user()
if this_user
existing_carts = this_user.shopping_carts
end
existing_carts = existing_carts.to_a
existing_carts << self
if Sale.is_mixed(existing_carts)
if free?
errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.")
return false
else
errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.")
return false
end
end
false
end
def cart_product
self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
@ -51,7 +79,18 @@ module JamRuby
marked_for_redeem == quantity
end
def any_user
if user
user
elsif anonymous_user_id
AnonymousUser.new(anonymous_user_id, nil)
else
nil
end
end
def self.create user, product, quantity = 1, mark_redeem = false
cart = ShoppingCart.new
if user.is_a?(User)
cart.user = user
@ -72,39 +111,42 @@ module JamRuby
cart_type == JamTrack::PRODUCT_TYPE
end
def is_gift_card?
cart_type == GiftCardType::PRODUCT_TYPE
end
# returns an array of adjustments for the shopping cart
def create_adjustment_attributes(current_user)
raise "not a jam track" unless is_jam_track?
raise "not a jam track or gift card" unless is_jam_track? || is_gift_card?
info = self.product_info
if free?
# create the credit, then the pseudo charge
[
{
accounting_code: PURCHASE_FREE_CREDIT,
currency: 'USD',
unit_amount_in_cents: -(info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name] + " (Credit)",
description: info[:sale_display] + " (Credit)",
tax_exempt: true
},
{
accounting_code: PURCHASE_FREE,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
description: info[:sale_display],
tax_exempt: true
}
]
else
[
{
accounting_code: PURCHASE_NORMAL,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
description: info[:sale_display],
tax_exempt: false
}
]
@ -113,8 +155,13 @@ module JamRuby
def self.move_to_user(user, anonymous_user, shopping_carts)
shopping_carts.each do |shopping_cart|
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem)
if shopping_cart.is_jam_track?
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem)
else
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false)
end
end
anonymous_user.destroy_all_shopping_carts
@ -134,28 +181,32 @@ module JamRuby
# if no shpping carts have been marked, then mark it redeemable
# should be wrapped in a TRANSACTION
def self.user_has_redeemable_jam_track?(any_user)
mark_redeem = false
if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack
mark_redeem = true # start out assuming we can redeem...
if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0
free_in_cart = 0
any_user.shopping_carts.each do |shopping_cart|
# but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true
if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0
mark_redeem = false
break
if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE
free_in_cart += shopping_cart.marked_for_redeem
end
end
any_user.free_jamtracks > free_in_cart
else
false
end
mark_redeem
end
# adds a jam_track to cart, checking for promotions
def self.add_jam_track_to_cart(any_user, jam_track)
def self.add_jam_track_to_cart(any_user, jam_track, clear:false)
cart = nil
ShoppingCart.transaction do
if any_user.has_redeemable_jamtrack
# if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart
any_user.destroy_all_shopping_carts
if clear
# if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario'
any_user.destroy_jam_track_shopping_carts
any_user.reload
end
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
@ -164,23 +215,66 @@ module JamRuby
cart
end
def self.add_item_to_cart(any_user, item)
cart = nil
ShoppingCart.transaction do
cart = ShoppingCart.create(any_user, item, 1, false)
end
cart
end
# deletes a jam track from the shopping cart, updating redeem flag as necessary
def self.remove_jam_track_from_cart(any_user, cart)
ShoppingCart.transaction do
cart.destroy
# check if we should move the redemption
# so that user.shopping_carts reflects truth
any_user.reload
# check if we should move the redemption around automatically
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
carts = any_user.shopping_carts
# if we find any carts on the account, mark one redeemable
# if we find any carts on the account that are not redeemed, mark first one redeemable
if mark_redeem && carts.length > 0
carts[0].redeem(mark_redeem)
carts[0].save
carts.each do |cart|
if cart.marked_for_redeem == 0
if cart.quantity > 1
raise 'unknown situation for redeemption juggling'
end
cart.redeem(mark_redeem)
cart.save
break
end
end
end
end
end
def self.remove_item_from_cart(any_user, cart)
ShoppingCart.transaction do
cart.destroy
end
end
# if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up
def self.apply_gifted_jamtracks(user)
jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE)
if jam_track_carts.count > user.gifted_jamtracks
# just whack everything in their shopping cart
user.destroy_all_shopping_carts
return
end
jam_track_carts.each do |cart|
cart.skip_mix_check = true
cart.marked_for_redeem = 1
cart.save!
end
end
def port(user, anonymous_user)
ShoppingCart.transaction do

View File

@ -40,10 +40,12 @@ module JamRuby
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection
# updating_password corresponds to a lost_password
attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json
attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card
belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id'
has_many :controlled_sessions, :class_name=> "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id
# authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
@ -149,6 +151,11 @@ module JamRuby
# events
has_many :event_sessions, :class_name => "JamRuby::EventSession"
# gift cards
has_many :gift_cards, :class_name=> "JamRuby::GiftCard"
has_many :gift_card_purchases, :class_name=> "JamRuby::GiftCardPurchase"
# affiliate_partner
has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user
belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count
@ -177,11 +184,13 @@ module JamRuby
has_one :musician_search, :class_name => 'JamRuby::MusicianSearch'
has_one :band_search, :class_name => 'JamRuby::BandSearch'
before_save :default_anonymous_names
before_save :create_remember_token, :if => :should_validate_password?
before_save :stringify_avatar_info , :if => :updating_avatar
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :first_name, length: {maximum: 50}, no_profanity: true
validates :last_name, length: {maximum: 50}, no_profanity: true
validates :last_name, length: {maximum: 50}, no_profanity: true
validates :biography, length: {maximum: 4000}, no_profanity: true
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
@ -193,6 +202,7 @@ module JamRuby
validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false }
validates :reuse_card, :inclusion => {:in => [true, false]}
validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]}
validates :gifted_jamtracks, presence: true, :numericality => { :less_than_or_equal_to => 100 }
validates :subscribe_email, :inclusion => {:in => [nil, true, false]}
validates :musician, :inclusion => {:in => [true, false]}
validates :show_whats_next, :inclusion => {:in => [nil, true, false]}
@ -213,6 +223,7 @@ module JamRuby
validate :email_case_insensitive_uniqueness
validate :update_email_case_insensitive_uniqueness, :if => :updating_email
validate :validate_mods
validate :presence_gift_card, :if => :expecting_gift_card
scope :musicians, where(:musician => true)
scope :fans, where(:musician => false)
@ -232,6 +243,18 @@ module JamRuby
end
end
def has_any_free_jamtracks
has_redeemable_jamtrack || gifted_jamtracks > 0
end
def free_jamtracks
(has_redeemable_jamtrack ? 1 : 0) + gifted_jamtracks
end
def show_free_jamtrack?
ShoppingCart.user_has_redeemable_jam_track?(self)
end
def failed_qualification(reason)
self.last_failed_certified_gear_at = DateTime.now
self.last_failed_certified_gear_reason = reason
@ -254,6 +277,12 @@ module JamRuby
end
end
def presence_gift_card
if self.gift_cards.length == 0
errors.add(:gift_card, ValidationMessages::NOT_FOUND)
end
end
def validate_current_password
# checks if the user put in their current password (used when changing your email, for instance)
errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password)
@ -296,8 +325,16 @@ module JamRuby
online?
end
def anonymous?
first_name == 'Anonymous' && last_name == 'Anonymous'
end
def name
"#{first_name} #{last_name}"
if anonymous?
'Anonymous'
else
"#{first_name} #{last_name}"
end
end
def location
@ -576,9 +613,7 @@ module JamRuby
def to_s
return email unless email.nil?
if !first_name.nil? && !last_name.nil?
return first_name + ' ' + last_name
end
return name unless name.nil?
id
end
@ -1018,6 +1053,7 @@ module JamRuby
reuse_card = options[:reuse_card]
signup_hint = options[:signup_hint]
affiliate_partner = options[:affiliate_partner]
gift_card = options[:gift_card]
user = User.new
@ -1029,6 +1065,9 @@ module JamRuby
user.terms_of_service = terms_of_service
user.musician = musician
user.reuse_card unless reuse_card.nil?
user.gifted_jamtracks = 0
user.has_redeemable_jamtrack = true
# FIXME: Setting random password for social network logins. This
# is because we have validations all over the place on this.
@ -1133,8 +1172,22 @@ module JamRuby
end
end
found_gift_card = nil
# if a gift card value was passed in, then try to find that gift card and apply it to user
if gift_card
user.expecting_gift_card = true
found_gift_card = GiftCard.where(code:gift_card).where(user_id:nil).first
user.gift_cards << found_gift_card if found_gift_card
end
user.save
if found_gift_card
user.reload
ShoppingCart.apply_gifted_jamtracks(user)
end
# if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it
# only_freebie_in_cart =
# signup_hint &&
@ -1174,6 +1227,7 @@ module JamRuby
end
end
end
user.reload if user.id# gift card adding gifted_jamtracks doesn't reflect here until reload
user
end # def signup
@ -1629,6 +1683,11 @@ module JamRuby
ShoppingCart.where("user_id=?", self).destroy_all
end
def destroy_jam_track_shopping_carts
ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE)
end
def unsubscribe_token
self.class.create_access_token(self)
end
@ -1681,14 +1740,17 @@ module JamRuby
else
false
end
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
def default_anonymous_names
self.first_name = 'Anonymous' if self.first_name.nil?
self.last_name = 'Anonymous' if self.last_name.nil?
end
def stringify_avatar_info
# fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR,
# so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object)

View File

@ -1,7 +1,7 @@
module JamRuby
class UserAuthorization < ActiveRecord::Base
attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user
attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user, :refresh_token
self.table_name = "user_authorizations"
@ -12,6 +12,36 @@ module JamRuby
validates_uniqueness_of :uid, scope: :provider
# token, secret, token_expiration can be missing
def self.refreshing_google_auth(user)
auth = self.where(:user_id => user.id)
.where(:provider => 'google_login')
.limit(1).first
# if we have an auth that will expire in less than 10 minutes
if auth && auth.refresh_token && auth.token_expiration < Time.now - 60 * 10
begin
oauth_client = OAuth2::Client.new(
Rails.application.config.google_client_id, Rails.application.config.google_secret,
:site => "https://accounts.google.com",
:token_url => "/o/oauth2/token",
:authorize_url => "/o/oauth2/auth")
access_token = OAuth2::AccessToken.from_hash(oauth_client, {:refresh_token => auth.refresh_token})
access_token = access_token.refresh!
auth.token = access_token.token
auth.token_expiration = Time.now + access_token.expires_in
auth.save
return auth
rescue Exception => e
# couldn't refresh; probably the user has revoked the app's rights
return nil
end
else
auth
end
end
def self.google_auth(user)
self
.where(:user_id => user.id)

View File

@ -0,0 +1,27 @@
module JamRuby
class UserBlacklist < ActiveRecord::Base
attr_accessible :user_id, :notes, as: :admin
@@log = Logging.logger[UserBlacklist]
belongs_to :user, :class_name => "JamRuby::User"
validates :user, presence:true, uniqueness: true
def self.listed(user)
UserBlacklist.count(:conditions => "user_id= '#{user.id}'") == 1
end
def self.admin_url
APP_CONFIG.admin_root_url + "/admin/user_blacklists/"
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + id
end
def to_s
user
end
end
end

View File

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

View File

@ -0,0 +1,771 @@
require 'json'
require 'resque'
require 'resque-retry'
require 'net/http'
require 'digest/md5'
module JamRuby
class JamTrackMixdownPackager
extend JamRuby::ResqueStats
include JamRuby::S3ManagerMixin
TAP_IN_PADDING = 2
MAX_PAN = 90
MIN_PAN = -90
KNOCK_SECONDS = 0.035
attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step
@queue = :jam_track_mixdown_packager
def log
@log || Logging.logger[JamTrackMixdownPackager]
end
def self.perform(mixdown_package_id, bitrate=48)
jam_track_builder = JamTrackMixdownPackager.new()
jam_track_builder.mixdown_package_id = mixdown_package_id
jam_track_builder.run
end
def compute_steps
@step = 0
number_downloads = @track_settings.length
number_volume_adjustments = (@track_settings.select { |track| should_alter_volume? track }).length
pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0
mix_steps = 1
package_steps = 1
number_downloads + number_volume_adjustments + pitch_shift_steps + mix_steps + package_steps
end
def run
begin
log.info("Mixdown job starting. mixdown_packager_id #{mixdown_package_id}")
begin
@mixdown_package = JamTrackMixdownPackage.find(mixdown_package_id)
# bailout check
if @mixdown_package.signed?
log.debug("package is already signed. bailing")
return
end
@mixdown = @mixdown_package.jam_track_mixdown
@settings = JSON.parse(@mixdown.settings)
process_jmep
track_settings
# compute the step count
total_steps = compute_steps
# track that it's started ( and avoid db validations )
signing_started_at = Time.now
last_step_at = Time.now
#JamTrackMixdownPackage.where(:id => @mixdown_package.id).update_all(:signing_started_at => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, :signing => true)
# because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
@mixdown_package.current_packaging_step = 0
@mixdown_package.packaging_steps = total_steps
@mixdown_package.signing_started_at = signing_started_at
@mixdown_package.signing = true
@mixdown_package.should_retry = false
@mixdown_package.last_step_at = last_step_at
@mixdown_package.queued = false
@mixdown_package.save
SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
package
log.info "Signed mixdown package to #{@mixdown_package[:url]}"
rescue Exception => e
# record the error in the database
post_error(e)
#SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
# and let the job fail, alerting ops too
raise
end
end
end
def should_alter_volume? track
# short cut is possible if vol = 1.0 and pan = 0
vol = track[:vol]
pan = track[:pan]
vol != 1.0 || pan != 0
end
def process_jmep
@start_points = []
@initial_padding = 0.0
@tap_in_initial_silence = 0
speed = @settings['speed'] || 0
@speed_factor = 1.0 + (-speed.to_f / 100.0)
@inverse_speed_factor = 1 - (-speed.to_f / 100)
log.info("speed factor #{@speed_factor}")
jmep = @mixdown.jam_track.jmep_json
if jmep
jmep = JSON.parse(jmep)
end
if jmep.nil?
log.debug("no jmep")
return
end
events = jmep["Events"]
return if events.nil? || events.length == 0
metronome = nil
events.each do |event|
if event.has_key?("metronome")
metronome = event["metronome"]
break
end
end
if metronome.nil? || metronome.length == 0
log.debug("no metronome events for jmep", jmep)
return
end
@start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" }
log.debug("found #{@start_points.length} metronome start points")
start_point = @start_points[0]
if start_point
start_time = parse_time(start_point["ts"])
if start_time < 2.0
padding = start_time - 2.0
@initial_padding = padding.abs
@initial_tap_in = start_time
end
end
if @speed_factor != 1.0
metronome.length.times do |count|
# we expect to find metronome start/stop grouped
if count % 2 == 0
start = metronome[count]
stop = metronome[count + 1]
if start["action"] != "start" || stop["action"] != "stop"
# bail out
log.error("found de-coupled metronome events #{start.to_json} | #{stop.to_json}")
next
end
bpm = start["bpm"].to_f
stop_time = parse_time(stop['ts'])
ticks = stop['ticks'].to_i
new_bpm = bpm * @inverse_speed_factor
new_stop_time = stop_time * @speed_factor
new_start_time = new_stop_time - (60.0/new_bpm * ticks)
log.info("original bpm:#{bpm} start: #{parse_time(start["ts"])} stop: #{stop_time}")
log.info("updated bpm:#{new_bpm} start: #{new_start_time} stop: #{new_stop_time}")
stop["ts"] = new_stop_time
start["ts"] = new_start_time
start["bpm"] = new_bpm
stop["bpm"] = new_bpm
@tap_in_initial_silence = (@initial_tap_in + @initial_padding) * @speed_factor
end
end
end
@start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" }
end
# format like: "-0:00:02:820"
def parse_time(ts)
if ts.is_a?(Float)
return ts
end
time = 0.0
negative = false
if ts.start_with?('-')
negative = true
end
# parse time_format
bits = ts.split(':').reverse
bit_position = 0
bits.each do |bit|
if bit_position == 0
# milliseconds
milliseconds = bit.to_f
time += milliseconds/1000
elsif bit_position == 1
# seconds
time += bit.to_f
elsif bit_position == 2
# minutes
time += 60 * bit.to_f
elsif bit_position == 3
# hours
# not bothering
end
bit_position += 1
end
if negative
time = 0.0 - time
end
time
end
def path_to_resources
File.join(File.dirname(File.expand_path(__FILE__)), '../../../lib/jam_ruby/app/assets/sounds')
end
def knock_file
if long_sample_rate == 44100
knock = File.join(path_to_resources, 'knock44.wav')
else
knock = File.join(path_to_resources, 'knock48.wav')
end
knock
end
def create_silence(tmp_dir, segment_count, duration)
file = File.join(tmp_dir, "#{segment_count}.wav")
# -c 2 means stereo
cmd("sox -n -r #{long_sample_rate} -c 2 #{file} trim 0.0 #{duration}", "silence")
file
end
def create_tapin_track(tmp_dir)
return nil if @start_points.length == 0
segment_count = 0
#initial_silence = @initial_tap_in + @initial_padding
initial_silence = @tap_in_initial_silence
#log.info("tapin data: initial_tap_in: #{@initial_tap_in}, initial_padding: #{@initial_padding}, initial_silence: #{initial_silence}")
time_points = []
files = []
if initial_silence > 0
files << create_silence(tmp_dir, segment_count, initial_silence)
time_points << {type: :silence, ts: initial_silence}
segment_count += 1
end
time_cursor = nil
@start_points.each do |start_point|
tap_time = parse_time(start_point["ts"])
if !time_cursor.nil?
between_silence = tap_time - time_cursor
files << create_silence(tmp_dir, segment_count, between_silence)
time_points << {type: :silence, ts: between_silence}
end
time_cursor = tap_time
bpm = start_point["bpm"].to_f
tick_silence = 60.0/bpm - KNOCK_SECONDS
ticks = start_point["ticks"].to_i
ticks.times do |tick|
files << knock_file
files << create_silence(tmp_dir, segment_count, tick_silence)
time_points << {type: :knock, ts: KNOCK_SECONDS}
time_points << {type: :silence, ts: tick_silence}
time_cursor + 60.0/bpm
segment_count += 1
end
end
log.info("time points for tap-in: #{time_points.inspect}")
# do we need to pad with time? not sure
sequence_cmd = "sox "
files.each do |file|
sequence_cmd << "\"#{file}\" "
end
count_in = File.join(tmp_dir, "count-in.wav")
sequence_cmd << "\"#{count_in}\""
cmd(sequence_cmd, "count_in")
@count_in_file = count_in
count_in
end
# creates a list of tracks to actually mix
def track_settings
altered_tracks = @settings["tracks"] || []
@track_settings = []
#void slider2Pan(int i, float *f);
stems = @mixdown.jam_track.stem_tracks
@track_count = stems.length
@include_count_in = @settings["count-in"] && @start_points.length > 0 && @mixdown_package.encrypt_type.nil?
# temp
# @include_count_in = true
if @include_count_in
@track_count += 1
end
stems.each do |stem|
vol = 1.0
pan = 0
match = false
skipped = false
# is this stem in the altered_tracks list?
altered_tracks.each do |alteration|
if alteration["id"] == stem.id
if alteration["mute"] || alteration["vol"] == 0
log.debug("leaving out track because muted or 0 volume #{alteration.inspect}")
skipped = true
next
else
vol = alteration["vol"] || vol
pan = alteration["pan"] || pan
end
@track_settings << {stem: stem, vol: vol, pan: pan}
match = true
break
end
end
# if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged
if !skipped && !match
@track_settings << {stem: stem, vol: vol, pan: pan}
end
end
if @include_count_in
@track_settings << {count_in: true, vol: 1.0, pan: 0}
end
@track_settings
end
def slider_to_pan(pan)
# transpose MIN_PAN to MAX_PAN to
# 0-1.0 range
#assumes abs(MIN_PAN) == abs(MAX_PAN)
# k = f(i) = (i)/(2*MAX_PAN) + 0.5
# so f(MIN_PAN) = -0.5 + 0.5 = 0
k = ((pan * (1.0))/ (2.0 * MAX_PAN)) + 0.5
l, r = 0
if k == 0
l = 0.0
r = 1.0
else
l = Math.sqrt(k)
r = Math.sqrt(1-k)
end
[l, r]
end
def package
log.info("Settings: #{@settings.to_json}")
Dir.mktmpdir do |tmp_dir|
# download all files
@track_settings.each do |track|
if track[:count_in]
file = create_tapin_track(tmp_dir)
bump_step(@mixdown_package)
else
jam_track_track = track[:stem]
file = File.join(tmp_dir, jam_track_track.id + '.ogg')
bump_step(@mixdown_package)
# download each track needed
s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file)
end
track[:file] = file
end
audio_process tmp_dir
end
end
def audio_process(tmp_dir)
# use sox remix to apply mute, volume, pan settings
# step 1: apply pan and volume per track. mute and vol of 0 has already been handled, by virtue of those tracks not being present in @track_settings
# step 2: mix all tracks into single track, dividing by constant number of jam tracks, which is same as done by client backend
# step 3: apply pitch and speed (if applicable)
# step 4: encrypt with jkz (if applicable)
apply_vol_and_pan tmp_dir
create_silence_padding tmp_dir
mix tmp_dir
pitch_speed tmp_dir
final_packaging tmp_dir
end
# output is :volumed_file in each track in @track_settings
def apply_vol_and_pan(tmp_dir)
@track_settings.each do |track|
jam_track_track = track[:stem]
count_in = track[:count_in]
file = track[:file]
unless should_alter_volume? track
track[:volumed_file] = file
else
pan_l, pan_r = slider_to_pan(track[:pan])
vol = track[:vol]
# short
channel_l = pan_l * vol
channel_r = pan_r * vol
bump_step(@mixdown_package)
# sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0
if count_in
volumed_file = File.join(tmp_dir, 'count-in' + '-volumed.ogg')
else
volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg')
end
cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_r} 2v#{channel_l}", 'vol_pan')
track[:volumed_file] = volumed_file
end
end
end
def create_silence_padding(tmp_dir)
if @initial_padding > 0 && @include_count_in
@padding_file = File.join(tmp_dir, "initial_padding.ogg")
# -c 2 means stereo
cmd("sox -n -r #{long_sample_rate} -c 2 #{@padding_file} trim 0.0 #{@initial_padding}", "initial_padding")
@track_settings.each do |track|
next if track[:count_in]
input = track[:volumed_file]
output = input[0..-5] + '-padded.ogg'
padd_cmd = "sox '#{@padding_file}' '#{input}' '#{output}'"
cmd(padd_cmd, "pad_track_with_silence")
track[:volumed_file] = output
end
end
end
# output is @mix_file
def mix(tmp_dir)
bump_step(@mixdown_package)
@mix_file = File.join(tmp_dir, "mix.ogg")
pitch = @settings['pitch'] || 0
speed = @settings['speed'] || 0
real_count = @track_settings.count
real_count -= 1 if @include_count_in
# if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks
if real_count <= 1
mix_divide = 1.0/@track_count
cmd = "sox -v #{mix_divide} \"#{@track_settings[0][:volumed_file]}\" \"#{@mix_file}\""
cmd(cmd, 'volume_adjust')
else
# sox -m will divide by number of inputs by default. But we purposefully leave out tracks that are mute/no volume (to save downloading/processing time in this job)
# so we need to tell sox to divide by how many tracks there are as a constant, because this is how the client works today
#sox -m -v 1/n file1 -v 1/n file2 out
cmd = "sox -m"
mix_divide = 1.0/@track_count
@track_settings.each do |track|
# if pitch/shifted, we lay the tap-in after pitch/speed shift
# next if (pitch != 0 || speed != 0) && track[:count_in]
next if track[:count_in]
volumed_file = track[:volumed_file]
cmd << " -v #{mix_divide} \"#{volumed_file}\""
end
cmd << " \"#{@mix_file}\""
cmd(cmd, 'mix_adjust')
end
end
def long_sample_rate
sample_rate = 48000
if @mixdown_package.sample_rate != 48
sample_rate = 44100
end
sample_rate
end
# output is @speed_mix_file
def pitch_speed tmp_dir
# # usage
# This app will take an ogg, wav, or mp3 file (for the uploads) as its input and output an ogg file.
# Usage:
# sbsms path-to-input.ogg path-to-output.ogg TimeStrech PitchShift
# input is @mix_file, created by mix()
# output is @speed_mix_file
pitch = @settings['pitch'] || 0
speed = @settings['speed'] || 0
# if pitch and speed are 0, we do nothing here
if pitch == 0 && speed == 0
@speed_mix_file = @mix_file
else
bump_step(@mixdown_package)
@speed_mix_file = File.join(tmp_dir, "speed_mix_file.ogg")
# usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz
sample_rate = long_sample_rate
# rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/
sbsms_speed = speed/100.0
sbsms_speed = 1.0 + sbsms_speed
sbsms_pitch = pitch
cmd("sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift')
end
if @include_count_in
# lay the tap-ins over the recording
layered = File.join(tmp_dir, "layered_speed_mix.ogg")
cmd("sox -m '#{@count_in_file}' '#{@speed_mix_file}' '#{layered}'", "layer_tap_in")
@speed_mix_file = layered
end
end
def final_packaging tmp_dir
bump_step(@mixdown_package)
url = nil
private_key = nil
md5 = nil
length = 0
output = nil
if @mixdown_package.encrypt_type
output, private_key = encrypt_jkz tmp_dir
else
# create output file to correct output format
output = convert tmp_dir
end
# upload output to S3
s3_url = "#{@mixdown_package.store_dir}/#{@mixdown_package.filename}"
s3_manager.upload(s3_url, output)
length = File.size(output)
computed_md5 = Digest::MD5.new
File.open(output, 'rb').each { |line| computed_md5.update(line) }
md5 = computed_md5.to_s
@mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s)
end
# returns output destination, converting if necessary
def convert(tmp_dir)
# if the file already ends with the desired file type, call it a win
if @speed_mix_file.end_with?(@mixdown_package.file_type)
@speed_mix_file
else
# otherwise we need to convert from lastly created file to correct
output = File.join(tmp_dir, "output.#{@mixdown_package.file_type}")
cmd("#{APP_CONFIG.normalize_ogg_path} --bitrate 192 \"#{@speed_mix_file}\"", 'normalize')
if @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_AAC
cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac')
elsif @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_MP3
cmd("ffmpeg -i \"#{@speed_mix_file}\" -ab 192k \"#{output}\"", 'convert_mp3')
else
raise 'unknown file_type'
end
output
end
end
def encrypt_jkz(tmp_dir)
py_root = APP_CONFIG.jamtracks_dir
step = 0
private_key = nil
# we need to make the id of the custom mix be the name of the file (ID.ogg)
custom_mix_name = File.join(tmp_dir, "#{@mixdown.id}.ogg")
FileUtils.mv(@speed_mix_file, custom_mix_name)
jam_file_opts = ""
jam_file_opts << " -i #{Shellwords.escape("#{custom_mix_name}+mixdown")}"
sku = @mixdown_package.id
title = @mixdown.name
output = File.join(tmp_dir, "#{title.parameterize}.jkz")
py_file = File.join(py_root, "jkcreate.py")
version = @mixdown_package.version
right = @mixdown.jam_track.right_for_user(@mixdown.user)
if @mixdown_package.sample_rate == 48
private_key = right.private_key_48
else
private_key = right.private_key_44
end
unless private_key
@error_reason = 'no_private_key'
@error_detail = 'user needs to generate JamTrack for given sample rate'
raise @error_reason
end
private_key_file = File.join(tmp_dir, 'skey.pem')
File.open(private_key_file, 'w') { |f| f.write(private_key) }
log.debug("PRIVATE KEY")
log.debug(private_key)
log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output})"
cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}"
Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
pid = wait_thr.pid
exit_status = wait_thr.value
err = stderr.read(1000)
out = stdout.read(1000)
#puts "stdout: #{out}, stderr: #{err}"
raise ArgumentError, "Error calling python script: #{err}" if err.present?
raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file"))
private_key = File.read(private_key_file)
end
return output, private_key
end
def cmd(cmd, type)
log.debug("executing #{cmd}")
output = `#{cmd}`
result_code = $?.to_i
if result_code == 0
output
else
@error_reason = type + "_fail"
@error_detail = "#{cmd}, #{output}"
raise "command `#{cmd}` failed."
end
end
# increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on
def bump_step(mixdown_package)
step = @step
last_step_at = Time.now
mixdown_package.current_packaging_step = step
mixdown_package.last_step_at = last_step_at
JamTrackMixdownPackage.where(:id => mixdown_package.id).update_all(last_step_at: last_step_at, current_packaging_step: step)
SubscriptionMessage.mixdown_signing_job_change(mixdown_package)
@step = step + 1
end
# set @error_reason before you raise an exception, and it will be sent back as the error reason
# otherwise, the error_reason will be unhandled-job-exception
def post_error(e)
begin
# if error_reason is null, assume this is an unhandled error
unless @error_reason
@error_reason = "unhandled-job-exception"
@error_detail = e.to_s
end
@mixdown_package.finish_errored(@error_reason, @error_detail)
rescue Exception => e
log.error "unable to post back to the database the error #{e}"
end
end
end
end

View File

@ -42,7 +42,7 @@ module JamRuby
signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44
signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44
last_step_at = Time.now
JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true)
JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true, queued: false)
# because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
@jam_track_right.current_packaging_step = 0
@jam_track_right.packaging_steps = total_steps
@ -50,6 +50,7 @@ module JamRuby
@jam_track_right[signing_state_symbol] = true
@jam_track_right.should_retry = false
@jam_track_right.last_step_at = Time.now
@jam_track_right.queued = false
SubscriptionMessage.jam_track_signing_job_change(@jam_track_right)
JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate)

View File

@ -24,6 +24,11 @@ module JamRuby
def perform
# this needs more testing
# let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough
JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
JamTrackMixdownPackage.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
return
#JamTrackRight.ready_to_clean.each do |jam_track_right|
# log.debug("deleting files for jam_track_right #{jam_track_right.id}")

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