diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index 6c90018e6..d51e92f31 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -24,7 +24,7 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do column :original_artist column :name - column :flags do |jam_track| jam_track.duplicate_positions? ? 'DUP POSITIONS' : '' end + column :onboarding_flags do |jam_track| jam_track.onboard_warnings end column :status column :master_track do |jam_track| jam_track.master_track.nil? ? 'None' : (link_to "Download", jam_track.master_track.url_by_sample_rate(44)) end column :licensor diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index fc0d9502a..51341d812 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 :genre, collection: JamRuby::Genre.all, include_blank: false + = f.input :duration, hint: 'this should rarely need editing because it comes from the import process' = f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false = f.input :price, :required => true, :input_html => {type: 'numeric'} = f.input :pro_ascap, :label => 'ASCAP royalties due?' diff --git a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim index 0ef6078ac..4439c9550 100644 --- a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim +++ b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim @@ -5,11 +5,14 @@ = f.input :instrument, collection: Instrument.all, include_blank: false = f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' } = f.input :position - = f.input :preview_start_time_raw, :label => 'Preview Start Time', :hint => 'MM:SS:MLS', :as => :string + - if !f.object.nil? && f.object.track_type != 'Master' + = f.input :preview_start_time_raw, :label => 'Preview Start Time', :hint => 'MM:SS:MLS', :as => :string - unless f.object.nil? || f.object[:preview_url].nil? .current_file_holder style='margin-bottom:10px' - a href=f.object.preview_sign_url(3600) style='padding:0 0 0 20px' - | Download Preview + a href=f.object.preview_public_url('ogg') style='padding:0 0 0 20px' + | Download Preview (ogg) + a href=f.object.preview_public_url('mp3') style='padding:0 0 0 20px' + | Download Preview (mp3) // temporarily disable - if f.object.new_record? diff --git a/admin/config/initializers/jam_track_tracks.rb b/admin/config/initializers/jam_track_tracks.rb index b6367cd58..b05126d52 100644 --- a/admin/config/initializers/jam_track_tracks.rb +++ b/admin/config/initializers/jam_track_tracks.rb @@ -13,7 +13,6 @@ class JamRuby::JamTrackTrack end - # this is used by active admin/jam-admin def preview_start_time_raw if self.preview_start_time.nil? || self.preview_start_time.nil? @@ -60,6 +59,7 @@ class JamRuby::JamTrackTrack input = File.join(tmp_dir, 'in.ogg') output = File.join(tmp_dir, 'out.ogg') + output_mp3 = File.join(tmp_dir, 'out.mp3') start = self.preview_start_time.to_f / 1000 stop = start + 20 @@ -80,16 +80,52 @@ class JamRuby::JamTrackTrack @@log.debug("fail #{result_code}") @preview_generate_error = "unable to execute cut command #{sox_output}" else - @@log.debug("uploading preview to #{self.preview_filename}") + # now create mp3 off of ogg preview - s3_manager.upload(self.preview_filename, output) + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - self.skip_uploader = true - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename - self["preview_md5"] = ::Digest::MD5.file(output).hexdigest - self["preview_length"] = File.new(output).size - self.save! + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" + else + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + # and finally update the JamTrackTrack with the new info + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + + end end end rescue Exception => e diff --git a/db/manifest b/db/manifest index 444624782..cc3038808 100755 --- a/db/manifest +++ b/db/manifest @@ -276,4 +276,6 @@ user_reuse_card_and_reedem.sql jam_track_id_to_varchar.sql drop_position_unique_jam_track.sql recording_client_metadata.sql +preview_support_mp3.sql +jam_track_duration.sql musician_search.sql diff --git a/db/up/jam_track_duration.sql b/db/up/jam_track_duration.sql new file mode 100644 index 000000000..a4dbee409 --- /dev/null +++ b/db/up/jam_track_duration.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks ADD COLUMN duration INTEGER; \ No newline at end of file diff --git a/db/up/preview_support_mp3.sql b/db/up/preview_support_mp3.sql new file mode 100644 index 000000000..90dd1c642 --- /dev/null +++ b/db/up/preview_support_mp3.sql @@ -0,0 +1,4 @@ +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT; +UPDATE jam_track_tracks SET preview_url = NULL where track_type = 'Master'; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 51ed2e45d..c97a035b6 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -30,6 +30,7 @@ require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/conflict_error" require "jam_ruby/lib/app_config" require "jam_ruby/lib/s3_manager_mixin" +require "jam_ruby/lib/s3_public_manager_mixin" require "jam_ruby/lib/module_overrides" require "jam_ruby/lib/s3_util" require "jam_ruby/lib/s3_manager" diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 8874a66fe..58138dbf9 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -19,6 +19,10 @@ module JamRuby @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def public_jamkazam_s3_manager + @public_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_public, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def finish(reason, detail) self.reason = reason self.detail = detail @@ -93,6 +97,7 @@ module JamRuby self.name = metadata["name"] || name if jam_track.new_record? + jam_track.id = "#{JamTrack.count + 1}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that jam_track.status = 'Staging' jam_track.metalocation = metalocation jam_track.original_artist = metadata["original_artist"] || original_artist @@ -228,7 +233,7 @@ module JamRuby part = potential_instrument_original if !part {instrument: instrument, - part: part} + part: part} end @@ -259,7 +264,7 @@ module JamRuby if stem_location bits = filename_no_ext[stem_location..-1].split('-') - bits.collect! {|bit| bit.strip} + bits.collect! { |bit| bit.strip } possible_instrument = nil possible_part = nil @@ -424,6 +429,15 @@ module JamRuby 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) @@ -456,15 +470,26 @@ module JamRuby 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"] = ::Digest::MD5.file(ogg_44100).hexdigest + track["md5_44"] = ogg_44100_digest.hexdigest track["length_44"] = File.new(ogg_44100).size track["url_48"] = ogg_48000_s3_path track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest track["length_48"] = File.new(ogg_48000).size + synchronize_duration(jam_track, ogg_44100) + + # convert entire master ogg file to mp3, and push both to public destination + preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) if track.track_type == 'Master' + + if !preview_succeeded + return false + end + + end track.save! @@ -478,6 +503,77 @@ module JamRuby return true end + def synchronize_duration(jam_track, ogg_44100) + duration_command = "soxi -D \"#{ogg_44100}\"" + output = `#{duration_command}` + + result_code = $?.to_i + + if result_code == 0 + duration = output.to_f.round + jam_track.duration = duration + else + @@log.warn("unable to determine duration for jam_track #{jam_track.name}. output #{output}") + end + true + end + + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) + + begin + mp3_44100 = File.join(tmp_dir, 'output-preview-44100.mp3') + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -ab 192k \"#{mp3_44100}\"" + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + mp3_digest = ::Digest::MD5.file(mp3_44100) + + track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + # upload 44100 ogg and mp3 to public location as well + @@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}") + public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}") + public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + + + track.skip_uploader = true + + original_ogg_preview_url = track["preview_url"] + original_mp3_preview_url = track["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + track["preview_url"] = track.preview_filename(ogg_md5, 'ogg') + track["preview_length"] = File.new(ogg_44100).size + # and finally update the JamTrackTrack with the new info + track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3') + track["preview_mp3_length"] = File.new(mp3_44100).size + track["preview_start_time"] = 0 + + if !track.save + finish("save_master_preview", track.errors.to_s) + return false + end + + # if all that worked, now delete old previews, if present + begin + public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"] + public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + rescue Exception => e + finish("sync_master_preview_exception", e.to_s) + return false + end + + + return true + + end + def fetch_all_files(s3_path) JamTrackImporter::s3_manager.list_files(s3_path) end @@ -533,6 +629,10 @@ module JamRuby @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def private_s3_manager + @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def dry_run s3_manager.list_directories('audio').each do |original_artist| @@ -554,6 +654,139 @@ module JamRuby end + def synchronize_jamtrack_master_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + master_track = jam_track.master_track + + if master_track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + if importer.synchronize_master_preview(master_track, tmp_dir, ogg_44100, ogg_44100_digest) + importer.finish("success", nil) + end + end + else + importer.finish('no_master_track', nil) + end + + importer + end + + def synchronize_jamtrack_master_previews + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_jamtrack_master_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + + def synchronize_duration(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + master_track = jam_track.master_track + if master_track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + + if importer.synchronize_duration(jam_track, ogg_44100) + jam_track.save! + importer.finish("success", nil) + end + end + else + importer.finish('no_duration', nil) + end + + importer + end + + def synchronize_durations + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_duration(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + + def download_master(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + Dir.mkdir('tmp') unless Dir.exists?('tmp') + Dir.mkdir('tmp/jam_track_masters') unless Dir.exists?('tmp/jam_track_masters') + + master_track = jam_track.master_track + if master_track + ogg_44100 = File.join('tmp/jam_track_masters', "#{jam_track.original_artist} - #{jam_track.name}.ogg") + private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100) + end + importer + end + + def download_masters + importers = [] + + JamTrack.all.each do |jam_track| + importers << download_master(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to download.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end def synchronize_all(options) importers = [] @@ -607,14 +840,14 @@ module JamRuby def load_metalocation(metalocation) begin - data = s3_manager.read_all(metalocation) + data = s3_manager.read_all(metalocation) return YAML.load(data) rescue AWS::S3::Errors::NoSuchKey return nil end end - def create_from_metalocation(meta, metalocation, options = {skip_audio_upload:false}) + def create_from_metalocation(meta, metalocation, options = {skip_audio_upload: false}) jam_track = JamTrack.new sync_from_metadata(jam_track, meta, metalocation, options) end @@ -628,7 +861,7 @@ module JamRuby JamTrack.transaction do #begin - jam_track_importer.synchronize(jam_track, meta, metalocation, options) + jam_track_importer.synchronize(jam_track, meta, metalocation, options) #rescue Exception => e # jam_track_importer.finish("unhandled_exception", e.to_s) #end diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index 8f8eead79..cf86fdc9b 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -44,6 +44,10 @@ module JamRuby s3_bucket.objects[key].url_for(operation, options).to_s end + def public_url(key, options = @@def_opts) + s3_bucket.objects[key].public_url(options).to_s + end + def presigned_post(key, options = @@def_opts) s3_bucket.objects[key].presigned_post(options) end @@ -72,8 +76,15 @@ module JamRuby s3_bucket.objects[filename].delete end - def upload(key, filename) - s3_bucket.objects[key].write(:file => filename) + def upload(key, filename, options={}) + options[:file] = filename + s3_bucket.objects[key].write(options) + end + + def cached_upload(key, filename, options={}) + options[:file] = filename + options.merge({expires: 5.years.from_now}) + s3_bucket.objects[key].write(filename, options) end def delete_folder(folder) diff --git a/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb b/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb new file mode 100644 index 000000000..6eba1990a --- /dev/null +++ b/ruby/lib/jam_ruby/lib/s3_public_manager_mixin.rb @@ -0,0 +1,17 @@ +module JamRuby + module S3PublicManagerMixin + extend ActiveSupport::Concern + include AppConfig + + included do + end + + module ClassMethods + + end + + def s3_public_manager() + @s3_public_manager ||= S3Manager.new(app_config.aws_bucket_public, app_config.aws_access_key_id, app_config.aws_secret_access_key) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 56341ff26..a4d293276 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -37,6 +37,7 @@ module JamRuby validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :reproduction_royalty, inclusion: {in: [nil, true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} + validates :duration, numericality: {only_integer: true}, :allow_nil => true validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/ validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,3}$/ @@ -44,7 +45,7 @@ module JamRuby belongs_to :genre, class_name: "JamRuby::Genre" belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' - has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'position ASC, part ASC, instrument_id ASC' + has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC' has_many :jam_track_tap_ins, :class_name => "JamRuby::JamTrackTapIn", order: 'offset_time ASC' has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" # ' @@ -70,7 +71,6 @@ module JamRuby if count.nil? count = 0 end - puts "count #{count}" counter[track.position] = count + 1 end @@ -84,6 +84,29 @@ module JamRuby duplicate end + def missing_previews? + missing_preview = false + self.jam_track_tracks.each do |track| + unless track.has_preview? + missing_preview = true + break + end + end + missing_preview + end + + def onboard_warnings + warnings = [] + warnings << 'POSITIONS' if duplicate_positions? + warnings << 'PREVIEWS'if missing_previews? + warnings << 'DURATION' if duration.nil? + warnings.join(',') + end + + def band_jam_track_count + JamTrack.where(original_artist: original_artist).count + end + class << self # @return array[artist_name(string)] def all_artists diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index 78255d07c..41514b2f8 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -3,6 +3,7 @@ module JamRuby # describes an audio track (like the drums, or guitar) that comprises a JamTrack class JamTrackTrack < ActiveRecord::Base include JamRuby::S3ManagerMixin + include JamRuby::S3PublicManagerMixin # there should only be one Master per JamTrack, but there can be N Track per JamTrack TRACK_TYPE = %w{Track Master} @@ -41,24 +42,27 @@ module JamRuby "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" end - # create name of the file - def preview_filename - filename("#{File.basename(self["url_44"], ".ogg")}-preview.ogg") + # create name of the preview file. + # md5-'ed because we cache forever + def preview_filename(md5, ext='ogg') + original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}" + "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" end def has_preview? - !self["preview_url"].nil? + !self["preview_url"].nil? && !self['preview_mp3_url'].nil? end - # creates a short-lived URL that has access to the object. - # the idea is that this is used when a user who has the rights to this tries to download this JamTrack - # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download - # but the url is short lived enough so that it wouldn't be easily shared - def preview_sign_url(expiration_time = 120) - s3_manager.sign_url(self[:preview_url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + # generates a URL that points to a public version of the preview + def preview_public_url(media_type='ogg') + url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url] + if url + s3_public_manager.public_url(url,{ :secure => false}) + else + nil + end end - def manually_uploaded_filename(mounted_as) if track_type == 'Master' filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") @@ -67,6 +71,10 @@ module JamRuby end end + def master? + track_type == 'Master' + end + def url_by_sample_rate(sample_rate=48) field_name = (sample_rate==48) ? "url_48" : "url_44" self[field_name] diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 7a1ca3646..426dcb679 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -901,6 +901,11 @@ module JamRuby user.email = user.update_email user.update_email_token = nil user.save + begin + RecurlyClient.new.update_account(user) + rescue Recurly::Error + @@log.debug("No recurly account found; continuing") + end return user end diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index 6b12332a0..516aa7235 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -22,6 +22,11 @@ module JamRuby account end + def has_account?(current_user) + account = get_account(current_user) + !!account + end + def delete_account(current_user) account = get_account(current_user) if (account) diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 1c66ea236..c402dca98 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'jam_ruby/recurly_client' RESET_PASSWORD_URL = "/reset_token" @@ -9,6 +10,7 @@ describe User do @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", password: "foobar", password_confirmation: "foobar", city: "Apex", state: "NC", country: "US", terms_of_service: true, musician: true) @user.musician_instruments << FactoryGirl.build(:musician_instrument, user: @user) + @recurly = RecurlyClient.new end subject { @user } @@ -434,6 +436,8 @@ describe User do describe "finalize email update" do before do + @recurly.has_account?(@user).should == false + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") UserMailer.deliveries.clear end @@ -464,6 +468,36 @@ describe User do end end + describe "finalize email updates recurly" do + before do + + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") + UserMailer.deliveries.clear + billing_info = { + first_name: @user.first_name, + last_name: @user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: @user.city, + state: @user.state, + country: @user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' + } + @recurly.find_or_create_account(@user, billing_info) + end + + it "should update recurly" do + @recurly.has_account?(@user).should == true + @recurly.get_account(@user).email.should_not == "somenewemail@blah.com" + @finalized = User.finalize_update_email(@user.update_email_token) + @recurly.get_account(@user).email.should == "somenewemail@blah.com" + end + end + describe "user_authorizations" do it "can create" do diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index b7189774a..c791fb567 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -18,11 +18,19 @@ var nilOptionStr = ''; var nilOptionText = 'n/a'; var $screen = $('#account-profile-basics'); + var $avatar = $screen.find('#avatar'); + var $country = $screen.find('#country'); + var $region = $screen.find('#region'); + var $city = $screen.find('#city'); + var $firstName = $screen.find('#first-name'); + var $lastName = $screen.find('#last-name'); + var $gender = $screen.find('#gender'); + var $biography = $screen.find('#biography'); + var $subscribe = $screen.find('#subscribe'); + var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnSubmit = $screen.find('#account-edit-profile-submit'); - var $biography = null; - function beforeShow(data) { userId = data.id; } @@ -39,27 +47,21 @@ } function populateAccountProfile(userDetail) { - var template = context.JK.fillTemplate($('#template-account-profile-basics').html(), { - country: userDetail.country, - region: userDetail.state, - city: userDetail.city, - first_name: userDetail.first_name, - last_name: userDetail.last_name, - photoUrl: context.JK.resolveAvatarUrl(userDetail.photo_url), - birth_date : userDetail.birth_date, - gender: userDetail.gender, - biography: userDetail.biography ? userDetail.biography : '', - subscribe_email: userDetail.subscribe_email ? "checked=checked" : "" - }); + + $avatar.attr('src', context.JK.resolveAvatarUrl(userDetail.photo_url)); + $country.val(userDetail.country); + $region.val(userDetail.state); + $city.val(userDetail.city); + $firstName.val(userDetail.first_name); + $lastName.val(userDetail.last_name); + $gender.val(userDetail.gender); + $biography.val(userDetail.biography); + + if (userDetail.subscribe_email) { + $subscribe.attr('checked', 'checked'); + } var content_root = $('#account-profile-content-scroller'); - content_root.html(template); - - $biography = $screen.find('#biography'); - - // now use javascript to fix up values too hard to do with templating - // set gender - $('select[name=gender]', content_root).val(userDetail.gender) // set birth_date if(userDetail.birth_date) { diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 8e96446b4..e884d3549 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -25,18 +25,12 @@ var $twitterUsername = $screen.find('#twitter-username'); // performance samples - var $soundCloudRecordingUrl = $screen.find('#soundcloud-recording'); - var $youTubeVideoUrl = $screen.find('#youtube-video'); - var $jamkazamSampleList = $screen.find('.samples.jamkazam'); var $soundCloudSampleList = $screen.find('.samples.soundcloud'); var $youTubeSampleList = $screen.find('.samples.youtube'); // buttons var $btnAddJkRecording = $screen.find('#btn-add-jk-recording'); - var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); - var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); - var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnBack = $screen.find('#account-edit-profile-back'); var $btnSubmit = $screen.find('#account-edit-profile-submit'); @@ -102,7 +96,7 @@ // JamKazam recordings var samples = profileUtils.jamkazamSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { }); @@ -110,7 +104,7 @@ // SoundCloud recordings samples = profileUtils.soundCloudSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { }); @@ -118,9 +112,9 @@ // YouTube videos samples = profileUtils.youTubeSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { - + }); } } @@ -137,24 +131,6 @@ return false; }); - $btnAddSoundCloudRecording.click(function(evt) { - var url = $soundCloudRecordingUrl.val(); - if (url.length > 0) { - if (extractSoundCloudUrlParts(url)) { - // add to list - } - } - }); - - $btnAddYouTubeVideo.click(function(evt) { - var url = $youTubeVideoUrl.val(); - if (url.length) { - if (extractYouTubeUrlParts(url)) { - // add to list - } - } - }); - $btnCancel.click(function(evt) { evt.stopPropagation(); navigateTo('/client#/profile/' + context.JK.currentUserId); @@ -176,22 +152,8 @@ } function validate() { - // website - if ($.trim($website.val()).length > 0) { - - } - - // SoundCloud - if ($.trim($soundCloudUsername.val()).length > 0) { - - } - - // ReverbNation - if ($.trim($reverbNationUsername.val())) { - - } - - return true; + var errors = $screen.find('.site_validator.error'); + return !(errors && errors.length > 0); } function navigateTo(targetLocation) { @@ -199,7 +161,7 @@ } function addOnlinePresence(presenceArray, username, type) { - if ($.trim($soundCloudUsername.val()).length > 0) { + if ($.trim(username).length > 0) { presenceArray.push({ service_type: type, username: username @@ -228,17 +190,17 @@ // extract performance samples var ps = []; var performanceSampleTypes = profileUtils.SAMPLE_TYPES; - addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM); - addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD); - addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE); + addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM.description); + addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); + addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); - // api.updateUser({ - // website: $website.val(), - // online_presences: op, - // performance_samples: ps - // }) - // .done(postUpdateProfileSuccess) - // .fail(postUpdateProfileFailure); + api.updateUser({ + website: $website.val(), + online_presences: op, + performance_samples: ps + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure); } function postUpdateProfileSuccess(response) { diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 73a57daad..bc065cf22 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -36,6 +36,7 @@ //= require jquery.custom-protocol //= require jquery.exists //= require jquery.payment +//= require howler.core.js //= require jstz //= require class //= require AAC_underscore diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 19fb63e9b..5ccbf04a3 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1448,6 +1448,24 @@ }); } + function getJamTrack(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/' + options['plan_code'] + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getJamTrackWithArtistInfo(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/band/' + options['plan_code'] + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function getJamtracks(options) { return $.ajax({ type: "GET", @@ -1769,6 +1787,8 @@ this.createDiagnostic = createDiagnostic; this.getLatencyTester = getLatencyTester; this.updateAudioLatency = updateAudioLatency; + this.getJamTrack = getJamTrack; + this.getJamTrackWithArtistInfo = getJamTrackWithArtistInfo; this.getJamtracks = getJamtracks; this.getPurchasedJamTracks = getPurchasedJamTracks; this.getPaymentHistory = getPaymentHistory; diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee new file mode 100644 index 000000000..926a8d824 --- /dev/null +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -0,0 +1,125 @@ +$ = jQuery +context = window +context.JK ||= {}; + + +context.JK.JamTrackPreview = {} +context.JK.JamTrackPreview = class JamTrackPreview + constructor: (app, $root, jamTrack, jamTrackTrack, options) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + @options = options || {master_shows_duration: false} + @app = app + @jamTrack = jamTrack + @jamTrackTrack = jamTrackTrack + @root = $root + @playButton = null + @stopButton = null + @instrumentIcon = null + @instrumentName = null + @part = null + + template = $('#template-jam-track-preview') + throw "no jam track preview template" if not template.exists() + + @root.html($(template.html())) + @playButton = @root.find('.play-button') + @stopButton = @root.find('.stop-button') + @instrumentIcon = @root.find('.instrument-icon') + @instrumentName = @root.find('.instrument-name') + @part = @root.find('.part') + + @playButton.on('click', @play) + @stopButton.on('click', @stop) + + @root.attr('data-track-type', @jamTrackTrack.track_type).attr('data-id', @jamTrackTrack.id) + instrumentId = null + instrumentDescription = '?' + if @jamTrackTrack.track_type == 'Track' + if @jamTrackTrack.instrument + instrumentId = @jamTrackTrack.instrument.id + instrumentDescription = @jamTrackTrack.instrument.description + else + instrumentId = 'other' + instrumentDescription= 'Master Mix' + + instrument_src = context.JK.getInstrumentIcon24(instrumentId) + + @instrumentIcon.attr('data-instrument-id', instrumentId).attr('src', instrument_src) + @instrumentName.text(instrumentDescription) + #context.JK.bindInstrumentHover(@root) + + part = '' + + if @jamTrackTrack.track_type == 'Track' + part = "#{@jamTrackTrack.part}" if @jamTrackTrack.part? && @jamTrackTrack.part != instrumentDescription + + else + if @options.master_shows_duration + duration = 'entire song' + if @jamTrack.duration + duration = "0:00 - #{context.JK.prettyPrintSeconds(@jamTrack.duration)}" + part = duration + else + part = @jamTrack.name + ' by ' + @jamTrack.original_artist + + @part.text("(#{part})") if part != '' + + if @jamTrackTrack.preview_mp3_url? + + urls = [@jamTrackTrack.preview_mp3_url] + if @jamTrackTrack.preview_ogg_url? + urls.push(@jamTrackTrack.preview_ogg_url) + + @no_audio = false + @sound = new Howl({ + src: urls, + autoplay: false, + loop: false, + volume: 1.0, + onend: @onHowlerEnd}) + else + @no_audio = true + + if @no_audio + @playButton.addClass('disabled') + @stopButton.addClass('disabled') + + onHowlerEnd: () => + @logger.debug("on end $(this)", $(this)) + @stopButton.addClass('hidden') + @playButton.removeClass('hidden') + + play: (e) => + if e? + e.stopPropagation() + + if @no_audio + context.JK.prodBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + logger.debug("play issued for jam track preview") + @sound.play() + @playButton.addClass('hidden') + @stopButton.removeClass('hidden') + + return false + + stop: (e) => + if e? + e.stopPropagation() + + if @no_audio + context.JK.helpBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + logger.debug("stop issued for jam track preview") + @sound.stop() + @stopButton.addClass('hidden') + @playButton.removeClass('hidden') + + return false + + + + + diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index d0935c698..ee61b4272 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -18,14 +18,14 @@ var COWRITING_GENRE_TYPE = 'cowriting'; // performance sample types - var SAMPLE_TYPES = { + profileUtils.SAMPLE_TYPES = { JAMKAZAM: {description: "jamkazam"}, SOUNDCLOUD: {description: "soundcloud"}, YOUTUBE: {description: "youtube"} }; // online presence types - var ONLINE_PRESENCE_TYPES = { + profileUtils.ONLINE_PRESENCE_TYPES = { SOUNDCLOUD: {description: "soundcloud"}, REVERBNATION: {description: "reverbnation"}, BANDCAMP: {description: "bandcamp"}, @@ -184,7 +184,7 @@ profileUtils.jamkazamSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.JAMKAZAM.description; + return s.service_type === profileUtils.SAMPLE_TYPES.JAMKAZAM.description; }); return matches; @@ -192,7 +192,7 @@ profileUtils.soundCloudSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.SOUNDCLOUD.description; + return s.service_type === profileUtils.SAMPLE_TYPES.SOUNDCLOUD.description; }); return matches; @@ -200,7 +200,7 @@ profileUtils.youTubeSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.YOUTUBE.description; + return s.service_type === profileUtils.SAMPLE_TYPES.YOUTUBE.description; }); return matches; @@ -208,7 +208,7 @@ profileUtils.soundCloudPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.SOUNDCLOUD.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.SOUNDCLOUD.description; }); return matches; @@ -216,7 +216,7 @@ profileUtils.reverbNationPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.REVERBNATION.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.REVERBNATION.description; }); return matches; @@ -224,7 +224,7 @@ profileUtils.bandCampPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.BANDCAMP.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.BANDCAMP.description; }); return matches; @@ -232,7 +232,7 @@ profileUtils.fandalismPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.FANDALISM.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.FANDALISM.description; }); return matches; @@ -240,7 +240,7 @@ profileUtils.youTubePresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.YOUTUBE.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.YOUTUBE.description; }); return matches; @@ -248,7 +248,7 @@ profileUtils.facebookPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.FACEBOOK.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.FACEBOOK.description; }); return matches; @@ -256,7 +256,7 @@ profileUtils.twitterPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.TWITTER.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.TWITTER.description; }); return matches; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index db44a05fa..2b6ea1e80 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -2740,6 +2740,12 @@ logger.debug("Unstable clocks: ", names, unstable) context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' })); } else { + var data = { + value: 1, + session_size: sessionModel.participants().length, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName } + context.stats.write('web.metronome.open', data) var bpm = 120; logger.debug("opening the metronome with bpm: " + bpm + ", sound:" + metroSound) rest.openMetronome({id: sessionModel.id()}) diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index fc54465dd..0c450cd72 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -83,7 +83,7 @@ context.JK.SiteValidator = class SiteValidator @fail_callback(@input_div) @deferred_status_check = null - @logger.debug("site_status = "+@site_status) + @logger.debug("site_status = " + @site_status) processSiteCheckFail: (response) => @logger.error("site check error") @@ -130,10 +130,12 @@ context.JK.SiteValidator = class SiteValidator context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator constructor: (site_type, success_callback, fail_callback) -> - super(site_type, success_callback, fail_callback) + super(site_type) @recording_sources = [] @is_rec_src = true @add_btn = @input_div.find('a.add-recording-source') + @site_success_callback = success_callback + @site_fail_callback = fail_callback init: (sources) => super() @@ -145,11 +147,17 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit processSiteCheckSucceed: (response) => super(response) @add_btn.removeClass('disabled') - @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + + if @site_status + @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + if @site_success_callback + @site_success_callback(@input_div) processSiteCheckFail: (response) => super(response) @add_btn.removeClass('disabled') + if @site_fail_callback + @site_fail_callback(@input_div) didBlur: () => # do nothing, validate on add only @@ -174,3 +182,6 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit src_data['url'] == url 0 < vals.length + recordingSources: () => + @recording_sources + diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index d4d91e6cb..52d47094a 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -133,12 +133,17 @@ } } else { - var $template = $('#template-help-' + templateName) - if($template.length == 0) { - var helpText = templateName; + try { + var $template = $('#template-help-' + templateName) + if ($template.length == 0) { + var helpText = templateName; + } + else { + var helpText = context._.template($template.html(), data, { variable: 'data' }); + } } - else { - var helpText = context._.template($template.html(), data, { variable: 'data' }); + catch(e) { + var helpText = templateName; } holder = $('
'); diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js new file mode 100644 index 000000000..bbfd05896 --- /dev/null +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -0,0 +1,62 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.IndividualJamTrack = function (app) { + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $page = null; + var $jamtrack_name = null; + var $previews = null; + var $jamTracksButton = null; + var $genericHeader = null; + var $individualizedHeader = null; + + function fetchJamTrack() { + rest.getJamTrack({plan_code: gon.jam_track_plan_code}) + .done(function (jam_track) { + logger.debug("jam_track", jam_track) + + if(!gon.just_previews) { + if (gon.generic) { + $genericHeader.removeClass('hidden'); + } + else { + $individualizedHeader.removeClass('hidden') + $jamtrack_name.text(jam_track.name); + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack') + } + } + + context._.each(jam_track.tracks, function (track) { + + var $element = $('
') + + $previews.append($element); + + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + }) + + $previews.append('
') + }) + .fail(function () { + app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) + }) + + } + function initialize() { + + $page = $('body') + $jamtrack_name = $page.find('.jamtrack_name') + $previews = $page.find('.previews') + $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') + $genericHeader = $page.find('h1.generic') + $individualizedHeader = $page.find('h1.individualized') + fetchJamTrack(); + } + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band.js new file mode 100644 index 000000000..0de60a2e7 --- /dev/null +++ b/web/app/assets/javascripts/web/individual_jamtrack_band.js @@ -0,0 +1,60 @@ + +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.IndividualJamTrackBand = function (app) { + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $page = null; + var $jamTrackBandInfo = null; + var $jamTrackNoun = null; + var $previews = null; + var $jamTracksButton = null; + var $checkItOut = null; + + function fetchJamTrack() { + rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) + .done(function (jam_track) { + logger.debug("jam_track", jam_track) + + $jamTrackBandInfo.text(jam_track.band_jam_track_count + ' ' + jam_track.original_artist); + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack') + + if(jam_track.band_jam_track_count == 1) { + $jamTrackNoun.text('JamTrack') + $checkItOut.text(', Check It Out!') + } + context._.each(jam_track.tracks, function (track) { + + var $element = $('
') + + $previews.append($element); + + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + }) + + $previews.append('
') + }) + .fail(function () { + app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) + }) + + } + function initialize() { + + $page = $('body') + $jamTrackBandInfo = $page.find('.jamtrack_band_info') + $previews = $page.find('.previews') + $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') + $jamTrackNoun = $page.find('.jamtrack_noun') + $checkItOut = $page.find('.check-it-out') + + fetchJamTrack(); + } + + this.initialize = initialize; + } +})(window, jQuery); diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 466533efd..a36ad12e2 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -20,6 +20,7 @@ //= require jquery.icheck //= require jquery.bt //= require jquery.exists +//= require howler.core.js //= require AAA_Log //= require AAC_underscore //= require alert @@ -52,6 +53,7 @@ //= require recording_utils //= require helpBubbleHelper //= require facebook_rest +//= require jam_track_preview //= require landing/init //= require landing/signup //= require web/downloads @@ -60,6 +62,8 @@ //= require web/session_info //= require web/recordings //= require web/welcome +//= require web/individual_jamtrack +//= require web/individual_jamtrack_band //= require fakeJamClient //= require fakeJamClientMessages //= require fakeJamClientRecordings diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index e5f1e310b..11b3bb704 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -31,8 +31,11 @@ } } - .sample-list { - + div.sample-list { + height: 250px; + width: 250px; + border: 2px solid #ccc; + overflow: auto; } } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index c48ea6bef..435fae16a 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -73,4 +73,5 @@ *= require icheck/minimal/minimal *= require users/syncViewer *= require ./downloadJamTrack + *= require ./jamTrackPreview */ \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index 69f430b7f..dbaaa80cf 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -318,3 +318,15 @@ $fair: #cc9900; } } +.white-bordered-button { + font-size:20px; + font-weight:bold; + background-color:white; + color:$ColorScreenPrimary; + border:3px solid $ColorScreenPrimary; + padding:18px; + -webkit-border-radius:8px; + -moz-border-radius:8px; + border-radius:8px; +} + diff --git a/web/app/assets/stylesheets/client/jamTrackPreview.css.scss b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss new file mode 100644 index 000000000..28213d8b4 --- /dev/null +++ b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss @@ -0,0 +1,35 @@ +@import "client/common"; + +.jam-track-preview { + + display:inline-block; + line-height:24px; + vertical-align: middle; + @include border_box_sizing; + color:$ColorTextTypical; + font-size:14px; + + .actions { + display:inline; + vertical-align: middle; + } + + img.instrument-icon { + display:inline; + vertical-align: middle; + margin-left:10px; + } + + .instrument-name { + display:inline; + vertical-align: middle; + margin-left:10px; + + } + + .part { + display:inline; + vertical-align: middle; + margin-left:4px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index d1e2b4192..b898d2958 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -134,7 +134,7 @@ input[type="button"] { } .hidden { - display:none; + display:none !important; } .small { diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss new file mode 100644 index 000000000..6111876ee --- /dev/null +++ b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss @@ -0,0 +1,32 @@ +body.web.landing_jamtrack.individual_jamtrack { + + .previews { + margin-top:10px; + } + .jamtrack-reasons { + margin: 10px 0 0 20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .browse-jamtracks-wrapper { + text-align:center; + width:90%; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss new file mode 100644 index 000000000..db25181b1 --- /dev/null +++ b/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss @@ -0,0 +1,32 @@ +body.web.landing_jamtrack.individual_jamtrack_band { + + .previews { + margin-top:10px; + } + .jamtrack-reasons { + margin: 10px 0 0 20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .browse-jamtracks-wrapper { + text-align:center; + width:90%; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/landing_page_new.css.scss b/web/app/assets/stylesheets/landings/landing_page_new.css.scss new file mode 100644 index 000000000..2fd40ea5f --- /dev/null +++ b/web/app/assets/stylesheets/landings/landing_page_new.css.scss @@ -0,0 +1,52 @@ +@import "client/common.css.scss"; + +body.web.landing_page { + + .two_by_two { + + h1 { + margin:0 0 5px; + padding:7px 0; + display:inline-block; + } + .row { + @include border_box_sizing; + + &:nth-of-type(1) { + padding:20px 0 0 0; + } + + + .column { + width:50%; + float:left; + @include border_box_sizing; + } + } + } + + &.landing_jamtrack, &.landing_product { + + .landing-tag { + left:50%; + text-align:center; + } + p, ul, li { + font-size:14px; + line-height:125%; + color:$ColorTextTypical; + } + p { + + } + ul { + list-style-type: disc; + } + li { + margin-left:20px; + } + .video-container { + margin-top:0; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/landing_product.css.scss b/web/app/assets/stylesheets/landings/landing_product.css.scss new file mode 100644 index 000000000..99739aa57 --- /dev/null +++ b/web/app/assets/stylesheets/landings/landing_product.css.scss @@ -0,0 +1,53 @@ +@import "client/common.css.scss"; + +body.web.landing_product { + + h1.product-headline { + color:white; + background-color:$ColorScreenPrimary; + margin-bottom:20px; + padding:7px; + border-radius:4px; + } + + .product-description { + margin-bottom:20px; + } + + .white-bordered-button { + margin-top: 20px; + } + + .cta-big-button { + text-align:center; + width:90%; + } + + .linked-video-holder { + text-align:center; + width:90%; + margin-top:20px; + } + + .previews { + margin-top:10px; + } + + .jamtrack-reasons { + margin-top:20px; + } + + .jam-track-preview-holder { + + margin-bottom: 7px; + float: left; + + &[data-track-type="Master"] { + width: 100%; + } + + &[data-track-type="Track"] { + width: 50%; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/web.css b/web/app/assets/stylesheets/web/web.css index 93d52b7c2..ac4463b15 100644 --- a/web/app/assets/stylesheets/web/web.css +++ b/web/app/assets/stylesheets/web/web.css @@ -2,6 +2,7 @@ *= require client/jamServer *= require client/ie *= require client/jamkazam +*= require jquery.bt *= require easydropdown *= require easydropdown_jk *= require client/screen_common @@ -14,6 +15,7 @@ *= require client/help *= require client/listenBroadcast *= require client/flash +*= require client/jamTrackPreview *= require web/main *= require web/footer *= require web/recordings @@ -24,6 +26,7 @@ *= require web/downloads *= require users/signinCommon *= require dialogs/dialog -*= require landings/landing_page +*= require client/help +*= require_directory ../landings *= require icheck/minimal/minimal */ \ No newline at end of file diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb index f1b4264fa..fd8019462 100644 --- a/web/app/controllers/api_jam_tracks_controller.rb +++ b/web/app/controllers/api_jam_tracks_controller.rb @@ -1,12 +1,20 @@ class ApiJamTracksController < ApiController # have to be signed in currently to see this screen - before_filter :api_signed_in_user, :except => [:index] - before_filter :api_any_user, :only => [:index] + before_filter :api_signed_in_user, :except => [:index, :show, :show_with_artist_info] + before_filter :api_any_user, :only => [:index, :show, :show_with_artist_info] before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right] respond_to :json + def show + @jam_track = JamTrack.find_by_plan_code!(params[:plan_code]) + end + + def show_with_artist_info + @jam_track = JamTrack.find_by_plan_code!(params[:plan_code]) + end + def index data = JamTrack.index(params, any_user) @jam_tracks, @next = data[0], data[1] diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index 1268f730b..e2df670cc 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -65,5 +65,37 @@ class LandingsController < ApplicationController def watch_overview_tight render 'watch_overview_tight', layout: 'web' end + + def individual_jamtrack + gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil + gon.generic = params[:generic] + render 'individual_jamtrack', layout: 'web' + end + + def individual_jamtrack_band + gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil + + render 'individual_jamtrack_band', layout: 'web' + end + + def product_jamblaster + render 'product_jamblaster', layout: 'web' + end + + def product_platform + render 'product_platform', layout: 'web' + end + + def product_jamtracks + gon.generic = true + gon.just_previews = true + jam_track = JamTrack.select('plan_code').where(plan_code: Rails.application.config.nominated_jam_track).first + unless jam_track + jam_track = JamTrack.first + end + + gon.jam_track_plan_code = jam_track.plan_code + render 'product_jamtracks', layout: 'web' + end end diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 1602b6a25..c5f6aa645 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -53,6 +53,13 @@ class SpikesController < ApplicationController render :layout => 'web' end + def jam_track_preview + gon.jamTrackPlanCode = params[:plan_code] + + + render :layout => 'web' + end + def site_validate render :layout => 'web' end diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl index e4226b553..65d887ea8 100644 --- a/web/app/views/api_jam_tracks/show.rabl +++ b/web/app/views/api_jam_tracks/show.rabl @@ -16,6 +16,13 @@ end child(:jam_track_tracks => :tracks) { attributes :id, :part, :instrument, :track_type + + node do |track| + { + preview_mp3_url: track.preview_public_url('mp3'), + preview_ogg_url: track.preview_public_url('ogg') + } + end } child(:licensor => :licensor) { diff --git a/web/app/views/api_jam_tracks/show_with_artist_info.rabl b/web/app/views/api_jam_tracks/show_with_artist_info.rabl new file mode 100644 index 000000000..76dc3b68b --- /dev/null +++ b/web/app/views/api_jam_tracks/show_with_artist_info.rabl @@ -0,0 +1,7 @@ +object @jam_track + +attributes :band_jam_track_count + +node do |jam_track| + partial "api_jam_tracks/show", object: @jam_track +end diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index 08312237b..a0f3cde6f 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -1,105 +1,98 @@
- +
- +
<%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %>
- +

my account

<%= render "screen_navigation" %>
- -
-
- - diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 79e63f5ae..e295830e0 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -74,21 +74,18 @@
+
-
+

- - - - - -
Test RecordingX
+  
+
@@ -98,14 +95,10 @@
- - - - - -
Test RecordingX
+  
+
@@ -115,22 +108,18 @@
- - - - - -
Test RecordingX
+  
+


- CANCEL   - BACK   + CANCEL  + BACK  SAVE & FINISH
@@ -152,6 +141,12 @@ } initialized = true; + var $screen = $('#account-profile-samples'); + var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); + var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); + var $soundCloudSampleList = $screen.find('.samples.soundcloud'); + var $youTubeSampleList = $screen.find('.samples.youtube'); + setTimeout(function() { window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback); urlValidator.init(); @@ -177,10 +172,10 @@ window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud', userNameSuccessCallback, userNameFailCallback); + window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', siteSuccessCallback, siteFailCallback); soundCloudRecordingValidator.init(); - window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube', userNameSuccessCallback, userNameFailCallback); + window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', siteSuccessCallback, siteFailCallback); youTubeRecordingValidator.init(); }, 1); @@ -198,6 +193,18 @@ function siteSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); + + var recordingSources = window.soundCloudRecordingValidator.recordingSources(); + if (recordingSources && recordingSources.length > 0) { + console.log('recordingSources=%o', recordingSources); + var $sampleList = $soundCloudSampleList.find('.sample-list'); + var addedRecording = recordingSources[recordingSources.length-1]; + $sampleList.append('
'); + $sampleList.append(addedRecording.url); + $sampleList.append('
'); + } + + $inputDiv.find('input').val(''); } function siteFailCallback($inputDiv) { diff --git a/web/app/views/clients/_jam_track_preview.html.slim b/web/app/views/clients/_jam_track_preview.html.slim new file mode 100644 index 000000000..f10de55c7 --- /dev/null +++ b/web/app/views/clients/_jam_track_preview.html.slim @@ -0,0 +1,10 @@ +script type="text/template" id='template-jam-track-preview' + .jam-track-preview + .actions + a.play-button href="#" + | Play + a.stop-button.hidden href="#" + | Stop + img.instrument-icon hoveraction="instrument" data-instrument-id="" width="24" height="24" + .instrument-name + .part \ No newline at end of file diff --git a/web/app/views/clients/_jamtrack_landing.html.slim b/web/app/views/clients/_jamtrack_landing.html.slim index 853ee05fe..06e3efc2f 100644 --- a/web/app/views/clients/_jamtrack_landing.html.slim +++ b/web/app/views/clients/_jamtrack_landing.html.slim @@ -1,4 +1,4 @@ -#jamtrackLanding.screen.secondary layout='screen' layout-id='jamtrackLanding' +#jamtrackLanding.screen.secondary.no-login-required layout='screen' layout-id='jamtrackLanding' .content .content-head .content-icon=image_tag("content/icon_jamtracks.png", height:19, width:19) diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 2825ee519..49bcfa5f9 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -73,6 +73,7 @@ <%= render "listenBroadcast" %> <%= render "sync_viewer_templates" %> <%= render "download_jamtrack_templates" %> +<%= render "jam_track_preview" %> <%= render "help" %> <%= render 'dialogs/dialogs' %>
diff --git a/web/app/views/landings/individual_jamtrack.html.slim b/web/app/views/landings/individual_jamtrack.html.slim new file mode 100644 index 000000000..ad52b67f3 --- /dev/null +++ b/web/app/views/landings/individual_jamtrack.html.slim @@ -0,0 +1,48 @@ +- provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack') + +.two_by_two + .row + .column + h1.hidden.individualized + | Check Out Our  + strong.jamtrack_name + |  JamTrack + h1.hidden.generic + | We Have 100+ Amazing JamTracks, Check One Out! + p Click the play buttons below to hear the master mix and each fully isolated track. All are included in each single JamTrack. + .previews + .column + h1 See What You Can Do With JamTracks + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen + br clear="all" + .row + .column + h1 + | Get Your First JamTrack Free Now! + p Click the GET A JAMTRACK FREE button below. Browse to find the one you want. Click Add to cart, and we'll apply a credit during checkout to make this first one free! We're confident you'll be back for more. + .browse-jamtracks-wrapper + a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE! + .column + h1 Why Are JamTracks Different & Better? + p + | JamTracks are the best way to play with your favorite music.  + | Unlike traditional backing tracks, JamTracks are complete multitrack recordings,  + | with fully isolated tracks for each part. Used with the free JamKazam app/service, you can: + ul.jamtrack-reasons + li Solo just the individual track you want to play to hear and learn it + li Mute just the track you want to play, and play along with the rest + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube or URL + li And even go online to play JamTracks with others in real time! + br clear="all" + br clear="all" + +javascript: + + + $(document).on('JAMKAZAM_READY', function(e, data) { + var song = new JK.IndividualJamTrack(data.app); + song.initialize(); + }) diff --git a/web/app/views/landings/individual_jamtrack_band.html.slim b/web/app/views/landings/individual_jamtrack_band.html.slim new file mode 100644 index 000000000..dad0f43c3 --- /dev/null +++ b/web/app/views/landings/individual_jamtrack_band.html.slim @@ -0,0 +1,46 @@ +- provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack_band') + +.two_by_two + .row + .column + h1 + | We Have  + span.jamtrack_band_info + |   + span.jamtrack_noun JamTracks + span.check-it-out , Check One Out! + p Click the play buttons below to hear the master mix and each fully isolated track. All are included in each single JamTrack. + .previews + .column + h1 See What You Can Do With JamTracks + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen + br clear="all" + .row + .column + h1 + | Get Your First JamTrack Free Now! + p Click the GET A JAMTRACK FREE button below. Browse to find the one you want. Click Add to cart, and we'll apply a credit during checkout to make this first one free! We're confident you'll be back for more. + .browse-jamtracks-wrapper + a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE! + .column + h1 Why Are JamTracks Different & Better? + p + | JamTracks are the best way to play with your favorite music.  + | Unlike traditional backing tracks, JamTracks are complete multitrack recordings,  + | with fully isolated tracks for each part. Used with the free JamKazam app/service, you can: + ul.jamtrack-reasons + li Solo just the individual track you want to play to hear and learn it + li Mute just the track you want to play, and play along with the rest + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube or URL + li And even go online to play JamTracks with others in real time! + br clear="all" + br clear="all" + +javascript: + $(document).on('JAMKAZAM_READY', function(e, data) { + var song = new JK.IndividualJamTrackBand(data.app); + song.initialize(); + }) diff --git a/web/app/views/landings/product_jamblaster.html.slim b/web/app/views/landings/product_jamblaster.html.slim new file mode 100644 index 000000000..c31e76678 --- /dev/null +++ b/web/app/views/landings/product_jamblaster.html.slim @@ -0,0 +1,37 @@ +- provide(:page_name, 'landing_page full landing_product product_jamblaster') + +.two_by_two + .row + .column + h1.product-headline + | The JamBlaster by JamKazam + p.product-description + | The JamBlaster is a device designed from the ground up to meet the unique requirements of real-time, online, distributed music performance. This device vastly extends the range/distance over which musicians can play together across the Internet. + ul + li Radically reduces audio processing latency compared to today's industry standard computers and audio interfaces. + li Delivers plug-and-play ease of use, with no worries about hardware and software incompatibilities, driver problems, and arcane configurations. + li Combines both a computer and an audio interface into a single elegant device. + li Works with computers (even old crappy ones), tablets or smartphones. + li Works with your favorite recording software applications like Garage Band, Reaper, Pro Tools, etc. + .column + h1 See What You Can Do With The JamBlaster + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/2Zk7-04IAx4" frameborder="0" allowfullscreen + br clear="all" + .row + .column + h1 + | Want a JamBlaster? Need One? + p If you are a registered member of the JamKazam community, and if you "know" you will buy a JamBlaster for $199 as soon as they become available, then click the button below to add yourself to our wait list. When we get enough "virtual orders", we'll reach back out to all signups to take real orders. + + .cta-big-button + a.white-bordered-button href="#" SIGN UP TO BUY A JAMBLASTER + .column + h1 Want To Know More About Latency? + p + | How is it possible that someone hundreds of miles away could feel like they are 20 feet away from you? Check out this video on latency to understand more: + .linked-video-holder + a href="https://www.youtube.com/watch?v=mB3_KMse-J4" rel="external" Watch Video About Latency - It's Pretty Fascinating + br clear="all" + br clear="all" \ No newline at end of file diff --git a/web/app/views/landings/product_jamtracks.html.slim b/web/app/views/landings/product_jamtracks.html.slim new file mode 100644 index 000000000..a20b16584 --- /dev/null +++ b/web/app/views/landings/product_jamtracks.html.slim @@ -0,0 +1,42 @@ +- provide(:page_name, 'landing_page full landing_product product_jamtracks') + +.two_by_two + .row + .column + h1.product-headline + | JamTracks by JamKazam + p.product-description + | We have 100+ amazing JamTracks. Click the play buttons to hear the master mix and fully isolated track. All are included in each JamTrack. + .previews + .column + h1 See What You Can With JamTracks + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/2Zk7-04IAx4" frameborder="0" allowfullscreen + br clear="all" + .row + .column + h1 + | Get Your First JamTrack Free Now! + p Click the GET A JAMTRACK FREE button below. Browse to find the one you want, click the Add to cart, and we'll apply a credit during checkout to make the first one free! We're confident you'll be back for more. + + .cta-big-button + a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE! + .column + h1 Why are JamTracks Better than Backing Tracks? + p + | JamTracks are the best way to play with your favorite music. Unlike traditional backing tracks, JamTracks are complete multitrack recordings, with fully isolated tracks for each part. Used with the free JamKazam app/service, you can: + ul.jamtrack-reasons + li Solo just the individual track you want to play to hear and learn it + li Mute just the track you want to play, and play along with the rest + li Make audio recordings and share them via Facebook or URL + li Make video recordings and share them via YouTube or URL + li And even go online to play JamTracks with others in real time! + br clear="all" + br clear="all" + +javascript: + $(document).on('JAMKAZAM_READY', function (e, data) { + var song = new JK.IndividualJamTrack(data.app); + song.initialize(); + }) \ No newline at end of file diff --git a/web/app/views/landings/product_platform.html.slim b/web/app/views/landings/product_platform.html.slim new file mode 100644 index 000000000..16c2f351c --- /dev/null +++ b/web/app/views/landings/product_platform.html.slim @@ -0,0 +1,40 @@ +- provide(:page_name, 'landing_page full landing_product product_platform') + +.two_by_two + .row + .column + h1.product-headline + | The JamKazam Platform + p.product-description + | JamKazam is an innovative live music platform and social network, enabling musicians to play music together in real time from different locations over the internet as if they are sitting in the same room. The core platform is free to use and delivers immense value: + ul + li Play music from home with your friends and bandmates without packing and transporting gear, and without needing a rehearsal space + li Connect with new musician friends from our community of thousands of musicians to play more often, explore new styles, learn from others + li Find musicians to join your band, or find a band to join, either virtual on JamKazam, or real world to meet and play in person + li Schedule sessions or jump into ad hoc jams + li Make and share recordings or session performances via Facebook or URL + li Live broadcast sessions to family, friends, and fans + li List your band for hire to play gigs at clubs and events + li List yourself for hire to play studio sessions or lay down recorded tracks remotely + .column + h1 See What You Can Do With JamKazam + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/ylYcvTY9CVo" frameborder="0" allowfullscreen + br clear="all" + .row + .column + h1 + | Sign Up for JamKazam Now, It's Free! + p Yep, seriously. Sign up and start playing music online in real time with your friends - or make new ones from our community of thousands of musicians. It's free to play with others as much as you want. + + .cta-big-button + a.white-bordered-button href="/signup" SIGN UP NOW FOR YOUR FREE ACCOUNT + .column + h1 Does This Really Work? + p + | Feeling skeptical about whether this can actually work? That's natural. We'd encourage you to watch a video of endorsements and kudos from just a few of the musicians who use JamKazam. + .linked-video-holder + a href="https://www.youtube.com/watch?v=_7qj5RXyHCo" rel="external" Check Out Endorsements Of Real Users + br clear="all" + br clear="all" diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb index f3cd75c75..8c13c4a5d 100644 --- a/web/app/views/layouts/web.html.erb +++ b/web/app/views/layouts/web.html.erb @@ -85,6 +85,7 @@ <%= render "clients/help" %> <%= render "clients/listenBroadcast" %> <%= render "clients/flash" %> + <%= render "clients/jam_track_preview" %> <%= render 'dialogs/dialogs' %> diff --git a/web/app/views/spikes/jam_track_preview.html.slim b/web/app/views/spikes/jam_track_preview.html.slim new file mode 100644 index 000000000..3ea46c7d3 --- /dev/null +++ b/web/app/views/spikes/jam_track_preview.html.slim @@ -0,0 +1,40 @@ + +- provide(:title, 'Jam Track Preview') + +.content-wrapper + h2 Jam Track Preview + + #players + + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + + var rest = JK.Rest(); + + if(gon.jamTrackPlanCode) { + rest.getJamTrack({plan_code: gon.jamTrackPlanCode}) + .done(function(jamTrack) { + var $players = $('#players') + + _.each(jamTrack.tracks, function(track) { + + var $element = $('
') + + $players.append($element); + + new JK.JamTrackPreview(data.app, $element, jamTrack, track, {master_shows_duration: true}) + }) + }) + .fail(function() { + alert("couldn't fetch jam track") + }) + + } + else { + alert("You need to add ?jam_track_plan_code=jamtracks-acdc-backinblack for this to work (or any jamtrack 'plancode')") + } + + + }) diff --git a/web/config/application.rb b/web/config/application.rb index aac520b7d..6ded2fb75 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -318,5 +318,7 @@ if defined?(Bundler) config.metronome_available = true config.backing_tracks_available = true config.one_free_jamtrack_per_user = true + + config.nominated_jam_track = 'jamtrack-pearljam-alive' end end diff --git a/web/config/routes.rb b/web/config/routes.rb index b10262fc3..393427b53 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -30,6 +30,13 @@ SampleApp::Application.routes.draw do match '/landing/kick2', to: 'landings#watch_overview_kick2', via: :get, as: 'landing_kick2' match '/landing/kick3', to: 'landings#watch_overview_kick3', via: :get, as: 'landing_kick3' match '/landing/kick4', to: 'landings#watch_overview_kick4', via: :get, as: 'landing_kick4' + match '/landing/jamtracks/:plan_code', to: 'landings#individual_jamtrack', via: :get, as: 'individual_jamtrack' + match '/landing/jamtracks/band/:plan_code', to: 'landings#individual_jamtrack_band', via: :get, as: 'individual_jamtrack_band' + + # product pages + match '/products/jamblaster', to: 'landings#product_jamblaster', via: :get, as: 'product_jamblaster' + match '/products/platform', to: 'landings#product_platform', via: :get, as: 'product_platform' + match '/products/jamtracks', to: 'landings#product_jamtracks', via: :get, as: 'product_jamtracks' # oauth match '/auth/:provider/callback', :to => 'sessions#oauth_callback' @@ -94,7 +101,8 @@ SampleApp::Application.routes.draw do match '/launch_app', to: 'spikes#launch_app' match '/websocket', to: 'spikes#websocket' match '/test_subscription', to: 'spikes#subscription' - match '/widgets/download_jam_track', to: 'spikes#download_jam_track' + match '/widgets/download_jam_track', to: 'spikes #download_jam_track' + match '/widgets/jam_track_preview', to: 'spikes#jam_track_preview' match '/site_validate', to: 'spikes#site_validate' match '/recording_source', to: 'spikes#recording_source' match '/musician_search_filter', to: 'spikes#musician_search_filter' @@ -205,6 +213,8 @@ SampleApp::Application.routes.draw do match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list' # Jamtracks + match '/jamtracks/:plan_code' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show' + match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info' match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list' match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased' match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download' diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index 58b4dcf7b..dfdb0c9d8 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -24,4 +24,60 @@ namespace :jam_tracks do JamTrackImporter.synchronize_all(skip_audio_upload:true) end + + + task sync_master_preview_all: :environment do |task, args| + importer = JamTrackImporter.synchronize_jamtrack_master_previews + end + + # syncs just one master track for a give JamTrack + task sync_master_preview: :environment do |task, args| + plan_code = ENV['PLAN_CODE'] + if !plan_code + puts "PLAN_CODE must be set to something like jamtrack-acdc-backinblack" + exit(1) + end + + jam_track = JamTrack.find_by_plan_code!(plan_code) + + importer = JamTrackImporter.synchronize_jamtrack_master_preview(jam_track) + + if importer.reason.nil? || importer.reason == "success" || importer.reason == "jam_track_exists" + puts("#{importer.name} #{importer.reason}") + else + puts("#{importer.name} failed to import.") + puts("#{importer.name} reason=#{importer.reason}") + puts("#{importer.name} detail=#{importer.detail}") + end + end + + + task sync_duration_all: :environment do |task, args| + importer = JamTrackImporter.synchronize_durations + end + + # syncs just one master track for a give JamTrack + task sync_duration: :environment do |task, args| + plan_code = ENV['PLAN_CODE'] + if !plan_code + puts "PLAN_CODE must be set to something like jamtrack-acdc-backinblack" + exit(1) + end + + jam_track = JamTrack.find_by_plan_code!(plan_code) + + importer = JamTrackImporter.synchronize_duration(jam_track) + + if importer.reason.nil? || importer.reason == "success" || importer.reason == "jam_track_exists" + puts("#{importer.name} #{importer.reason}") + else + puts("#{importer.name} failed to import.") + puts("#{importer.name} reason=#{importer.reason}") + puts("#{importer.name} detail=#{importer.detail}") + end + end + + task download_masters: :environment do |task, arg| + JamTrackImporter.download_masters + end end diff --git a/web/spec/features/individual_jamtrack_band_spec.rb b/web/spec/features/individual_jamtrack_band_spec.rb new file mode 100644 index 000000000..5db9ac971 --- /dev/null +++ b/web/spec/features/individual_jamtrack_band_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe "Individual JamTrack Band", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + before(:all) do + ShoppingCart.delete_all + JamTrackRight.delete_all + JamTrack.delete_all + JamTrackTrack.delete_all + JamTrackLicensor.delete_all + end + + let(:user) { FactoryGirl.create(:user) } + let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack } + + let(:billing_info) { + { + first_name: 'Seth', + last_name: 'Call', + address1: '10704 Buckthorn Drive', + city: 'Austin', + state: 'Texas', + country: 'US', + zip: '78759', + number: '4111111111111111', + month: '08', + year: '2017', + verification_value: '012' + } + } + + def create_account(user, billing_info) + @recurlyClient.create_account(user, billing_info) + @created_accounts << user + end + + + before(:all) do + + @recurlyClient = RecurlyClient.new + @created_accounts = [] + + @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack') + + # make sure plans are there + @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack) + end + + + after(:each) do + @created_accounts.each do |user| + if user.recurly_code + begin + @account = Recurly::Account.find(user.recurly_code) + if @account.present? + @account.destroy + end + rescue + end + end + end + end + + describe "AC/DC Back in Black" do + + it "logged out" do + visit "/landing/jamtracks/band/acdc-backinblack" + + find('h1', text: "We Have 1 #{@jamtrack_acdc_backinblack.original_artist} JamTrack, Check It Out!") + jamtrack_acdc_backinblack.jam_track_tracks.each do |track| + if track.master? + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix') + else + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description) + end + end + find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack") + + find('a.white-bordered-button').trigger(:click) + find('h1', text: 'jamtracks') + end + + it "logged in" do + fast_signin(user, "/landing/jamtracks/band/acdc-backinblack") + + find('h1', text: "We Have 1 #{@jamtrack_acdc_backinblack.original_artist} JamTrack, Check It Out!") + jamtrack_acdc_backinblack.jam_track_tracks.each do |track| + if track.master? + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix') + else + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description) + end + end + find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack") + + find('a.white-bordered-button').trigger(:click) + find('h1', text: 'jamtracks') + end + end +end diff --git a/web/spec/features/individual_jamtrack_spec.rb b/web/spec/features/individual_jamtrack_spec.rb new file mode 100644 index 000000000..7aa9db507 --- /dev/null +++ b/web/spec/features/individual_jamtrack_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +describe "Individual JamTrack", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + before(:all) do + ShoppingCart.delete_all + JamTrackRight.delete_all + JamTrack.delete_all + JamTrackTrack.delete_all + JamTrackLicensor.delete_all + end + + let(:user) { FactoryGirl.create(:user) } + let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack } + + let(:billing_info) { + { + first_name: 'Seth', + last_name: 'Call', + address1: '10704 Buckthorn Drive', + city: 'Austin', + state: 'Texas', + country: 'US', + zip: '78759', + number: '4111111111111111', + month: '08', + year: '2017', + verification_value: '012' + } + } + + def create_account(user, billing_info) + @recurlyClient.create_account(user, billing_info) + @created_accounts << user + end + + + before(:all) do + + @recurlyClient = RecurlyClient.new + @created_accounts = [] + + @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack') + + # make sure plans are there + @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack) + end + + + after(:each) do + @created_accounts.each do |user| + if user.recurly_code + begin + @account = Recurly::Account.find(user.recurly_code) + if @account.present? + @account.destroy + end + rescue + end + end + end + end + + describe "AC/DC Back in Black" do + + it "logged out" do + visit "/landing/jamtracks/acdc-backinblack" + + find('h1', text: 'Check Out Our Back in Black JamTrack') + jamtrack_acdc_backinblack.jam_track_tracks.each do |track| + if track.master? + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix') + else + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description) + end + end + find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack") + + find('a.white-bordered-button').trigger(:click) + find('h1', text: 'jamtracks') + end + + it "logged in" do + fast_signin(user, "/landing/jamtracks/acdc-backinblack") + + find('h1', text: 'Check Out Our Back in Black JamTrack') + jamtrack_acdc_backinblack.jam_track_tracks.each do |track| + if track.master? + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix') + else + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description) + end + end + find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack") + + find('a.white-bordered-button').trigger(:click) + find('h1', text: 'jamtracks') + end + + it "generic version" do + visit "/landing/jamtracks/acdc-backinblack?generic=true" + + find('h1', text: 'We Have 100+ Amazing JamTracks, Check One Out!') + jamtrack_acdc_backinblack.jam_track_tracks.each do |track| + if track.master? + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix') + else + find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]') + find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description) + end + end + find('a.white-bordered-button')['href'].should eq("/client#/jamtrack") + + find('a.white-bordered-button').trigger(:click) + find('h1', text: 'jamtracks') + end + end +end diff --git a/web/spec/features/products_spec.rb b/web/spec/features/products_spec.rb new file mode 100644 index 000000000..634d768f3 --- /dev/null +++ b/web/spec/features/products_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe "Product Pages", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + before(:all) do + ShoppingCart.delete_all + JamTrackRight.delete_all + JamTrack.delete_all + JamTrackTrack.delete_all + JamTrackLicensor.delete_all + end + + let(:user) { FactoryGirl.create(:user) } + let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack } + + let(:billing_info) { + { + first_name: 'Seth', + last_name: 'Call', + address1: '10704 Buckthorn Drive', + city: 'Austin', + state: 'Texas', + country: 'US', + zip: '78759', + number: '4111111111111111', + month: '08', + year: '2017', + verification_value: '012' + } + } + + def create_account(user, billing_info) + @recurlyClient.create_account(user, billing_info) + @created_accounts << user + end + + + before(:all) do + + @recurlyClient = RecurlyClient.new + @created_accounts = [] + + @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack') + + # make sure plans are there + @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack) + end + + + after(:each) do + @created_accounts.each do |user| + if user.recurly_code + begin + @account = Recurly::Account.find(user.recurly_code) + if @account.present? + @account.destroy + end + rescue + end + end + end + end + + describe "JamBlaster" do + it "logged out" do + visit "/products/jamblaster" + + find('h1', text: 'The JamBlaster by JamKazam') + find('a.white-bordered-button')['href'].should eq("#") # nowhere to go yet + end + + it "logged in" do + fast_signin(user, "/products/jamblaster") + + find('h1', text: 'The JamBlaster by JamKazam') + find('a.white-bordered-button')['href'].should eq("#") # nowhere to go yet + end + end + + describe "Platform" do + it "logged out" do + visit "/products/platform" + + find('h1', text: 'The JamKazam Platform') + find('a.white-bordered-button').trigger(:click) + + find('h2', text: 'Create your free JamKazam account') + end + + it "logged in" do + fast_signin(user, "/products/platform") + + find('h1', text: 'The JamKazam Platform') + find('a.white-bordered-button').trigger(:click) + + # clicking /signup just redirects you to the client + find('h2', text: 'create session') + end + end + + describe "JamTracks" do + it "logged out" do + visit "/products/jamtracks" + + find('h1', text: 'JamTracks by JamKazam') + find('a.white-bordered-button').trigger(:click) + + find('h1', text: 'jamtracks') + end + + it "logged in" do + fast_signin(user, "/products/jamtracks") + + find('h1', text: 'JamTracks by JamKazam') + find('a.white-bordered-button').trigger(:click) + + find('h1', text: 'jamtracks') + end + end +end diff --git a/web/vendor/assets/javascripts/howler.core.js b/web/vendor/assets/javascripts/howler.core.js new file mode 100644 index 000000000..33b8bfbc3 --- /dev/null +++ b/web/vendor/assets/javascripts/howler.core.js @@ -0,0 +1,1651 @@ +/*! + * howler.js v2.0.0-beta + * howlerjs.com + * + * (c) 2013-2015, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + + 'use strict'; + + // Setup our audio context. + var ctx = null; + var usingWebAudio = true; + var noAudio = false; + setupAudioContext(); + + // Create a master gain node. + if (usingWebAudio) { + var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); + masterGain.gain.value = 1; + masterGain.connect(ctx.destination); + } + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + var HowlerGlobal = function() { + this.init(); + }; + HowlerGlobal.prototype = { + /** + * Initialize the global Howler object. + * @return {Howler} + */ + init: function() { + var self = this || Howler; + + // Internal properties. + self._codecs = {}; + self._howls = []; + self._muted = false; + self._volume = 1; + + // Set to false to disable the auto iOS enabler. + self.iOSAutoEnable = true; + + // No audio is available on this system if this is set to true. + self.noAudio = noAudio; + + // This will be true if the Web Audio API is available. + self.usingWebAudio = usingWebAudio; + + // Expose the AudioContext when using Web Audio. + self.ctx = ctx; + + // Check for supported codecs. + if (!noAudio) { + self._setupCodecs(); + } + + return self; + }, + + /** + * Get/set the global volume for all sounds. + * @param {Float} vol Volume from 0.0 to 1.0. + * @return {Howler/Float} Returns self or current volume. + */ + volume: function(vol) { + var self = this || Howler; + vol = parseFloat(vol); + + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + self._volume = vol; + + // When using Web Audio, we just need to adjust the master gain. + if (usingWebAudio) { + masterGain.gain.value = vol; + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000; + var duration = ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek; + + // Create a timer to fire at the end of playback or the start of a new loop. + var ended = function() { + // Should this sound loop? + var loop = !!(sound._loop || self._sprite[sprite][2]); + + // Fire the ended event. + self._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!self._webAudio && loop) { + self.stop(sound._id).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._playStart = ctx.currentTime; + self._endTimers[sound._id] = setTimeout(ended, ((sound._stop - sound._start) * 1000) / Math.abs(self._rate)); + } + + // Mark the node as paused. + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + self._clearTimer(sound._id); + + // Clean up the buffer source. + sound._node.bufferSource = null; + } + + // When using a sprite, end the track. + if (!self._webAudio && !loop) { + self.stop(sound._id); + } + }; + self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate)); + + // Update the parameters of the sound + sound._paused = false; + sound._ended = false; + sound._sprite = sprite; + sound._seek = seek; + sound._start = self._sprite[sprite][0] / 1000; + sound._stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + sound._loop = !!(sound._loop || self._sprite[sprite][2]); + + // Begin the actual playback. + var node = sound._node; + if (self._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function() { + self._refreshBuffer(sound); + + // Setup the playback params. + var vol = (sound._muted || self._muted) ? 0 : sound._volume * Howler.volume(); + node.gain.setValueAtTime(vol, ctx.currentTime); + sound._playStart = ctx.currentTime; + + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + node.bufferSource.noteGrainOn(0, seek, duration); + } else { + node.bufferSource.start(0, seek, duration); + } + + // Start a new timer if none is present. + if (!self._endTimers[sound._id]) { + self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate)); + } + + if (!args[1]) { + setTimeout(function() { + self._emit('play', sound._id); + }, 0); + } + }; + + if (self._loaded) { + playWebAudio(); + } else { + // Wait for the audio to load and then begin playback. + self.once('load', playWebAudio); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + var playHtml5 = function() { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = self._rate; + setTimeout(function() { + node.play(); + if (!args[1]) { + self._emit('play', sound._id); + } + }, 0); + }; + + // Play immediately if ready, or wait for the 'canplaythrough'e vent. + if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { + playHtml5(); + } else { + var listener = function() { + // Setup the new end timer. + self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate)); + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener('canplaythrough', listener, false); + }; + node.addEventListener('canplaythrough', listener, false); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } + + return sound._id; + }, + + /** + * Pause playback and save current position. + * @param {Number} id The sound ID (empty to pause all in group). + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // Wait for the sound to begin playing before pausing it. + if (!self._loaded) { + self.once('play', function() { + self.pause(id); + }); + + return self; + } + + // If no id is passed, get all ID's to be paused. + var ids = self._getSoundIds(id); + + for (var i=0; i Returns the group's volume value. + * volume(id) -> Returns the sound id's current volume. + * volume(vol) -> Sets the volume of all sounds in this Howl group. + * volume(vol, id) -> Sets the volume of passed sound id. + * @return {Howl/Number} Returns self or current volume. + */ + volume: function() { + var self = this; + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return self._volume; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new volume. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + vol = parseFloat(args[0]); + } + } else if (args.length === 2) { + vol = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the volume or return the current volume. + var sound; + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + // Wait for the sound to begin playing before changing the volume. + if (!self._loaded) { + self.once('play', function() { + self.volume.apply(self, args); + }); + + return self; + } + + // Set the group volume. + if (typeof id === 'undefined') { + self._volume = vol; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i 0 ? Math.ceil((end - ctx.currentTime) * 1000) : 0); + }.bind(self, ids[i], sound), len); + } else { + var diff = Math.abs(from - to); + var dir = from > to ? 'out' : 'in'; + var steps = diff / 0.01; + var stepLen = len / steps; + + (function() { + var vol = from; + var interval = setInterval(function(id) { + // Update the volume amount. + vol += (dir === 'in' ? 0.01 : -0.01); + + // Make sure the volume is in the right bounds. + vol = Math.max(0, vol); + vol = Math.min(1, vol); + + // Round to within 2 decimal points. + vol = Math.round(vol * 100) / 100; + + // Change the volume. + self.volume(vol, id); + + // When the fade is complete, stop it and fire event. + if (vol === to) { + clearInterval(interval); + self._emit('faded', id); + } + }.bind(self, ids[i]), stepLen); + })(); + } + } + } + + return self; + }, + + /** + * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. + * loop() -> Returns the group's loop value. + * loop(id) -> Returns the sound id's loop value. + * loop(loop) -> Sets the loop value for all sounds in this Howl group. + * loop(loop, id) -> Sets the loop value of passed sound id. + * @return {Howl/Boolean} Returns self or current loop value. + */ + loop: function() { + var self = this; + var args = arguments; + var loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + self._loop = loop; + } else { + // Return this sound's loop value. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + + // If no id is passed, get all ID's to be looped. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current seek position. + * seek(id) -> Returns the sound id's current seek position. + * seek(seek) -> Sets the seek position of the first sound node. + * seek(seek, id) -> Sets the seek position of passed sound id. + * @return {Howl/Number} Returns self or the current seek position. + */ + seek: function() { + var self = this; + var args = arguments; + var seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new seek position. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + id = self._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // If there is no ID, bail out. + if (typeof id === 'undefined') { + return self; + } + + // Wait for the sound to load before seeking it. + if (!self._loaded) { + self.once('load', function() { + self.seek.apply(self, args); + }); + + return self; + } + + // Get the sound. + var sound = self._soundById(id); + + if (sound) { + if (seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + self._clearTimer(id); + + // Restart the playback if the sound was playing. + if (playing) { + self.play(id, true); + } + } else { + if (self._webAudio) { + return (sound._seek + self.playing(id) ? ctx.currentTime - sound._playStart : 0); + } else { + return sound._node.currentTime; + } + } + } + + return self; + }, + + /** + * Check if a specific sound is currently playing or not. + * @param {Number} id The sound id to check. If none is passed, first sound is used. + * @return {Boolean} True if playing and false if not. + */ + playing: function(id) { + var self = this; + var sound = self._soundById(id) || self._sounds[0]; + + return sound ? !sound._paused : false; + }, + + /** + * Get the duration of this sound. + * @return {Number} Audio duration. + */ + duration: function() { + return this._duration; + }, + + /** + * Unload and destroy the current Howl object. + * This will immediately stop all sound instances attached to this group. + */ + unload: function() { + var self = this; + + // Stop playing any active sounds. + var sounds = self._sounds; + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + } + + // Delete this sound from the cache. + if (cache) { + delete cache[self._src]; + } + + // Clear out `self`. + self = null; + + return null; + }, + + /** + * Listen to a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @return {Howl} + */ + on: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + + if (typeof fn === 'function') { + events.push({id: id, fn: fn}); + } + + return self; + }, + + /** + * Remove a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to remove. Leave empty to remove all. + * @param {Number} id (optional) Only remove events for this sound. + * @return {Howl} + */ + off: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + + if (fn) { + // Loop through event store and remove the passed function. + for (var i=0; i=0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + self._sounds.splice(i, 1); + cnt--; + } + } + }, + + /** + * Get all ID's from the sounds pool. + * @param {Number} id Only return one ID if one is passed. + * @return {Array} Array of IDs. + */ + _getSoundIds: function(id) { + var self = this; + + if (typeof id === 'undefined') { + var ids = []; + for (var i=0; i> (-2 * bc & 6)) : 0 + ) { + buffer = chars.indexOf(buffer); + } + + return output; + }; + + // Decode the base64 data URI without XHR, since some browsers don't support it. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i