diff --git a/admin/Gemfile b/admin/Gemfile index 756225a1a..1a1a7c248 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -44,7 +44,7 @@ gem 'rails3-jquery-autocomplete' gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable' gem 'mime-types', '1.25' gem 'meta_search' -gem 'fog', "~> 1.32.0" +gem 'fog' gem 'unf', '0.1.3' #optional fog dependency gem 'country-select' gem 'aasm', '3.0.16' diff --git a/admin/app/admin/affiliate_quarterly_totals.rb b/admin/app/admin/affiliate_quarterly_totals.rb new file mode 100644 index 000000000..4f065c267 --- /dev/null +++ b/admin/app/admin/affiliate_quarterly_totals.rb @@ -0,0 +1,37 @@ +ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quarterly Payments' do + + menu :label => 'Quarterly Reports', :parent => 'Affiliates' + + config.sort_order = 'due_amount_in_cents DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_partner + filter :year + filter :quarter + filter :closed + filter :paid + + form :partial => 'form' + + index do + + # default_actions # use this for all view/edit/delete links + + column 'Year' do |oo| oo.year end + column 'Quarter' do |oo| oo.quarter end + column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end + column "Due (\u00A2)" do |oo| oo.due_amount_in_cents end + column 'JamTracks Sold' do |oo| oo.jamtracks_sold end + column 'Paid' do |oo| oo.paid end + column 'Closed' do |oo| oo.paid end + + end + + controller do + + end +end diff --git a/admin/app/admin/affiliate_traffic_totals.rb b/admin/app/admin/affiliate_traffic_totals.rb new file mode 100644 index 000000000..f3f24b8ab --- /dev/null +++ b/admin/app/admin/affiliate_traffic_totals.rb @@ -0,0 +1,36 @@ +ActiveAdmin.register JamRuby::AffiliateTrafficTotal, :as => 'Affiliate Daily Stats' do + + menu :label => 'Daily Stats', :parent => 'Affiliates' + + config.sort_order = 'referral_user_count DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_partner + filter :day + filter :signups + filter :visits + + form :partial => 'form' + + scope("Active", default: true) { |scope| scope.where('visits != 0 or signups != 0').order('day desc') } + + index do + + # default_actions # use this for all view/edit/delete links + + column 'Day' do |oo| oo.day end + column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end + column 'Signups' do |oo| oo.signups end + column 'Visits' do |oo| oo.visits end + + end + + + controller do + + end +end diff --git a/admin/app/admin/affiliate_users.rb b/admin/app/admin/affiliate_users.rb index b21d512eb..1658d926d 100644 --- a/admin/app/admin/affiliate_users.rb +++ b/admin/app/admin/affiliate_users.rb @@ -2,15 +2,18 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do menu :label => 'Referrals', :parent => 'Affiliates' + config.sort_order = 'created_at DESC' config.batch_actions = false config.clear_action_items! - config.filters = false + config.filters = true + + filter :affiliate_referral index do - column 'User' do |oo| link_to(oo.name, "http://www.jamkazam.com/client#/profile/#{oo.id}", {:title => oo.name}) end + column 'User' do |oo| link_to(oo.name, oo.admin_url, {:title => oo.name}) end column 'Email' do |oo| oo.email end column 'Created' do |oo| oo.created_at end - column 'Partner' do |oo| oo.affiliate_referral.partner_name end + column 'Partner' do |oo| oo.affiliate_referral.display_name end end controller do diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb index 0409ee36c..db7bfa53d 100644 --- a/admin/app/admin/affiliates.rb +++ b/admin/app/admin/affiliates.rb @@ -6,10 +6,12 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do config.batch_actions = false # config.clear_action_items! config.filters = false + config.per_page = 50 + config.paginate = true form :partial => 'form' - scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') } + scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') } scope("Unpaid") { |partner| partner.unpaid } index do diff --git a/admin/app/admin/crash_dumps.rb b/admin/app/admin/crash_dumps.rb index bcfcd9106..4d85ae8ae 100644 --- a/admin/app/admin/crash_dumps.rb +++ b/admin/app/admin/crash_dumps.rb @@ -1,28 +1,27 @@ ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do # Note: a lame thing is it's not obvious how to make it search on email instead of user_id. filter :timestamp - filter :user_email, :as => :string filter :client_id + filter :user_id + menu :parent => 'Misc' + config.sort_order = 'created_at DESC' + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column "Client Version", :client_version + column "Client Type", :client_type + column "Download" do |post| + link_to 'Link', post.sign_url + end column "Timestamp" do |post| (post.timestamp || post.created_at).strftime('%b %d %Y, %H:%M') end - column "Client Type", :client_type - column "Dump URL" do |post| - link_to post.uri, post.uri + column "Description" do |post| + post.description end - - column "User ID", :user_id - - # FIXME (?): This isn't performant (though it likely doesn't matter). Could probably do a join. - column "User Email" do |post| - unless post.user_id.nil? - post.user_email - end - end - column "Client ID", :client_id actions + end end diff --git a/admin/app/admin/download_tracker.rb b/admin/app/admin/download_tracker.rb new file mode 100644 index 000000000..6d0daa013 --- /dev/null +++ b/admin/app/admin/download_tracker.rb @@ -0,0 +1,35 @@ +ActiveAdmin.register JamRuby::DownloadTracker, :as => 'DownloadTrackers' do + + menu :label => 'Download Trackers', :parent => 'JamTracks' + + config.batch_actions = false + config.filters = true + config.per_page = 50 + + filter :remote_ip + + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column 'Created' do |oo| oo.created_at end + column 'JamTrack' do |oo| oo.jam_track end + column 'Paid' do |oo| oo.paid end + column 'Blacklisted?' do |oo| IpBlacklist.listed(oo.remote_ip) ? 'Yes' : 'No' end + column 'Remote IP' do |oo| oo.remote_ip end + column "" do |oo| + link_to 'Blacklist This IP', "download_trackers/#{oo.id}/blacklist_by_ip" + end + end + + member_action :blacklist_by_ip, :method => :get do + tracker = DownloadTracker.find(params[:id]) + + if !IpBlacklist.listed(tracker.remote_ip) + ip = IpBlacklist.new + ip.remote_ip = tracker.remote_ip + ip.save! + end + + redirect_to admin_download_trackers_path, :notice => "IP address #{tracker.remote_ip} blacklisted." + end + +end diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb index b8c5caa31..87d167330 100644 --- a/admin/app/admin/fake_purchaser.rb +++ b/admin/app/admin/fake_purchaser.rb @@ -32,6 +32,7 @@ ActiveAdmin.register_page "Fake Purchaser" do jam_track_right.user = user jam_track_right.jam_track = jam_track jam_track_right.is_test_purchase = true + jam_track_right.version = jam_track.version jam_track_right.save! count = count + 1 end diff --git a/admin/app/admin/gift_card_upload.rb b/admin/app/admin/gift_card_upload.rb new file mode 100644 index 000000000..87e69850e --- /dev/null +++ b/admin/app/admin/gift_card_upload.rb @@ -0,0 +1,41 @@ +ActiveAdmin.register_page "Giftcarduploads" do + + menu :label => 'Gift Cards Upload', :parent => 'JamTracks' + + page_action :upload_giftcards, :method => :post do + GiftCard.transaction do + + puts params + + file = params[:jam_ruby_gift_card][:csv] + array_of_arrays = CSV.read(file.tempfile.path) + array_of_arrays.each do |row| + if row.length != 1 + raise "UKNONWN CSV FORMAT! Must be 1 column" + end + + code = row[0] + + gift_card = GiftCard.new + gift_card.code = code + gift_card.card_type = params[:jam_ruby_gift_card][:card_type] + gift_card.origin = file .original_filename + gift_card.save! + end + + redirect_to admin_giftcarduploads_path, :notice => "Created #{array_of_arrays.length} gift cards!" + end + end + + content do + semantic_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Upload Gift Cards" do + f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)" + f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES + end + f.actions + end + end + +end + diff --git a/admin/app/admin/gift_cards.rb b/admin/app/admin/gift_cards.rb new file mode 100644 index 000000000..8e1d4e80d --- /dev/null +++ b/admin/app/admin/gift_cards.rb @@ -0,0 +1,24 @@ +ActiveAdmin.register JamRuby::GiftCard, :as => 'GiftCards' do + + menu :label => 'Gift Cards', :parent => 'JamTracks' + + config.batch_actions = false + config.filters = true + config.per_page = 50 + + scope("Redeemed Most Recently", default: true) { |scope| scope.where('user_id IS NOT NULL').order('updated_at DESC') } + scope("Available") { |scope| scope.where('user_id is NULL') } + + filter :card_type + filter :origin + filter :code + + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column 'Code' do |oo| oo.code end + column 'Card Type' do |oo| oo.card_type end + column 'Origin' do |oo| oo.origin end + column 'Created' do |oo| oo.created_at end + end + +end diff --git a/admin/app/admin/ip_blacklist.rb b/admin/app/admin/ip_blacklist.rb new file mode 100644 index 000000000..3a8f1e14e --- /dev/null +++ b/admin/app/admin/ip_blacklist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::IpBlacklist, :as => 'IP Blacklist' do + + menu :label => 'IP Blacklist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :remote_ip + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index 534b65903..938cf0bf5 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -5,13 +5,16 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do config.sort_order = 'name_asc' config.batch_actions = false + filter :name + filter :original_artist filter :genres filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS scope("Default", default: true) { |scope| scope } scope("Onboarding TODO") { |scope| scope.where('onboarding_exceptions is not null') } scope("Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'") } - scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') } + scope("TimTracks Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tim Waurick'") } + # scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') } form :partial => 'form' diff --git a/admin/app/admin/sale_line_items.rb b/admin/app/admin/sale_line_items.rb new file mode 100644 index 000000000..b43c9cfee --- /dev/null +++ b/admin/app/admin/sale_line_items.rb @@ -0,0 +1,40 @@ +ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do + + menu :label => 'Line Items', :parent => 'Purchases' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_referral_id + filter :free + + form :partial => 'form' + + scope("Non Free", default: true) { |scope| scope.where(free: false).order('created_at desc') } + scope("Free") { |scope| scope.where(free: true).order('created_at desc') } + + index do + # default_actions # use this for all view/edit/delete links + + column 'Product' do |oo| oo.product end + column "Partner" do |oo| + link_to("#{oo.affiliate_referral.display_name} #{oo.affiliate_referral_fee_in_cents ? "#{oo.affiliate_referral_fee_in_cents}\u00A2" : ''}", oo.affiliate_referral.admin_url, {:title => oo.affiliate_referral.display_name}) if oo.affiliate_referral + end + column 'User' do |oo| + link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name}) + end + column 'When' do |oo| + oo.created_at + end + + end + + + controller do + + end +end diff --git a/admin/app/admin/user_blacklist.rb b/admin/app/admin/user_blacklist.rb new file mode 100644 index 000000000..538125600 --- /dev/null +++ b/admin/app/admin/user_blacklist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::UserBlacklist, :as => 'User Blacklist' do + + menu :label => 'User Blacklist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/user_progression.rb b/admin/app/admin/user_progression.rb index 6ea4bb285..aa72d8014 100644 --- a/admin/app/admin/user_progression.rb +++ b/admin/app/admin/user_progression.rb @@ -9,7 +9,7 @@ ActiveAdmin.register JamRuby::User, :as => 'User Progression' do config.filters = false index do - column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.first_name} #{user.last_name} (#{user.email})"}) end + column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.name} (#{user.email})"}) end column :updated_at do |uu| uu.updated_at.strftime(PROGRESSION_DATE) end column :created_at do |uu| uu.created_at.strftime(PROGRESSION_DATE) end column :city diff --git a/admin/app/controllers/email_controller.rb b/admin/app/controllers/email_controller.rb index 1e0c55182..45af1bb2e 100644 --- a/admin/app/controllers/email_controller.rb +++ b/admin/app/controllers/email_controller.rb @@ -15,5 +15,10 @@ class EmailController < ApplicationController headers['Content-Type'] ||= 'text/csv' @users = User.where(subscribe_email: true) + + # if specified, return only users that have redeemed or bought a JamTrack + if params[:any_jam_track] + @users = @users.select('DISTINCT users.id, email, first_name, last_name').joins(:sales => :sale_line_items).where("sale_line_items.product_type = 'JamTrack'") + end end end \ No newline at end of file diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb index b7a3b047d..78a1dce70 100644 --- a/admin/app/views/admin/affiliates/_form.html.erb +++ b/admin/app/views/admin/affiliates/_form.html.erb @@ -2,6 +2,8 @@ <%= f.semantic_errors *f.object.errors.keys %> <%= f.inputs do %> <%= f.input(:partner_name, :input_html => {:maxlength => 128}) %> + <%= f.input(:entity_type, :as => :select, :collection => AffiliatePartner::ENTITY_TYPES) %> + <%= f.input(:rate) %> <% end %> <%= f.actions %> <% end %> diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index 334cdc27e..ac038e5ca 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -13,6 +13,7 @@ = f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 } = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true = f.input :genres + = f.input :year = f.input :duration, hint: 'this should rarely need editing because it comes from the import process' = f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false = f.input :price, :required => true, :input_html => {type: 'numeric'} diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb index efa44b0f8..f226e68fa 100644 --- a/admin/app/views/email/dump_emailables.csv.erb +++ b/admin/app/views/email/dump_emailables.csv.erb @@ -1,2 +1,2 @@ <%- headers = ['email', 'name', 'unsubscribe_token'] -%> -<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file +<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.anonymous? ? '-' : user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file diff --git a/admin/config/application.rb b/admin/config/application.rb index 0e13be779..22e3df96f 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -110,6 +110,7 @@ module JamAdmin config.redis_host = "localhost:6379" + config.email_social_alias = 'social@jamkazam.com' config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails config.email_generic_from = 'nobody@jamkazam.com' config.email_smtp_address = 'smtp.sendgrid.net' @@ -153,5 +154,12 @@ module JamAdmin config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep")) config.email_dump_code = 'rcAUyC3TZCbgGx4YQpznBRbNnQMXW5iKTzf9NSBfzMLsnw9dRQ' + + config.admin_port = ENV['ADMIN_PORT'] || 3333 + config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}" + + config.download_tracker_day_range = 30 + config.max_user_ip_address = 10 + config.max_multiple_users_same_ip = 2 end end diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb index b449898e8..8ae1ae4ed 100644 --- a/admin/config/environments/development.rb +++ b/admin/config/environments/development.rb @@ -46,4 +46,5 @@ JamAdmin::Application.configure do config.email_generic_from = 'nobody-dev@jamkazam.com' config.email_alerts_alias = 'alerts-dev@jamkazam.com' + config.email_social_alias = 'social-dev@jamkazam.com' end diff --git a/admin/config/initializers/gift_cards.rb b/admin/config/initializers/gift_cards.rb new file mode 100644 index 000000000..8c967ccb4 --- /dev/null +++ b/admin/config/initializers/gift_cards.rb @@ -0,0 +1,9 @@ +class JamRuby::GiftCard + + attr_accessor :csv + + + def process_csv + + end +end diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb index 33efd995c..60537c467 100644 --- a/admin/config/initializers/jam_tracks.rb +++ b/admin/config/initializers/jam_tracks.rb @@ -2,28 +2,5 @@ class JamRuby::JamTrack # add a custom validation - attr_accessor :preview_generate_error - before_save :jmep_json_generate - validate :jmep_text_validate - - def jmep_text_validate - begin - JmepManager.execute(self.jmep_text) - rescue ArgumentError => err - errors.add(:jmep_text, err.to_s) - end - end - - def jmep_json_generate - self.licensor_id = nil if self.licensor_id == '' - self.jmep_json = nil if self.jmep_json == '' - self.time_signature = nil if self.time_signature == '' - - begin - self[:jmep_json] = JmepManager.execute(self.jmep_text) - rescue ArgumentError => err - #errors.add(:jmep_text, err.to_s) - end - end end diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 88080fb81..62739af59 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -16,6 +16,3 @@ PLATFORMS DEPENDENCIES pg_migrate (= 0.1.13)! - -BUNDLED WITH - 1.10.5 diff --git a/db/manifest b/db/manifest index 3d992ef76..00d3c8c44 100755 --- a/db/manifest +++ b/db/manifest @@ -298,11 +298,25 @@ musician_search.sql enhance_band_profile.sql alter_band_profile_rate_defaults.sql repair_band_profile.sql -profile_teacher.sql jam_track_onboarding_enhancements.sql jam_track_name_drop_unique.sql -populate_languages.sql -populate_subjects.sql jam_track_searchability.sql harry_fox_agency.sql jam_track_slug.sql +mixdown.sql +aac_master.sql +video_recording.sql +web_playable_jamtracks.sql +affiliate_partner_rate.sql +track_downloads.sql +jam_track_lang_idx.sql +giftcard.sql +add_description_to_crash_dumps.sql +acappella.sql +purchasable_gift_cards.sql +versionable_jamtracks.sql +session_controller.sql +jam_tracks_bpm.sql +profile_teacher.sql +populate_languages.sql +populate_subjects.sql \ No newline at end of file diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql new file mode 100644 index 000000000..28f67f81d --- /dev/null +++ b/db/up/aac_master.sql @@ -0,0 +1,3 @@ +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint; \ No newline at end of file diff --git a/db/up/acappella.sql b/db/up/acappella.sql new file mode 100644 index 000000000..cf17e052d --- /dev/null +++ b/db/up/acappella.sql @@ -0,0 +1,2 @@ +INSERT INTO genres (id, description) values ('acapella', 'A Capella'); +ALTER TABLE jam_track_licensors ADD COLUMN slug VARCHAR UNIQUE; diff --git a/db/up/add_description_to_crash_dumps.sql b/db/up/add_description_to_crash_dumps.sql new file mode 100644 index 000000000..e08540d85 --- /dev/null +++ b/db/up/add_description_to_crash_dumps.sql @@ -0,0 +1 @@ +ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(20000); \ No newline at end of file diff --git a/db/up/affiliate_partner_rate.sql b/db/up/affiliate_partner_rate.sql new file mode 100644 index 000000000..d36c80c81 --- /dev/null +++ b/db/up/affiliate_partner_rate.sql @@ -0,0 +1 @@ +ALTER TABLE affiliate_partners ADD COLUMN rate NUMERIC(8,2) DEFAULT 0.10; \ No newline at end of file diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql new file mode 100644 index 000000000..96a5374b1 --- /dev/null +++ b/db/up/crash_dumps_2.sql @@ -0,0 +1,6 @@ +ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255); +ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000); +ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100); +ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100); +ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey; + diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql new file mode 100644 index 000000000..eae33c13e --- /dev/null +++ b/db/up/giftcard.sql @@ -0,0 +1,13 @@ +CREATE TABLE gift_cards ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(64) UNIQUE NOT NULL, + user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE, + card_type VARCHAR(64) NOT NULL, + origin VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id); + +ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0; diff --git a/db/up/jam_track_lang_idx.sql b/db/up/jam_track_lang_idx.sql new file mode 100644 index 000000000..aa5c84c26 --- /dev/null +++ b/db/up/jam_track_lang_idx.sql @@ -0,0 +1 @@ +CREATE INDEX ON jam_tracks(language); diff --git a/db/up/jam_tracks_bpm.sql b/db/up/jam_tracks_bpm.sql new file mode 100644 index 000000000..0fce5154d --- /dev/null +++ b/db/up/jam_tracks_bpm.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_tracks ADD COLUMN bpm numeric(8,3); +INSERT INTO instruments (id, description) VALUES ('percussion', 'Percussion'); \ No newline at end of file diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql new file mode 100644 index 000000000..24295cdae --- /dev/null +++ b/db/up/mixdown.sql @@ -0,0 +1,61 @@ +CREATE TABLE jam_track_mixdowns ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + settings JSON NOT NULL, + name VARCHAR(1000) NOT NULL, + description VARCHAR(1000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE jam_track_mixdown_packages ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE CASCADE, + file_type VARCHAR NOT NULL , + sample_rate INTEGER NOT NULL, + url VARCHAR(2048), + md5 VARCHAR, + length INTEGER, + downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE, + last_step_at TIMESTAMP, + last_signed_at TIMESTAMP, + download_count INTEGER NOT NULL DEFAULT 0, + signed_at TIMESTAMP, + downloaded_at TIMESTAMP, + signing_queued_at TIMESTAMP, + error_count INTEGER NOT NULL DEFAULT 0, + error_reason VARCHAR, + error_detail VARCHAR, + should_retry BOOLEAN NOT NULL DEFAULT FALSE, + packaging_steps INTEGER, + current_packaging_step INTEGER, + private_key VARCHAR, + signed BOOLEAN, + signing_started_at TIMESTAMP, + first_downloaded TIMESTAMP, + signing BOOLEAN NOT NULL DEFAULT FALSE, + encrypt_type VARCHAR, + first_downloaded_at TIMESTAMP, + last_downloaded_at TIMESTAMP, + version VARCHAR NOT NULL DEFAULT '1', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL; + +ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE; + +ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP; +ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE; + +CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued); +CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at); +CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at); + +CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued); +CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at); +CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at); diff --git a/db/up/purchasable_gift_cards.sql b/db/up/purchasable_gift_cards.sql new file mode 100644 index 000000000..9ef8d29fa --- /dev/null +++ b/db/up/purchasable_gift_cards.sql @@ -0,0 +1,24 @@ + + +CREATE TABLE gift_card_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + card_type VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5'); +INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10'); + +CREATE TABLE gift_card_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL, + gift_card_type_id VARCHAR(64) REFERENCES gift_card_types(id) ON DELETE SET NULL, + recurly_adjustment_uuid VARCHAR(500), + recurly_adjustment_credit_uuid VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE sale_line_items ADD COLUMN gift_card_purchase_id VARCHAR(64) REFERENCES gift_card_purchases(id); diff --git a/db/up/session_controller.sql b/db/up/session_controller.sql new file mode 100644 index 000000000..d146481b6 --- /dev/null +++ b/db/up/session_controller.sql @@ -0,0 +1 @@ +ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id); \ No newline at end of file diff --git a/db/up/track_downloads.sql b/db/up/track_downloads.sql new file mode 100644 index 000000000..f328a9579 --- /dev/null +++ b/db/up/track_downloads.sql @@ -0,0 +1,30 @@ +CREATE TABLE download_trackers ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + remote_ip VARCHAR(400) NOT NULL, + jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + paid BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id); +CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip); +CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid); + + +CREATE TABLE ip_blacklists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + remote_ip VARCHAR(400) UNIQUE NOT NULL, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE user_blacklists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/versionable_jamtracks.sql b/db/up/versionable_jamtracks.sql new file mode 100644 index 000000000..9751bb940 --- /dev/null +++ b/db/up/versionable_jamtracks.sql @@ -0,0 +1 @@ +ALTER TABLE jam_track_rights ADD COLUMN version VARCHAR NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql new file mode 100644 index 000000000..46502b104 --- /dev/null +++ b/db/up/video_recording.sql @@ -0,0 +1,3 @@ +ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE user_authorizations ADD refresh_token VARCHAR; +ALTER TABLE recordings ADD external_video_id VARCHAR; \ No newline at end of file diff --git a/db/up/web_playable_jamtracks.sql b/db/up/web_playable_jamtracks.sql new file mode 100644 index 000000000..b3d6e8a4e --- /dev/null +++ b/db/up/web_playable_jamtracks.sql @@ -0,0 +1,16 @@ +ALTER TABLE users ADD COLUMN first_opened_jamtrack_web_player TIMESTAMP; +ALTER TABLE jam_track_rights ADD COLUMN last_stem_id VARCHAR(64) REFERENCES jam_track_tracks(id) ON DELETE SET NULL; +ALTER TABLE jam_track_tracks ADD COLUMN url_mp3_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN md5_mp3_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN length_mp3_48 BIGINT; +ALTER TABLE jam_track_tracks ADD COLUMN url_aac_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN md5_aac_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN length_aac_48 BIGINT; + +CREATE TABLE user_events ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, + name VARCHAR(100) NOT NULL, + detail JSON, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 5d5ecf100..4acccc86d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -82,6 +82,10 @@ message ClientMessage { JAM_TRACK_SIGN_COMPLETE = 260; JAM_TRACK_SIGN_FAILED = 261; + // jamtracks mixdown notifications + MIXDOWN_SIGN_COMPLETE = 270; + MIXDOWN_SIGN_FAILED = 271; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -188,6 +192,10 @@ message ClientMessage { optional JamTrackSignComplete jam_track_sign_complete = 260; optional JamTrackSignFailed jam_track_sign_failed = 261; + // jamtrack mixdown notification + optional MixdownSignComplete mixdown_sign_complete = 270; + optional MixdownSignFailed mixdown_sign_failed = 271; + // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -612,6 +620,15 @@ message JamTrackSignFailed { required int32 jam_track_right_id = 1; // jam track right id } +message MixdownSignComplete { + required string mixdown_package_id = 1; // jam track mixdown package id +} + +message MixdownSignFailed { + required string mixdown_package_id = 1; // jam track mixdown package id +} + + message SubscriptionMessage { optional string type = 1; // the type of the subscription optional string id = 2; // data about what to subscribe to, specifically diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 139dc579a..2502ca2a7 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -64,6 +64,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner" require "jam_ruby/resque/scheduled/stats_maker" require "jam_ruby/resque/scheduled/tally_affiliates" require "jam_ruby/resque/jam_tracks_builder" +require "jam_ruby/resque/jam_track_mixdown_packager" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/long_running" @@ -105,10 +106,14 @@ require "jam_ruby/models/max_mind_release" require "jam_ruby/models/genre_player" require "jam_ruby/models/genre" require "jam_ruby/models/user" +require "jam_ruby/models/user_event" require "jam_ruby/models/anonymous_user" require "jam_ruby/models/signup_hint" require "jam_ruby/models/machine_fingerprint" require "jam_ruby/models/machine_extra" +require "jam_ruby/models/download_tracker" +require "jam_ruby/models/ip_blacklist" +require "jam_ruby/models/user_blacklist" require "jam_ruby/models/fraud_alert" require "jam_ruby/models/fingerprint_whitelist" require "jam_ruby/models/rsvp_request" @@ -209,6 +214,8 @@ require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" require "jam_ruby/models/jam_track_tap_in" require "jam_ruby/models/jam_track_file" +require "jam_ruby/models/jam_track_mixdown" +require "jam_ruby/models/jam_track_mixdown_package" require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" @@ -249,6 +256,10 @@ require "jam_ruby/models/language" require "jam_ruby/models/subject" require "jam_ruby/models/band_search" require "jam_ruby/import/tency_stem_mapping" +require "jam_ruby/models/jam_track_search" +require "jam_ruby/models/gift_card" +require "jam_ruby/models/gift_card_purchase" +require "jam_ruby/models/gift_card_type" include Jampb diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav new file mode 100644 index 000000000..33c1aea5f Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav differ diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav new file mode 100644 index 000000000..3a3fd3d68 Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav differ diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb index f14d8e424..abe1b9564 100644 --- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -1,6 +1,6 @@ module JamRuby # sends out a boring ale - class AdminMailer < ActionMailer::Base + class AdminMailer < ActionMailer::Base include SendGrid @@ -20,6 +20,14 @@ module JamRuby subject: options[:subject]) end + def social(options) + mail(to: APP_CONFIG.email_social_alias, + from: APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + def recurly_alerts(user, options) body = options[:body] diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb index 6f760961e..2ab1bd292 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb @@ -1,5 +1,9 @@ -<% provide(:title, 'Confirm Email') %> +<% provide(:title, 'Welcome to JamKazam!') %> -

Welcome to JamKazam, <%= @user.first_name %>!

+

We’re delighted you have joined our community of 20,000+ musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click here to confirm this email has reached you successfully and we will then send the orientation email.

-

To confirm this email address, please go to the signup confirmation page.

+

If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.

+ +

Best Regards,
+ Team JamKazam +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb index d412a7b92..26503a494 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb @@ -1,3 +1,8 @@ -Welcome to JamKazam, <%= @user.first_name %>! +Welcome to JamKazam! -To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>. \ No newline at end of file +We’re delighted you have joined our community of 20,000+ musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click <%= @signup_confirm_url %> to confirm this email has reached you successfully and we will then send the orientation email. + +If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email. + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb index be9aa27fb..65bf1295f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb @@ -1,5 +1,7 @@ <% provide(:title, 'New Musicians You Should Check Out') %> -Hi <%= @user.first_name %>, +<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>,

+<% end %>

The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb index 05fbbd268..ee100dbff 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb @@ -1,7 +1,8 @@ New Musicians You Should Check Out +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, - +<% end %> The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend: <% @new_musicians.each do |user| %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb index 2fdc11256..769b5bcda 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb @@ -1,7 +1,8 @@ <% provide(:title, @title) %> -

Hello <%= @user.first_name %> -- -

+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>,

+<% end %>

The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb index 282c6dd90..3d1e15346 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb @@ -1,7 +1,8 @@ <% provide(:title, @title) %> -Hello <%= @user.first_name %> -- - +<% if !@user.anonymous? %> +Hi <%= @user.first_name %>, +<% end %> The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP. GENRE | NAME | DESCRIPTION | LATENCY diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb index b72d3c133..cf4816bf1 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb @@ -1,18 +1,20 @@ <% provide(:title, 'JamKazam Session Reminder') %> -
+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>, -

+

+<% end %>
-
+

This is a reminder that your JamKazam session <%= @session_name %> is scheduled for tomorrow. We hope you have fun! -

+


-
+

Best Regards,
Team JamKazam -

\ No newline at end of file +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb index c3f0576bf..333acdb74 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb @@ -1,5 +1,6 @@ +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, - +<% end %> This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun! Best Regards, diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb index 4fbc59ace..119d57b16 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb @@ -1,17 +1,20 @@ <% provide(:title, 'Your JamKazam session starts in 1 hour!') %> -
+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>, -

+

+<% end %> +
-
+

This is a reminder that your JamKazam session <%= @session_name %> starts in 1 hour. We hope you have fun! -

+


-
+

Best Regards,
Team JamKazam -

+

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb index 70726a9e6..d4719bd4c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb @@ -1,4 +1,6 @@ +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, +<% end %> This is a reminder that your JamKazam session <%=@session_name%> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb index 35e380618..177f38c92 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb @@ -1,63 +1,118 @@ <% provide(:title, 'Welcome to JamKazam!') %> +<% if !@user.anonymous? %>

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

We're delighted you have decided to join the JamKazam community of musicians, and we hope + + you will enjoy using JamKazam and JamTracks to play more music. Following are some + + resources and some things you can do to get the most out of JamKazam.

+

Playing With JamTracks
+ + JamTracks are the best way to play along with your favorite songs. Far better and different than + + traditional backing tracks, our JamTracks are complete multi-track professional recordings, with + + fully isolated tracks for each part of the music. And our free app and Internet service are packed + + with features that give you unmatched creative freedom to learn, practice, record, play with + + others, and share your performances. Here are some great JamTracks resources: +

+ + +

Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over + + the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio + + quality. You can set up online sessions that are public or private, for you alone or for others to + + join. You can find and join others’ sessions, use backing tracks and loops in sessions, make + + audio and video recordings of session performances, and more. Click here for a set of tutorial + + videos that show how to use these features. +

+ +

Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how + + unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and + + student playing together at all. JamKazam is a terrific service for teaching and taking online + + music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and + + your students in getting set up and ready to go. +

+ +

Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who + + you are as a musician, as well as your musical interests. For example, what instruments do you + + play? What musical genres do you like best? Are you interested in getting into a virtual/online + + or a real-world band? And so on. Filling out your profile will help you connect with others with + + common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then + + click on the Profile tile, and click the Edit Profile button. +

+ +

Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do + + this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the + + user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in + + this menu, and you can then use the options to invite your friends using their email addresses, + + or via Facebook, or using your Google contacts.

-

Getting Started
- There are basically three kinds of setups you can use to play on JamKazam.
-

-

+

Get Help
-

JamKazam Features
- JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch - to easily get up to speed on some of the things you can do with JamKazam:
-

-

+ If you run into trouble and need help, please reach out to us. We will be glad to do everything -

Getting Help
- If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. - You can visit our - Support Portal - to find knowledge base articles and post questions that have - not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, - you can visit our Community Forum - . -

+ we can to answer your questions and get you up and running. You can visit our Support Portal + + to find knowledge base articles and post questions that have not already been answered. You + + can email us at support@jamkazam.com. And if you just want to chat, share tips and war + + stories, and hang out with fellow JamKazamers, you can visit our Community Forum. +

- Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! +
+
+ Again, welcome to JamKazam, and we hope you have a great time here!

Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb index a9f2ed06b..52dc8ab65 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb @@ -1,4 +1,4 @@ -Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 352048dc0..12a915ce3 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -28,13 +28,13 @@ module JamRuby ##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb def gather_friends(connection, user_id) - friend_ids = [] - connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results| - friend_results.each do |friend_result| - friend_ids.push(friend_result['friend_id']) - end + friend_ids = [] + connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results| + friend_results.each do |friend_result| + friend_ids.push(friend_result['friend_id']) end - return friend_ids + end + return friend_ids end # this simulates music_session destroy callbacks with activerecord @@ -42,7 +42,7 @@ module JamRuby music_session = ActiveMusicSession.find_by_id(music_session_id) music_session.before_destroy if music_session end - + # reclaim the existing connection, if ip_address is not nil then perhaps a new address as well def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time, udp_reachable, gateway) music_session_id = nil @@ -65,11 +65,19 @@ module JamRuby isp = JamIsp.lookup(addr) #puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============") - if isp.nil? then ispid = 0 else ispid = isp.coid end + if isp.nil? then + ispid = 0 + else + ispid = isp.coid + end block = GeoIpBlocks.lookup(addr) #puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============") - if block.nil? then locid = 0 else locid = block.locid end + if block.nil? then + locid = 0 + else + locid = block.locid + end location = GeoIpLocations.find_by_locid(locid) if location.nil? || isp.nil? || block.nil? @@ -183,11 +191,19 @@ SQL isp = JamIsp.lookup(addr) #puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============") - if isp.nil? then ispid = 0 else ispid = isp.coid end + if isp.nil? then + ispid = 0 + else + ispid = isp.coid + end block = GeoIpBlocks.lookup(addr) #puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============") - if block.nil? then locid = 0 else locid = block.locid end + if block.nil? then + locid = 0 + else + locid = block.locid + end location = GeoIpLocations.find_by_locid(locid) if location.nil? || isp.nil? || block.nil? @@ -199,11 +215,11 @@ SQL lock_connections(conn) conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", - [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear + [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear # we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result| - count = result.getvalue(0, 0) .to_i + count = result.getvalue(0, 0).to_i # we're passing all this stuff so that the user record might be updated as well... blk.call(conn, count) unless blk.nil? end @@ -291,7 +307,7 @@ SQL # destroy the music_session if it's empty num_participants = nil - conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1", + conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1", [previous_music_session_id]) do |result| num_participants = result.getvalue(0, 0).to_i end @@ -324,11 +340,65 @@ SQL conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2", [user_id, previous_music_session_id]) + + update_session_controller(previous_music_session_id) end end end + def update_session_controller(music_session_id) + active_music_session = ActiveMusicSession.find(music_session_id) + + if active_music_session + music_session = active_music_session.music_session + if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller) + # find next in line, because the current 'session controller' is not part of the session + next_in_line(music_session, active_music_session) + end + end + end + + # determine who should be session controller after someone leaves + def next_in_line(music_session, active_music_session) + session_users = active_music_session.users + + # check friends 1st + session_friends = music_session.creator.friends && session_users + if session_friends.length > 0 + music_session.session_controller = session_friends[0] + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + + # check invited 2nd + invited = music_session.invited_musicians && session_users + if invited.length > 0 + music_session.session_controller = invited[0] + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + + # go by who joined earliest + earliest = active_music_session.connections.order(:joined_session_at).first + + if earliest + music_session.session_controller = earliest + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + music_session.creator + end + def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil) connection = nil @@ -349,7 +419,10 @@ SQL if connection.errors.any? raise ActiveRecord::Rollback + else + update_session_controller(music_session.id) end + end connection @@ -383,6 +456,8 @@ SQL if result.cmd_tuples == 1 @log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}") + update_session_controller(music_session.id) + JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id) session_checks(conn, previous_music_session_id, user_id) blk.call() unless blk.nil? diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index e05c8e00e..60dfe9f1b 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -51,4 +51,7 @@ module NotificationTypes JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE" JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED" + MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" + MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 662fce608..e84dff7c3 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -19,6 +19,8 @@ module ValidationMessages # sessions SESSION_NOT_FOUND = "Session not found." + NOT_FOUND = 'not found' + # genres RECORDING_GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed." BAND_GENRE_LIMIT_EXCEEDED = "No more than 3 genres are allowed." diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 403c75330..c6a6549b3 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -12,6 +12,7 @@ module JamRuby @@log = Logging.logger[JamTrackImporter] attr_accessor :name + attr_accessor :metadata attr_accessor :reason attr_accessor :detail attr_accessor :storage_format @@ -29,10 +30,265 @@ module JamRuby end def finish(reason, detail) + @@log.info("JamTrackImporter:#{self.name} #{reason} #{detail}") self.reason = reason self.detail = detail + + if ENV['END_ON_FAIL'] == "1" && reason != 'success' && reason != 'jam_track_exists' + raise "#{reason} #{detail}" + end end + def import_click_track(jam_track) + # we need to download the click track, if it exists. + Dir.mktmpdir do |tmp_dir| + + @@log.info("importing clicking track for #{jam_track.original_artist}:#{jam_track.name}") + + if jam_track.click_track + @@log.info("already has click track: #{jam_track.original_artist}:#{jam_track.name}") + finish('success', 'already_has_click_track') + return + end + + click_track_file = jam_track.click_track_file + if click_track_file.nil? + @@log.info("no click track for #{jam_track.original_artist}:#{jam_track.name}") + finish('success', 'no_click_track') + return + end + + original_filename = click_track_file[:original_filename] + + if original_filename.nil? + @@log.info("no click track s3 path for #{jam_track.original_artist}:#{jam_track.name}") + finish('no_original_source', 'click track is missing s3 path:' + click_track_file.id) + return + end + + #wav_file = File.join(tmp_dir, File.basename(click_track_file[:original_filename])) + #JamTrackImporter.song_storage_manager.download(click_track_file[:original_filename], wav_file) + + JamTrack.transaction do + click_track = jam_track.click_track + + if click_track.nil? + click_track = JamTrackTrack.new + click_track.original_filename = click_track_file[:original_filename] + click_track.original_audio_s3_path = click_track_file[:original_filename] + click_track.track_type = 'Click' + click_track.part = 'Clicktrack' + click_track.instrument_id = 'computer' + click_track.jam_track = jam_track + click_track.position = 10000 + if !click_track.save + @@log.error("unable to create jamtrack click track #{click_track.errors.inspect}") + finish("jam_track_click", "unable to create: #{click_track.errors.inspect}") + return false + end + end + + jam_track.increment_version! + + # with the click track in hand, flesh out the details + synchronize_audio_track(jam_track, tmp_dir, false, click_track) + + finish('success', nil) + end + end + end + + def generate_jmep(jam_track) + if !jam_track.blank? + finish('success', 'jmep already exists') + return + else + # we need to download the click track, if it exists. + Dir.mktmpdir do |tmp_dir| + + master_track = jam_track.master_track + + click_track = jam_track.click_track_file + + if master_track.nil? + finish('no_master_track', nil) + return + end + + master_track_file = File.join(tmp_dir, File.basename(master_track[:url_48])) + begin + JamTrackImporter.private_s3_manager.download(master_track.url_by_sample_rate(44), master_track_file) + rescue Exception => e + @@log.error("unable to download master track") + finish("no-download-master", master_track.url_by_sample_rate(44)) + return + end + + if click_track + click_track_file = File.join(tmp_dir, File.basename(click_track[:original_filename])) + JamTrackImporter.song_storage_manager.download(click_track[:original_filename], click_track_file) + else + # we'll use the master for click analysis. not ideal, but would work + click_track_file = master_track_file + end + + start_time = determine_start_time(master_track_file, tmp_dir, master_track[:url]) + + # bpm comes from git clone http://www.pogo.org.uk/~mark/bpm-tools.git + + sox="sox #{Shellwords.escape(click_track_file)} -t raw -r 44100 -e float -c 1 - | bpm" + cmd = "bash -c #{Shellwords.escape(sox)}" + @@log.debug("executing cmd #{cmd}") + output=`#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + bpm = output.to_f + + @@log.debug("bpm: #{bpm} start_time: #{start_time}") + + metro_fin = "#{Time.at(start_time).utc.strftime("%H:%M:%S")}:#{((start_time - start_time.to_i) * 1000).round}" + + jmep = "" + jmep << "# created via code using bpm/silence detection (bpm:#{bpm})\r\n" + jmep << "prelude@10.0 #number of seconds before music starts\r\n" + jmep << "metro_fin@#{metro_fin} bpm=#{bpm}, ticks=8, pmode=stream, name=Beep, play=mono" + + @@log.info("jmep generated: #{jmep}") + + jam_track.jmep_text = jmep + if jam_track.save + finish('success', nil) + else + @@log.error("jamtrack did not save. #{jam_track.errors.inspect}") + finish("no-save", "jamtrack did not save. #{jam_track.errors.inspect}") + return + end + else + finish("bpm-fail", "failed to run bpm: #{output}") + return + end + end + end + end + + + + def determine_start_time(audio_file, tmp_dir, original_filename) + burp_gaps = ['0.3', '0.2', '0.1', '0.05'] + + out_wav = File.join(tmp_dir, 'stripped.wav') + total_time_command = "soxi -D \"#{audio_file}\"" + total_time = `#{total_time_command}`.to_f + + result_code = -20 + stripped_time = total_time # default to the case where we just start the preview at the beginning + + burp_gaps.each do |gap| + command_strip_lead_silence = "sox \"#{audio_file}\" \"#{out_wav}\" silence 1 #{gap} 1%" + + @@log.debug("stripping silence: " + command_strip_lead_silence) + + output = `#{command_strip_lead_silence}` + + result_code = $?.to_i + + if result_code == 0 + stripped_time_command = "soxi -D \"#{out_wav}\"" + stripped_time_test = `#{stripped_time_command}`.to_f + + if stripped_time_test < 1 # meaning a very short duration + @@log.warn("could not determine the start of non-silence. assuming beginning") + stripped_time = total_time # default to the case where we just start the preview at the beginning + else + stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break + break + end + else + @@log.warn("unable to determine silence for jam_track #{original_filename}, #{output}") + stripped_time = total_time # default to the case where we just start the preview at the beginning + end + end + + preview_start_time = total_time - stripped_time + + preview_start_time + end + + def synchronize_preview_dev(jam_track) + jam_track.jam_track_tracks.each do |track| + next if track.track_type != 'Master' + + + most_recent_aac = nil + most_recent_ogg = nil + most_recent_mp3 = nil + public_jamkazam_s3_manager.list_files(track.preview_directory).each do |s3_preview_item| + + s3_object = public_jamkazam_s3_manager.object(s3_preview_item) + + if s3_preview_item.end_with?('.aac') + if most_recent_aac + if s3_object.last_modified > most_recent_aac.last_modified + most_recent_aac = s3_object + end + else + most_recent_aac = s3_object + end + end + + if s3_preview_item.end_with?('.mp3') + if most_recent_mp3 + if s3_object.last_modified > most_recent_mp3.last_modified + most_recent_mp3 = s3_object + end + else + most_recent_mp3 = s3_object + end + end + + if s3_preview_item.end_with?('.ogg') + if most_recent_ogg + if s3_object.last_modified > most_recent_ogg.last_modified + most_recent_ogg = s3_object + end + else + most_recent_ogg = s3_object + end + end + end + + if most_recent_aac + track['preview_aac_md5'] = 'md5' + track['preview_aac_url'] = most_recent_aac.key + track['preview_aac_length'] = most_recent_aac.content_length + end + + if most_recent_mp3 + track['preview_mp3_md5'] = 'md5' + track['preview_mp3_url'] = most_recent_mp3.key + track['preview_mp3_length'] = most_recent_mp3.content_length + end + + if most_recent_ogg + track['preview_md5'] = 'md5' + track['preview_url'] = most_recent_ogg.key + track['preview_length'] = most_recent_ogg.content_length + end + + track.save + end + end + + def create_silence(tmp_dir, segment_count, duration, sample_rate, channels = 2) + file = File.join(tmp_dir, "#{segment_count}.wav") + + # -c 2 means stereo + cmd("sox -n -r #{sample_rate} -c #{channels} #{file} trim 0.0 #{duration}", "silence") + + file + end # this method was created due to Tency-sourced data having no master track # it goes through all audio tracks, and creates a master mix from it. (mix + normalize) @@ -116,7 +372,6 @@ module JamRuby end - temp_file = File.join(tmp_dir, "temp.wav") output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav") output_file = File.join(tmp_dir, output_filename) @@ -149,7 +404,8 @@ module JamRuby # now we need to upload the output back up s3_target = audio_path + '/' + output_filename @@log.debug("uploading #{output_file} to #{s3_target}") - JamTrackImporter.song_storage_manager.upload(s3_target, output_file ) + JamTrackImporter.song_storage_manager.upload(s3_target, output_file) + finish('success', nil) end @@ -159,6 +415,9 @@ module JamRuby end def dry_run(metadata, metalocation) + # STDIN.gets + + @@log.debug("dry_run: #{metadata.inspect}") metadata ||= {} parsed_metalocation = parse_metalocation(metalocation) @@ -195,7 +454,7 @@ module JamRuby meta[:licensor] = vendor - File.open(meta_yml, 'w') {|f| f.write meta.to_yaml } + File.open(meta_yml, 'w') { |f| f.write meta.to_yaml } jamkazam_s3_manager.upload(metalocation, meta_yml) end @@ -206,6 +465,16 @@ module JamRuby @storage_format == 'Tency' end + def is_paris_storage? + assert_storage_set + @storage_format == 'Paris' + end + + def is_tim_tracks_storage? + assert_storage_set + @storage_format == 'TimTracks' + end + def assert_storage_set raise "no storage_format set" if @storage_format.nil? end @@ -213,7 +482,7 @@ module JamRuby def parse_metalocation(metalocation) # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml - if is_tency_storage? + if is_tency_storage? || is_tim_tracks_storage? suffix = '/meta.yml' @@ -242,16 +511,67 @@ module JamRuby return nil end - last_dash = metalocation.rindex('-') - if last_dash - song = metalocation[(first_dash+3)...last_dash].strip + if is_tim_tracks_storage? + song = metalocation[(first_dash+3)..-1].strip bits << song - else + elsif is_tency_storage? + last_dash = metalocation.rindex('-') + if last_dash + song = metalocation[(first_dash+3)...last_dash].strip + bits << song + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + end + + bits << 'meta.yml' + bits + elsif is_paris_storage? + suffix = '/meta.yml' + + unless metalocation.end_with? suffix finish("invalid_metalocation", "metalocation not valid #{metalocation}") return nil end + metalocation = metalocation[0...-suffix.length] + + first_path = metalocation.index('/') + if first_path.nil? + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + metalocation = metalocation[(first_path + 1)..-1] + + bits = ['audio'] + + + last_slash = metalocation.rindex('/') + + # example: S4863-Mike Oldfield-Moonlight Shadow-000bpm + + + if last_slash + paris_artist_song_id = metalocation[0...last_slash] + else + paris_artist_song_id = metalocation + end + + + bitbits = paris_artist_song_id.split('-') + song_id = bitbits[0].strip + + artist = bitbits[1] + song_name = bitbits[2] + bpm = bitbits[-1] + + bits << artist + bits << song_name + bits << 'meta.yml' + bits << song_id + bits << bpm bits else bits = metalocation.split('/') @@ -275,24 +595,14 @@ module JamRuby end end - # if you change this, it will (at least without some work )break development usage of jamtracks - def gen_plan_code(original_artist, name) - # remove all non-alphanumeric chars from artist as well as name - artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase - name_code = name.gsub(/[^0-9a-z]/i, '').downcase - "jamtrack-#{artist_code[0...20]}-#{name_code}"[0...50] # make sure it's a max of 50 long - end - def dry_run_metadata(metadata, original_artist, name) self.name = metadata["name"] || name original_artist = metadata["original_artist"] || original_artist - plan_code = metadata["plan_code"] || gen_plan_code(original_artist, self.name) description = metadata["description"] @@log.debug("#{self.name} original_artist=#{original_artist}") - @@log.debug("#{self.name} plan_code=#{plan_code}") true end @@ -309,7 +619,7 @@ module JamRuby genres << Genre.find('holiday') elsif genre == 'alternative' genres << Genre.find('alternative rock') - elsif genre == '80s' + elsif genre == '80s' || genre == "50's" || genre == "60's" || genre == "70's" || genre == "80's" || genre == "90's" || genre == "50/60's" || genre == "00's" || genre == "2010's" # swallow elsif genre == 'love' # swallow @@ -336,14 +646,62 @@ module JamRuby # swallow elsif genre == 'oriental' genres << Genre.find('asian') + elsif genre == 'abba' + genres << Genre.find('pop') + elsif genre == 'movies tv show' || genre == "movies" + genres << Genre.find('tv & movie soundtrack') + elsif genre == 'ballad' + # swallow + elsif genre == "r'n'b" || genre == "pop rnb" + genres << Genre.find("r&b") + elsif genre == "rock & roll" + genres << Genre.find('rock') + elsif genre == "dance pop" + genres << Genre.find('dance') + elsif genre == "soul/motown" || genre == "soul motown" + genres << Genre.find('soul') + elsif genre == "party" + # swallow + elsif genre == "reggae/ska" || genre == "reggae ska" + genres << Genre.find('reggae') + genres << Genre.find('ska') + elsif genre == "pop rock" || genre == "pop/rock" + genres << Genre.find("rock") + genres << Genre.find("pop") + elsif genre == "singalong" + #swallow + elsif genre == "folk rock" + genres << Genre.find('folk') + genres << Genre.find('rock') + elsif genre == "swing" || genre == "swing/big band" || genre == "swing big band" + genres << Genre.find('oldies') + elsif genre == "rap/hip hop" || genre == "rap hip hop" + genres << Genre.find("rap") + elsif genre == "folk traditional" || genre == "folk/traditional" + genres << Genre.find('folk') + elsif genre == "elvis" + genres << Genre.find('rock') + elsif genre == "irish" + genres << Genre.find('celtic') + elsif genre == "dance/pop" + genres << Genre.find('dance') + genres << Genre.find('pop') + elsif genre == "the beatles" + genres << Genre.find("rock") else found = Genre.find_by_id(genre) - genres << found if found + genres << found if found end end end + # just throw them into rock/pop if not known. can fix later... + if genres.length == 0 + genres << Genre.find('rock') + genres << Genre.find('pop') + end + genres end @@ -408,7 +766,7 @@ module JamRuby prevent_concurrent_processing(metalocation) if jam_track.new_record? - latest_jamtrack = JamTrack.order('created_at desc').first + latest_jamtrack = JamTrack.order('id::int desc').first id = latest_jamtrack.nil? ? 1 : latest_jamtrack.id.to_i + 1 if ENV['NODE_NUMBER'] @@ -418,9 +776,9 @@ module JamRuby node_count = ENV['NODE_COUNT'].to_i raise "NO NODE_COUNT" if node_count == 0 r = id % node_count - id = r + id # get to the same base number if both are working at the same time + id = (node_count - r) + id # get to the same base number if both are working at the same time id = id + node_number # offset by your node number - @@log.debug("JAM TRACK ID: #{id}") + @@log.info("JAM TRACK ID: #{id}") end jam_track.id = "#{id}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that jam_track.status = 'Staging' @@ -431,7 +789,6 @@ module JamRuby jam_track.year = metadata[:year] jam_track.genres = determine_genres(metadata) jam_track.language = determine_language(metadata) - jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name) jam_track.price = 1.99 jam_track.reproduction_royalty_amount = nil jam_track.reproduction_royalty = true @@ -444,13 +801,30 @@ module JamRuby jam_track.alternative_license_status = false jam_track.hfa_license_desired = true jam_track.server_fixation_date = Time.now - jam_track.slug = metadata['slug'] || jam_track.generate_slug - if is_tency_storage? jam_track.vendor_id = metadata[:id] - jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music') + jam_track.licensor = JamTrackLicensor.find_by_name!('Tency Music') #add_licensor_metadata('Tency Music', metalocation) + elsif is_paris_storage? + raise 'no vendor id' if metadata[:id].nil? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Paris Music') + jam_track.bpm = metadata[:bpm] + elsif is_tim_tracks_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Tim Waurick') end + jam_track.slug = metadata['slug'] + if jam_track.slug.nil? + jam_track.generate_slug + end + jam_track.plan_code = metadata["plan_code"] + if jam_track.plan_code.nil? + jam_track.gen_plan_code + end + + + else if !options[:resync_audio] #@@log.debug("#{self.name} skipped because it already exists in database") @@ -511,8 +885,11 @@ module JamRuby instrument = 'acoustic guitar' elsif potential_instrument == 'acoutic guitar' instrument = 'electric guitar' - elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' + elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' || potential_instrument == 'elec guitar' instrument = 'electric guitar' + elsif potential_instrument == 'lead guitar' + instrument = 'electric guitar' + part = 'Lead' elsif potential_instrument == 'keys' instrument = 'keyboard' elsif potential_instrument == 'vocal' || potential_instrument == 'vocals' @@ -555,7 +932,7 @@ module JamRuby instrument = 'computer' part = 'Bells' elsif potential_instrument == 'percussion' - instrument = 'drums' + instrument = 'percussion' part = 'Percussion' elsif potential_instrument == 'fretless bass' instrument = 'bass guitar' @@ -583,8 +960,9 @@ module JamRuby elsif potential_instrument == 'strings' instrument = 'orchestra' part = 'Strings' - elsif potential_instrument == 'celesta' + elsif potential_instrument == 'celesta' || potential_instrument == 'celeste' instrument = 'keyboard' + part = 'Celesta' elsif potential_instrument == 'balalaika' instrument = 'other' part = 'Balalaika' @@ -598,8 +976,11 @@ module JamRuby instrument = 'other' part = 'Bouzouki' elsif potential_instrument == 'claps' || potential_instrument == 'hand claps' - instrument = 'computer' + instrument = 'other' part = 'Claps' + elsif potential_instrument == 'snaps' || potential_instrument == 'snap' + instrument = 'other' + part = 'Snaps' else found_instrument = Instrument.find_by_id(potential_instrument) if found_instrument @@ -631,7 +1012,7 @@ module JamRuby part = nil precount_num = nil no_precount_detail = nil - if comparable_filename == "click" || comparable_filename.include?("clicktrack") + if comparable_filename == "click" || comparable_filename.include?("clicktrack") || comparable_filename.include?("click track") || comparable_filename.end_with?('click') || comparable_filename.end_with?('click trac') if filename.end_with?('.txt') type = :clicktxt else @@ -650,8 +1031,7 @@ module JamRuby precount_num = precount.to_i end - - elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") + elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") || (@metadata && (@metadata[:id] && comparable_filename.start_with?(@metadata[:id].downcase))) master = true type = :master else @@ -706,24 +1086,80 @@ module JamRuby instrument = result[:instrument] part = result[:part] end + elsif is_paris_storage? + # example: Eternal Flame-Guide Lead Vocal.wav + # or with a part: Eternal Flame-Keyboard-Stab.wav + bits = filename_no_ext.split('-') + bits.collect! { |bit| bit.strip } + + while true + instrument, part = paris_instrument_parse(bits) + + if instrument.nil? && bits.length > 2 + bits.shift + instrument, part = paris_instrument_parse(bits) + else + break + end + end end end - end - {filename: filename, master: master, instrument: instrument, part: part, type: type, precount_num: precount_num, no_precount_detail: no_precount_detail} end + def paris_instrument_parse(bits) + instrument = nil + part = nil + possible_instrument = nil + possible_part = nil + + if bits.length == 2 + # second bit is instrument + possible_instrument = bits[1] + elsif bits.length == 3 + # second bit is instrument, third bit is part + possible_instrument = bits[1] + possible_part = bits[2] + elsif bits.length >= 4 + possible_instrument = bits[1] + possible_part = "#{bits[2]} #{bits[3]}" + end + + # otherwise, try mapping + if instrument.nil? && possible_instrument + mapping = JamTrackImporter.paris_mapping[possible_instrument.downcase] + if mapping + instrument = mapping[:instrument].downcase + part = mapping[:part] + part = nil if part.blank? + end + end + + # paris mapping didn't work; let's retry one more time with our own home-grown mapping + if instrument.nil? + result = determine_instrument(possible_instrument, possible_part) + instrument = result[:instrument] + part = result[:part] + end + return instrument, part + end + def dry_run_audio(metadata, s3_path) all_files = fetch_important_files(s3_path) + masters = 0 all_files.each do |file| # ignore click/precount parsed_wav = parse_file(file) if parsed_wav[:master] @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + masters += 1 + if masters > 1 + JamTrackImporter.summaries[:multiple_masters] += 1 + end elsif parsed_wav[:type] == :track JamTrackImporter.summaries[:total_tracks] += 1 @@ -770,9 +1206,9 @@ module JamRuby instrument_weight = nil # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end - if track.persisted? - instrument_weight = track.position - else + #if track.persisted? + # instrument_weight = track.position + #else if track.instrument_id == 'voice' if track.part && track.part.start_with?('Lead') @@ -792,7 +1228,8 @@ module JamRuby else instrument_weight = 170 end - + elsif track.instrument_id == 'percussion' + instrument_weight = 175 elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass' instrument_weight = 180 @@ -834,7 +1271,11 @@ module JamRuby if track.track_type == 'Master' instrument_weight = 1000 end - end + + if track.track_type == 'Click' + instrument_weight = 10000 + end + #end instrument_weight @@ -895,26 +1336,70 @@ module JamRuby end end - # default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track position = 1 sorted_tracks.each do |track| - if track.persisted? - # persisted tracks should be sorted at the beginning of the sorted_tracks, - # so this just keeps moving the 'position builder' up to +1 of the last persisted track - position = track.position + 1 - else - track.position = position - position = position + 1 - end - - + track.position = position + position = position + 1 end - sorted_tracks[sorted_tracks.length - 1].position = 1000 + # get click/master tracks position re-set correctly + + last_track = sorted_tracks[sorted_tracks.length - 1] + second_to_last = sorted_tracks[sorted_tracks.length - 2] + + if last_track.track_type == 'Master' + last_track.position = 1000 + elsif last_track.track_type == 'Click' + last_track.position = 10000 + end + + if second_to_last.track_type == 'Master' + second_to_last.position = 1000 + elsif second_to_last.track_type == 'Click' + second_to_last.position = 10000 + end sorted_tracks end + # this will put original_audio_s3_path on each jam_track_track + def associate_tracks_with_original_stems(jam_track, s3_path) + attempt_to_match_existing_tracks = true + + # find all wav files in the JamTracks s3 bucket + wav_files = fetch_important_files(s3_path) + + tracks = [] + + wav_files.each do |wav_file| + + if attempt_to_match_existing_tracks + # try to find a matching track from the JamTrack based on the name of the 44.1 path + basename = File.basename(wav_file) + ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" + + found_track = nil + jam_track.jam_track_tracks.each do |jam_track_track| + + if jam_track_track["url_44"] && jam_track_track["url_44"].end_with?(ogg_44100_filename) + # found a match! + found_track = jam_track_track + break + end + end + + if found_track + @@log.debug("found a existing track to reuse") + found_track.original_audio_s3_path = wav_file + tracks << found_track + next + end + end + end + + tracks + end + def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload) attempt_to_match_existing_tracks = true @@ -952,52 +1437,9 @@ module JamRuby @@log.debug("no existing track found; creating a new one") - track = JamTrackTrack.new - track.original_filename = wav_file - track.original_audio_s3_path = wav_file - - file = JamTrackFile.new - file.original_filename = wav_file - file.original_audio_s3_path = wav_file - - parsed_wav = parse_file(wav_file) - - unknowns = 0 - if parsed_wav[:master] - track.track_type = 'Master' - track.part = 'Master Mix' - track.instrument_id = 'computer' - tracks << track - @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - elsif parsed_wav[:type] == :track - - if !parsed_wav[:instrument] || !parsed_wav[:part] - @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - unknowns += 1 - else - @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - end - - - track.instrument_id = parsed_wav[:instrument] || 'other' - track.track_type = 'Track' - track.part = parsed_wav[:part] || "Other #{unknowns}" - tracks << track - elsif parsed_wav[:type] == :clicktxt - file.file_type = 'ClickTxt' - addt_files << file - elsif parsed_wav[:type] == :clickwav - file.file_type = 'ClickWav' - addt_files << file - elsif parsed_wav[:type] == :precount - file.file_type = 'Precount' - file.precount_num = parsed_wav[:precount_num] - addt_files << file - else - finish("unknown_file_type", "unknown file type #{wave_file}") + if !assign_instrument_parts(wav_file, tracks, addt_files) return false end - end jam_track.jam_track_tracks.each do |jam_track_track| @@ -1033,101 +1475,130 @@ module JamRuby return synchronize_audio_files(jam_track, skip_audio_upload) end + def reassign_instrument_parts(jam_track) + + tracks = [] + addt_files = [] + + jam_track.jam_track_tracks.each do |track| + return if !assign_instrument_parts(track.original_filename, tracks, addt_files, true) + end + + @@log.info("sorting #{tracks.length} tracks") + tracks = sort_tracks(tracks) + + deduplicate_parts(tracks) + + changed = false + tracks.each do |track| + if track.changed? + changed = true + puts "CHANGE: #{track.changes.inspect}" + track.skip_inst_part_uniq = true + track.save! + end + end + + if changed + # if we messed up any instrument/parts by making a dup, this will catch it + tracks.each do |track| + track.skip_inst_part_uniq = false + track.save! + end + end + end + + def assign_instrument_parts(wav_file, tracks, addt_files, reassign = false) + if !reassign + track = JamTrackTrack.new + track.original_filename = wav_file + track.original_audio_s3_path = wav_file + + file = JamTrackFile.new + file.original_filename = wav_file + file.original_audio_s3_path = wav_file + else + matches = JamTrackTrack.where(original_filename: wav_file) + if matches.count > 1 + raise "multiple jam track tracks encountered with #{wav_file} as original_filename" + elsif matches.count == 0 + raise "unable to locate jam track wit h#{wav_file} as original_filename" + end + track = matches[0] + track.original_audio_s3_path = wav_file + file = nil + end + + parsed_wav = parse_file(wav_file) + + unknowns = 0 + if parsed_wav[:master] + track.track_type = 'Master' + track.part = 'Master Mix' + track.instrument_id = 'computer' + tracks << track + @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + elsif parsed_wav[:type] == :track + + if !parsed_wav[:instrument] || !parsed_wav[:part] + @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + unknowns += 1 + else + @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + end + + track.instrument_id = parsed_wav[:instrument] || 'other' + track.track_type = 'Track' + track.part = parsed_wav[:part]; + tracks << track + elsif parsed_wav[:type] == :clicktxt + file.file_type = 'ClickTxt' + addt_files << file + elsif parsed_wav[:type] == :clickwav + if file + file.file_type = 'ClickWav' + addt_files << file + end + + # and also add a JamTrackTrack for this click track + track.track_type = 'Click' + track.part = 'Clicktrack' + track.instrument_id = 'computer' + track.position = 10000 + tracks << track + elsif parsed_wav[:type] == :precount + file.file_type = 'Precount' + file.precount_num = parsed_wav[:precount_num] + addt_files << file + else + finish("unknown_file_type", "unknown file type #{wave_file}") + return false + end + + return true + end + def synchronize_audio_files(jam_track, skip_audio_upload) begin Dir.mktmpdir do |tmp_dir| + # download each jam track here, and then do processing to determine: + # what's the longest stem + # and to then pad the rest of the tracks to make them all match in length jam_track.jam_track_tracks.each do |track| - basename = File.basename(track.original_audio_s3_path) - s3_dirname = File.dirname(track.original_audio_s3_path) + wav_file = File.join(tmp_dir, basename) - # make a 44100 version, and a 48000 version - ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" - ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg" + # bring the original wav file down from S3 to local file system + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) + track.wav_file = wav_file + end - ogg_44100_s3_path = track.filename(ogg_44100_filename) - ogg_48000_s3_path = track.filename(ogg_48000_filename) + same_lengthening(jam_track, tmp_dir) - track.skip_uploader = true - - if skip_audio_upload - track["url_44"] = ogg_44100_s3_path - track["md5_44"] = 'md5' - track["length_44"] = 1 - - track["url_48"] = ogg_48000_s3_path - track["md5_48"] = 'md5' - track["length_48"] = 1 - - # we can't fake the preview as easily because we don't know the MD5 of the current item - #track["preview_md5"] = 'md5' - #track["preview_mp3_md5"] = 'md5' - #track["preview_url"] = track.preview_filename('md5', 'ogg') - #track["preview_length"] = 1 - #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') - #track["preview_mp3_length"] = 1 - #track["preview_start_time"] = 0 - else - wav_file = File.join(tmp_dir, basename) - - # bring the original wav file down from S3 to local file system - JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) - - sample_rate = `soxi -r "#{wav_file}"`.strip - - ogg_44100 = File.join(tmp_dir, ogg_44100_filename) - ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg") - - if sample_rate == "44100" - `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"` - else - `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"` - end - - if sample_rate == "48000" - `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"` - else - `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"` - end - - # upload the new ogg files to s3 - @@log.debug("uploading 44100 to #{ogg_44100_s3_path}") - - jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100) - - @@log.debug("uploading 48000 to #{ogg_48000_s3_path}") - - jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000) - - ogg_44100_digest = ::Digest::MD5.file(ogg_44100) - # and finally update the JamTrackTrack with the new info - track["url_44"] = ogg_44100_s3_path - track["md5_44"] = ogg_44100_digest.hexdigest - track["length_44"] = File.new(ogg_44100).size - - track["url_48"] = ogg_48000_s3_path - track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest - track["length_48"] = File.new(ogg_48000).size - - synchronize_duration(jam_track, ogg_44100) - jam_track.save! - - # convert entire master ogg file to mp3, and push both to public destination - if track.track_type == 'Master' - preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) - - if !preview_succeeded - return false - end - elsif track.track_type == 'Track' - synchronize_track_preview(track, tmp_dir, ogg_44100) - end - - end - - track.save! + jam_track.jam_track_tracks.each do |track| + synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track) end end rescue Exception => e @@ -1138,6 +1609,287 @@ module JamRuby return true end + # make all stems be the same length + def same_lengthening(jam_track, tmp_dir) + longest_duration = nil + jam_track.jam_track_tracks.each do |track| + duration_command = "soxi -D \"#{track.wav_file}\"" + output = `#{duration_command}` + + result_code = $?.to_i + + if result_code == 0 + duration = output.to_f.round + + track.tmp_duration = duration + if longest_duration.nil? + longest_duration = duration + else + if duration > longest_duration + longest_duration = duration + end + end + else + @@log.warn("unable to determine duration for jam_track track #{jam_track.name} #{jam_track_track.instrument} #{jam_track_track.part}. output #{output}") + end + end + + @@log.info("duration determined to be #{longest_duration}") + jam_track.duration = longest_duration + jam_track.jam_track_tracks.each do |track| + if track.tmp_duration < longest_duration + # need to pad with silence to make all match in length + + amount = longest_duration - track.tmp_duration + + @@log.info("track #{track.instrument_id}:#{track.part} needs to be lengthened by #{amount}") + + output = cmd("soxi -c \"#{track.wav_file}\"", "padded_silence") + channels = output.to_i + + output = cmd("soxi -r \"#{track.wav_file}\"", "get_sample_rate") + sample_rate = output.to_i + + create_silence(tmp_dir, "padded_silence#{track.id}", amount, sample_rate, channels) + + output_file = File.join(tmp_dir, "with_padding_#{track.id}.wav") + + cmd("sox \"#{track.wav_file}\" \"#{output_file}\"", "same_lengthening") + + track.wav_file = output_file + end + end + end + + def cmd(cmd, type) + + @@log.debug("executing #{cmd}") + + output = `#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + output + else + @error_reason = type + "_fail" + @error_detail = "#{cmd}, #{output}" + raise "command `#{cmd}` failed. #{type}, #{output}" + end + end + + def synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track) + basename = File.basename(track.original_audio_s3_path) + + # make a 44100 version, and a 48000 version + ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" + ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg" + + + # make a 44100 version, and a 48000 version + mp3_48000_filename = File.basename(basename, ".wav") + "-48000.mp3" + aac_48000_filename = File.basename(basename, ".wav") + "-48000.aac" + + ogg_44100_s3_path = track.filename(ogg_44100_filename) + ogg_48000_s3_path = track.filename(ogg_48000_filename) + + mp3_48000_s3_path = track.filename(mp3_48000_filename) + aac_48000_s3_path = track.filename(aac_48000_filename) + + track.skip_uploader = true + + if skip_audio_upload + track["url_44"] = ogg_44100_s3_path + track["md5_44"] = 'md5' + track["length_44"] = 1 + + track["url_48"] = ogg_48000_s3_path + track["md5_48"] = 'md5' + track["length_48"] = 1 + + track["url_mp3_48"] = mp3_48000_filename + track["md5_mp3_48"] = 'md5' + track["length_mp3_48"] = 1 + + track["url_aac_48"] = aac_48000_filename + track["md5_aac_48"] = 'md5' + track["length_aac_48"] = 1 + + # we can't fake the preview as easily because we don't know the MD5 of the current item + #track["preview_md5"] = 'md5' + #track["preview_mp3_md5"] = 'md5' + #track["preview_url"] = track.preview_filename('md5', 'ogg') + #track["preview_length"] = 1 + #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') + #track["preview_mp3_length"] = 1 + #track["preview_start_time"] = 0 + else + wav_file = track.wav_file + + sample_rate = `soxi -r "#{wav_file}"`.strip + + ogg_44100 = File.join(tmp_dir, ogg_44100_filename) + ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg") + + if sample_rate == "44100" + `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"` + else + `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"` + end + + if sample_rate == "48000" + `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"` + else + `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"` + end + + # upload the new ogg files to s3 + @@log.debug("uploading 44100 to #{ogg_44100_s3_path}") + + jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100) + + @@log.debug("uploading 48000 to #{ogg_48000_s3_path}") + + jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000) + + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + # and finally update the JamTrackTrack with the new info + track["url_44"] = ogg_44100_s3_path + track["md5_44"] = ogg_44100_digest.hexdigest + track["length_44"] = File.new(ogg_44100).size + + track["url_48"] = ogg_48000_s3_path + track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest + track["length_48"] = File.new(ogg_48000).size + + # now create mp3 and aac files + mp3_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.mp3") + aac_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.aac") + + `ffmpeg -i "#{wav_file}" -ar 48000 -ab 192k "#{mp3_48000}"` + + `ffmpeg -i "#{wav_file}" -c:a libfdk_aac -b:a 192k "#{aac_48000}"` + + # upload the new ogg files to s3 + @@log.debug("uploading mp3 48000 to #{mp3_48000_s3_path}") + + jamkazam_s3_manager.upload(mp3_48000_s3_path, mp3_48000) + + @@log.debug("uploading aac 48000 to #{aac_48000_s3_path}") + + jamkazam_s3_manager.upload(aac_48000_s3_path, aac_48000) + + mp3_48000_digest = ::Digest::MD5.file(mp3_48000) + # and finally update the JamTrackTrack with the new info + track["url_mp3_48"] = mp3_48000_s3_path + track["md5_mp3_48"] = mp3_48000_digest.hexdigest + track["length_mp3_48"] = File.new(mp3_48000).size + + track["url_aac_48"] = aac_48000_s3_path + track["md5_aac_48"] = ::Digest::MD5.file(aac_48000).hexdigest + track["length_aac_48"] = File.new(aac_48000).size + + jam_track.save! + + # convert entire master ogg file to mp3, and push both to public destination + if track.track_type == 'Master' + preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) + + if !preview_succeeded + return false + end + elsif track.track_type == 'Track' || track.track_type == 'Click' + synchronize_track_preview(track, tmp_dir, ogg_44100) + end + + end + + track.save! + end + + def generate_mp3_aac_stem(jam_track, tmp_dir, skip_audio_upload) + jam_track.jam_track_tracks.each do |track| + + if track.original_audio_s3_path.nil? + + @@log.error("jam_track #{jam_track.name} has empty stem. stem: #{track.id}") + next + end + + puts "track.original_audio_s3_path #{track.original_audio_s3_path}" + basename = File.basename(track.original_audio_s3_path) + s3_dirname = File.dirname(track.original_audio_s3_path) + + # make a 44100 version, and a 48000 version + mp3_48000_filename = File.basename(basename, ".wav") + "-48000.mp3" + aac_48000_filename = File.basename(basename, ".wav") + "-48000.aac" + + mp3_48000_s3_path = track.filename(mp3_48000_filename) + aac_48000_s3_path = track.filename(aac_48000_filename) + + puts "mp3_48000_s3_path #{mp3_48000_s3_path}" + track.skip_uploader = true + + if skip_audio_upload + track["url_mp3_48"] = mp3_48000_filename + track["md5_mp3_48"] = 'md5' + track["length_mp3_48"] = 1 + + track["url_aac_48"] = aac_48000_filename + track["md5_aac_48"] = 'md5' + track["length_aac_48"] = 1 + + # we can't fake the preview as easily because we don't know the MD5 of the current item + #track["preview_md5"] = 'md5' + #track["preview_mp3_md5"] = 'md5' + #track["preview_url"] = track.preview_filename('md5', 'ogg') + #track["preview_length"] = 1 + #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') + #track["preview_mp3_length"] = 1 + #track["preview_start_time"] = 0 + else + wav_file = File.join(tmp_dir, basename) + + # the wave file might already be on the system... + + # don't bother with the same track twice + + next if track["url_mp3_48"] && track["url_aac_48"] + + # bring the original wav file down from S3 to local file system + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) unless File.exists?(wav_file) + + mp3_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.mp3") + aac_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.aac") + + `ffmpeg -i "#{wav_file}" -ar 48000 -ab 192k "#{mp3_48000}"` + + `ffmpeg -i "#{wav_file}" -c:a libfdk_aac -b:a 192k "#{aac_48000}"` + + # upload the new ogg files to s3 + @@log.debug("uploading mp3 48000 to #{mp3_48000_s3_path}") + + jamkazam_s3_manager.upload(mp3_48000_s3_path, mp3_48000) + + @@log.debug("uploading aac 48000 to #{aac_48000_s3_path}") + + jamkazam_s3_manager.upload(aac_48000_s3_path, aac_48000) + + mp3_48000_digest = ::Digest::MD5.file(mp3_48000) + # and finally update the JamTrackTrack with the new info + track["url_mp3_48"] = mp3_48000_s3_path + track["md5_mp3_48"] = mp3_48000_digest.hexdigest + track["length_mp3_48"] = File.new(mp3_48000).size + + track["url_aac_48"] = aac_48000_s3_path + track["md5_aac_48"] = ::Digest::MD5.file(aac_48000).hexdigest + track["length_aac_48"] = File.new(aac_48000).size + track.save + end + end + end + + def synchronize_duration(jam_track, ogg_44100) duration_command = "soxi -D \"#{ogg_44100}\"" output = `#{duration_command}` @@ -1155,58 +1907,15 @@ module JamRuby def synchronize_track_preview(track, tmp_dir, ogg_44100) - out_wav = File.join(tmp_dir, 'stripped.wav') - - burp_gaps = ['0.3', '0.2', '0.1', '0.05'] - - total_time_command = "soxi -D \"#{ogg_44100}\"" - total_time = `#{total_time_command}`.to_f - - result_code = -20 - stripped_time = total_time # default to the case where we just start the preview at the beginning - - burp_gaps.each do |gap| - command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%" - - @@log.debug("stripping silence: " + command_strip_lead_silence) - - output = `#{command_strip_lead_silence}` - - result_code = $?.to_i - - if result_code == 0 - stripped_time_command = "soxi -D \"#{out_wav}\"" - stripped_time_test = `#{stripped_time_command}`.to_f - - if stripped_time_test < 1 # meaning a very short duration - @@log.warn("could not determine the start of non-silencea. assuming beginning") - stripped_time = total_time # default to the case where we just start the preview at the beginning - else - stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break - break - end - else - @@log.warn("unable to determine silence for jam_track #{track.original_filename}, #{output}") - stripped_time = total_time # default to the case where we just start the preview at the beginning - end - - end - - preview_start_time = total_time - stripped_time + preview_start_time = determine_start_time(ogg_44100, tmp_dir, track.original_filename) # this is in seconds; convert to integer milliseconds preview_start_time = (preview_start_time * 1000).to_i - preview_start_time = nil if preview_start_time < 0 + preview_start_time = 0 if preview_start_time < 0 track.preview_start_time = preview_start_time - if track.preview_start_time - @@log.debug("determined track start time to be #{track.preview_start_time}") - else - @@log.debug("determined track start time to be #{track.preview_start_time}") - end - track.process_preview(ogg_44100, tmp_dir) if track.preview_start_time if track.preview_generate_error @@ -1215,6 +1924,53 @@ module JamRuby end + def synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_digest) + begin + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest + + # upload 44100 aac to public location + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) + + + track.skip_uploader = true + + original_aac_preview_url = track["preview_aac_url"] + + # and finally update the JamTrackTrack with the new info + track["preview_aac_url"] = track.preview_filename(aac_md5, 'aac') + track["preview_aac_length"] = File.new(aac_44100).size + track["preview_start_time"] = 0 + + if !track.save + finish("save_master_preview", track.errors.to_s) + return false + end + + # if all that worked, now delete old previews, if present + begin + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + rescue Exception => e + finish("sync_master_preview_exception", e.to_s) + return false + end + + + return true + + end + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) begin @@ -1226,20 +1982,33 @@ module JamRuby mp3_digest = ::Digest::MD5.file(mp3_44100) + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest - # upload 44100 ogg and mp3 to public location as well + # upload 44100 ogg, mp3, aac to public location as well @@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}") public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) @@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}") public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) track.skip_uploader = true original_ogg_preview_url = track["preview_url"] original_mp3_preview_url = track["preview_mp3_url"] + original_aac_preview_url = track["preview_aac_url"] # and finally update the JamTrackTrack with the new info track["preview_url"] = track.preview_filename(ogg_md5, 'ogg') @@ -1247,6 +2016,8 @@ module JamRuby # and finally update the JamTrackTrack with the new info track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3') track["preview_mp3_length"] = File.new(mp3_44100).size + track["preview_aac_url"] = track.preview_filename(aac_md5, 'mp3') + track["preview_aac_length"] = File.new(aac_44100).size track["preview_start_time"] = 0 if !track.save @@ -1258,6 +2029,7 @@ module JamRuby begin public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"] public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] rescue puts "UNABLE TO CLEANUP OLD PREVIEW URL" end @@ -1293,6 +2065,15 @@ module JamRuby original_artist = parsed_metalocation[1] name = parsed_metalocation[2] + if is_paris_storage? + bpm = parsed_metalocation[-1] + bpm.downcase! + if bpm.end_with?('bpm') + bpm = bpm[0..-4].to_f + end + metadata[:bpm] = bpm + end + success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) return unless success @@ -1331,6 +2112,8 @@ module JamRuby attr_accessor :storage_format attr_accessor :tency_mapping attr_accessor :tency_metadata + attr_accessor :paris_mapping + attr_accessor :paris_metadata attr_accessor :summaries def report_summaries @@ -1357,19 +2140,31 @@ module JamRuby def song_storage_manager if is_tency_storage? tency_s3_manager + elsif is_paris_storage? + paris_s3_manager + elsif is_tim_tracks_storage? + tim_tracks_s3_manager else s3_manager end end def summaries - @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new} + @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new, multiple_masters: 0, total:0} end def tency_s3_manager @tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def paris_s3_manager + @paris_s3_manager ||= S3Manager.new('jamkazam-paris', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def tim_tracks_s3_manager + @tim_tracks_s3_manager ||= S3Manager.new('jamkazam-timtracks', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end @@ -1378,6 +2173,25 @@ module JamRuby @private_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def extract_paris_song_id(metalocation) + + first_path = metalocation.index('/') + return nil unless first_path + metalocation = metalocation[(first_path + 1)..-1] + + suffix = '/meta.yml' + metalocation = metalocation[0...-suffix.length] + + first_dash = metalocation.index('-') + return nil if first_dash.nil? + + id = metalocation[0...first_dash].strip + + return nil unless id.start_with?('S') # all start with S + return nil if id[1..-1].to_i == 0 # and number after that + id + end + def extract_tency_song_id(metalocation) # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml @@ -1398,15 +2212,69 @@ module JamRuby id end + def is_default_storage? + assert_storage_set + @storage_format == 'default' + end + def is_tency_storage? assert_storage_set @storage_format == 'Tency' end + def is_paris_storage? + assert_storage_set + @storage_format == 'Paris' + end + + def is_tim_tracks_storage? + assert_storage_set + @storage_format == 'TimTracks' + end + def assert_storage_set raise "no storage_format set" if @storage_format.nil? end + def iterate_tim_tracks_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + count += 1 + #break if count > 100 + + end + end + + def iterate_paris_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + #next if song != 'mapped/S1555-Ashlee Simpson-L-O-V-E-96bpm/' + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + if metadata.nil? + # we don't do a paris song unless it has metadata + next + end + blk.call(metadata, metalocation) + + count += 1 + #break if count > 1000 + end + end + def iterate_tency_song_storage(&blk) count = 0 song_storage_manager.list_directories('mapped').each do |song| @@ -1445,6 +2313,14 @@ module JamRuby if is_tency_storage? iterate_tency_song_storage do |metadata, metalocation| blk.call(metadata, metalocation) + end + elsif is_paris_storage? + iterate_paris_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + elsif is_tim_tracks_storage? + iterate_tim_tracks_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) end else iterate_default_song_storage do |metadata, metalocation| @@ -1456,6 +2332,9 @@ module JamRuby def dry_run iterate_song_storage do |metadata, metalocation| jam_track_importer = JamTrackImporter.new(@storage_format) + jam_track_importer.metadata = metadata + + JamTrackImporter.summaries[:total] += 1 jam_track_importer.dry_run(metadata, metalocation) end @@ -1477,6 +2356,7 @@ module JamRuby iterate_song_storage do |metadata, metalocation| importer = JamTrackImporter.new(@storage_format) + importer.metadata = metadata song_id = JamTrackImporter.extract_tency_song_id(metalocation) parsed_metalocation = importer.parse_metalocation(metalocation) @@ -1496,13 +2376,13 @@ module JamRuby CSV.open("only_in_s3.csv", "wb") do |csv| only_in_s3.each do |song_id| - csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ] + csv << [song_id, in_s3[song_id][:artist], in_s3[song_id][:song]] end end CSV.open("only_in_2k_selection.csv", "wb") do |csv| only_in_mapping.each do |song_id| - csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ] + csv << [song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song]] end end @@ -1515,11 +2395,12 @@ module JamRuby break end end + def create_masters iterate_song_storage do |metadata, metalocation| next if metadata.nil? jam_track_importer = JamTrackImporter.new(@storage_format) - + jam_track_importer.metadata = metadata jam_track_importer.create_master(metadata, metalocation) end end @@ -1547,6 +2428,7 @@ module JamRuby metadata = load_metalocation(metalocation) jam_track_importer = JamTrackImporter.new + jam_track_importer.metadata = metadata jam_track_importer.dry_run(metadata, metalocation) end @@ -1581,6 +2463,38 @@ module JamRuby importer end + # hunts for the most recent .aac, .mp3, or .ogg file + def synchronize_preview_dev(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + importer.synchronize_preview_dev(jam_track) + + importer.finish('success', nil) + importer + end + + def synchronize_jamtrack_aac_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + track = jam_track.master_track + + if track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(track.url_by_sample_rate(44), ogg_44100) + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + if importer.synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) + importer.finish("success", nil) + end + end + else + importer.finish('no_master_track', nil) + end + importer + end + def synchronize_jamtrack_master_preview(jam_track) importer = JamTrackImporter.new importer.name = jam_track.name @@ -1603,6 +2517,97 @@ module JamRuby importer end + def synchronize_previews_dev + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_preview_dev(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "no_preview_start_time" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + + def import_click_track(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + importer.import_click_track(jam_track) + + importer + end + def generate_jmep(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + importer.generate_jmep(jam_track) + + importer + end + + def import_click_tracks + importers = [] + + JamTrack.all.each do |jam_track| + #jam_track = JamTrack.find('126') + importers << import_click_track(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to generate jmep.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + + def generate_jmeps + importers = [] + + JamTrack.all.each do |jam_track| + importers << generate_jmep(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to generate jmep.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + def synchronize_previews importers = [] @@ -1629,6 +2634,33 @@ module JamRuby end end + def synchronize_jamtrack_aac_previews + + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_jamtrack_aac_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + + end + end + def synchronize_jamtrack_master_previews importers = [] @@ -1716,6 +2748,20 @@ module JamRuby importer end + def generate_mp3_aac_stem(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + Dir.mktmpdir do |tmp_dir| + + audio_path = jam_track.metalocation[0...-"/meta.yml".length] + importer.associate_tracks_with_original_stems(jam_track, audio_path) + importer.generate_mp3_aac_stem(jam_track, tmp_dir, false) + end + + importer + end + def download_masters importers = [] @@ -1745,6 +2791,70 @@ module JamRuby filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '') end + def generate_mp3_aac_stems(format) + importers = [] + + jam_tracks = [] + + tency = JamTrackLicensor.find_by_name('Tency Music') + + @@log.info("processing storage #{@storage_format}") + if is_tency_storage? + tency = JamTrackLicensor.find_by_name!('Tency Music') + jam_tracks = JamTrack.where(licensor_id: tency.id) + elsif is_paris_storage? + paris = JamTrackLicensor.find_by_name!('Paris Music') + jam_tracks = JamTrack.where(licensor_id: paris.id) + elsif is_default_storage? + # XXX IF WE ADD ANOTHER STORAGE, UPDATE THE WHERE TO EXCLUDE IT AS WELL + jam_tracks = JamTrack.where('licensor_id is null OR licensor_id != ?', tency.id ) + else + raise 'unknown storage format!' + end + + jam_tracks.each do |jam_track| + + if ENV['NODE_COUNT'] + node_count = ENV['NODE_COUNT'].to_i + node_number = ENV['NODE_NUMBER'].to_i + raise "NO NODE_COUNT" if node_count == 0 + + jam_track_id = jam_track.id.to_i + jam_track_id = jam_track_id + node_number + if jam_track_id == 0 + @@log.warn("skipping #{jam_track_id} because non-numeric ID") + next + elsif jam_track_id % node_count == 0 + @@log.warn("starting JamTrack #{jam_track.id} (#{jam_track_id})") + importers << generate_mp3_aac_stem(jam_track) + else + @@log.warn("skipping #{jam_track_id}") + next + end + else + importers << generate_mp3_aac_stem(jam_track) + end + + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to download.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + def generate_slugs JamTrack.all.each do |jam_track| jam_track.generate_slug @@ -1764,7 +2874,7 @@ module JamRuby count = 0 iterate_song_storage do |metadata, metalocation| - next if metadata.nil? && is_tency_storage? + next if metadata.nil? && (is_tency_storage? || is_paris_storage?) importer = synchronize_from_meta(metalocation, options) importers << importer @@ -1812,7 +2922,134 @@ module JamRuby end end - def genre_dump + def paris_genre_dump + load_paris_mappings + + genres = {} + @paris_metadata.each do |id, value| + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + + genres[genre1.downcase.strip] = genre1.downcase.strip if genre1 + genres[genre2.downcase.strip] = genre2.downcase.strip if genre2 + genres[genre3.downcase.strip] = genre3.downcase.strip if genre3 + end + + all_genres = Genre.select(:id).all.map(&:id) + + all_genres = Set.new(all_genres) + genres.each do |genre, value| + found = all_genres.include? genre + + puts "#{genre}" unless found + end + end + + + def create_importer_from_existing(jam_track) + importer = JamTrackImporter.new(@storage_format) + importer.name = jam_track.name + importer.metadata = load_metalocation(jam_track.metalocation) + importer + end + + def resync_instruments(licensor) + + load_paris_mappings if @paris_mapping.nil? + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + if @paris_metadata[jam_track.vendor_id].nil? + next + end + puts "RESYNCING JAMTRACK #{jam_track.id}" + JamTrackTrack.where(jam_track_id: jam_track.id).order(:position).each do |track| + puts "BEFORE TRACK #{track.instrument_id} #{track.part}" + end + + importer = create_importer_from_existing(jam_track) + importer.reassign_instrument_parts(jam_track) + + #puts ">>>>>>>>> HIT KEY TO CONTINUE <<<<<<<<<<" + #STDIN.gets + end + + end + + + def fix_artist_song_name (licensor) + + load_paris_mappings if @paris_mapping.nil? + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + metadata = @paris_metadata[jam_track.vendor_id] + + if metadata.nil? + puts "OH NO! A Paris Song that does not belong! #{jam_track.id} #{jam_track.vendor_id}" + next + end + + puts "STARTING JAM_TRACK #{jam_track.id} #{jam_track.original_artist} #{jam_track.name}" + + jam_track.generate_slug + + if jam_track.changed? + puts "SLUG CHANGED! #{jam_track.changes.inspect}" + end + + if !jam_track.save + puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}" + end + + if jam_track.changed? + jam_track.reload + end + + jam_track.original_artist = metadata[:original_artist] + jam_track.name = metadata[:name] + if jam_track.changed? + puts "ARTIST/NAME CHANGE: #{jam_track.changes.inspect}" + end + + if !jam_track.save + puts "unable to save new artist/song!" + end + end + end + + def fix_slugs(licensor) + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + if jam_track.slug.end_with?('-') + puts "removing trailing dash" + jam_track.slug = jam_track.slug[0...-1] + if !jam_track.save + puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}" + end + end + end + end + + def missing_masters(licensor) + + count = 0 + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + + if jam_track.master_track.nil? + puts "MISSING #{jam_track.metalocation}" + count += 1 + end + + end + + puts "missing master count: #{count}" + end + + def tency_genre_dump load_tency_mappings genres = {} @@ -1841,6 +3078,65 @@ module JamRuby end end + def load_paris_mappings + Dir.mktmpdir do |tmp_dir| + mapping_file = File.join(tmp_dir, 'mapping.csv') + metadata_file = File.join(tmp_dir, 'metadata.csv') + + # this is a developer option to skip the download and look in the CWD to grab mapping.csv and metadata.csv + if ENV['PARIS_ALREADY_DOWNLOADED'] == '1' + mapping_file = 'paris_mapping.csv' + metadata_file = 'paris_metadata.csv' + else + paris_s3_manager.download('mapping/mapping.csv', mapping_file) + paris_s3_manager.download('mapping/metadata.csv', metadata_file) + end + + mapping_csv = CSV.read(mapping_file) + metadata_csv = CSV.read(metadata_file, headers: true, return_headers: false) + + @paris_mapping = {} + @paris_metadata = {} + # convert both to hashes + mapping_csv.each do |line| + instrument = line[1] + instrument.strip! if instrument + + part = line[2] + part.strip! if part + @paris_mapping[line[0].strip.downcase] = {instrument: instrument, part: part} + end + + metadata_csv.each do |line| + paris_artist = line[2] + # Paris artist in metadata file is often all caps + artist = paris_artist.split(' ').collect do |item| + if item == 'DJ' + 'DJ' + else + item.titleize + end + end.join(' ') + @paris_metadata[line[1].strip] = {id: line[1].strip, original_artist: artist, name: line[3], genre1: line[4], genre2: line[5], genre3: line[6]} + end + + @paris_metadata.each do |id, value| + + genres = [] + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + + value[:genres] = genres + end + end + end + def load_tency_mappings Dir.mktmpdir do |tmp_dir| mapping_file = File.join(tmp_dir, 'mapping.csv') @@ -1880,11 +3176,11 @@ module JamRuby genre4 = value[:genre4] genre5 = value[:genre5] - genres << genre1.downcase.strip if genre1 - genres << genre2.downcase.strip if genre2 - genres << genre3.downcase.strip if genre3 - genres << genre4.downcase.strip if genre4 - genres << genre5.downcase.strip if genre5 + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + genres << genre4.downcase.strip if genre4 + genres << genre5.downcase.strip if genre5 value[:genres] = genres end @@ -1912,10 +3208,31 @@ module JamRuby end return tency_data + elsif is_paris_storage? + load_paris_mappings if @paris_mapping.nil? + song_id = extract_paris_song_id(metalocation) + + if song_id.nil? + puts "missing_song_id #{metalocation}" + return nil + end + + paris_data = @paris_metadata[song_id] + + if paris_data.nil? + @@log.warn("missing paris metadata '#{song_id}'") + end + + return paris_data else begin data = s3_manager.read_all(metalocation) - return YAML.load(data) + meta = YAML.load(data) + + if is_tim_tracks_storage? + meta[:genres] = ['acapella'] + end + meta rescue AWS::S3::Errors::NoSuchKey return nil end @@ -1933,6 +3250,7 @@ module JamRuby def sync_from_metadata(jam_track, meta, metalocation, options) jam_track_importer = JamTrackImporter.new(@storage_format) + jam_track_importer.metadata = meta JamTrack.connection.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED') @@ -1958,7 +3276,7 @@ module JamRuby meta = load_metalocation(metalocation) - if meta.nil? && is_tency_storage? + if meta.nil? && is_paris_storage? raise "no tency song matching this metalocation #{metalocation}" end jam_track_importer = nil @@ -1982,3 +3300,4 @@ module JamRuby end end end + diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index d6ae37b1e..96ccf87f8 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -41,7 +41,7 @@ module JamRuby jam_file_opts="" jam_track.jam_track_tracks.each do |jam_track_track| - next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ + next if jam_track_track.track_type == "Master" # master mixes do not go into the JKZ # use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate)) @@ -52,7 +52,8 @@ module JamRuby step = bump_step(jam_track_right, step) copy_url_to_file(track_url, track_filename) - jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}" + part = jam_track_track.track_type == 'Click' ? 'ClickTrack' : jam_track_track.part + jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{part}")}" end #puts "LS + " + `ls -la '#{tmp_dir}'` diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index cf86fdc9b..3398a6a3c 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -121,6 +121,10 @@ module JamRuby s3_bucket.objects[filename].exists? end + def object(filename) + s3_bucket.objects[filename] + end + def length(filename) s3_bucket.objects[filename].content_length end diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb index 6be9f7d16..02b98f3b9 100644 --- a/ruby/lib/jam_ruby/lib/subscription_message.rb +++ b/ruby/lib/jam_ruby/lib/subscription_message.rb @@ -14,15 +14,29 @@ module JamRuby end def self.mount_source_up_requested(mount) - Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json ) + Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json) end def self.mount_source_down_requested(mount) - Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json ) + Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json) end def self.jam_track_signing_job_change(jam_track_right) - Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json ) + Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, + {signing_state: jam_track_right.signing_state, + current_packaging_step: jam_track_right.current_packaging_step, + packaging_steps: jam_track_right.packaging_steps}.to_json) + end + + def self.mixdown_signing_job_change(jam_track_mixdown_package) + Notification.send_subscription_message('mixdown', jam_track_mixdown_package.id.to_s, + {signing_state: jam_track_mixdown_package.signing_state, + current_packaging_step: jam_track_mixdown_package.current_packaging_step, + packaging_steps: jam_track_mixdown_package.packaging_steps}.to_json) + end + + def self.test + Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json) end end end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index e8cf40b1b..6b7e98034 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -736,6 +736,30 @@ module JamRuby ) end + def mixdown_sign_complete(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignComplete.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_complete => signed + ) + end + + def mixdown_sign_failed(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignFailed.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_FAILED, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_failed=> signed + ) + end + def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at) recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new( diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 4408ffa42..e88aed9c7 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -9,7 +9,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner - attr_accessible :partner_name, :partner_code, :partner_user_id + attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } @@ -50,6 +50,14 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base record.entity_type ||= ENTITY_TYPES.first end + def display_name + partner_name || (partner_user ? partner_user.name : 'abandoned') + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/affiliates/#{id}" + end + # used by admin def self.create_with_params(params={}) raise 'not supported' @@ -111,18 +119,16 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base end def should_attribute_sale?(shopping_cart) - if shopping_cart.is_jam_track? - if created_within_affiliate_window(shopping_cart.user, Time.now) - product_info = shopping_cart.product_info - # subtract the total quantity from the freebie quantity, to see how much we should attribute to them - real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i - {fee_in_cents: real_quantity * 20} - else - false - end + + if created_within_affiliate_window(shopping_cart.user, Time.now) + product_info = shopping_cart.product_info + # subtract the total quantity from the freebie quantity, to see how much we should attribute to them + real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i + {fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round} else - raise 'shopping cart type not implemented yet' + false end + end def cumulative_earnings_in_dollars @@ -469,4 +475,8 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base def affiliate_query_params AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s end + + def to_s + display_name + end end diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index 24bc2b104..55c788e90 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -15,20 +15,47 @@ module JamRuby ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC') end + def destroy_all_shopping_carts ShoppingCart.destroy_all(anonymous_user_id: @id) end + def destroy_jam_track_shopping_carts + ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE) + end + def admin false end def has_redeemable_jamtrack + raise "not a cookied anonymous user" if @cookies.nil? + APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack] end + def gifted_jamtracks + 0 + end + + def free_jamtracks + if has_redeemable_jamtrack + 1 + else + 0 + end + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + def signup_hint SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first end + + def reload + + end end end diff --git a/ruby/lib/jam_ruby/models/base_search.rb b/ruby/lib/jam_ruby/models/base_search.rb index ed2feefb7..685133be8 100644 --- a/ruby/lib/jam_ruby/models/base_search.rb +++ b/ruby/lib/jam_ruby/models/base_search.rb @@ -102,11 +102,19 @@ module JamRuby def self.search_target_class end + def self.genre_ids + @@genre_ids ||= Hash[ *Genre.pluck(:id).collect { |v| [ v, v ] }.flatten ] + end + + def self.instrument_ids + @@instrument_ids ||= Hash[ *Instrument.pluck(:id).collect { |v| [ v, v ] }.flatten ] + end + def _genres(rel, query_data=json) gids = query_data[KEY_GENRES] unless gids.blank? - allgids = Genre.order(:id).pluck(:id) - gids = gids.select { |gg| allgids.index(gg).present? } + allgids = self.class.genre_ids + gids = gids.select { |gg| allgids.has_key?(gg) } unless gids.blank? gidsql = gids.join("','") @@ -119,8 +127,8 @@ module JamRuby def _instruments(rel, query_data=json) unless (instruments = query_data[KEY_INSTRUMENTS]).blank? - instrids = Instrument.order(:id).pluck(:id) - instruments = instruments.select { |ii| instrids.index(ii['instrument_id']).present? } + instrids = self.class.instrument_ids + instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) } unless instruments.blank? instsql = "SELECT player_id FROM musicians_instruments WHERE ((" diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb index 6c4e54d84..22874a299 100644 --- a/ruby/lib/jam_ruby/models/crash_dump.rb +++ b/ruby/lib/jam_ruby/models/crash_dump.rb @@ -1,6 +1,8 @@ module JamRuby class CrashDump < ActiveRecord::Base + include JamRuby::S3ManagerMixin + self.table_name = "crash_dumps" self.primary_key = 'id' @@ -15,7 +17,7 @@ module JamRuby before_validation(:on => :create) do self.created_at ||= Time.now self.id = SecureRandom.uuid - self.uri = "dump/#{self.id}-#{self.created_at.to_i}" + self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}" end def user_email @@ -23,5 +25,8 @@ module JamRuby self.user.email end + def sign_url(expiration_time = 3600 * 24 * 7, secure=true) + s3_manager.sign_url(self[:ri], {:expires => expiration_time, :secure => secure}) + end end end diff --git a/ruby/lib/jam_ruby/models/download_tracker.rb b/ruby/lib/jam_ruby/models/download_tracker.rb new file mode 100644 index 000000000..6a2bcf301 --- /dev/null +++ b/ruby/lib/jam_ruby/models/download_tracker.rb @@ -0,0 +1,121 @@ +module JamRuby + class DownloadTracker < ActiveRecord::Base + + @@log = Logging.logger[DownloadTracker] + + belongs_to :user, :class_name => "JamRuby::User" + belongs_to :mixdown, :class_name => "JamRuby::JamTrackMixdownPackage", foreign_key: 'mixdown_id' + belongs_to :stem, :class_name => "JamRuby::JamTrackTrack", foreign_key: 'stem_id' + belongs_to :jam_track, :class_name => "JamRuby::JamTrack" + + # one of mixdown or stem need to be specified. could validate this? + validates :user, presence:true + validates :remote_ip, presence: true + #validates :paid, presence: true + validates :jam_track, presence: :true + + def self.create(user, remote_ip, target, owned) + dt = DownloadTracker.new + dt.user = user + dt.remote_ip = remote_ip + dt.paid = owned + if target.is_a?(JamTrackTrack) + dt.jam_track_id = target.jam_track_id + elsif target.is_a?(JamTrackMixdownPackage) + dt.jam_track_id = target.jam_track_mixdown.jam_track_id + end + if !dt.save + @@log.error("unable to create Download Tracker: #{dt.errors.inspect}") + end + dt + end + + def self.check(user, remote_ip, target, owned) + + return unless APP_CONFIG.guard_against_browser_fraud + + create(user, remote_ip, target, owned) + + # let's check the following + alert_freebies_snarfer(remote_ip) + + alert_user_sharer(user) + end + + # somebody who has shared account info with a large number of people + # high number of downloads of the same user from different IP addresses that were or were not paid for + # raw query created by this code: + # SELECT distinct(user_id), count(user_id) FROM "download_trackers" WHERE (created_at > NOW() - '30 days'::interval) GROUP BY user_id HAVING count(distinct(remote_ip)) >= 2 + def self.check_user_sharer(max, user_id = nil) + query = DownloadTracker.select('distinct(user_id), count(user_id)') + query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval") + if !user_id.nil? + query = query.where('user_id = ?', user_id) + end + + query.group(:user_id).having("count(distinct(remote_ip)) >= #{max}") + end + + + + + # somebody who has figured out how to bypass cookie based method of identity checking, and is getting lots of free JamTracks + # high number of downloads of different jam tracks from different users for the same IP address that weren't paid for + # raw query created by this code: + # SELECT distinct(remote_ip), count(remote_ip) FROM "download_trackers" WHERE (paid = false) AND (created_at > NOW() - '30 days'::interval) GROUP BY remote_ip HAVING count(distinct(jam_track_id)) >= 2 + def self.check_freebie_snarfer(max, remote_ip = nil) + + query = DownloadTracker.select('distinct(remote_ip), count(remote_ip)').where("paid = false") + query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval") + if !remote_ip.nil? + query = query.where('remote_ip = ?', remote_ip) + end + query.group(:remote_ip).having("count(distinct(jam_track_id)) >= #{max}") + end + + def self.alert_user_sharer(user) + violation = check_user_sharer(APP_CONFIG.max_user_ip_address, user.id).first + + if violation + body = "User has downloaded from too many IP addresses #{user.id}\n" + body << "Download Count: #{violation['count']}\n" + body << "User URL #{user.admin_url}\n" + body << "Add to blacklist: #{UserBlacklist.admin_url}" + + AdminMailer.alerts({ + subject:"Account IP Access Violation. USER: #{user.email}", + body:body + }).deliver + end + end + + def self.alert_freebies_snarfer(remote_ip) + + violation = check_freebie_snarfer(APP_CONFIG.max_multiple_users_same_ip, remote_ip).first + + if violation + body = "IP Address: #{remote_ip}\n" + body << "Download Count: #{violation['count']}\n" + body << "Add to blacklist: #{IpBlacklist.admin_url}" + body << "Check Activity: #{IpBlacklist.admin_activity_url(remote_ip)}" + + AdminMailer.alerts({ + subject:"Single IP Access Violation. IP:#{remote_ip}", + body:body + }).deliver + end + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/download_trackers/" + id + end + + def to_s + if stem? + "stem:#{stem} #{remote_ip} #{user}" + else + "mixdown:#{mixdown} #{remote_ip} #{user}" + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index b6ab0bd05..3f81b8bbf 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -25,5 +25,13 @@ module JamRuby def to_s description end + + def self.jam_track_list + sql = "SELECT DISTINCT genre_id FROM genres_jam_tracks WHERE genre_id IS NOT NULL" + Genre.select("DISTINCT(genres.id), genres.*") + .where("genres.id IN (#{sql})") + .order('genres.description ASC, genres.id') + end + end end diff --git a/ruby/lib/jam_ruby/models/genre_jam_track.rb b/ruby/lib/jam_ruby/models/genre_jam_track.rb index aa05e4fd8..933ef26bc 100644 --- a/ruby/lib/jam_ruby/models/genre_jam_track.rb +++ b/ruby/lib/jam_ruby/models/genre_jam_track.rb @@ -2,7 +2,10 @@ module JamRuby class GenreJamTrack < ActiveRecord::Base self.table_name = 'genres_jam_tracks' - belongs_to :jam_track, class_name: 'JamRuby::JamTrack' - belongs_to :genre, class_name: 'JamRuby::Genre' + + attr_accessible :jam_track_id, :genre_id + + belongs_to :jam_track, class_name: 'JamRuby::JamTrack', inverse_of: :genres_jam_tracks + belongs_to :genre, class_name: 'JamRuby::Genre', inverse_of: :genres_jam_tracks end end diff --git a/ruby/lib/jam_ruby/models/gift_card.rb b/ruby/lib/jam_ruby/models/gift_card.rb new file mode 100644 index 000000000..0dc5f94e8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card.rb @@ -0,0 +1,36 @@ +# represents the gift card you hold in your hand +module JamRuby + class GiftCard < ActiveRecord::Base + + @@log = Logging.logger[GiftCard] + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10 + ] + + + belongs_to :user, class_name: "JamRuby::User" + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + validates :code, presence: true, uniqueness: true + + after_save :check_gifted + + def check_gifted + if user && user_id_changed? + if card_type == JAM_TRACKS_5 + user.gifted_jamtracks += 5 + elsif card_type == JAM_TRACKS_10 + user.gifted_jamtracks += 10 + else + raise "unknown card type #{card_type}" + end + user.save! + end + end + end +end diff --git a/ruby/lib/jam_ruby/models/gift_card_purchase.rb b/ruby/lib/jam_ruby/models/gift_card_purchase.rb new file mode 100644 index 000000000..0cfb00807 --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card_purchase.rb @@ -0,0 +1,17 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class GiftCardPurchase < ActiveRecord::Base + + @@log = Logging.logger[GiftCardPurchase] + + attr_accessible :user, :gift_card_type + + def name + gift_card_type.sale_display + end + + # who purchased the card? + belongs_to :user, class_name: "JamRuby::User" + belongs_to :gift_card_type, class_name: "JamRuby::GiftCardType" + end +end diff --git a/ruby/lib/jam_ruby/models/gift_card_type.rb b/ruby/lib/jam_ruby/models/gift_card_type.rb new file mode 100644 index 000000000..8294dbfed --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card_type.rb @@ -0,0 +1,70 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class GiftCardType < ActiveRecord::Base + + @@log = Logging.logger[GiftCardType] + + PRODUCT_TYPE = 'GiftCardType' + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10 + ] + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + + def self.jam_track_5 + GiftCardType.find(JAM_TRACKS_5) + end + + def self.jam_track_10 + GiftCardType.find(JAM_TRACKS_10) + end + + def name + sale_display + end + + def price + if card_type == JAM_TRACKS_5 + 10.00 + elsif card_type == JAM_TRACKS_10 + 20.00 + else + raise "unknown card type #{card_type}" + end + end + + + def sale_display + if card_type == JAM_TRACKS_5 + 'JamTracks Gift Card (5)' + elsif card_type == JAM_TRACKS_10 + 'JamTracks Gift Card (10)' + else + raise "unknown card type #{card_type}" + end + end + + def plan_code + if card_type == JAM_TRACKS_5 + "jamtrack-giftcard-5" + elsif card_type == JAM_TRACKS_10 + "jamtrack-giftcard-10" + else + raise "unknown card type #{card_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb index cc6fe40a9..3f11a01e5 100644 --- a/ruby/lib/jam_ruby/models/instrument.rb +++ b/ruby/lib/jam_ruby/models/instrument.rb @@ -50,6 +50,12 @@ module JamRuby return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC') end + def self.jam_track_list + sql = "SELECT DISTINCT instrument_id FROM jam_track_tracks WHERE instrument_id IS NOT NULL" + Instrument.where("instruments.id IN (#{sql})") + .order('instruments.description ASC') + end + def icon_name MAP_ICON_NAME[self.id] end diff --git a/ruby/lib/jam_ruby/models/ip_blacklist.rb b/ruby/lib/jam_ruby/models/ip_blacklist.rb new file mode 100644 index 000000000..a7099cba8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/ip_blacklist.rb @@ -0,0 +1,30 @@ +module JamRuby + class IpBlacklist < ActiveRecord::Base + + attr_accessible :remote_ip, :notes, as: :admin + + @@log = Logging.logger[IpBlacklist] + + validates :remote_ip, presence:true, uniqueness:true + + def self.listed(remote_ip) + IpBlacklist.count(:conditions => "remote_ip = '#{remote_ip}'") == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + end + + def self.admin_activity_url(remote_ip) + APP_CONFIG.admin_root_url + "/admin/download_trackers?q[remote_ip_equals]=#{URI.escape(remote_ip)}&commit=Filter&order=id_desc" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + id + end + + def to_s + remote_ip + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 29995e9ad..eca9cd7ea 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- module JamRuby class JamTrack < ActiveRecord::Base include JamRuby::S3ManagerMixin @@ -18,7 +19,7 @@ module JamRuby :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, :jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, - :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin + :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -52,7 +53,7 @@ module JamRuby belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks - has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id" + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id", inverse_of: :jam_track has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC' @@ -85,6 +86,10 @@ module JamRuby after_save :sync_reproduction_royalty after_save :sync_onboarding_exceptions + def increment_version! + self.version = version.to_i + 1 + save! + end def sync_reproduction_royalty @@ -154,6 +159,9 @@ module JamRuby true end + def sale_display + "JamTrack: " + name + end def duplicate_positions? counter = {} jam_track_tracks.each do |track| @@ -255,12 +263,12 @@ module JamRuby limit = options[:limit] limit ||= 20 limit = limit.to_i + per_page = limit else limit = per_page end start = (page -1 )* per_page - limit = per_page else limit = options[:limit] limit ||= 20 @@ -337,9 +345,18 @@ module JamRuby query = query.where('genre_id = ? ', options[:genre]) end - query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank? + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type = 'Track'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + # FIXME: n+1 queries for rights and genres + # query = query.includes([{ jam_track_tracks: :instrument }, + # :jam_track_tap_ins, + # :jam_track_rights, + # :genres]) + # { genres_jam_tracks: :genre }, + # query = query.includes([{ jam_track_tracks: :instrument }, + # { genres_jam_tracks: :genre }]) + count = query.total_entries if count == 0 @@ -421,13 +438,43 @@ module JamRuby end end + def click_track_file + JamTrackFile.where(jam_track_id: self.id).where(file_type: 'ClickWav').first + end + + def click_track + JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Click').first + end + + def has_count_in? + has_count_in = false + if jmep_json + jmep = JSON.parse(jmep_json) + + if jmep["Events"] + events = jmep["Events"] + metronome = nil + events.each do |event| + if event.has_key?("metronome") + metronome = event["metronome"] + break + end + end + if metronome + has_count_in = true + end + end + end + + has_count_in + end def master_track JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first end def stem_tracks - JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track') + JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'") end def can_download?(user) @@ -438,6 +485,10 @@ module JamRuby jam_track_rights.where("user_id=?", user).first end + def mixdowns_for_user(user) + JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id) + end + def short_plan_code prefix = 'jamtrack-' plan_code[prefix.length..-1] @@ -450,7 +501,80 @@ module JamRuby def generate_slug self.slug = sluggarize(original_artist) + '-' + sluggarize(name) - puts "Self.slug #{self.slug}" + + if licensor && licensor.slug.present? + #raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? + self.slug << "-" + licensor.slug + end + end + + def gen_plan_code + # remove all non-alphanumeric chars from artist as well as name + artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase + name_code = name.gsub(/[^0-9a-z]/i, '').downcase + self.plan_code = "jamtrack-#{artist_code[0...20]}-#{name_code}" + + if licensor && licensor.slug + raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? + self.plan_code << "-" + licensor.slug + end + + self.plan_code = self.plan_code[0...50] # make sure it's a max of 50 long + + + end + + def to_s + "#{self.name} (#{self.original_artist})" + end + + def self.latestPurchase(user_id) + jtx_created = JamTrackRight + .select('created_at') + .where(user_id: user_id) + .order('created_at DESC') + .limit(1) + jtx_created.first.created_at.to_i + end + + attr_accessor :preview_generate_error + + before_save :jmep_json_generate + validate :jmep_text_validate + + def jmep_text_validate + begin + JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + errors.add(:jmep_text, err.to_s) + end + end + + def jmep_json_generate + self.licensor_id = nil if self.licensor_id == '' + self.jmep_json = nil if self.jmep_json == '' + self.time_signature = nil if self.time_signature == '' + + begin + self[:jmep_json] = JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + #errors.add(:jmep_text, err.to_s) + end + end + + # used in mobile simulate purchase + def self.forsale(user) + sql =<= 5 + errors.add(:jam_track, 'allowed 5 mixes') + end + end + + def verify_settings + + # the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do + + parsed = JSON.parse(self.settings) + specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0 + + tweaked = false + all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track' + + + if parsed["speed"] + tweaked = true + end + if parsed["pitch"] + tweaked = true + end + + + if parsed["tracks"] + parsed["tracks"].each do |track| + if track["mute"] + tweaked = true + end + if track["vol"] && track["vol"] != 0 + tweaked = true + end + if track["pan"] && track["pan"] != 0 + tweaked = true + end + + # there is at least one track with volume specified. + if !track["mute"] && track["vol"] != 0 + all_quiet = false + end + end + end + + if parsed["count-in"] + all_quiet = false + tweaked = true + end + + if all_quiet + errors.add(:settings, 'are all muted') + end + if !tweaked && !parsed['full'] + errors.add(:settings, 'have nothing specified') + end + + if parsed["speed"] && !parsed["speed"].is_a?(Integer) + errors.add(:settings, 'has non-integer speed') + end + + if parsed["pitch"] && !parsed["pitch"].is_a?(Integer) + errors.add(:settings, 'has non-integer pitch') + end + end + + def self.create(name, description, user, jam_track, settings) + mixdown = JamTrackMixdown.new + mixdown.name = name + mixdown.description = description + mixdown.user = user + mixdown.jam_track = jam_track + mixdown.settings = settings.to_json # RAILS 4 CAN REMOVE .to_json + mixdown.save + mixdown + end + + def will_pitch_shift? + self.settings["pitch"] != 0 || self.settings["speed"] != 0 + end + + def self.mixdownChecksum(user_id, jam_track_id) + dates = self + .select('created_at') + .where(user_id: user_id, jam_track_id: jam_track_id) + .order(:id) + + dates = dates.map do |date| + date.created_at.to_i.to_s + end.join('') + + Digest::MD5.hexdigest(dates) + end + + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb new file mode 100644 index 000000000..648bdfffd --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb @@ -0,0 +1,253 @@ +module JamRuby + + # describes what users have rights to which tracks + class JamTrackMixdownPackage < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + @@log = Logging.logger[JamTrackMixdownPackage] + + # these are used as extensions for the files stored in s3 + FILE_TYPE_MP3 = 'mp3' + FILE_TYPE_OGG = 'ogg' + FILE_TYPE_AAC = 'aac' + FILE_TYPES = [FILE_TYPE_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC] + + SAMPLE_RATE_44 = 44 + SAMPLE_RATE_48 = 48 + SAMPLE_RATES = [SAMPLE_RATE_44, SAMPLE_RATE_48] + + ENCRYPT_TYPE_JKZ = 'jkz' + ENCRYPT_TYPES = [ENCRYPT_TYPE_JKZ, nil] + + default_scope { order('created_at desc') } + + belongs_to :jam_track_mixdown, class_name: "JamRuby::JamTrackMixdown", dependent: :destroy + + validates :jam_track_mixdown, presence: true + + validates :file_type, inclusion: {in: FILE_TYPES} + validates :sample_rate, inclusion: {in: SAMPLE_RATES} + validates :encrypt_type, inclusion: {in: ENCRYPT_TYPES} + validates_uniqueness_of :file_type, scope: [:sample_rate, :encrypt_type, :jam_track_mixdown_id] + validates :signing, inclusion: {in: [true, false]} + validates :signed, inclusion: {in: [true, false]} + + validate :verify_download_count + before_destroy :delete_s3_files + after_save :after_save + + MAX_JAM_TRACK_DOWNLOADS = 1000 + + def self.estimated_queue_time + jam_track_signing_count = JamTrackRight.where(queued: true).count + mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).first + total_mixdowns = mixdowns['queue_count'].to_i + slow_mixdowns = mixdowns['speed_pitch_count'].to_i + fast_mixdowns = total_mixdowns - slow_mixdowns + + guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns + + Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns}) + guess + end + + def after_save + # try to catch major transitions: + + # if just queue time changes, start time changes, or signed time changes, send out a notice + if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was + SubscriptionMessage.mixdown_signing_job_change(self) + end + end + + def self.create(mixdown, file_type, sample_rate, encrypt_type) + + package = JamTrackMixdownPackage.new + package.speed_pitched = mixdown.will_pitch_shift? + package.jam_track_mixdown = mixdown + package.file_type = file_type + package.sample_rate = sample_rate + package.signed = false + package.signing = false + package.encrypt_type = encrypt_type + package.save + package + end + + def verify_download_count + if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin + errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}") + end + end + + def is_pitch_speed_shifted? + mix_settings = JSON.parse(self.settings) + mix_settings["speed"] || mix_settings["pitch"] + end + + def finish_errored(error_reason, error_detail) + self.last_errored_at = Time.now + self.last_signed_at = Time.now + self.error_count = self.error_count + 1 + self.error_reason = error_reason + self.error_detail = error_detail + self.should_retry = self.error_count < 5 + self.signing = false + self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts + + if save + Notification.send_mixdown_sign_failed(self) + else + raise "Error sending notification #{self.errors}" + end + end + + def finish_sign(url, private_key, length, md5) + self.url = url + self.private_key = private_key + self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts + self.downloaded_since_sign = false + self.last_signed_at = Time.now + self.length = length + self.md5 = md5 + self.signed = true + self.signing = false + self.error_count = 0 + self.error_reason = nil + self.error_detail = nil + self.should_retry = false + save! + end + + def store_dir + "jam_track_mixdowns/#{created_at.strftime('%m-%d-%Y')}/#{self.jam_track_mixdown.user_id}" + end + + def filename + if encrypt_type + "#{id}.#{encrypt_type}" + else + "#{id}.#{file_type}" + end + end + + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120, content_type = nil, response_content_disposition = nil) + options = {:expires => expiration_time, :secure => true} + options[:response_content_type] = content_type if content_type + options[:response_content_disposition] = response_content_disposition if response_content_disposition + s3_manager.sign_url(self['url'], options) + end + + + def enqueue + begin + self.signing_queued_at = Time.now + self.signing_started_at = nil + self.last_signed_at = nil + self.queued = true + self.save + + queue_time = JamTrackMixdownPackage.estimated_queue_time + + # is_pitch_speed_shifted? + Resque.enqueue(JamTrackMixdownPackager, self.id) + return queue_time + rescue Exception => e + puts "e: #{e}" + # implies redis is down. we don't update started_at by bailing out here + false + end + end + + # if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off + def enqueue_if_needed + state = signing_state + if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' + false + else + return enqueue + end + end + + def ready? + self.signed && self.url.present? + end + + # returns easy to digest state field + # SIGNED - the package is ready to be downloaded + # ERROR - the package was built unsuccessfully + # SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung + # SIGNING - the package is currently signing + # QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed + # QUEUED - the package is queued to sign + # QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued + def signing_state + state = nil + + if signed + state = 'SIGNED' + elsif signing_started_at && signing + # the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long. + if Time.now - signing_started_at > APP_CONFIG.signing_job_signing_max_time + state = 'SIGNING_TIMEOUT' + elsif Time.now - last_step_at > APP_CONFIG.mixdown_step_max_time + state = 'SIGNING_TIMEOUT' + else + state = 'SIGNING' + end + elsif signing_queued_at + if Time.now - signing_queued_at > APP_CONFIG.mixdown_job_queue_max_time + state = 'QUEUED_TIMEOUT' + else + state = 'QUEUED' + end + elsif error_count > 0 + state = 'ERROR' + else + if Time.now - created_at > 60 # it should not take more than a minute to get QUIET out + state = 'QUIET_TIMEOUT' + else + state = 'QUIET' # needs to be poked to go build + end + + end + state + end + + def signed? + signed + end + + def update_download_count(count=1) + self.download_count = self.download_count + count + self.last_downloaded_at = Time.now + + if self.signed + self.downloaded_since_sign = true + end + end + + + def self.stats + stats = {} + + result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count') + + stats['count'] = result[0]['total'].to_i + stats['signing_count'] = result[0]['signing_count'].to_i + stats + end + + + def delete_s3_files + s3_manager.delete(self.url) if self.url && s3_manager.exists?(self.url) + end + + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index 5217b620d..b400771a5 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -11,7 +11,10 @@ module JamRuby attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44 belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track belongs_to :jam_track, class_name: "JamRuby::JamTrack" + belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right + belongs_to :last_stem, class_name: 'JamRuby::JamTrackTrack', foreign_key: 'last_stem_id', inverse_of: :jam_track_right + validates :version, presence: true validates :user, presence: true validates :jam_track, presence: true validates :is_test_purchase, inclusion: {in: [true, false]} @@ -25,9 +28,16 @@ module JamRuby mount_uploader :url_48, JamTrackRightUploader mount_uploader :url_44, JamTrackRightUploader before_destroy :delete_s3_files + before_create :create_private_keys MAX_JAM_TRACK_DOWNLOADS = 1000 + def create_private_keys + rsa_key = OpenSSL::PKey::RSA.new(1024) + key = rsa_key.to_pem() + self.private_key_44 = key + self.private_key_48 = key + end def after_save # try to catch major transitions: @@ -58,6 +68,7 @@ module JamRuby def finish_errored(error_reason, error_detail, sample_rate) self.last_signed_at = Time.now + self.queued = false self.error_count = self.error_count + 1 self.error_reason = error_reason self.error_detail = error_detail @@ -77,6 +88,7 @@ module JamRuby def finish_sign(length, md5, bitrate) self.last_signed_at = Time.now + self.queued = false if bitrate==48 self.length_48 = length self.md5_48 = md5 @@ -99,10 +111,10 @@ module JamRuby # the idea is that this is used when a user who has the rights to this tries to download this JamTrack # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120, bitrate=48, secure=true) - field_name = (bitrate==48) ? "url_48" : "url_44" - s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure}) - end + def sign_url(expiration_time = 120, bitrate=48, secure=true) + field_name = (bitrate==48) ? "url_48" : "url_44" + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure}) + end def delete_s3_files remove_url_48! @@ -112,7 +124,7 @@ module JamRuby def enqueue(sample_rate=48) begin - JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil) + JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil, :queued => true) Resque.enqueue(JamTracksBuilder, self.id, sample_rate) true rescue Exception => e @@ -122,8 +134,33 @@ module JamRuby end end + def cleanup_old_package! + if self.jam_track.version != self.version + delete_s3_files + self[:url_48] = nil + self[:url_44] = nil + self.signing_queued_at = nil + self.signing_started_at_48 = nil + self.signing_started_at_44 = nil + self.last_signed_at = nil + self.current_packaging_step = nil + self.packaging_steps = nil + self.should_retry = false + self.signing_44 = false + self.signing_48 = false + self.signed_44 = false + self.signed_48 = false + self.queued = false + self.version = self.jam_track.version + self.save! + end + end # if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off def enqueue_if_needed(sample_rate=48) + + # delete any package that's out dated + cleanup_old_package! + state = signing_state(sample_rate) if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' false @@ -137,9 +174,9 @@ module JamRuby # @return true if signed && file exists for the sample_rate specifed: def ready?(sample_rate=48) if sample_rate==48 - self.signed_48 && self.url_48.present? && self.url_48.file.exists? + self.signed_48 && self.url_48.present? && self.url_48.file.exists? && self.version == self.jam_track.version else - self.signed_44 && self.url_44.present? && self.url_44.file.exists? + self.signed_44 && self.url_44.present? && self.url_44.file.exists? && self.version == self.jam_track.version end end diff --git a/ruby/lib/jam_ruby/models/jam_track_search.rb b/ruby/lib/jam_ruby/models/jam_track_search.rb new file mode 100644 index 000000000..a83e7e647 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_search.rb @@ -0,0 +1,168 @@ +module JamRuby + class JamTrackSearch < BaseSearch + + cattr_accessor :jschema, :search_meta + attr_accessor :user_counters + + KEY_QUERY = 'query' + KEY_SEARCH_STR = 'search_str' + KEY_RESULT_TYPES = 'result_types' + KEY_SONGS = 'songs' + KEY_ARTISTS = 'artists' + KEY_RESULTS = 'results' + KEY_RESULT_SETS = 'result_sets' + KEY_PAGE_NUM = 'page_num' + KEY_TOTAL_COUNT = 'total_count' + KEY_PAGE_COUNT = 'page_count' + KEY_PER_PAGE = 'per_page' + PER_PAGE = 'development'==Rails.env ? 8 : 20 + KEY_GENRES = 'genres' + KEY_INSTRUMENTS = 'instruments' + KEY_LANGUAGE = 'language' + KEY_ORIGINAL_ARTIST = 'original_artist' + + def self.json_schema + return @@jschema ||= { + KEY_QUERY => { + KEY_SEARCH_STR => '', + KEY_INSTRUMENTS => [], + KEY_GENRES => [], + KEY_LANGUAGE => '', + KEY_ORIGINAL_ARTIST => '', + KEY_RESULT_TYPES => [], + KEY_PAGE_NUM => 1, + KEY_PER_PAGE => PER_PAGE, + }, + KEY_RESULT_SETS => { + KEY_SONGS => { + KEY_RESULTS => [], + KEY_PAGE_NUM => 1, + KEY_TOTAL_COUNT => 0, + KEY_PAGE_COUNT => 0, + }, + KEY_ARTISTS => { + KEY_RESULTS => [], + KEY_PAGE_NUM => 1, + KEY_TOTAL_COUNT => 0, + KEY_PAGE_COUNT => 0, + }, + }, + } + end + + def self.search_target_class + JamTrack + end + + def do_search(query) + rel = JamTrack.unscoped + + unless (gids = query[KEY_GENRES]).blank? + allgids = self.class.genre_ids + gids = gids.select { |gg| allgids.has_key?(gg) } + + unless gids.blank? + sqlstr = "'#{gids.join("','")}'" + rel = rel.joins(:genres_jam_tracks) + rel = rel.where("genres_jam_tracks.genre_id IN (#{sqlstr})") + end + end + unless (instruments = query[KEY_INSTRUMENTS]).blank? + instrids = self.class.instrument_ids + instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) } + + unless instruments.blank? + sqlstr = "'#{instruments.join("','")}'" + rel = rel.joins(:jam_track_tracks) + rel = rel.where("jam_track_tracks.instrument_id IN (#{sqlstr})") + rel = rel.where("jam_track_tracks.track_type = 'Track'") + end + end + + unless (artist_name = query[KEY_ORIGINAL_ARTIST]).blank? + rel = rel.where(original_artist: artist_name) + end + + rel + end + + def search_results_page(query=nil) + filter = { + KEY_QUERY => query, + } + result_types = query[KEY_RESULT_TYPES] + if result_types + has_songs, has_artists = result_types.index(KEY_SONGS), result_types.index(KEY_ARTISTS) + else + has_songs, has_artists = true, true + end + result_sets = filter[KEY_RESULT_SETS] = self.class.json_schema[KEY_RESULT_SETS].clone + if has_songs + rel = do_search(query) + unless (val = query[KEY_SEARCH_STR]).blank? + tsquery = Search.create_tsquery(val) + rel = rel.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) if tsquery + end + rel = rel.order(:name).includes(:genres) + + pgnum = [query[KEY_PAGE_NUM].to_i, 1].max + rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE]) + + results = rel.all.collect do |jt| + { + 'id' => jt.id, + 'name' => jt.name, + 'artist' => jt.original_artist, + 'genre' => jt.genres.map(&:description).join(', '), + 'plan_code' => jt.plan_code, + 'year' => jt.year + } + end + + result_sets[KEY_SONGS] = { + KEY_RESULTS => results, + KEY_PAGE_NUM => pgnum, + KEY_TOTAL_COUNT => rel.total_entries, + KEY_PAGE_COUNT => rel.total_pages, + } + end + + if has_artists + rel = do_search(query) + counter = rel.select("DISTINCT(jam_tracks.original_artist)") + rel = rel.select("DISTINCT ON(jam_tracks.original_artist) jam_tracks.id, jam_tracks.original_artist") + + unless (val = query[KEY_SEARCH_STR]).blank? + rel = rel.where("original_artist ILIKE ?","%#{val}%") + counter = counter.where("original_artist ILIKE ?","%#{val}%") + end + rel = rel.order(:original_artist) + + pgnum = [query[KEY_PAGE_NUM].to_i, 1].max + rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE]) + + results = rel.all.collect do |jt| + { 'id' => jt.id, 'artist' => jt.original_artist } + end + + artist_count = counter.count + + result_sets[KEY_ARTISTS] = { + KEY_RESULTS => results, + KEY_PAGE_NUM => pgnum, + KEY_TOTAL_COUNT => artist_count, + KEY_PAGE_COUNT => (artist_count / query[KEY_PER_PAGE].to_f).ceil, + } + end + + filter + end + + def self.all_languages + JamTrack.select("SELECT DISTINCT(language)").order(:language).collect do |lang| + { description: ISO_639.find_by_code(lang), id: lang } + end + end + + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index c0548e6fd..77938d0c3 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -6,7 +6,7 @@ module JamRuby include JamRuby::S3PublicManagerMixin # there should only be one Master per JamTrack, but there can be N Track per JamTrack - TRACK_TYPE = %w{Track Master} + TRACK_TYPE = %w{Track Master Click} @@log = Logging.logger[JamTrackTrack] @@ -19,7 +19,7 @@ module JamRuby attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin - attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error + attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error, :wav_file, :tmp_duration, :skip_inst_part_uniq before_destroy :delete_s3_files @@ -27,29 +27,44 @@ module JamRuby validates :part, length: {maximum: 35} validates :track_type, inclusion: {in: TRACK_TYPE } validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true - validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id] + validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id], unless: :skip_inst_part_uniq # validates :jam_track, presence: true belongs_to :instrument, class_name: "JamRuby::Instrument" belongs_to :jam_track, class_name: "JamRuby::JamTrack" has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy + has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_stem_id', inverse_of: :last_stem # create storage directory that will house this jam_track, as well as def store_dir "jam_track_tracks" end + + def licensor_suffix + suffix = '' + if jam_track.licensor + raise "no licensor name" if jam_track.licensor.name.nil? + suffix = " - #{jam_track.licensor.name}" + end + suffix + end + # create name of the file def filename(original_name) - "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}/#{original_name}" end # create name of the preview file. # md5-'ed because we cache forever def preview_filename(md5, ext='ogg') original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}" - "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + "#{preview_directory}/#{original_name}" + end + + def preview_directory + "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}" end def has_preview? @@ -58,7 +73,16 @@ module JamRuby # generates a URL that points to a public version of the preview def preview_public_url(media_type='ogg') - url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url] + case media_type + when 'ogg' + url = self[:preview_url] + when 'mp3' + url = self[:preview_mp3_url] + when 'aac' + url = self[:preview_aac_url] + else + raise "unknown media_type #{media_type}" + end if url s3_public_manager.public_url(url,{ :secure => true}) else @@ -66,6 +90,18 @@ module JamRuby end end + def display_name + if track_type == 'Master' + 'Master Mix' + else + display_part = '' + if part + display_part = "-(#{part})" + end + "#{instrument.description}#{display_part}" + end + end + def manually_uploaded_filename(mounted_as) if track_type == 'Master' filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") @@ -89,6 +125,18 @@ module JamRuby def sign_url(expiration_time = 120, sample_rate=48) s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true}) end + + def web_download_sign_url(expiration_time = 120, type='mp3', content_type = nil, response_content_disposition = nil) + options = {:expires => expiration_time, :secure => true} + options[:response_content_type] = content_type if content_type + options[:response_content_disposition] = response_content_disposition if response_content_disposition + + url_field = self['url_' + type + '_48'] + url_field = self['url_48'] if type == 'ogg' # ogg has different column format in database + + + s3_manager.sign_url(url_field, options) + end def can_download?(user) # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? @@ -157,6 +205,7 @@ module JamRuby uuid = SecureRandom.uuid output = File.join(tmp_dir, "#{uuid}.ogg") output_mp3 = File.join(tmp_dir, "#{uuid}.mp3") + output_aac = File.join(tmp_dir, "#{uuid}.aac") start = self.preview_start_time.to_f / 1000 stop = start + 20 @@ -176,7 +225,6 @@ module JamRuby # now create mp3 off of ogg preview convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - @@log.debug("converting to mp3 using: " + convert_mp3_cmd) convert_output = `#{convert_mp3_cmd}` @@ -187,35 +235,55 @@ module JamRuby @@log.debug("fail #{result_code}") @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" else - ogg_digest = ::Digest::MD5.file(output) - mp3_digest = ::Digest::MD5.file(output_mp3) - self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest - self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest - @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") - s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) - @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") - s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -c:a libfdk_aac -b:a 192k \"#{output_aac}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) - self.skip_uploader = true + convert_output = `#{convert_aac_cmd}` - original_ogg_preview_url = self["preview_url"] - original_mp3_preview_url = self["preview_mp3_url"] + result_code = $?.to_i - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') - self["preview_length"] = File.new(output).size - # and finally update the JamTrackTrack with the new info - self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') - self["preview_mp3_length"] = File.new(output_mp3).size - self.save! + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute aac convert command #{convert_output}" + else - # if all that worked, now delete old previews, if present - begin - s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] - s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] - rescue - puts "UNABLE TO CLEANUP OLD PREVIEW URL" + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + aac_digest = ::Digest::MD5.file(output_aac) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + self["preview_aac_md5"] = aac_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + @@log.debug("uploading aac preview to #{self.preview_filename('aac')}") + s3_public_manager.upload(self.preview_filename(aac_md5, 'aac'), output_aac, content_type: 'audio/aac', content_md5: aac_digest.base64digest) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + original_aac_preview_url = self["preview_aac_url"] + + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self["preview_aac_url"] = self.preview_filename(aac_md5, 'aac') + self["preview_aac_length"] = File.new(output_aac).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + s3_public_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end end end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 1f56efc08..d2998a72b 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -36,6 +36,8 @@ module JamRuby belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id + belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions + has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id" has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id" @@ -116,6 +118,7 @@ module JamRuby new_session.open_rsvps = self.open_rsvps new_session.is_unstructured_rsvp = self.is_unstructured_rsvp new_session.legal_terms = true + new_session.session_controller = self.session_controller # copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots RsvpSlot.find_each(:conditions => "music_session_id = '#{self.id}'") do |slot| @@ -255,6 +258,30 @@ module JamRuby end end + def set_session_controller(current_user, user) + + # only allow update of session controller by the creator or the currently marked user + + should_tick = false + + if current_user != creator && current_user != self.session_controller + return should_tick + end + + if active_music_session + if user + if active_music_session.users.exists?(user) + self.session_controller = user + should_tick = save + end + else + self.session_controller = nil + should_tick = save + end + end + should_tick + end + def self.index(current_user, user_id, band_id = nil, genre = nil) hide_private = false if current_user.id != user_id @@ -343,6 +370,7 @@ module JamRuby ms.legal_terms = true ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps] ms.creator = user + ms.session_controller = user ms.create_type = options[:create_type] ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp] ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone] diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index 86d132025..c8b8c0834 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -21,6 +21,10 @@ module JamRuby .first end + def name + user.name + end + def music_session @msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id) end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 5b008a55f..d86d73d12 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -14,6 +14,7 @@ module JamRuby belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id" belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id" + belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id" validates :target_user, :presence => true validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message? @@ -1255,7 +1256,7 @@ module JamRuby def send_jam_track_sign_complete(jam_track_right) notification = Notification.new - notification.jam_track_right_id = jam_track_right.id + notification.jam_track_mixdown_package = jam_track_right.id notification.description = NotificationTypes::JAM_TRACK_SIGN_COMPLETE notification.target_user_id = jam_track_right.user_id notification.save! @@ -1265,6 +1266,30 @@ module JamRuby #@@mq_router.publish_to_all_clients(msg) end + def send_mixdown_sign_failed(jam_track_mixdown_package) + + notification = Notification.new + notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_FAILED + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_failed(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + + def send_mixdown_sign_complete(jam_track_mixdown_package) + + notification = Notification.new + notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_COMPLETE + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_complete(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + def send_client_update(product, version, uri, size) msg = @@message_factory.client_update( product, version, uri, size) diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 99f21c09e..0fbf186e8 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -205,7 +205,7 @@ module JamRuby end # Start recording a session. - def self.start(music_session, owner) + def self.start(music_session, owner, record_video: false) recording = nil # Use a transaction and lock to avoid races. music_session.with_lock do @@ -213,6 +213,7 @@ module JamRuby recording.music_session = music_session recording.owner = owner recording.band = music_session.band + recording.video = record_video if recording.save GoogleAnalyticsEvent.report_band_recording(recording.band) @@ -700,6 +701,10 @@ module JamRuby self.save(:validate => false) end + def add_video_data(data) + Recording.where(id: self.id).update_all(external_video_id: data[:video_id]) + end + def add_timeline(timeline) global = timeline["global"] raise JamArgumentError, "global must be specified" unless global diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb index 9d0fc9bd4..006611f61 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -92,53 +92,15 @@ module JamRuby transaction.save! # now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided - - if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void' sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id) - if sale && sale.is_jam_track_sale? - if sale.sale_line_items.length == 1 - if sale.recurly_total_in_cents == transaction.amount_in_cents - line_item = sale.sale_line_items[0] - jam_track = line_item.product - jam_track_right = jam_track.right_for_user(transaction.user) if jam_track - if jam_track_right - line_item.affiliate_refunded = true - line_item.affiliate_refunded_at = Time.now - line_item.save! + if sale + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice", + body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" + }).deliver - jam_track_right.destroy - - # associate which JamTrack we assume this is related to in this one success case - transaction.jam_track = jam_track - transaction.save! - - AdminMailer.recurly_alerts(transaction.user, { - subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked", - body: "A #{transaction.transaction_type} event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result." - }).deliver - else - AdminMailer.recurly_alerts(transaction.user, { - subject: "NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete", - body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..." - }).deliver - end - - else - AdminMailer.recurly_alerts(transaction.user, { - subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale", - body: "We received a #{transaction.transaction_type} notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}" - }).deliver - end - - - else - AdminMailer.recurly_alerts(transaction.user, { - subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks", - body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" - }).deliver - end else AdminMailer.recurly_alerts(transaction.user, { subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales", diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 70279f8eb..cddf6b8d8 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -69,27 +69,12 @@ module JamRuby } end - def self.preview_invoice(current_user, shopping_carts) - line_items = {jam_tracks: []} - shopping_carts_jam_tracks = [] - shopping_carts_subscriptions = [] - shopping_carts.each do |shopping_cart| - - if shopping_cart.is_jam_track? - shopping_carts_jam_tracks << shopping_cart - else - # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase - shopping_carts_subscriptions << shopping_cart - end + def self.ios_purchase(current_user, jam_track, receipt) + jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right| + jam_track_right.redeemed = false + jam_track_right.version = jam_track.version end - - jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks) - line_items[:jam_tracks] = jam_track_items if jam_track_items - - # TODO: process shopping_carts_subscriptions - - line_items end # place_order will create one or more sales based on the contents of shopping_carts for the current user @@ -99,19 +84,14 @@ module JamRuby def self.place_order(current_user, shopping_carts) sales = [] - shopping_carts_jam_tracks = [] - shopping_carts_subscriptions = [] - shopping_carts.each do |shopping_cart| - if shopping_cart.is_jam_track? - shopping_carts_jam_tracks << shopping_cart - else - # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase - shopping_carts_subscriptions << shopping_cart - end + + if Sale.is_mixed(shopping_carts) + # the controller checks this too; this is just an extra-level of sanity checking + return sales end - jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks) + jam_track_sale = order_jam_tracks(current_user, shopping_carts) sales << jam_track_sale if jam_track_sale # TODO: process shopping_carts_subscriptions @@ -119,22 +99,52 @@ module JamRuby sales end - def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks) - ### XXX TODO; - # we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js + def self.is_only_freebie(shopping_carts) + free = true + shopping_carts.each do |cart| + free = cart.product_info[:free] - # if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct) - # just a pain to implement + if !free + break + end + end + free end - def self.is_only_freebie(shopping_carts_jam_tracks) - shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free] + # we don't allow mixed shopping carts :/ + def self.is_mixed(shopping_carts) + free = false + non_free = false + shopping_carts.each do |cart| + if cart.product_info[:free] + free = true + else + non_free = true + end + end + free && non_free end # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned - def self.order_jam_tracks(current_user, shopping_carts_jam_tracks) + def self.order_jam_tracks(current_user, shopping_carts) + + shopping_carts_jam_tracks = [] + shopping_carts_subscriptions = [] + shopping_carts_gift_cards = [] + + shopping_carts.each do |shopping_cart| + if shopping_cart.is_jam_track? + shopping_carts_jam_tracks << shopping_cart + elsif shopping_cart.is_gift_card? + shopping_carts_gift_cards << shopping_cart + else + # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase + raise "unknown shopping cart type #{shopping_cart.cart_type}" + shopping_carts_subscriptions << shopping_cart + end + end client = RecurlyClient.new @@ -143,8 +153,8 @@ module JamRuby sale = create_jam_track_sale(current_user) if sale.valid? - if is_only_freebie(shopping_carts_jam_tracks) - sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil) + if is_only_freebie(shopping_carts) + sale.process_shopping_carts(current_user, shopping_carts, nil) sale.recurly_subtotal_in_cents = 0 sale.recurly_tax_in_cents = 0 @@ -159,11 +169,13 @@ module JamRuby return sale end - sale_line_item = sale.sale_line_items[0] - sale_line_item.recurly_tax_in_cents = 0 - sale_line_item.recurly_total_in_cents = 0 - sale_line_item.recurly_currency = 'USD' - sale_line_item.recurly_discount_in_cents = 0 + sale.sale_line_items.each do |sale_line_item| + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents = 0 + sale_line_item.recurly_total_in_cents = 0 + sale_line_item.recurly_currency = 'USD' + sale_line_item.recurly_discount_in_cents = 0 + end sale.save else @@ -173,7 +185,7 @@ module JamRuby purge_pending_adjustments(account) - created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) # now invoice the sale ... almost done @@ -229,13 +241,13 @@ module JamRuby sale end - def process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + def process_shopping_carts(current_user, shopping_carts, account) created_adjustments = [] begin - shopping_carts_jam_tracks.each do |shopping_cart| - process_jam_track(current_user, shopping_cart, account, created_adjustments) + shopping_carts.each do |shopping_cart| + process_shopping_cart(current_user, shopping_cart, account, created_adjustments) end rescue Recurly::Error, NoMethodError => x # rollback any adjustments created if error @@ -251,7 +263,7 @@ module JamRuby end - def process_jam_track(current_user, shopping_cart, account, created_adjustments) + def process_shopping_cart(current_user, shopping_cart, account, created_adjustments) recurly_adjustment_uuid = nil recurly_adjustment_credit_uuid = nil @@ -259,15 +271,20 @@ module JamRuby shopping_cart.reload # get the JamTrack in this shopping cart - jam_track = shopping_cart.cart_product + cart_product = shopping_cart.cart_product - if jam_track.right_for_user(current_user) - # if the user already owns the JamTrack, we should just skip this cart item, and destroy it - # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop - ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart) - return + if shopping_cart.is_jam_track? + jam_track = cart_product + if jam_track.right_for_user(current_user) + # if the user already owns the JamTrack, we should just skip this cart item, and destroy it + # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop + ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart) + return + end end + + if account # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack adjustments = shopping_cart.create_adjustment_attributes(current_user) @@ -300,38 +317,70 @@ module JamRuby # if the sale line item is invalid, blow up the transaction unless sale_line_item.valid? - @log.error("sale item invalid! #{sale_line_item.errors.inspect}") + @@log.error("sale item invalid! #{sale_line_item.errors.inspect}") puts("sale item invalid! #{sale_line_item.errors.inspect}") Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1}) raise RecurlyClientError.new(sale_line_item.errors) end - # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident) - jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right| - jam_track_right.redeemed = shopping_cart.free? - end + if shopping_cart.is_jam_track? + jam_track = cart_product - # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks - if shopping_cart.free? - User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) - current_user.has_redeemable_jamtrack = false # make sure model reflects the truth - end - - - # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path - if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid - jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid - jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid - unless jam_track_right.save - raise RecurlyClientError.new(jam_track_right.errors) + # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident) + jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right| + jam_track_right.redeemed = shopping_cart.free? + jam_track_right.version = jam_track.version end + + # also if the purchase was a free one, then: + # first, mark the free has_redeemable_jamtrack field if that's still true + # and if still they have more free things, then redeem the giftable_jamtracks + if shopping_cart.free? + if user.has_redeemable_jamtrack + User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) + current_user.has_redeemable_jamtrack = false + else + User.where(id: current_user.id).update_all(gifted_jamtracks: current_user.gifted_jamtracks - 1) + current_user.gifted_jamtracks = current_user.gifted_jamtracks - 1 + end + end + + + # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path + if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid + jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid + jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid + unless jam_track_right.save + raise RecurlyClientError.new(jam_track_right.errors) + end + end + + # blow up the transaction if the JamTrackRight did not get created + raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any? + + elsif shopping_cart.is_gift_card? + gift_card_type = cart_product + raise "gift card is null" if gift_card_type.nil? + raise if current_user.nil? + + shopping_cart.quantity.times do |item| + gift_card_purchase = GiftCardPurchase.new( + { + user: current_user, + gift_card_type: gift_card_type + }) + + unless gift_card_purchase.save + raise RecurlyClientError.new(gift_card_purchase.errors) + end + end + + else + raise 'unknown shopping cart type: ' + shopping_cart.cart_type end # delete the shopping cart; it's been dealt with shopping_cart.destroy if shopping_cart - - # blow up the transaction if the JamTrackRight did not get created - raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any? end @@ -361,7 +410,7 @@ module JamRuby def self.create_jam_track_sale(user) sale = Sale.new sale.user = user - sale.sale_type = JAMTRACK_SALE + sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale sale.order_total = 0 sale.save sale diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 7d744cfe5..5cbe5ad87 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -4,14 +4,16 @@ module JamRuby JAMBLASTER = 'JamBlaster' JAMCLOUD = 'JamCloud' JAMTRACK = 'JamTrack' + GIFTCARD = 'GiftCardType' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' + belongs_to :gift_card, class_name: 'JamRuby::GiftCard' belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' - validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -21,9 +23,19 @@ module JamRuby validates :recurly_plan_code, presence:true validates :sale, presence:true + def is_jam_track? + product_type == JAMTRACK + end + + def is_gift_card? + product_type == GIFTCARD + end + def product if product_type == JAMTRACK JamTrack.find_by_id(product_id) + elsif product_type == GIFTCARD + GiftCardType.find_by_id(product_id) else raise 'unsupported product type' end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index 568d94048..fb531c2bd 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -12,6 +12,8 @@ module JamRuby attr_accessible :quantity, :cart_type, :product_info + attr_accessor :skip_mix_check + validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id] belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" @@ -20,12 +22,13 @@ module JamRuby validates :cart_type, presence: true validates :cart_class_name, presence: true validates :marked_for_redeem, numericality: {only_integer: true} + validate :not_mixed default_scope order('created_at DESC') def product_info product = self.cart_product - {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region} unless product.nil? + {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil? end # multiply quantity by price @@ -38,6 +41,31 @@ module JamRuby (quantity - marked_for_redeem) * product.price end + def not_mixed + + return if @skip_mix_check + existing_carts = [] + this_user = any_user() + + if this_user + existing_carts = this_user.shopping_carts + end + + existing_carts = existing_carts.to_a + existing_carts << self + + if Sale.is_mixed(existing_carts) + if free? + errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.") + return false + else + errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.") + return false + end + end + + false + end def cart_product self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? @@ -51,7 +79,18 @@ module JamRuby marked_for_redeem == quantity end + def any_user + if user + user + elsif anonymous_user_id + AnonymousUser.new(anonymous_user_id, nil) + else + nil + end + end + def self.create user, product, quantity = 1, mark_redeem = false + cart = ShoppingCart.new if user.is_a?(User) cart.user = user @@ -72,39 +111,42 @@ module JamRuby cart_type == JamTrack::PRODUCT_TYPE end + def is_gift_card? + cart_type == GiftCardType::PRODUCT_TYPE + end # returns an array of adjustments for the shopping cart def create_adjustment_attributes(current_user) - raise "not a jam track" unless is_jam_track? + raise "not a jam track or gift card" unless is_jam_track? || is_gift_card? info = self.product_info if free? - # create the credit, then the pseudo charge [ { accounting_code: PURCHASE_FREE_CREDIT, currency: 'USD', unit_amount_in_cents: -(info[:total_price] * 100).to_i, - description: "JamTrack: " + info[:name] + " (Credit)", + description: info[:sale_display] + " (Credit)", tax_exempt: true }, { accounting_code: PURCHASE_FREE, currency: 'USD', unit_amount_in_cents: (info[:total_price] * 100).to_i, - description: "JamTrack: " + info[:name], + description: info[:sale_display], tax_exempt: true } ] else + [ { accounting_code: PURCHASE_NORMAL, currency: 'USD', unit_amount_in_cents: (info[:total_price] * 100).to_i, - description: "JamTrack: " + info[:name], + description: info[:sale_display], tax_exempt: false } ] @@ -113,8 +155,13 @@ module JamRuby def self.move_to_user(user, anonymous_user, shopping_carts) shopping_carts.each do |shopping_cart| - mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) - cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) + if shopping_cart.is_jam_track? + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) + else + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false) + end + end anonymous_user.destroy_all_shopping_carts @@ -134,28 +181,32 @@ module JamRuby # if no shpping carts have been marked, then mark it redeemable # should be wrapped in a TRANSACTION def self.user_has_redeemable_jam_track?(any_user) - mark_redeem = false - if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack - mark_redeem = true # start out assuming we can redeem... + + if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0 + + free_in_cart = 0 any_user.shopping_carts.each do |shopping_cart| # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true - if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0 - mark_redeem = false - break + if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE + free_in_cart += shopping_cart.marked_for_redeem end end + + any_user.free_jamtracks > free_in_cart + else + false end - mark_redeem end # adds a jam_track to cart, checking for promotions - def self.add_jam_track_to_cart(any_user, jam_track) + def self.add_jam_track_to_cart(any_user, jam_track, clear:false) cart = nil ShoppingCart.transaction do - if any_user.has_redeemable_jamtrack - # if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart - any_user.destroy_all_shopping_carts + if clear + # if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario' + any_user.destroy_jam_track_shopping_carts + any_user.reload end mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) @@ -164,23 +215,66 @@ module JamRuby cart end + def self.add_item_to_cart(any_user, item) + cart = nil + ShoppingCart.transaction do + cart = ShoppingCart.create(any_user, item, 1, false) + end + cart + end + # deletes a jam track from the shopping cart, updating redeem flag as necessary def self.remove_jam_track_from_cart(any_user, cart) ShoppingCart.transaction do cart.destroy - # check if we should move the redemption + + # so that user.shopping_carts reflects truth + any_user.reload + + # check if we should move the redemption around automatically mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) carts = any_user.shopping_carts - # if we find any carts on the account, mark one redeemable + # if we find any carts on the account that are not redeemed, mark first one redeemable if mark_redeem && carts.length > 0 - carts[0].redeem(mark_redeem) - carts[0].save + carts.each do |cart| + if cart.marked_for_redeem == 0 + if cart.quantity > 1 + raise 'unknown situation for redeemption juggling' + end + cart.redeem(mark_redeem) + cart.save + break + end + end end end end + def self.remove_item_from_cart(any_user, cart) + ShoppingCart.transaction do + cart.destroy + end + end + + # if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up + def self.apply_gifted_jamtracks(user) + jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE) + + if jam_track_carts.count > user.gifted_jamtracks + # just whack everything in their shopping cart + user.destroy_all_shopping_carts + return + end + + jam_track_carts.each do |cart| + cart.skip_mix_check = true + cart.marked_for_redeem = 1 + cart.save! + end + end + def port(user, anonymous_user) ShoppingCart.transaction do diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index f7d48c669..53327a746 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -40,10 +40,12 @@ module JamRuby attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection # updating_password corresponds to a lost_password - attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json + attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' + has_many :controlled_sessions, :class_name=> "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id + # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" @@ -149,6 +151,11 @@ module JamRuby # events has_many :event_sessions, :class_name => "JamRuby::EventSession" + # gift cards + has_many :gift_cards, :class_name=> "JamRuby::GiftCard" + has_many :gift_card_purchases, :class_name=> "JamRuby::GiftCardPurchase" + + # affiliate_partner has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count @@ -177,11 +184,13 @@ module JamRuby has_one :musician_search, :class_name => 'JamRuby::MusicianSearch' has_one :band_search, :class_name => 'JamRuby::BandSearch' + before_save :default_anonymous_names before_save :create_remember_token, :if => :should_validate_password? before_save :stringify_avatar_info , :if => :updating_avatar - validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true - validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true + validates :first_name, length: {maximum: 50}, no_profanity: true + validates :last_name, length: {maximum: 50}, no_profanity: true + validates :last_name, length: {maximum: 50}, no_profanity: true validates :biography, length: {maximum: 4000}, no_profanity: true validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email @@ -193,6 +202,7 @@ module JamRuby validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false } validates :reuse_card, :inclusion => {:in => [true, false]} validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} + validates :gifted_jamtracks, presence: true, :numericality => { :less_than_or_equal_to => 100 } validates :subscribe_email, :inclusion => {:in => [nil, true, false]} validates :musician, :inclusion => {:in => [true, false]} validates :show_whats_next, :inclusion => {:in => [nil, true, false]} @@ -213,6 +223,7 @@ module JamRuby validate :email_case_insensitive_uniqueness validate :update_email_case_insensitive_uniqueness, :if => :updating_email validate :validate_mods + validate :presence_gift_card, :if => :expecting_gift_card scope :musicians, where(:musician => true) scope :fans, where(:musician => false) @@ -232,6 +243,18 @@ module JamRuby end end + def has_any_free_jamtracks + has_redeemable_jamtrack || gifted_jamtracks > 0 + end + + def free_jamtracks + (has_redeemable_jamtrack ? 1 : 0) + gifted_jamtracks + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + def failed_qualification(reason) self.last_failed_certified_gear_at = DateTime.now self.last_failed_certified_gear_reason = reason @@ -254,6 +277,12 @@ module JamRuby end end + def presence_gift_card + if self.gift_cards.length == 0 + errors.add(:gift_card, ValidationMessages::NOT_FOUND) + end + end + def validate_current_password # checks if the user put in their current password (used when changing your email, for instance) errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password) @@ -296,8 +325,16 @@ module JamRuby online? end + def anonymous? + first_name == 'Anonymous' && last_name == 'Anonymous' + end + def name - "#{first_name} #{last_name}" + if anonymous? + 'Anonymous' + else + "#{first_name} #{last_name}" + end end def location @@ -576,9 +613,7 @@ module JamRuby def to_s return email unless email.nil? - if !first_name.nil? && !last_name.nil? - return first_name + ' ' + last_name - end + return name unless name.nil? id end @@ -1018,6 +1053,7 @@ module JamRuby reuse_card = options[:reuse_card] signup_hint = options[:signup_hint] affiliate_partner = options[:affiliate_partner] + gift_card = options[:gift_card] user = User.new @@ -1029,6 +1065,9 @@ module JamRuby user.terms_of_service = terms_of_service user.musician = musician user.reuse_card unless reuse_card.nil? + user.gifted_jamtracks = 0 + user.has_redeemable_jamtrack = true + # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. @@ -1133,8 +1172,22 @@ module JamRuby end end + found_gift_card = nil + + # if a gift card value was passed in, then try to find that gift card and apply it to user + if gift_card + user.expecting_gift_card = true + found_gift_card = GiftCard.where(code:gift_card).where(user_id:nil).first + user.gift_cards << found_gift_card if found_gift_card + end + user.save + if found_gift_card + user.reload + ShoppingCart.apply_gifted_jamtracks(user) + end + # if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it # only_freebie_in_cart = # signup_hint && @@ -1174,6 +1227,7 @@ module JamRuby end end end + user.reload if user.id# gift card adding gifted_jamtracks doesn't reflect here until reload user end # def signup @@ -1629,6 +1683,11 @@ module JamRuby ShoppingCart.where("user_id=?", self).destroy_all end + def destroy_jam_track_shopping_carts + ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE) + end + + def unsubscribe_token self.class.create_access_token(self) end @@ -1681,14 +1740,17 @@ module JamRuby else false end - - end private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 end + def default_anonymous_names + self.first_name = 'Anonymous' if self.first_name.nil? + self.last_name = 'Anonymous' if self.last_name.nil? + end + def stringify_avatar_info # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb index e83bb8db4..f4c1523eb 100644 --- a/ruby/lib/jam_ruby/models/user_authorization.rb +++ b/ruby/lib/jam_ruby/models/user_authorization.rb @@ -1,7 +1,7 @@ module JamRuby class UserAuthorization < ActiveRecord::Base - attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user + attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user, :refresh_token self.table_name = "user_authorizations" @@ -12,6 +12,36 @@ module JamRuby validates_uniqueness_of :uid, scope: :provider # token, secret, token_expiration can be missing + def self.refreshing_google_auth(user) + auth = self.where(:user_id => user.id) + .where(:provider => 'google_login') + .limit(1).first + + # if we have an auth that will expire in less than 10 minutes + if auth && auth.refresh_token && auth.token_expiration < Time.now - 60 * 10 + + begin + oauth_client = OAuth2::Client.new( + Rails.application.config.google_client_id, Rails.application.config.google_secret, + :site => "https://accounts.google.com", + :token_url => "/o/oauth2/token", + :authorize_url => "/o/oauth2/auth") + access_token = OAuth2::AccessToken.from_hash(oauth_client, {:refresh_token => auth.refresh_token}) + access_token = access_token.refresh! + + auth.token = access_token.token + auth.token_expiration = Time.now + access_token.expires_in + auth.save + return auth + rescue Exception => e + # couldn't refresh; probably the user has revoked the app's rights + return nil + end + else + auth + end + end + def self.google_auth(user) self .where(:user_id => user.id) diff --git a/ruby/lib/jam_ruby/models/user_blacklist.rb b/ruby/lib/jam_ruby/models/user_blacklist.rb new file mode 100644 index 000000000..287ae5391 --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_blacklist.rb @@ -0,0 +1,27 @@ +module JamRuby + class UserBlacklist < ActiveRecord::Base + + attr_accessible :user_id, :notes, as: :admin + @@log = Logging.logger[UserBlacklist] + + belongs_to :user, :class_name => "JamRuby::User" + + validates :user, presence:true, uniqueness: true + + def self.listed(user) + UserBlacklist.count(:conditions => "user_id= '#{user.id}'") == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + id + end + + def to_s + user + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_event.rb b/ruby/lib/jam_ruby/models/user_event.rb new file mode 100644 index 000000000..21446c005 --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_event.rb @@ -0,0 +1,8 @@ +module JamRuby + class UserEvent < ActiveRecord::Base + + belongs_to :user, class_name: 'JamRuby::User' + + validates :name, presence: true + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb new file mode 100644 index 000000000..05b7a926d --- /dev/null +++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb @@ -0,0 +1,771 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + class JamTrackMixdownPackager + extend JamRuby::ResqueStats + + include JamRuby::S3ManagerMixin + + TAP_IN_PADDING = 2 + + MAX_PAN = 90 + MIN_PAN = -90 + KNOCK_SECONDS = 0.035 + + attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step + @queue = :jam_track_mixdown_packager + + def log + @log || Logging.logger[JamTrackMixdownPackager] + end + + def self.perform(mixdown_package_id, bitrate=48) + jam_track_builder = JamTrackMixdownPackager.new() + jam_track_builder.mixdown_package_id = mixdown_package_id + jam_track_builder.run + end + + def compute_steps + @step = 0 + number_downloads = @track_settings.length + number_volume_adjustments = (@track_settings.select { |track| should_alter_volume? track }).length + + pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0 + mix_steps = 1 + package_steps = 1 + + number_downloads + number_volume_adjustments + pitch_shift_steps + mix_steps + package_steps + end + + def run + begin + log.info("Mixdown job starting. mixdown_packager_id #{mixdown_package_id}") + begin + @mixdown_package = JamTrackMixdownPackage.find(mixdown_package_id) + + + # bailout check + if @mixdown_package.signed? + log.debug("package is already signed. bailing") + return + end + + @mixdown = @mixdown_package.jam_track_mixdown + @settings = JSON.parse(@mixdown.settings) + + process_jmep + + track_settings + + # compute the step count + total_steps = compute_steps + + # track that it's started ( and avoid db validations ) + signing_started_at = Time.now + last_step_at = Time.now + #JamTrackMixdownPackage.where(:id => @mixdown_package.id).update_all(:signing_started_at => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, :signing => true) + + # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly... + + @mixdown_package.current_packaging_step = 0 + @mixdown_package.packaging_steps = total_steps + @mixdown_package.signing_started_at = signing_started_at + @mixdown_package.signing = true + @mixdown_package.should_retry = false + @mixdown_package.last_step_at = last_step_at + @mixdown_package.queued = false + @mixdown_package.save + + SubscriptionMessage.mixdown_signing_job_change(@mixdown_package) + + package + + log.info "Signed mixdown package to #{@mixdown_package[:url]}" + + rescue Exception => e + # record the error in the database + post_error(e) + + #SubscriptionMessage.mixdown_signing_job_change(@mixdown_package) + # and let the job fail, alerting ops too + raise + end + end + end + + def should_alter_volume? track + + # short cut is possible if vol = 1.0 and pan = 0 + vol = track[:vol] + pan = track[:pan] + + vol != 1.0 || pan != 0 + end + + def process_jmep + @start_points = [] + @initial_padding = 0.0 + @tap_in_initial_silence = 0 + + speed = @settings['speed'] || 0 + + @speed_factor = 1.0 + (-speed.to_f / 100.0) + @inverse_speed_factor = 1 - (-speed.to_f / 100) + + log.info("speed factor #{@speed_factor}") + + jmep = @mixdown.jam_track.jmep_json + if jmep + jmep = JSON.parse(jmep) + end + + if jmep.nil? + log.debug("no jmep") + return + end + + events = jmep["Events"] + + return if events.nil? || events.length == 0 + + metronome = nil + events.each do |event| + if event.has_key?("metronome") + metronome = event["metronome"] + break + end + end + + if metronome.nil? || metronome.length == 0 + log.debug("no metronome events for jmep", jmep) + return + end + + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + log.debug("found #{@start_points.length} metronome start points") + + start_point = @start_points[0] + + if start_point + start_time = parse_time(start_point["ts"]) + + if start_time < 2.0 + padding = start_time - 2.0 + @initial_padding = padding.abs + @initial_tap_in = start_time + end + end + + if @speed_factor != 1.0 + metronome.length.times do |count| + + # we expect to find metronome start/stop grouped + if count % 2 == 0 + + start = metronome[count] + stop = metronome[count + 1] + + if start["action"] != "start" || stop["action"] != "stop" + # bail out + log.error("found de-coupled metronome events #{start.to_json} | #{stop.to_json}") + next + end + + bpm = start["bpm"].to_f + stop_time = parse_time(stop['ts']) + ticks = stop['ticks'].to_i + + + new_bpm = bpm * @inverse_speed_factor + new_stop_time = stop_time * @speed_factor + new_start_time = new_stop_time - (60.0/new_bpm * ticks) + + log.info("original bpm:#{bpm} start: #{parse_time(start["ts"])} stop: #{stop_time}") + log.info("updated bpm:#{new_bpm} start: #{new_start_time} stop: #{new_stop_time}") + + stop["ts"] = new_stop_time + start["ts"] = new_start_time + start["bpm"] = new_bpm + stop["bpm"] = new_bpm + + @tap_in_initial_silence = (@initial_tap_in + @initial_padding) * @speed_factor + + end + + end + end + + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + end + + # format like: "-0:00:02:820" + def parse_time(ts) + + if ts.is_a?(Float) + return ts + end + + time = 0.0 + negative = false + + if ts.start_with?('-') + negative = true + end + + # parse time_format + bits = ts.split(':').reverse + + bit_position = 0 + bits.each do |bit| + if bit_position == 0 + # milliseconds + milliseconds = bit.to_f + time += milliseconds/1000 + elsif bit_position == 1 + # seconds + time += bit.to_f + elsif bit_position == 2 + # minutes + time += 60 * bit.to_f + elsif bit_position == 3 + # hours + # not bothering + end + + bit_position += 1 + end + + if negative + time = 0.0 - time + end + + time + end + + def path_to_resources + File.join(File.dirname(File.expand_path(__FILE__)), '../../../lib/jam_ruby/app/assets/sounds') + end + + def knock_file + if long_sample_rate == 44100 + knock = File.join(path_to_resources, 'knock44.wav') + else + knock = File.join(path_to_resources, 'knock48.wav') + end + knock + end + + def create_silence(tmp_dir, segment_count, duration) + file = File.join(tmp_dir, "#{segment_count}.wav") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{file} trim 0.0 #{duration}", "silence") + + file + end + + def create_tapin_track(tmp_dir) + + return nil if @start_points.length == 0 + + segment_count = 0 + + + #initial_silence = @initial_tap_in + @initial_padding + + initial_silence = @tap_in_initial_silence + + #log.info("tapin data: initial_tap_in: #{@initial_tap_in}, initial_padding: #{@initial_padding}, initial_silence: #{initial_silence}") + + time_points = [] + files = [] + if initial_silence > 0 + + files << create_silence(tmp_dir, segment_count, initial_silence) + + time_points << {type: :silence, ts: initial_silence} + segment_count += 1 + end + + + time_cursor = nil + @start_points.each do |start_point| + tap_time = parse_time(start_point["ts"]) + if !time_cursor.nil? + between_silence = tap_time - time_cursor + files << create_silence(tmp_dir, segment_count, between_silence) + time_points << {type: :silence, ts: between_silence} + end + time_cursor = tap_time + bpm = start_point["bpm"].to_f + + tick_silence = 60.0/bpm - KNOCK_SECONDS + + ticks = start_point["ticks"].to_i + + ticks.times do |tick| + files << knock_file + files << create_silence(tmp_dir, segment_count, tick_silence) + time_points << {type: :knock, ts: KNOCK_SECONDS} + time_points << {type: :silence, ts: tick_silence} + time_cursor + 60.0/bpm + segment_count += 1 + end + end + + log.info("time points for tap-in: #{time_points.inspect}") + # do we need to pad with time? not sure + + sequence_cmd = "sox " + files.each do |file| + sequence_cmd << "\"#{file}\" " + end + + count_in = File.join(tmp_dir, "count-in.wav") + sequence_cmd << "\"#{count_in}\"" + + cmd(sequence_cmd, "count_in") + @count_in_file = count_in + count_in + end + + # creates a list of tracks to actually mix + def track_settings + altered_tracks = @settings["tracks"] || [] + + @track_settings = [] + + #void slider2Pan(int i, float *f); + + stems = @mixdown.jam_track.stem_tracks + @track_count = stems.length + + @include_count_in = @settings["count-in"] && @start_points.length > 0 && @mixdown_package.encrypt_type.nil? + + # temp + # @include_count_in = true + + if @include_count_in + @track_count += 1 + end + + stems.each do |stem| + + vol = 1.0 + pan = 0 + match = false + skipped = false + # is this stem in the altered_tracks list? + altered_tracks.each do |alteration| + + if alteration["id"] == stem.id + if alteration["mute"] || alteration["vol"] == 0 + log.debug("leaving out track because muted or 0 volume #{alteration.inspect}") + skipped = true + next + else + vol = alteration["vol"] || vol + pan = alteration["pan"] || pan + end + @track_settings << {stem: stem, vol: vol, pan: pan} + match = true + break + end + end + + # if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged + if !skipped && !match + @track_settings << {stem: stem, vol: vol, pan: pan} + end + end + + if @include_count_in + @track_settings << {count_in: true, vol: 1.0, pan: 0} + end + + @track_settings + end + + def slider_to_pan(pan) + # transpose MIN_PAN to MAX_PAN to + # 0-1.0 range + #assumes abs(MIN_PAN) == abs(MAX_PAN) + # k = f(i) = (i)/(2*MAX_PAN) + 0.5 + # so f(MIN_PAN) = -0.5 + 0.5 = 0 + + k = ((pan * (1.0))/ (2.0 * MAX_PAN)) + 0.5 + l, r = 0 + + if k == 0 + l = 0.0 + r = 1.0 + else + l = Math.sqrt(k) + r = Math.sqrt(1-k) + end + + [l, r] + end + + def package + + log.info("Settings: #{@settings.to_json}") + + Dir.mktmpdir do |tmp_dir| + + # download all files + @track_settings.each do |track| + + if track[:count_in] + file = create_tapin_track(tmp_dir) + bump_step(@mixdown_package) + else + jam_track_track = track[:stem] + + file = File.join(tmp_dir, jam_track_track.id + '.ogg') + + bump_step(@mixdown_package) + + # download each track needed + s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file) + end + + track[:file] = file + end + + audio_process tmp_dir + end + end + + def audio_process(tmp_dir) + # use sox remix to apply mute, volume, pan settings + + + # step 1: apply pan and volume per track. mute and vol of 0 has already been handled, by virtue of those tracks not being present in @track_settings + # step 2: mix all tracks into single track, dividing by constant number of jam tracks, which is same as done by client backend + # step 3: apply pitch and speed (if applicable) + # step 4: encrypt with jkz (if applicable) + + apply_vol_and_pan tmp_dir + + create_silence_padding tmp_dir + + mix tmp_dir + + pitch_speed tmp_dir + + final_packaging tmp_dir + end + + # output is :volumed_file in each track in @track_settings + def apply_vol_and_pan(tmp_dir) + @track_settings.each do |track| + + jam_track_track = track[:stem] + count_in = track[:count_in] + file = track[:file] + + unless should_alter_volume? track + track[:volumed_file] = file + else + pan_l, pan_r = slider_to_pan(track[:pan]) + + vol = track[:vol] + + # short + channel_l = pan_l * vol + channel_r = pan_r * vol + + bump_step(@mixdown_package) + + # sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0 + + if count_in + volumed_file = File.join(tmp_dir, 'count-in' + '-volumed.ogg') + else + volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg') + end + + cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_r} 2v#{channel_l}", 'vol_pan') + + track[:volumed_file] = volumed_file + end + end + end + + def create_silence_padding(tmp_dir) + if @initial_padding > 0 && @include_count_in + + @padding_file = File.join(tmp_dir, "initial_padding.ogg") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{@padding_file} trim 0.0 #{@initial_padding}", "initial_padding") + + @track_settings.each do |track| + + next if track[:count_in] + + input = track[:volumed_file] + output = input[0..-5] + '-padded.ogg' + + padd_cmd = "sox '#{@padding_file}' '#{input}' '#{output}'" + + cmd(padd_cmd, "pad_track_with_silence") + track[:volumed_file] = output + end + end + end + + # output is @mix_file + def mix(tmp_dir) + + bump_step(@mixdown_package) + + @mix_file = File.join(tmp_dir, "mix.ogg") + + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + + real_count = @track_settings.count + real_count -= 1 if @include_count_in + + # if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks + if real_count <= 1 + mix_divide = 1.0/@track_count + cmd = "sox -v #{mix_divide} \"#{@track_settings[0][:volumed_file]}\" \"#{@mix_file}\"" + cmd(cmd, 'volume_adjust') + else + # sox -m will divide by number of inputs by default. But we purposefully leave out tracks that are mute/no volume (to save downloading/processing time in this job) + # so we need to tell sox to divide by how many tracks there are as a constant, because this is how the client works today + #sox -m -v 1/n file1 -v 1/n file2 out + cmd = "sox -m" + mix_divide = 1.0/@track_count + @track_settings.each do |track| + + # if pitch/shifted, we lay the tap-in after pitch/speed shift + # next if (pitch != 0 || speed != 0) && track[:count_in] + next if track[:count_in] + + volumed_file = track[:volumed_file] + cmd << " -v #{mix_divide} \"#{volumed_file}\"" + end + + + cmd << " \"#{@mix_file}\"" + cmd(cmd, 'mix_adjust') + end + + + end + + def long_sample_rate + sample_rate = 48000 + if @mixdown_package.sample_rate != 48 + sample_rate = 44100 + end + sample_rate + end + + # output is @speed_mix_file + def pitch_speed tmp_dir + + # # usage + # This app will take an ogg, wav, or mp3 file (for the uploads) as its input and output an ogg file. + # Usage: + # sbsms path-to-input.ogg path-to-output.ogg TimeStrech PitchShift + + # input is @mix_file, created by mix() + # output is @speed_mix_file + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + # if pitch and speed are 0, we do nothing here + if pitch == 0 && speed == 0 + @speed_mix_file = @mix_file + else + bump_step(@mixdown_package) + + @speed_mix_file = File.join(tmp_dir, "speed_mix_file.ogg") + + # usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz + + sample_rate = long_sample_rate + + # rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/ + sbsms_speed = speed/100.0 + sbsms_speed = 1.0 + sbsms_speed + + sbsms_pitch = pitch + cmd("sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift') + end + + if @include_count_in + # lay the tap-ins over the recording + layered = File.join(tmp_dir, "layered_speed_mix.ogg") + cmd("sox -m '#{@count_in_file}' '#{@speed_mix_file}' '#{layered}'", "layer_tap_in") + @speed_mix_file = layered + end + end + + def final_packaging tmp_dir + + bump_step(@mixdown_package) + + url = nil + private_key = nil + md5 = nil + length = 0 + output = nil + + if @mixdown_package.encrypt_type + output, private_key = encrypt_jkz tmp_dir + else + # create output file to correct output format + output = convert tmp_dir + end + + # upload output to S3 + s3_url = "#{@mixdown_package.store_dir}/#{@mixdown_package.filename}" + s3_manager.upload(s3_url, output) + + length = File.size(output) + computed_md5 = Digest::MD5.new + File.open(output, 'rb').each { |line| computed_md5.update(line) } + md5 = computed_md5.to_s + + @mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s) + end + + # returns output destination, converting if necessary + def convert(tmp_dir) + # if the file already ends with the desired file type, call it a win + if @speed_mix_file.end_with?(@mixdown_package.file_type) + @speed_mix_file + else + # otherwise we need to convert from lastly created file to correct + output = File.join(tmp_dir, "output.#{@mixdown_package.file_type}") + + cmd("#{APP_CONFIG.normalize_ogg_path} --bitrate 192 \"#{@speed_mix_file}\"", 'normalize') + + if @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_AAC + cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac') + elsif @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_MP3 + cmd("ffmpeg -i \"#{@speed_mix_file}\" -ab 192k \"#{output}\"", 'convert_mp3') + else + raise 'unknown file_type' + end + + + output + end + end + + def encrypt_jkz(tmp_dir) + py_root = APP_CONFIG.jamtracks_dir + step = 0 + + private_key = nil + # we need to make the id of the custom mix be the name of the file (ID.ogg) + custom_mix_name = File.join(tmp_dir, "#{@mixdown.id}.ogg") + FileUtils.mv(@speed_mix_file, custom_mix_name) + jam_file_opts = "" + jam_file_opts << " -i #{Shellwords.escape("#{custom_mix_name}+mixdown")}" + + sku = @mixdown_package.id + title = @mixdown.name + output = File.join(tmp_dir, "#{title.parameterize}.jkz") + py_file = File.join(py_root, "jkcreate.py") + version = @mixdown_package.version + + right = @mixdown.jam_track.right_for_user(@mixdown.user) + + if @mixdown_package.sample_rate == 48 + private_key = right.private_key_48 + else + private_key = right.private_key_44 + end + + unless private_key + @error_reason = 'no_private_key' + @error_detail = 'user needs to generate JamTrack for given sample rate' + raise @error_reason + end + + private_key_file = File.join(tmp_dir, 'skey.pem') + File.open(private_key_file, 'w') { |f| f.write(private_key) } + + log.debug("PRIVATE KEY") + log.debug(private_key) + log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output})" + + cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}" + + Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid + exit_status = wait_thr.value + err = stderr.read(1000) + out = stdout.read(1000) + #puts "stdout: #{out}, stderr: #{err}" + raise ArgumentError, "Error calling python script: #{err}" if err.present? + raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file")) + + private_key = File.read(private_key_file) + end + return output, private_key + end + + def cmd(cmd, type) + + log.debug("executing #{cmd}") + + output = `#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + output + else + @error_reason = type + "_fail" + @error_detail = "#{cmd}, #{output}" + raise "command `#{cmd}` failed." + end + end + + # increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on + def bump_step(mixdown_package) + step = @step + last_step_at = Time.now + mixdown_package.current_packaging_step = step + mixdown_package.last_step_at = last_step_at + JamTrackMixdownPackage.where(:id => mixdown_package.id).update_all(last_step_at: last_step_at, current_packaging_step: step) + SubscriptionMessage.mixdown_signing_job_change(mixdown_package) + + @step = step + 1 + end + + # set @error_reason before you raise an exception, and it will be sent back as the error reason + # otherwise, the error_reason will be unhandled-job-exception + def post_error(e) + begin + # if error_reason is null, assume this is an unhandled error + unless @error_reason + @error_reason = "unhandled-job-exception" + @error_detail = e.to_s + end + @mixdown_package.finish_errored(@error_reason, @error_detail) + + rescue Exception => e + log.error "unable to post back to the database the error #{e}" + end + end + end +end diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb index 359bdc514..381bb8b83 100644 --- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb +++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb @@ -42,7 +42,7 @@ module JamRuby signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44 signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44 last_step_at = Time.now - JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true) + JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true, queued: false) # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly... @jam_track_right.current_packaging_step = 0 @jam_track_right.packaging_steps = total_steps @@ -50,6 +50,7 @@ module JamRuby @jam_track_right[signing_state_symbol] = true @jam_track_right.should_retry = false @jam_track_right.last_step_at = Time.now + @jam_track_right.queued = false SubscriptionMessage.jam_track_signing_job_change(@jam_track_right) JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate) diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb index 5039fc862..bf0942cd6 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -24,6 +24,11 @@ module JamRuby def perform # this needs more testing + + # let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough + JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false) + JamTrackMixdownPackage.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false) + return #JamTrackRight.ready_to_clean.each do |jam_track_right| # log.debug("deleting files for jam_track_right #{jam_track_right.id}") diff --git a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb index 6bff4192e..78a40aa6c 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb @@ -31,6 +31,7 @@ module JamRuby Stats.write('users', User.stats) Stats.write('sessions', ActiveMusicSession.stats) Stats.write('jam_track_rights', JamTrackRight.stats) + Stats.write('jam_track_mixdown_packages', JamTrackMixdownPackage.stats) end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 49f14b592..f7763f6dd 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -19,6 +19,8 @@ FactoryGirl.define do terms_of_service true last_jam_audio_latency 5 reuse_card true + has_redeemable_jamtrack true + gifted_jamtracks 0 #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -733,6 +735,23 @@ FactoryGirl.define do sequence(:phone) { |n| "phone-#{n}" } end + factory :jam_track_mixdown, :class => JamRuby::JamTrackMixdown do + association :user, factory: :user + association :jam_track, factory: :jam_track + sequence(:name) { |n| "mixdown-#{n}"} + settings '{"speed":5}' + end + + factory :jam_track_mixdown_package, :class => JamRuby::JamTrackMixdownPackage do + file_type JamRuby::JamTrackMixdownPackage::FILE_TYPE_OGG + sample_rate 48 + signing false + signed false + + association :jam_track_mixdown, factory: :jam_track_mixdown + end + + factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } @@ -783,6 +802,13 @@ FactoryGirl.define do tap_in_count 3 end + factory :download_tracker, :class => JamRuby::DownloadTracker do + remote_ip '1.1.1.1' + paid false + association :user, factory: :user + association :jam_track, factory: :jam_track + end + factory :sale, :class => JamRuby::Sale do order_total 0 association :user, factory:user @@ -844,4 +870,18 @@ FactoryGirl.define do legalese Faker::Lorem.paragraphs(6).join("\n\n") end + factory :gift_card, class: 'JamRuby::GiftCard' do + sequence(:code) {n.to_s} + card_type JamRuby::GiftCardType::JAM_TRACKS_5 + end + + factory :gift_card_type, class: 'JamRuby::GiftCardType' do + card_type JamRuby::GiftCardType::JAM_TRACKS_5 + end + + factory :gift_card_purchase, class: 'JamRuby::GiftCardPurchase' do + + association :user, factory: :user + end + end diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 822a1f527..3c23bdc73 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -110,6 +110,15 @@ describe AffiliatePartner do user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:20}) end + it "user with an affiliate relationship (with a custom rate) buying a jamtrack" do + user.affiliate_referral = partner + user.save! + partner.rate = 0.25 + partner.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:50}) + end + it "user with an affiliate relationship redeeming a jamtrack" do user.affiliate_referral = partner user.save! diff --git a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb index bf3d4da8c..003f8f173 100644 --- a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb @@ -212,6 +212,7 @@ describe 'Band Search Model' do expect(search.results[0].id).to eq(band.id) end it "filters by genre" do + pending band_id = band.id filter[BandSearch::KEY_GENRES] = [band_id] search.search_results_page(BandSearch::TO_JOIN, filter) diff --git a/ruby/spec/jam_ruby/models/download_tracker_spec.rb b/ruby/spec/jam_ruby/models/download_tracker_spec.rb new file mode 100644 index 000000000..f50c4c903 --- /dev/null +++ b/ruby/spec/jam_ruby/models/download_tracker_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +describe DownloadTracker do + + let(:user1) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:user3) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "check_user_sharer" do + describe "with max 1" do + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(1) + results.all.count.should eq 1 + results[0]['user_id'].should eql (user1.id) + end + end + + describe "with max 2" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 0 + + # and then add that same user at different IP, and see that something shows up + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 1 + end + + it "and there are two rows from different IP address, different jam tracks" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql('2') + + # now add a second user with one of the previous IP addresses; shouldn't matter yet + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql('2') + end + + it "and there are two rows from same IP adresss, same jam track" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql('3') + end + + it "and there are two rows from same user one paid for, one not" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.count.should eq 1 + end + end + end + + describe "check_freebie_snarfer" do + + describe "with max 1" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(1) + results.all.count.should eq 1 + end + + it "and there is one row that is paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(1) + results.all.count.should eq 0 + end + end + describe "with max 2" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 0 + + # and then add a second user at same IP, and see that something shows up + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 1 + end + + it "and there are two rows from same user, different jam tracks" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql('2') + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql('2') + end + + it "and there are two rows from same user, same jam track" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 0 + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql('3') + end + + it "and there are two rows from same user one paid for, one not" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.count.should eq 0 + end + end + end +end diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb new file mode 100644 index 000000000..3a90ffcc3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe JamTrackMixdownPackage do + include UsesTempFiles + + it "can be created (factory girl)" do + package = FactoryGirl.create(:jam_track_mixdown_package) + end + + it "can be created" do + mixdown= FactoryGirl.create(:jam_track_mixdown) + + package = JamTrackMixdownPackage.create(mixdown, JamTrackMixdownPackage::FILE_TYPE_OGG, 48, 'jkz') + + package.errors.any?.should == false + end + + + describe "signing_state" do + it "quiet" do + package = FactoryGirl.create(:jam_track_mixdown_package) + package.signing_state.should eq('QUIET') + end + + it "signed" do + package = FactoryGirl.create(:jam_track_mixdown_package, signed: true, signing_started_at: Time.now) + package.signing_state.should eq('SIGNED') + end + + it "error" do + package = FactoryGirl.create(:jam_track_mixdown_package, error_count: 1) + package.signing_state.should eq('ERROR') + end + + it "signing" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing:true, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + package.signing_state.should eq('SIGNING') + end + + it "signing timeout" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing: true, signing_started_at: Time.now - (APP_CONFIG.signing_job_signing_max_time + 1), packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + package.signing_state.should eq('SIGNING_TIMEOUT') + end + + it "queued" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now) + package.signing_state.should eq('QUEUED') + end + + it "signing timeout" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.mixdown_job_queue_max_time + 1)) + package.signing_state.should eq('QUEUED_TIMEOUT') + end + end + + describe "stats" do + + it "empty" do + JamTrackMixdownPackage.stats['count'].should eq(0) + end + + it "signing" do + package = FactoryGirl.create(:jam_track_mixdown_package) + JamTrackMixdownPackage.stats.should eq('count' => 1, + 'signing_count' => 0) + + package.signing = true + package.save! + + JamTrackMixdownPackage.stats.should eq('count' => 1, + 'signing_count' => 1) + end + end + + describe "estimated_queue_time" do + it "succeeds with no data" do + JamTrackMixdownPackage.estimated_queue_time.should eq(0) + end + + it "mixdown packages of different sorts" do + package = FactoryGirl.create(:jam_track_mixdown_package, speed_pitched: true) + JamTrackMixdownPackage.estimated_queue_time.should eq(0) + + package.queued = true + package.save! + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_slow_mixdown_time * 1) + + package.speed_pitched = false + package.save! + + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1) + + right = FactoryGirl.create(:jam_track_right) + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1) + + right.queued = true + right.save! + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1 + APP_CONFIG.estimated_jam_track_time * 1) + end + + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb new file mode 100644 index 000000000..3dd7edd8d --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe JamTrackMixdown do + + let(:user) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:settings) { {speed:5} } + + it "can be created (factory girl)" do + mixdown = FactoryGirl.create(:jam_track_mixdown) + + mixdown = JamTrackMixdown.find(mixdown.id) + mixdown.settings.should eq('{"speed":5}') + end + + it "can be created" do + mixdown = JamTrackMixdown.create('abc', 'description', user, jam_track, settings) + mixdown.errors.any?.should == false + end + + it "index" do + query, start, count = JamTrackMixdown.index({id: jam_track}, user) + + query.length.should eq(0) + start.should be_nil + count.should eq(0) + + mixdown = FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track) + + query, start, count = JamTrackMixdown.index({id: jam_track}, user) + query[0].should eq(mixdown) + start.should be_nil + count.should eq(1) + end + + describe "settings" do + it "validates empty settings" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["have nothing specified"]) + end + + it "validates speed numeric" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => "5"}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer speed"]) + end + + it "validates pitch numeric" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => "5"}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer pitch"]) + end + + it "validates speed not-float" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => 5.5}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer speed"]) + end + + it "validates pitch not-float" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => 10.5}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer pitch"]) + end + end + + +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 9119bdfd9..9e7bc225e 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -29,6 +29,15 @@ describe JamTrackRight do end end + describe "private keys automatically created" do + it "created automatically" do + jam_track_right = FactoryGirl.create(:jam_track_right) + jam_track_right.private_key_44.should_not be_nil + jam_track_right.private_key_48.should_not be_nil + jam_track_right.private_key_44.should eq(jam_track_right.private_key_48) + end + end + describe "JKZ" do before(:all) do original_storage = JamTrackTrackUploader.storage = :fog @@ -109,12 +118,14 @@ describe JamTrackRight do end it "valid track with rights to it by querying user" do - jam_track_right = FactoryGirl.create(:jam_track_right, private_key_44: 'keyabc') + jam_track_right = FactoryGirl.create(:jam_track_right) keys = JamTrackRight.list_keys(jam_track_right.user, [jam_track_right.jam_track.id]) keys.should have(1).items keys[0].id.should == jam_track_right.jam_track.id - keys[0]['private_key_44'].should eq('keyabc') - keys[0]['private_key_48'].should be_nil + keys[0]['private_key_44'].should_not be_nil + keys[0]['private_key_48'].should_not be_nil + keys[0]['private_key_44'].should eq(jam_track_right.private_key_44) + keys[0]['private_key_48'].should eq(jam_track_right.private_key_48) end end diff --git a/ruby/spec/jam_ruby/models/jam_track_search_spec.rb b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb new file mode 100644 index 000000000..d4a3dc93d --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'JamTrack Search Model' do + + let(:artist_filter) { + filter = JamTrackSearch.json_schema.clone + filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_ARTISTS] + filter + } + let(:song_filter) { + filter = JamTrackSearch.json_schema.clone + filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_SONGS] + filter + } + let(:freebird) { + FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'freebird') + } + let(:stairway) { + FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'stairway to heaven') + } + + before :each do + JamTrack.delete_all + JamTrackTrack.delete_all + freebird + stairway + end + + describe "Search filter" do + + it "finds by artist" do + pending + filter = artist_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist + filter = JamTrackSearch.new.search_results_page(filter['query']) + expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1) + end + + it "paginates by artist" do + pending + JamTrackSearch::PER_PAGE.times do |nn| + FactoryGirl.create(:jam_track_with_tracks, + original_artist: freebird.original_artist + nn.to_s, + name: 'abc'+nn.to_s) + end + filter = artist_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min) + num_page = (JamTrack.count / JamTrackSearch::PER_PAGE) + 1 + expect(out_filter[JamTrackSearch::KEY_ARTISTS]['page_count']).to be(num_page) + + filter[JamTrackSearch::KEY_ARTISTS]['page_num'] = 2 + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1) + end + + it "finds by song" do + pending + filter = song_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.name + filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(1) + end + + it "paginates by song" do + pending + (JamTrackSearch::PER_PAGE + 2).times do |nn| + FactoryGirl.create(:jam_track_with_tracks, + original_artist: freebird.original_artist, + name: 'abc'+nn.to_s) + end + filter = song_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = 'abc' + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min) + + total_count = JamTrack.where("name LIKE 'abc%'").count + num_page = (total_count / JamTrackSearch::PER_PAGE) + (0==(total_count % JamTrackSearch::PER_PAGE) ? 0 : 1) + expect(out_filter[JamTrackSearch::KEY_SONGS]['page_count']).to be(num_page) + + filter[JamTrackSearch::KEY_SONGS]['page_num'] = 2 + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(2) + end + + end + +end diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index 814a4cbb3..4ed8e7b28 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -234,6 +234,17 @@ describe JamTrack do query.size.should == 2 end + + it "deals with aggregration (regression)" do + + query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user) + count.should == 0 + + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'Take a Chance On Me', original_artist: 'K.C. And The Sunshine Band') + + query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user) + count.should == 1 + end end describe "validations" do diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index bfd46c1c2..af9715b38 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -238,6 +238,7 @@ describe 'Musician Search Model' do end it "sorts by latency", intermittent: true do + pending search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[0]) results = search.do_search expect(results[0].id).to eq(@user1.id) # HAS FAILED HERE TOO diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb index ba89aee20..bce945d20 100644 --- a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb +++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb @@ -135,7 +135,7 @@ describe RecurlyTransactionWebHook do RecurlyTransactionWebHook.create_from_xml(document) - JamTrackRight.find_by_id(jam_track_right.id).should be_nil + JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil end it "deletes jam_track_right when voided" do @@ -154,7 +154,7 @@ describe RecurlyTransactionWebHook do RecurlyTransactionWebHook.create_from_xml(document) - JamTrackRight.find_by_id(jam_track_right.id).should be_nil + JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil end end diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb index 334166734..4d0340259 100644 --- a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb @@ -6,6 +6,7 @@ describe SaleLineItem do let(:user) {FactoryGirl.create(:user)} let(:user2) {FactoryGirl.create(:user)} let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:gift_card) {FactoryGirl.create(:gift_card_type, card_type: GiftCardType::JAM_TRACKS_10)} describe "associations" do @@ -23,7 +24,7 @@ describe SaleLineItem do describe "state" do - it "success" do + it "jam track success" do sale = Sale.create_jam_track_sale(user) shopping_cart = ShoppingCart.create(user, jam_track) sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil) @@ -37,5 +38,20 @@ describe SaleLineItem do success: true }) end + + it "gift card success" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, gift_card) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil) + transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid') + + sale_line_item.reload + sale_line_item.state.should eq({ + void: false, + refund: false, + fail: false, + success: true + }) + end end end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 685d8dfbd..fc89145cb 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -5,6 +5,24 @@ describe Sale do let(:user) {FactoryGirl.create(:user)} let(:user2) {FactoryGirl.create(:user)} let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:jam_track2) {FactoryGirl.create(:jam_track)} + let(:jam_track3) {FactoryGirl.create(:jam_track)} + let(:gift_card) {GiftCardType.jam_track_5} + + def assert_free_line_item(sale_line_item, jamtrack) + sale_line_item.recurly_tax_in_cents.should be_nil + sale_line_item.recurly_total_in_cents.should be_nil + sale_line_item.recurly_currency.should be_nil + sale_line_item.recurly_discount_in_cents.should be_nil + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(1) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + end describe "index" do it "empty" do @@ -47,7 +65,11 @@ describe Sale do let(:user) {FactoryGirl.create(:user)} let(:jamtrack) { FactoryGirl.create(:jam_track) } + let(:jamtrack2) { FactoryGirl.create(:jam_track) } + let(:jamtrack3) { FactoryGirl.create(:jam_track) } + let(:jamtrack4) { FactoryGirl.create(:jam_track) } let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i } + let(:gift_card_price_in_cents) { (gift_card.price * 100).to_i } let(:client) { RecurlyClient.new } let(:billing_info) { info = {} @@ -75,6 +97,77 @@ describe Sale do end end + it "for a gift card" do + shopping_cart = ShoppingCart.create user, gift_card, 1, false + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should_not be_nil + + sale.recurly_subtotal_in_cents.should eq(gift_card_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(gift_card_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(gift_card.price) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + # validate we are storing pricing info from recurly + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(gift_card_price_in_cents) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(GiftCardType::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(gift_card.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(0) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(gift_card.plan_code) + sale_line_item.product_id.should eq(gift_card.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(1).items + purchase= adjustments[0] + purchase.unit_amount_in_cents.should eq((gift_card.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTracks Gift Card (5)") + purchase.state.should eq('invoiced') + purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + + invoices = recurly_account.invoices + invoices.should have(1).items + invoice = invoices[0] + invoice.uuid.should eq(sale.recurly_invoice_id) + invoice.line_items.should have(1).items # should have single adjustment associated + invoice.line_items[0].should eq(purchase) + invoice.subtotal_in_cents.should eq((gift_card.price * 100).to_i) + invoice.total_in_cents.should eq((gift_card.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.gift_card_purchases.should_not be_nil + user.gift_card_purchases.should have(1).items + user.gift_card_purchases.last.gift_card_type.should eq(GiftCardType.jam_track_5) + user.has_redeemable_jamtrack.should be_true + + sale_line_item.affiliate_referral.should be_nil + sale_line_item.affiliate_referral_fee_in_cents.should be_nil + end + it "for a free jam track" do shopping_cart = ShoppingCart.create user, jamtrack, 1, true @@ -87,6 +180,7 @@ describe Sale do sales.should eq(user.sales) sale = sales[0] + sale.recurly_invoice_id.should be_nil sale.recurly_subtotal_in_cents.should eq(0) @@ -132,6 +226,69 @@ describe Sale do user.has_redeemable_jamtrack.should be_false end + it "for two jam tracks (1 freebie, 1 gifted), then 1 gifted/1 pay" do + user.gifted_jamtracks = 2 + user.save! + + shopping_cart1 = ShoppingCart.create user, jamtrack, 1, true + shopping_cart2 = ShoppingCart.create user, jamtrack2, 1, true + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart1, shopping_cart2]) + + user.reload + user.sales.length.should eq(1) + sale = sales[0] + sale.reload + + sale.recurly_invoice_id.should be_nil + + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 2 + + assert_free_line_item(sale.sale_line_items[0], jamtrack) + assert_free_line_item(sale.sale_line_items[1], jamtrack2) + + # verify jam_track_rights data + right1 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack.id).first + right2 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack2.id).first + user.jam_track_rights.should have(2).items + + right1.redeemed.should be_true + right2.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + user.gifted_jamtracks.should eq(1) + + + + # OK! Now make a second purchase; this time, buy one free, one not free + shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart3]) + + user.reload + user.sales.length.should eq(2) + sale = sales[0] + sale.reload + + sale.recurly_invoice_id.should be_nil + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 1 + + assert_free_line_item(sale.sale_line_items[0], jamtrack3) + end + it "for a free jam track with an affiliate association" do partner = FactoryGirl.create(:affiliate_partner) user.affiliate_referral = partner diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index 6be02e8d4..8d3b724b1 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -3,8 +3,15 @@ require 'spec_helper' describe ShoppingCart do let(:user) { FactoryGirl.create(:user) } - let(:jam_track) {FactoryGirl.create(:jam_track) } - let(:jam_track2) {FactoryGirl.create(:jam_track) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:jam_track4) { FactoryGirl.create(:jam_track) } + let(:jam_track5) { FactoryGirl.create(:jam_track) } + let(:jam_track6) { FactoryGirl.create(:jam_track) } + let(:jam_track7) { FactoryGirl.create(:jam_track) } + let(:gift_card) {FactoryGirl.create(:gift_card_type)} + let(:gift_card2) {FactoryGirl.create(:gift_card_type)} before(:each) do ShoppingCart.delete_all @@ -13,6 +20,9 @@ describe ShoppingCart do it "can reference a shopping cart" do shopping_cart = ShoppingCart.create user, jam_track, 1 + shopping_cart.errors.any?.should be_false + shopping_cart.valid?.should be_true + user.reload ShoppingCart.count.should == 1 user.shopping_carts.count.should == 1 user.shopping_carts[0].product_info[:name].should == jam_track.name @@ -21,18 +31,21 @@ describe ShoppingCart do user.shopping_carts[0].quantity.should == 1 end - - it "maintains only one fre JamTrack in ShoppingCart" do - cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + it "maintains only one free JamTrack in ShoppingCart" do + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart1.should_not be_nil cart1.errors.any?.should be_false user.reload - cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart2.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) - cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) - cart3.errors.any?.should be_false + cart3 = ShoppingCart.add_item_to_cart(user, gift_card) + cart3.errors.any?.should be_true + user.reload + user.shopping_carts.length.should eq(1) + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart4.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) end @@ -48,24 +61,159 @@ describe ShoppingCart do cart2.errors.any?.should be_true end + it "a second giftcard just adds quantity" do + + end + describe "redeemable behavior" do it "removes redeemable item to shopping cart (maintains only one in cart)" do user.has_redeemable_jamtrack.should be_true + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) + cart1.should_not be_nil + cart1.errors.any?.should be_false + cart1.marked_for_redeem.should eq(1) + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart2.should_not be_nil + cart2.errors.any?.should be_false + cart2.marked_for_redeem.should eq(1) + + ShoppingCart.find_by_id(cart1.id).should be nil + + + ShoppingCart.remove_jam_track_from_cart(user, cart2) + + user.reload + user.shopping_carts.length.should eq(0) + ShoppingCart.find_by_id(cart2.id).should be nil + end + end + + describe "multiple free jamtracks" do + + before(:each) do + user.gifted_jamtracks = 5 + user.save! + end + + it "user can add and remove jamtracks without issue, until 'mixed' free/non-free is hit" do cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart1.should_not be_nil + cart1.errors.any?.should be_false + user.reload cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) - cart2.should_not be_nil - + cart2.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(2) cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) - ShoppingCart.remove_jam_track_from_cart(user, jam_track) - user.shopping_carts.length.should eq(0) - cart2.reload + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track3) + cart3.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(3) + cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track4) + cart4.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(4) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + + cart5 = ShoppingCart.add_jam_track_to_cart(user, jam_track5) + cart5.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(5) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + + cart6 = ShoppingCart.add_jam_track_to_cart(user, jam_track6) + cart6.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(6) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + cart6.marked_for_redeem.should eq(1) + + cart7 = ShoppingCart.add_jam_track_to_cart(user, jam_track7) + cart7.errors.any?.should be_true + user.reload + user.shopping_carts.length.should eq(6) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + cart6.marked_for_redeem.should eq(1) + end + end + + describe "gift cards" do + it "can not add multiple of same type" do + cart1 = ShoppingCart.add_item_to_cart(user, gift_card) + cart1.should_not be_nil + cart1.errors.any?.should be_false + + user.reload + user.has_redeemable_jamtrack = true + user.shopping_carts.length.should eq(1) + user.shopping_carts[0].quantity.should eql(1) + + cart2 = ShoppingCart.add_item_to_cart(user, gift_card) + cart2.should_not be_nil + # it's the same type, so it's blocked + cart2.errors.any?.should be_true + cart2.errors[:cart_id].should eq(["has already been taken"]) + end + end + + describe "mixed" do + it "non-free then free" do + # you shouldn't be able to add a free after a non-free + user.has_redeemable_jamtrack = false + user.save! + + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + cart1.errors.any?.should be_false + + user.has_redeemable_jamtrack = true + user.save! + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a free JamTrack to a cart with non-free items. Please clear out your cart."]) + + user.shopping_carts.length.should eq(1) + end + + it "free then non-free" do + + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + cart1.errors.any?.should be_false + + user.reload + + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart."]) + + user.shopping_carts.length.should eq(1) end end end diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 0fb434442..89abe6e16 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -101,12 +101,12 @@ describe User do describe "when first name is not present" do before { @user.first_name = " " } - it { should_not be_valid } + it { should be_valid } end describe "when last name is not present" do before { @user.last_name = " " } - it { should_not be_valid } + it { should be_valid } end describe "when email is not present" do diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 93ec5dda7..7f189e159 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -95,13 +95,13 @@ end config.before(:suite) do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) + DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) end config.around(:each) do |example| # set no_transaction: true as metadata on your test to use deletion strategy instead if example.metadata[:no_transaction] - DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } + DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } else DatabaseCleaner.strategy = :transaction end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 2717f63dc..425d40a2f 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -7,6 +7,10 @@ def app_config 'http://localhost:3333' end + def email_social_alias + 'social@jamkazam.com' + end + def email_alerts_alias 'alerts@jamkazam.com' end @@ -179,7 +183,7 @@ def app_config end def signing_job_queue_max_time - 20 # 20 seconds + 600 # 20 seconds end def one_free_jamtrack_per_user @@ -210,6 +214,42 @@ def app_config "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" end + def estimated_jam_track_time + 40 + end + + def estimated_fast_mixdown_time + 30 + end + + def estimated_slow_mixdown_time + 80 + end + + def num_packaging_nodes + 2 + end + + def signing_job_signing_max_time + 300 + end + + def mixdown_job_queue_max_time + 600 + end + + def mixdown_step_max_time + 300 + end + + def download_tracker_day_range + 30 + end + + def guard_against_browser_fraud + true + end + private def audiomixer_workspace_path diff --git a/web/Gemfile b/web/Gemfile index 96604366d..4cfc8e21f 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -41,12 +41,12 @@ gem 'eventmachine', '1.0.4' gem 'faraday', '~>0.9.0' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' -gem 'omniauth', '1.1.1' -gem 'omniauth-facebook', '1.4.1' +gem 'omniauth' +gem 'omniauth-facebook' gem 'omniauth-twitter' -gem 'omniauth-google-oauth2', '0.2.1' -gem 'google-api-client', '0.7.1' -gem 'google-api-omniauth', '0.1.1' +gem 'omniauth-google-oauth2' +gem 'google-api-client' #, '0.7.1' +#gem 'google-api-omniauth' #, '0.1.1' gem 'signet', '0.5.0' gem 'twitter' gem 'fb_graph', '2.5.9' @@ -63,6 +63,7 @@ gem 'haml-rails' gem 'unf' #optional fog dependency gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) gem 'postgres-copy' +gem 'prawn-table' #group :libv8 do # gem 'libv8', "~> 3.11.8" #end @@ -97,7 +98,7 @@ gem 'react-rails', '~> 1.0' source 'https://rails-assets.org' do gem 'rails-assets-reflux' gem 'rails-assets-classnames' - gem 'rails-assets-react-select' + gem 'rails-assets-react-select', '0.6.7' end #group :development, :production do diff --git a/web/README.md b/web/README.md index 2b5096424..5fa0629f8 100644 --- a/web/README.md +++ b/web/README.md @@ -4,4 +4,3 @@ Jasmine Javascript Unit Tests Open browser to localhost:3000/teaspoon - diff --git a/web/app/assets/images/content/icon-delete.png b/web/app/assets/images/content/icon-delete.png new file mode 100644 index 000000000..cae694fba Binary files /dev/null and b/web/app/assets/images/content/icon-delete.png differ diff --git a/web/app/assets/images/content/icon-delete@2X.png b/web/app/assets/images/content/icon-delete@2X.png new file mode 100644 index 000000000..b120befa5 Binary files /dev/null and b/web/app/assets/images/content/icon-delete@2X.png differ diff --git a/web/app/assets/images/content/icon-edit.png b/web/app/assets/images/content/icon-edit.png new file mode 100644 index 000000000..26ed5892f Binary files /dev/null and b/web/app/assets/images/content/icon-edit.png differ diff --git a/web/app/assets/images/content/icon-edit@2X.png b/web/app/assets/images/content/icon-edit@2X.png new file mode 100644 index 000000000..7cd9052d5 Binary files /dev/null and b/web/app/assets/images/content/icon-edit@2X.png differ diff --git a/web/app/assets/images/content/icon-mix-fail@2X.png b/web/app/assets/images/content/icon-mix-fail@2X.png new file mode 100644 index 000000000..265022204 Binary files /dev/null and b/web/app/assets/images/content/icon-mix-fail@2X.png differ diff --git a/web/app/assets/images/content/icon-play.png b/web/app/assets/images/content/icon-play.png new file mode 100644 index 000000000..ecb26e78c Binary files /dev/null and b/web/app/assets/images/content/icon-play.png differ diff --git a/web/app/assets/images/content/icon-retry@2X.png b/web/app/assets/images/content/icon-retry@2X.png new file mode 100644 index 000000000..584a0dca1 Binary files /dev/null and b/web/app/assets/images/content/icon-retry@2X.png differ diff --git a/web/app/assets/images/content/icon-save@2X.png b/web/app/assets/images/content/icon-save@2X.png new file mode 100644 index 000000000..ac80ef9c4 Binary files /dev/null and b/web/app/assets/images/content/icon-save@2X.png differ diff --git a/web/app/assets/images/content/icon_download.png b/web/app/assets/images/content/icon_download.png new file mode 100644 index 000000000..d56bde46c Binary files /dev/null and b/web/app/assets/images/content/icon_download.png differ diff --git a/web/app/assets/images/content/icon_download@2X.png b/web/app/assets/images/content/icon_download@2X.png new file mode 100644 index 000000000..5473ff7df Binary files /dev/null and b/web/app/assets/images/content/icon_download@2X.png differ diff --git a/web/app/assets/images/content/icon_download@3X.png b/web/app/assets/images/content/icon_download@3X.png new file mode 100644 index 000000000..13c2a1fd1 Binary files /dev/null and b/web/app/assets/images/content/icon_download@3X.png differ diff --git a/web/app/assets/images/content/icon_open@2X.png b/web/app/assets/images/content/icon_open@2X.png new file mode 100644 index 000000000..1d188a2ea Binary files /dev/null and b/web/app/assets/images/content/icon_open@2X.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - Avatar.png b/web/app/assets/images/landing/Andy Crowley - Avatar.png new file mode 100644 index 000000000..4a675f607 Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Avatar.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png new file mode 100644 index 000000000..9dc63b79b Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - YouTube.png b/web/app/assets/images/landing/Andy Crowley - YouTube.png new file mode 100644 index 000000000..0dad41bf8 Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - YouTube.png differ diff --git a/web/app/assets/images/landing/Carl Brown - Avatar.png b/web/app/assets/images/landing/Carl Brown - Avatar.png new file mode 100644 index 000000000..a991220a3 Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Avatar.png differ diff --git a/web/app/assets/images/landing/Carl Brown - Speech Bubble.png b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png new file mode 100644 index 000000000..071f8c40f Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Carl Brown - YouTube.png b/web/app/assets/images/landing/Carl Brown - YouTube.png new file mode 100644 index 000000000..289b170d3 Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - YouTube.png differ diff --git a/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png new file mode 100644 index 000000000..61d12e9aa Binary files /dev/null and b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png differ diff --git a/web/app/assets/images/landing/Julie Bonk - Avatar.png b/web/app/assets/images/landing/Julie Bonk - Avatar.png new file mode 100644 index 000000000..0b5019588 Binary files /dev/null and b/web/app/assets/images/landing/Julie Bonk - Avatar.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - Avatar.png b/web/app/assets/images/landing/Ryan Jones - Avatar.png new file mode 100644 index 000000000..09b908884 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Avatar.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png new file mode 100644 index 000000000..5c40b4727 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png new file mode 100644 index 000000000..9cf7c90d9 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 1.png b/web/app/assets/images/landing/Top 10 Image - Number 1.png new file mode 100644 index 000000000..82e1a3847 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 1.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 10.png b/web/app/assets/images/landing/Top 10 Image - Number 10.png new file mode 100644 index 000000000..23f1fd862 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 10.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 2.png b/web/app/assets/images/landing/Top 10 Image - Number 2.png new file mode 100644 index 000000000..9d79989d7 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 2.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 3.png b/web/app/assets/images/landing/Top 10 Image - Number 3.png new file mode 100644 index 000000000..7084feaf4 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 3.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 4.png b/web/app/assets/images/landing/Top 10 Image - Number 4.png new file mode 100644 index 000000000..48f40e9be Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 4.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 5.png b/web/app/assets/images/landing/Top 10 Image - Number 5.png new file mode 100644 index 000000000..d50812947 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 5.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 6.png b/web/app/assets/images/landing/Top 10 Image - Number 6.png new file mode 100644 index 000000000..b76dd8555 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 6.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 7.png b/web/app/assets/images/landing/Top 10 Image - Number 7.png new file mode 100644 index 000000000..0de272f34 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 7.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 9.png b/web/app/assets/images/landing/Top 10 Image - Number 9.png new file mode 100644 index 000000000..8fdcb8d46 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 9.png differ diff --git a/web/app/assets/images/landing/arrow-jamblaster-order.png b/web/app/assets/images/landing/arrow-jamblaster-order.png new file mode 100644 index 000000000..d42844afb Binary files /dev/null and b/web/app/assets/images/landing/arrow-jamblaster-order.png differ diff --git a/web/app/assets/images/landing/broadcast_video.png b/web/app/assets/images/landing/broadcast_video.png new file mode 100644 index 000000000..b3a281b60 Binary files /dev/null and b/web/app/assets/images/landing/broadcast_video.png differ diff --git a/web/app/assets/images/landing/find_musicians.png b/web/app/assets/images/landing/find_musicians.png new file mode 100644 index 000000000..b2160ba9d Binary files /dev/null and b/web/app/assets/images/landing/find_musicians.png differ diff --git a/web/app/assets/images/landing/gift_card.png b/web/app/assets/images/landing/gift_card.png new file mode 100644 index 000000000..e652e81db Binary files /dev/null and b/web/app/assets/images/landing/gift_card.png differ diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_1.png b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png new file mode 100644 index 000000000..c19bc6faf Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png differ diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_2.png b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png new file mode 100644 index 000000000..0cb8cb55a Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png differ diff --git a/web/app/assets/images/landing/online_lessons.png b/web/app/assets/images/landing/online_lessons.png new file mode 100644 index 000000000..7e4da04ec Binary files /dev/null and b/web/app/assets/images/landing/online_lessons.png differ diff --git a/web/app/assets/images/landing/phone_control_jamblaster.png b/web/app/assets/images/landing/phone_control_jamblaster.png new file mode 100644 index 000000000..4b28201e1 Binary files /dev/null and b/web/app/assets/images/landing/phone_control_jamblaster.png differ diff --git a/web/app/assets/images/landing/preorder_jamblaster.png b/web/app/assets/images/landing/preorder_jamblaster.png new file mode 100644 index 000000000..932f2089e Binary files /dev/null and b/web/app/assets/images/landing/preorder_jamblaster.png differ diff --git a/web/app/assets/images/landing/recording_with_video.png b/web/app/assets/images/landing/recording_with_video.png new file mode 100644 index 000000000..b07fbe180 Binary files /dev/null and b/web/app/assets/images/landing/recording_with_video.png differ diff --git a/web/app/assets/images/landing/us_latency_diagram.png b/web/app/assets/images/landing/us_latency_diagram.png new file mode 100644 index 000000000..6dddd6256 Binary files /dev/null and b/web/app/assets/images/landing/us_latency_diagram.png differ diff --git a/web/app/assets/images/shared/mobile-preview-load.gif b/web/app/assets/images/shared/mobile-preview-load.gif new file mode 100755 index 000000000..d0dee5844 Binary files /dev/null and b/web/app/assets/images/shared/mobile-preview-load.gif differ diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png index 8650fe322..932f2089e 100644 Binary files a/web/app/assets/images/web/button_cta_jamblaster.png and b/web/app/assets/images/web/button_cta_jamblaster.png differ diff --git a/web/app/assets/images/web/button_cta_jamtrack.png b/web/app/assets/images/web/button_cta_jamtrack.png index 998d89b75..d8dd6e40c 100644 Binary files a/web/app/assets/images/web/button_cta_jamtrack.png and b/web/app/assets/images/web/button_cta_jamtrack.png differ diff --git a/web/app/assets/images/web/button_cta_platform.png b/web/app/assets/images/web/button_cta_platform.png index dba8d499b..a2ddf74ef 100644 Binary files a/web/app/assets/images/web/button_cta_platform.png and b/web/app/assets/images/web/button_cta_platform.png differ diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index ee570a513..b893ef7c7 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -23,6 +23,7 @@ var mode = null; // heartbeat + var startHeartbeatTimeout = null; var heartbeatInterval = null; var heartbeatMS = null; var connection_expire_time = null; @@ -79,6 +80,7 @@ function initiateReconnect(activeElementVotes, in_error) { var initialConnect = !!activeElementVotes; + console.log("activeElementVotes", activeElementVotes) freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true)); if (in_error) { @@ -104,6 +106,12 @@ heartbeatInterval = null; } + // stop the heartbeat start delay from happening + if (startHeartbeatTimeout != null) { + clearTimeout(startHeartbeatTimeout); + startHeartbeatTimeout = null; + } + // stop checking for heartbeat acks if (heartbeatAckCheckInterval != null) { clearTimeout(heartbeatAckCheckInterval); @@ -218,7 +226,7 @@ app.clientId = payload.client_id; - if (isClientMode()) { + if (isClientMode() && context.jamClient) { // tell the backend that we have logged in context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION $.cookie('client_id', payload.client_id); @@ -236,9 +244,41 @@ heartbeatMS = payload.heartbeat_interval * 1000; connection_expire_time = payload.connection_expire_time * 1000; logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); - lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + + // add some randomness to help move heartbeats apart from each other + + // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time + var randomStartTime = connection_expire_time * (Math.random() / 2) + + if (startHeartbeatTimeout) { + logger.warn("start heartbeat timeout is active; should be null") + clearTimeout(startHeartbeatTimeout) + } + + if (heartbeatInterval != null) { + logger.warn("heartbeatInterval is active; should be null") + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + + if (heartbeatAckCheckInterval != null) { + logger.warn("heartbeatAckCheckInterval is active; should be null") + clearInterval(heartbeatAckCheckInterval); + heartbeatAckCheckInterval = null; + } + + startHeartbeatTimeout = setTimeout(function() { + if(server.connected) { + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + } + }, randomStartTime) + + logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's') + + + connectDeferred.resolve(); $self.triggerHandler(EVENTS.CONNECTION_UP) @@ -281,7 +321,7 @@ function socketClosed(in_error) { // tell the backend that we have logged out - context.jamClient.OnLoggedOut(); + if (context.jamClient) context.jamClient.OnLoggedOut(); } @@ -295,6 +335,11 @@ logger.debug(payload.error_code + ": no longer reconnecting") server.noReconnect = true; // stop trying to log in!! } + else if (payload.error_code == 'no_reconnect') { + logger.debug(payload.error_code + ": no longer reconnecting") + server.noReconnect = true; // stop trying to log in!! + context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.") + } } /////////////////// @@ -384,7 +429,7 @@ } function formatDelaySecs(secs) { - return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); + return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); } function setCountdown($parent) { @@ -548,7 +593,10 @@ clientType = context.JK.clientType(); } if(!mode) { - mode = context.jamClient.getOperatingMode ? context.jamClient.getOperatingMode() : 'client'; + mode = 'client' + if (context.jamClient && context.jamClient.getOperatingMode) { + mode = context.jamClient.getOperatingMode() + } } connectDeferred = new $.Deferred(); diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 08785bc50..5793d7941 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -19,7 +19,7 @@ context.JK.AccountJamTracks = class AccountJamTracks @screen = $('#account-jamtracks') beforeShow:() => - rest.getPurchasedJamTracks({}) + rest.getPurchasedJamTracks({limit: 40}) .done(@populateJamTracks) .fail(@app.ajaxError); diff --git a/web/app/assets/javascripts/accounts_profile_avatar.js b/web/app/assets/javascripts/accounts_profile_avatar.js index 63d1aca1f..2ab8858cd 100644 --- a/web/app/assets/javascripts/accounts_profile_avatar.js +++ b/web/app/assets/javascripts/accounts_profile_avatar.js @@ -40,8 +40,8 @@ var template= context.JK.fillTemplate($('#template-account-profile-avatar').html(), { "fp_apikey" : gon.fp_apikey, "data-fp-store-path" : createStorePath(userDetail) + createOriginalFilename(userDetail), - "fp_policy" : filepicker_policy.policy, - "fp_signature" : filepicker_policy.signature + "fp_policy" : encodeURIComponent(filepicker_policy.policy), + "fp_signature" : encodeURIComponent(filepicker_policy.signature) }); $('#account-profile-avatar-content-scroller').html(template); @@ -202,7 +202,6 @@ renderNoAvatar(avatarSpace); } else { - rest.getFilepickerPolicy({handle: fpfile.url}) .done(function(filepickerPolicy) { avatarSpace.children().remove(); diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 811277c13..2153fb9c1 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -61,6 +61,7 @@ //= require web/tracking //= require webcam_viewer //= require react-components +//= require playbackControls //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index bf5b1993b..705f9ec29 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -137,6 +137,10 @@ else if(type === ALERT_NAMES.VIDEO_WINDOW_CLOSED) { context.VideoActions.videoWindowClosed() } + else if (type === ALERT_NAMES.VST_CHANGED) { + console.log("VST CHANGED!") + context.ConfigureTracksActions.vstChanged() + } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { // squelch these events if not in session diff --git a/web/app/assets/javascripts/checkout_complete.js b/web/app/assets/javascripts/checkout_complete.js index 4f1e96110..aa52214b9 100644 --- a/web/app/assets/javascripts/checkout_complete.js +++ b/web/app/assets/javascripts/checkout_complete.js @@ -15,6 +15,7 @@ var $templatePurchasedJamTrack = null; var $thanksPanel = null; var $jamTrackInBrowser = null; + var $giftCardPurchased = null; var $purchasedJamTrack = null; var $purchasedJamTrackHeader = null; var $purchasedJamTracks = null; @@ -75,9 +76,17 @@ else { $thanksPanel.removeClass('hidden') handleJamTracksPurchased(purchaseResponse.jam_tracks) + handleGiftCardsPurchased(purchaseResponse.gift_cards) } } + + function handleGiftCardsPurchased(gift_cards) { + // were any GiftCards purchased? + if(gift_cards && gift_cards.length > 0) { + $giftCardPurchased.removeClass('hidden') + } + } function handleJamTracksPurchased(jamTracks) { // were any JamTracks purchased? var jamTracksPurchased = jamTracks && jamTracks.length > 0; @@ -194,6 +203,7 @@ $templatePurchasedJamTrack = $('#template-purchased-jam-track'); $thanksPanel = $screen.find(".thanks-panel"); $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $giftCardPurchased = $screen.find('.thanks-detail.gift-card') $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js index 168b965d6..a69c0a39c 100644 --- a/web/app/assets/javascripts/checkout_order.js +++ b/web/app/assets/javascripts/checkout_order.js @@ -135,15 +135,7 @@ } } - function displayTax(effectiveQuantity, item_tax, total_with_tax) { - var totalTax = 0; - var totalPrice = 0; - - var unitTax = item_tax * effectiveQuantity; - totalTax += unitTax; - - var totalUnitPrice = total_with_tax * effectiveQuantity; - totalPrice += totalUnitPrice; + function displayTax(totalTax, totalPrice) { $screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2)) $screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2)) @@ -181,8 +173,16 @@ taxRate = 0.0825; } - var unitTax = 1.99 * taxRate; - displayTax(effectiveQuantity, unitTax, 1.99 + unitTax) + var estimatedTax = 0; + var estimatedTotal = 0; + + context._.each(carts, function(cart) { + var cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem + estimatedTax += cart.product_info.price * cart_quantity * taxRate; + estimatedTotal += cart.product_info.price * cart_quantity; + }) + + displayTax(Math.round(estimatedTax*100)/100, Math.round((estimatedTotal + estimatedTax)*100)/100) } else { checkoutUtils.configureRecurly() diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index 9b72f4fc5..956c11886 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -95,7 +95,7 @@ $reuseExistingCardChk.iCheck(userDetail.reuse_card && userDetail.has_recurly_account ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card) // show appropriate prompt text based on whether user has a free jamtrack - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $freeJamTrackPrompt.removeClass('hidden') } else { diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index c16e09f9b..c789ba71d 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -55,6 +55,16 @@ class CheckoutUtils return carts[0].product_info.free + hasOnlyFreeItemsInShoppingCart: (carts) => + if carts.length == 0 + return false + + for cart in carts + if !cart.product_info.free + return false + + return true + configureRecurly: () => unless @configuredRecurly context.recurly.configure(gon.global.recurly_public_api_key) diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js index ca5bdd428..dd3142e76 100644 --- a/web/app/assets/javascripts/clientUpdate.js +++ b/web/app/assets/javascripts/clientUpdate.js @@ -61,7 +61,10 @@ }) } - app.layout.showDialog('client-update') + if(!app.layout.isDialogShowing('client-update')) { + app.layout.showDialog('client-update') + } + //$('#client_update').show() //$('#client_update_overlay').show() } @@ -192,6 +195,11 @@ function runCheck(product, version, uri, size, currentVersion) { + if (app.clientUpdating) { + logger.debug("client is already updating; skipping") + return + } + if(currentVersion === undefined) { currentVersion = context.jamClient.ClientUpdateVersion(); @@ -302,7 +310,7 @@ $(document).on(EVENTS.SESSION_ENDED, function(e, data){ if(app.clientUpdating) { - updateClientUpdateDialog("update-start", { uri: updateUri }) + updateClientUpdateDialog("update-start", { uri: updateUri}) } }); diff --git a/web/app/assets/javascripts/configureTracksHelper2.js b/web/app/assets/javascripts/configureTracksHelper2.js new file mode 100644 index 000000000..d87319a53 --- /dev/null +++ b/web/app/assets/javascripts/configureTracksHelper2.js @@ -0,0 +1,294 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.ConfigureTracksHelper2 = function (app) { + var logger = context.JK.logger; + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var MAX_TRACKS = context.JK.MAX_TRACKS; + var MAX_OUTPUTS = context.JK.MAX_OUTPUTS; + var gearUtils = context.JK.GearUtils; + + var $parent = null; + var $templateAssignablePort = null; + var $templateTrackTarget = null; + var $templateOutputTarget = null; + var $unassignedInputsHolder = null; + var $unassignedOutputsHolder = null; + var $tracksHolder = null; + var $outputChannelHolder = null; + var $instrumentsHolder = null; + var isDragging = false; + + + + // inputChannelFilter is an optional argument that is used by the Gear Wizard. + // basically, if an input channel isn't in there, it's not going to be displayed + function loadChannels(forceInputsToUnassign, inputChannelFilter) { + var musicPorts = jamClient.FTUEGetChannels(); + + $unassignedInputsHolder.empty(); + $unassignedOutputsHolder.empty(); + + var inputChannels = musicPorts.inputs; + var outputChannels = musicPorts.outputs; + + // uncomment to add a bunch of bogus channels + //inputChannels = inputChannels.concat(inputChannels.concat(inputChannels.concat(inputChannels))) + + context._.each(inputChannels, function (inputChannel) { + + if(inputChannelFilter && !(inputChannelFilter.indexOf(inputChannel.id) > -1)) { + // skipping input channel because it's not in the filter + return; + } + + var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, inputChannel, {direction:'in'}), { variable: 'data' })); + + if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + unassignInputChannel($channel); + } + else if(inputChannel.assignment == ASSIGNMENT.CHAT) { + // well, we can't show it as unused... if there were a place to show chat inputs, we would put it there. + // but we don't have it, so just skip + logger.debug("skipping chat channel ", inputChannel) + return; + } + else { + // find the track this belongs in + + var trackNumber = inputChannel.assignment - 1; + + // plop down a track holder on the UI + + addChannelToTrack($channel, trackNumber); + } + }) + + var outputAssignment = 0; + context._.each(outputChannels, function (outputChannel, index) { + var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, outputChannel, {direction:'out'}), { variable: 'data' })); + + + + if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + unassignOutputChannel($channel); + } + else { + var $output = $outputChannelHolder.find('.output[data-num="' + outputAssignment + '"]'); + outputAssignment++; + + if($output.length == 0) { + context.JK.alertSupportedNeeded('Unable to find an output holder for channel: ' + outputChannel.id + ' with assignment ' + outputChannel.assignment); + return false; + } + addChannelToOutput($channel, $output.find('.output-target')); + } + }); + } + + // iterates through the dom and returns a pure data structure for track associations and output channels + function getCurrentState() { + + var state = {}; + state.tracks = []; + state.unassignedChannels = []; + state.outputs = []; + var $unassignedInputChannels = $unassignedInputsHolder.find('.ftue-input'); + var $unassignedOutputChannels = $unassignedOutputsHolder.find('.ftue-input'); + var $tracks = $tracksHolder.find('.track-target'); + var $outputs = $outputChannelHolder.find('.output-target'); + + context._.each($unassignedInputChannels, function($unassignedInput) { + $unassignedInput = $($unassignedInput); + var channelId = $unassignedInput.attr('data-input-id'); + state.unassignedChannels.push(channelId); + }) + + context._.each($unassignedOutputChannels, function($unassignedOutput) { + $unassignedOutput = $($unassignedOutput); + var channelId = $unassignedOutput.attr('data-input-id'); + state.unassignedChannels.push(channelId); + }) + + context._.each($tracks, function($track, index) { + $track = $($track); + var $assignedChannels = $track.find('.ftue-input'); + + var track = {index: index, channels:[]}; + context._.each($assignedChannels, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + track.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(track.channels.length > 0) { + state.tracks.push(track); + } + var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select'); + track.instrument_id = $instrument.data('instrument_id'); + }) + + context._.each($outputs, function($output, index) { + $output = $($output); + var $assignedChannel = $output.find('.ftue-input'); + + // this is overkill since there should only be 1 or 0 .ftue-inputs in a given .output + var outputSlot = {index: index, channels:[]}; + context._.each($assignedChannel, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + outputSlot.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(outputSlot.channels.length > 0) { + state.outputs.push(outputSlot); + } + }) + return state; + } + + function validate(tracks) { + // there must be at least one assigned channel + if(tracks.tracks.length == 0) { + logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track."); + context.JK.Banner.showAlert('Must have assigned at least one input port to a track.'); + return false; + } + + // there must be some instruments + context._.each(tracks.tracks, function(track) { + if(!track.instrument_id) { + logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); + context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.'); + return false; + } + }); + + // there must be exactly 2 output channels assigned + if(tracks.outputs.length != 2 || (tracks.outputs[0].channels.length != 1 && track.outputs[1].channels.length != 1)) { + logger.debug("ConfigureTracks validation error: must have assigned exactly two output ports"); + context.JK.Banner.showAlert('Must have assigned exactly 2 output ports.'); + return false; + } + + return true; + } + + function save(state) { + + context._.each(state.unassignedChannels, function(unassignedChannelId) { + context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED); + }); + + // save input/tracks + context._.each(state.tracks, function(track, index) { + + var trackNumber = index + 1; + + context._.each(track.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, trackNumber); + + }); + logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id); + context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id); + }); + + // save outputs + context._.each(state.outputs, function(output, index) { + context._.each(output.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); + }); + }); + + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + return true; + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + } + + function loadTrackInstruments(forceInputsToUnassign) { + var $trackInstruments = $instrumentsHolder.find('.track-instrument'); + + context._.each($trackInstruments, function(trackInstrument) { + var $trackInstrument = $(trackInstrument); + + var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1; + + var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex); + + var instrument = context.JK.client_to_server_instrument_map[clientInstrument]; + + $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument); + }); + } + + function trySave() { + var state = getCurrentState(); + + if(!validate(state)) { + return false; + } + + var saved = save(state); + + if(saved) { + context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS()); + } + + return saved; + } + + function reset(forceInputsToUnassign, inputChannelFilter) { + loadChannels(forceInputsToUnassign, inputChannelFilter); + loadTrackInstruments(forceInputsToUnassign); + } + + function unassignOutputChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.output-target'); + $unassignedOutputsHolder.append($channel); + $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); + } + + function unassignInputChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $unassignedInputsHolder.append($channel); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); + + } + + function addChannelToTrack($channel, $track) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $track.append($channel); + $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + } + + function addChannelToOutput($channel, $slot) { + var $originallyAssignedTrack = $channel.closest('.output-target'); + $slot.append($channel); + $slot.attr('output-count', $slot.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + } + + function initialize(_$parent) { + $parent = _$parent; + + } + + this.initialize = initialize; + this.trySave = trySave; + this.reset = reset; + + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index 20801cfa6..ccdee9b1f 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -25,6 +25,7 @@ var voiceChatHelper = null; var profiles = null; var currentProfile = null; + var enableVstTimeout = null; var configure_audio_instructions = { @@ -50,7 +51,7 @@ function setInstructions(type) { if (type === 'audio') { - $instructions.html('Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.') + $instructions.html("Click the 'ADD LIVE TRACK' button to add more tracks. You may set up a live track for each instrumental and/or vocal part to perform in sessions. You must also set up exactly two Session Audio Output ports to deliver the stereo audio in your sessions.") return; var os = context.jamClient.GetOSAsString(); $instructions.html(configure_audio_instructions[os]); @@ -91,7 +92,7 @@ } function validateAudioSettings() { - return configureTracksHelper.trySave(); + return true; } function showVoiceChatPanel() { @@ -103,7 +104,7 @@ $musicAudioTabSelector.click(function () { // validate voice chat settings if (validateVoiceChatSettings()) { - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); voiceChatHelper.reset(); showMusicAudioPanel(); } @@ -113,7 +114,7 @@ // validate audio settings if (validateAudioSettings()) { logger.debug("initializing voice chat helper") - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); voiceChatHelper.reset(); showVoiceChatPanel(); } @@ -133,7 +134,7 @@ //}); $btnUpdateTrackSettings.click(function() { - if(configureTracksHelper.trySave() && voiceChatHelper.trySave()) { + if(voiceChatHelper.trySave()) { app.layout.closeDialog('configure-tracks'); } @@ -152,7 +153,7 @@ }); $certifiedAudioProfile.html(optionsHtml); - context.JK.dropdown($certifiedAudioProfile); + //context.JK.dropdown($certifiedAudioProfile); } function deviceChanged() { @@ -183,7 +184,7 @@ currentProfile = profile; - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); } function beforeShow() { @@ -207,13 +208,45 @@ return; } - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); + if(!window.SessionStore.inSession()) { + delayEnableVst() + } + else { + logger.debug("in a session, so no delayEnableVst()"); + } voiceChatHelper.reset(); voiceChatHelper.beforeShow(); } + function delayEnableVst() { + if (enableVstTimeout) { + clearTimeout(enableVstTimeout) + } + var isVstLoaded = context.jamClient.IsVstLoaded() + var hasVstAssignment = context.jamClient.hasVstAssignment() + + if (hasVstAssignment && !isVstLoaded) { + enableVstTimeout = setTimeout(function() { enableVst() }, 1000) + } + } + + function enableVst () { + enableVstTimeout = null + + if (app.layout.isDialogShowing('configure-tracks')) { + ConfigureTracksActions.enableVst() + } + else { + logger.debug("no longer in configure tracks dialog; not enabling VSTs at this time") + } + } + function afterShow() { sessionUtils.SessionPageEnter(); + + //context.ConfigureTracksActions.vstScan(); + } function onCancel() { @@ -247,8 +280,8 @@ $btnAddNewGear = $dialog.find('.btn-add-new-audio-gear'); $btnUpdateTrackSettings = $dialog.find('.btn-update-settings'); - configureTracksHelper = new context.JK.ConfigureTracksHelper(app); - configureTracksHelper.initialize($dialog); + //configureTracksHelper = new context.JK.ConfigureTracksHelper(app); + //configureTracksHelper.initialize($dialog); voiceChatHelper = new context.JK.VoiceChatHelper(app); voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191); diff --git a/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js new file mode 100644 index 000000000..8b6c06811 --- /dev/null +++ b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js @@ -0,0 +1,69 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.DeleteVideoConfirmDialog = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var recordingId = null; + var $dialog = null; + var $deleteFromDiskChkBox = null; + var $deleteBtn = null; + var deleting = false; + function resetForm() { + + } + + function beforeShow(args) { + + recordingId = args.d1; + + if(!recordingId) throw "recordingId must be specified"; + + $dialog.data('result', null); + deleting = false; + + } + + function afterHide() { + + } + + function attemptDelete() { + if(deleting) return; + + deleting = true; + + rest.deleteRecordingVideoData(recordingId) + .done(function(){ + $dialog.data('result', true); + app.layout.closeDialog('delete-video-confirm-dialog'); + }) + .fail(app.ajaxError) + } + + function events() { + $deleteBtn.click(attemptDelete); + } + + + function initialize() { + var dialogBindings = { + 'beforeShow': beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('delete-video-confirm-dialog', dialogBindings); + + $dialog = $('#deleteVideoConfirmDialog'); + $deleteFromDiskChkBox = $dialog.find('.delete-from-disk'); + $deleteBtn = $dialog.find('.delete-btn'); + + events(); + + context.JK.checkbox($deleteFromDiskChkBox); + }; + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/editRecordingDialog.js b/web/app/assets/javascripts/dialog/editRecordingDialog.js index 9aac9d33a..409318c0a 100644 --- a/web/app/assets/javascripts/dialog/editRecordingDialog.js +++ b/web/app/assets/javascripts/dialog/editRecordingDialog.js @@ -14,7 +14,7 @@ var $isPublic = null; var $saveBtn = null; var $deleteBtn = null; - + var $videoField = null; var updating = false; var deleting = false; diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 867a411f2..aa35e80e7 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -50,7 +50,7 @@ $browserJamTrackBtn.click(function() { app.layout.closeDialog('getting-started') - window.location = '/client#/jamtrack/search' + window.location = '/client#/jamtrack' return false; }) @@ -69,9 +69,9 @@ function beforeShow() { app.user().done(function(user) { - var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' + var jamtrackRule = user.has_redeemable_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' $jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule) - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $jamTracksLimitedTime.removeClass('hidden') } }) diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 8f58b0728..ed400d8c1 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -103,9 +103,8 @@ sampleRate = context.jamClient.GetSampleRate() sampleRateForFilename = sampleRate == 48 ? '48' : '44'; doSearch(); - - } + function afterHide() { showing = false; } diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index cceaaedc2..97d746144 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -8,11 +8,29 @@ var playbackControls = null; var recording = null; // deferred object var $dialog = null; + var $saveVideoCheckbox = null + var $uploadToYoutube = null function resetForm() { // remove all display errors $('#recording-finished-dialog form .error-text').remove() $('#recording-finished-dialog form .error').removeClass("error") + if(recording.video) { + if(recording.owner.id == context.JK.currentUserId) { + // only the owner of the video gets to see video options + $dialog.find('.save-video').show() + $dialog.find('.upload-to-youtube').show() + } + else { + $dialog.find('.save-video').hide() + $dialog.find('.upload-to-youtube').hide() + } + + } + else { + $dialog.find('.save-video').hide() + $dialog.find('.upload-to-youtube').hide() + } removeGoogleLoginErrors() } @@ -103,6 +121,20 @@ } function afterHide() { + if(recording && recording.video) { + var name = $('#recording-finished-dialog form input[name=name]').val(); + name = name.replace(/[^A-Za-z0-9\-\ ]/g, ''); + + + var saveToDisk = $('#recording-finished-dialog form input[name=save_video]').is(':checked') + var keepResult = $dialog.data('result'); + keepResult = keepResult && keepResult.keep + + logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keepResult=" + keepResult + ", saveToDisk=" + saveToDisk); + + context.jamClient.VideoDecision(recording.id, name, keepResult && saveToDisk) + } + recording = null; playbackControls.stopMonitor(); context.jamClient.ClosePreviewRecording(); @@ -141,6 +173,7 @@ window._oauth_callback = function() { window._oauth_win.close() + logger.debug("closing window") setGoogleAuthState() } return false; @@ -152,6 +185,8 @@ var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked') + upload_to_youtube = false // don't prevent user from getting through dialog because popup now handles auth + if (upload_to_youtube) { $.ajax({ type: "GET", @@ -182,6 +217,9 @@ var save_video = $('#recording-finished-dialog form input[name=save_video]').is(':checked') var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked') + var recording_id = recording.id + var recording_video = recording.video + rest.claimRecording({ id: recording.id, name: name, @@ -195,6 +233,11 @@ $dialog.data('result', {keep:true}); app.layout.closeDialog('recordingFinished'); context.JK.GA.trackMakeRecording(); + if(recording_video && save_video && upload_to_youtube) { + // you have to have elected to save video to have upload to youtube have + context.VideoUploaderActions.showUploader(recording_id); + } + }) .fail(function (jqXHR) { if (jqXHR.status == 422) { @@ -281,9 +324,9 @@ // Check for google authorization using AJAX and show/hide the // google login button / "signed in" label as appropriate: - $(window).on('focus', function() { + /**$(window).on('focus', function() { setGoogleAuthState(); - }); + }); */ } function setGoogleAuthState() { @@ -318,10 +361,24 @@ } function initializeButtons() { + $saveVideoCheckbox = $('#recording-finished-dialog input[name="save_video"]') + $uploadToYoutube = $('#recording-finished-dialog input[name="upload_to_youtube"]') var isPublic = $('#recording-finished-dialog input[name="is_public"]'); context.JK.checkbox(isPublic); - context.JK.checkbox($('#recording-finished-dialog input[name="save_video"]')); - context.JK.checkbox($('#recording-finished-dialog input[name="upload_to_youtube"]')); + context.JK.checkbox($saveVideoCheckbox); + context.JK.checkbox($uploadToYoutube); + + $saveVideoCheckbox.on('ifChanged', function() { + var saveVideoToDisk = $saveVideoCheckbox.is(':checked') + + if(saveVideoToDisk) { + $uploadToYoutube.iCheck('enable') + } + else { + $uploadToYoutube.iCheck('disable') + } + + }) } function initialize() { diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index 960d44cfd..92e577ecf 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -66,6 +66,20 @@ $('#session-settings-fan-access').val('listen-chat-band'); } + var $controllerSelect = $('#session-settings-master-mix-controller') + + $controllerSelect.empty() + var sessionUsers = context.SessionStore.helper.users() + + $controllerSelect.append('') + $.each(sessionUsers, function(userId, user) { + var selected = currentSession.session_controller_id == userId ? 'selected="selected"' : '' + $controllerSelect.append('') + }) + + var canEditController = currentSession.session_controller_id == context.JK.currentUserId || context.JK.currentUserId == currentSession.user_id + $controllerSelect.easyDropDown(canEditController ? 'enable' : 'disable') + /** // notation files in the account screen. ugh. $selectedFilenames.empty(); @@ -84,10 +98,12 @@ context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); + context.JK.dropdown($('#session-settings-master-mix-controller')); var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable' $('#session-settings-musician-access').easyDropDown(easyDropDownState) $('#session-settings-fan-access').easyDropDown(easyDropDownState) + } function addNotation(notation) { @@ -121,6 +137,7 @@ data.name = $('#session-settings-name').val(); data.description = $('#session-settings-description').val(); data.language = $('#session-settings-language').val(); + data.session_controller = $('#session-settings-master-mix-controller').val() // musician access var musicianAccess = $('#session-settings-musician-access').val(); @@ -148,7 +165,13 @@ data.fan_chat = true; } - rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved); + rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved) + .done(function(response) { + context.SessionActions.updateSession.trigger(response); + }) + .fail(function() { + app.notify({title: "Can't Update", text: "Unable to update session settings."}) + }) return false; } diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 8890e6b87..eb9846546 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -190,7 +190,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack showDownloading: () => @logger.debug("showing #{@state.name}") # while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually - context.jamClient.JamTrackDownload(@jamTrack.id, context.JK.currentUserId, + context.jamClient.JamTrackDownload(@jamTrack.id, null, context.JK.currentUserId, this.makeDownloadProgressCallback(), this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback()) @@ -369,8 +369,8 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @trackDetail = context.jamClient.JamTrackGetTrackDetail ("#{@jamTrack.id}-#{@sampleRateForFilename}") if @trackDetail.version? - @logger.error("after invalidating package, the version is still wrong!") - throw "after invalidating package, the version is still wrong!" + @logger.error("after invalidating package, the version is still wrong!", @trackDetail) + throw "after invalidating package, the version is still wrong! #{@trackDetail.version}" switch @trackDetail.key_state when 'pending' diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 43a844a32..42a0f410d 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -185,6 +185,7 @@ context.stats.write = context.stats.writePoint; } + function initializeStun(app) { stun = new context.JK.Stun(app); context.JK.StunInstance = stun; diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index 6d573a2c8..7cbab2f28 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -23,7 +23,14 @@ var $fader = $(this); var floaterConvert = $fader.data('floaterConverter') var sessionModel = window.JK.CurrentSessionModel || null; - + + /** + if(!$fader.data('has-session-control')) { + var sessionControllerName = $fader.data('session-controller-name'); + window.JK.prodBubble($fader, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $fader.closest('.top-parent'), duration:12000}) + return false; + }*/ + var mediaControlsDisabled = $fader.data('media-controls-disabled'); if(mediaControlsDisabled) { var mediaTrackOpener = $fader.data('media-track-opener'); @@ -173,7 +180,14 @@ var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener'); var sessionModel = window.JK.CurrentSessionModel || null; - + + /** + if(!$draggingFaderHandle.data('has-session-control')) { + var sessionControllerName = $draggingFaderHandle.data('session-controller-name'); + window.JK.prodBubble($draggingFaderHandle, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $draggingFaderHandle.closest('.top-parent'), duration:12000}) + return false; + }*/ + if(mediaControlsDisabled) { return false; } @@ -267,16 +281,33 @@ throw ("renderFader: userOptions is required"); } var renderDefaults = { - faderType: "vertical" + faderType: "vertical", + sessionController: null }; var options = $.extend({}, renderDefaults, userOptions); + var sessionCanControl = true + var sessionControllerName = null + if(userOptions.sessionController) { + if(!userOptions.sessionController.can_control) { + sessionCanControl = false + sessionControllerName = userOptions.sessionController.session_controller.name + } + } + selector.find('div[data-control="fader"]') .data('media-controls-disabled', selector.data('media-controls-disabled')) .data('media-track-opener', selector.data('media-track-opener')) .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) .data('floaterConverter', floaterConverter) .data('snap', userOptions.snap) + .data('has-session-control', sessionCanControl) + .data('session-controller-name', sessionControllerName) + + + if(userOptions.sessionController) { + + } selector.find('div[data-control="fader-handle"]').draggable({ drag: onFaderDrag, @@ -289,6 +320,8 @@ .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) .data('floaterConverter', floaterConverter) .data('snap', userOptions.snap) + .data('has-session-control', sessionCanControl) + .data('session-controller-name', sessionControllerName) // Embed any custom styles, applied to the .fader below selector if ("style" in options) { diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7ff48dd44..7acd44450 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -121,6 +121,9 @@ return 30; } + function GetSampleRate() { + return 48; + } function FTUESetVideoShareEnable(){ } @@ -502,6 +505,9 @@ return 0; } + function GetJamTrackSettings() { + return {tracks:[]} + } function SessionGetJamTracksPlayDurationMs() { return 60000; } @@ -1046,6 +1052,17 @@ function GetAutoStart() { return true; } function SaveSettings() {} + + function VSTLoad() {} + function VSTScan(callback) {setTimeout(eval(callback+ "()"), 1000)} + function hasVstHost() { return false;} + function getPluginList() { return {vsts:[]} } + + function clearPluginList() {} + function listTrackAssignments() { + return {} + } + // Javascript Bridge seems to camel-case // Set the instance functions: this.AbortRecording = AbortRecording; @@ -1211,6 +1228,7 @@ this.TrackGetChatUsesMusic = TrackGetChatUsesMusic; this.TrackSetChatUsesMusic = TrackSetChatUsesMusic; + this.GetJamTrackSettings = GetJamTrackSettings; this.JamTrackStopPlay = JamTrackStopPlay; this.JamTrackPlay = JamTrackPlay; this.JamTrackIsPlayable = JamTrackIsPlayable; @@ -1275,6 +1293,7 @@ this.FTUESetSendFrameRates = FTUESetSendFrameRates; this.GetCurrentVideoResolution = GetCurrentVideoResolution; this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate; + this.GetSampleRate = GetSampleRate; this.FTUESetVideoShareEnable = FTUESetVideoShareEnable; this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable; this.isSessVideoShared = isSessVideoShared; @@ -1307,6 +1326,12 @@ this.StopNetworkTest = StopNetworkTest; this.log = log; this.getOperatingMode = getOperatingMode; + this.VSTLoad = VSTLoad; + this.VSTScan = VSTScan; + this.hasVstHost = hasVstHost; + this.getPluginList = getPluginList; + this.clearPluginList = clearPluginList; + this.listTrackAssignments = listTrackAssignments; this.clientID = "devtester"; }; diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js index cdf32c7f9..d7ce6765f 100644 --- a/web/app/assets/javascripts/feedHelper.js +++ b/web/app/assets/javascripts/feedHelper.js @@ -471,6 +471,43 @@ $controls.data('server-info', feed.mix) // for recordingUtils helper methods $controls.data('view-context', 'feed') + // tack on video if available + if(feed.external_video_id) { + + var $videoWrapper = $feedItem.find('.video-wrapper') + var $videoContainer = $feedItem.find('.video-container') + if(gon.isNativeClient) { + var $embed = $('' + + '' + + '') + $videoContainer.append($embed).addClass('no-embed') + $videoWrapper.removeClass('hidden') + $embed.click(function() { + context.JK.popExternalLink($(this).attr('href')) + return false; + }) + } + else { + var $embed = $('