We're delighted that you have decided to try the JamKazam service,
- and we hope that you will enjoy using JamKazam to play music with others.
- Following are links to some resources that can help to get you up and running quickly.
+ and we hope that you will enjoy using JamKazam to play
+ music with others.
+ Following are some resources that can help you get oriented and get the most out of JamKazam.
+
+
+
+
+
+
Getting Started
+ There are basically three kinds of setups you can use to play on JamKazam.
+
+
Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to
+ handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly,
+ creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
+
Computer with External Audio Interface - You can use a Windows or Mac computer with an external audio interface that you
+ already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer
+ to our Minimum System Requirements
+ to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our
+ Getting Started Video to learn more about your options here.
+
The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster.
+ It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians
+ who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the
+ system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only
+ through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the
+ JamBlaster Video to learn more about this amazing new product.
+
+
+
+
JamKazam Features
+ JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch
+ to easily get up to speed on some of the things you can do with JamKazam:
+
Getting Help
+ If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running.
+ You can visit our
+ Support Portal
+ to find knowledge base articles and post questions that have
+ not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers,
+ you can visit our Community Forum
+ .
-Getting Started Video
-We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
-https://www.youtube.com/watch?v=DBo--aj_P1w
+ Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!
-
-Other Great Tutorial Videos
-There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
-https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles
-
-
-
-Knowledge Base Articles
-You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here:
-https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles
-
-
-
-JamKazam Support Portal
-If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here:
-https://jamkazam.desk.com/
-
-
-
-JamKazam Community Forum
-And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here:
-http://forums.jamkazam.com/
-
-
-
-Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon!
-
-
- -- Team JamKazam
+
Best Regards,
+ Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
index 90efc88ef..a9f2ed06b 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
@@ -1,27 +1,43 @@
Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
-We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly.
+We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam.
-Getting Started Video
-We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
-https://www.youtube.com/watch?v=DBo--aj_P1w
-Other Great Tutorial Videos
-There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
-https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles
+Getting Started
+---------------
-Knowledge Base Articles
-You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here:
-https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles
+There are basically three kinds of setups you can use to play on JamKazam.
-JamKazam Support Portal
-If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here:
-https://jamkazam.desk.com
+* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
-JamKazam Community Forum
-And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here:
-http://forums.jamkazam.com
+* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here.
-Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon!
+* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product.
+
+
+JamKazam Features
+-----------------
+
+JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam:
+
+* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk
+
+* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU
+
+* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA
+
+* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc
+
+* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY
+
+
+Getting Help
+------------
+
+If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/.
+
+Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!
+
+Best Regards,
+Team JamKazam
--- Team JamKazam
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
index 3b11eebd1..4f184062f 100644
--- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
@@ -39,7 +39,7 @@
-
This email was sent to you because you have an account at JamKazam. Click here to unsubscribe and update your profile settings.
+
This email was sent to you because you have an account at JamKazam. Click here to unsubscribe and update your profile settings.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
index 5c8262f63..49655e237 100644
--- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
@@ -4,8 +4,8 @@
<%= yield %>
<% end %>
-<% unless @suppress_user_has_account_footer == true %>
-This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile.
+<% unless @user.nil? || @suppress_user_has_account_footer == true %>
+This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. To unsubscribe: http://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb
index bcf06b4c5..352048dc0 100644
--- a/ruby/lib/jam_ruby/connection_manager.rb
+++ b/ruby/lib/jam_ruby/connection_manager.rb
@@ -89,7 +89,7 @@ module JamRuby
udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable
sql =< b_weight
+ end
+
+ position = 1
+ sorted_tracks.each do |track|
+ track.position = position
+ position = position + 1
+ end
+
+ sorted_tracks[sorted_tracks.length - 1].position = 1000
+
+ sorted_tracks
+ end
+
+ def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload)
+
+ wav_files = fetch_wav_files(s3_path)
+
+ tracks = []
+
+ wav_files.each do |wav_file|
+ track = JamTrackTrack.new
+ track.original_audio_s3_path = wav_file
+
+ parsed_wav = parse_wav(wav_file)
+
+ if parsed_wav[:master]
+ track.track_type = 'Master'
+ track.part = 'Master'
+ @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}")
+ else
+ if !parsed_wav[:instrument] || !parsed_wav[:part]
+ @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
+ else
+ @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ")
+ end
+
+ track.instrument_id = parsed_wav[:instrument] || 'other'
+ track.track_type = 'Track'
+ track.part = parsed_wav[:part] || 'Other'
+ end
+
+ tracks << track
+ end
+
+ tracks = sort_tracks(tracks)
+
+ jam_track.jam_track_tracks = tracks
+
+ saved = jam_track.save
+
+ if !saved
+ finish('invalid_audio', jam_track.errors.inspect)
+ return false
+ end
+
+ return synchronize_audio_files(jam_track, skip_audio_upload)
+ end
+
+ def synchronize_audio_files(jam_track, skip_audio_upload)
+
+ begin
+ Dir.mktmpdir do |tmp_dir|
+
+ jam_track.jam_track_tracks.each do |track|
+
+ basename = File.basename(track.original_audio_s3_path)
+ s3_dirname = File.dirname(track.original_audio_s3_path)
+
+ # make a 44100 version, and a 48000 version
+ ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg"
+ ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg"
+
+ ogg_44100_s3_path = track.filename(ogg_44100_filename)
+ ogg_48000_s3_path = track.filename(ogg_48000_filename)
+
+ track.skip_uploader = true
+
+ if skip_audio_upload
+ track["url_44"] = ogg_44100_s3_path
+ track["md5_44"] = 'md5'
+ track["length_44"] = 1
+
+ track["url_48"] = ogg_48000_s3_path
+ track["md5_48"] = 'md5'
+ track["length_48"] = 1
+
+ # we can't fake the preview as easily because we don't know the MD5 of the current item
+ #track["preview_md5"] = 'md5'
+ #track["preview_mp3_md5"] = 'md5'
+ #track["preview_url"] = track.preview_filename('md5', 'ogg')
+ #track["preview_length"] = 1
+ #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3')
+ #track["preview_mp3_length"] = 1
+ #track["preview_start_time"] = 0
+ else
+ wav_file = File.join(tmp_dir, basename)
+
+ # bring the original wav file down from S3 to local file system
+ JamTrackImporter::s3_manager.download(track.original_audio_s3_path, wav_file)
+
+ sample_rate = `soxi -r "#{wav_file}"`.strip
+
+ ogg_44100 = File.join(tmp_dir, ogg_44100_filename)
+ ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg")
+
+ if sample_rate == "44100"
+ `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"`
+ else
+ `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"`
+ end
+
+ if sample_rate == "48000"
+ `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"`
+ else
+ `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"`
+ end
+
+ # upload the new ogg files to s3
+ @@log.debug("uploading 44100 to #{ogg_44100_s3_path}")
+
+ jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100)
+
+ @@log.debug("uploading 48000 to #{ogg_48000_s3_path}")
+
+ jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000)
+
+ ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
+ # and finally update the JamTrackTrack with the new info
+ track["url_44"] = ogg_44100_s3_path
+ track["md5_44"] = ogg_44100_digest.hexdigest
+ track["length_44"] = File.new(ogg_44100).size
+
+ track["url_48"] = ogg_48000_s3_path
+ track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest
+ track["length_48"] = File.new(ogg_48000).size
+
+ synchronize_duration(jam_track, ogg_44100)
+
+ # convert entire master ogg file to mp3, and push both to public destination
+ preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) if track.track_type == 'Master'
+
+ if !preview_succeeded
+ return false
+ end
+
+
+ end
+
+ track.save!
+ end
+ end
+ rescue Exception => e
+ finish("sync_audio_exception", e.to_s)
+ return false
+ end
+
+ return true
+ end
+
+ def synchronize_duration(jam_track, ogg_44100)
+ duration_command = "soxi -D \"#{ogg_44100}\""
+ output = `#{duration_command}`
+
+ result_code = $?.to_i
+
+ if result_code == 0
+ duration = output.to_f.round
+ jam_track.duration = duration
+ else
+ @@log.warn("unable to determine duration for jam_track #{jam_track.name}. output #{output}")
+ end
+ true
+ end
+
+ def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest)
+
+ begin
+ mp3_44100 = File.join(tmp_dir, 'output-preview-44100.mp3')
+ convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -ab 192k \"#{mp3_44100}\""
+ @@log.debug("converting to mp3 using: " + convert_mp3_cmd)
+
+ convert_output = `#{convert_mp3_cmd}`
+
+ mp3_digest = ::Digest::MD5.file(mp3_44100)
+
+ track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
+ track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
+
+ # upload 44100 ogg and mp3 to public location as well
+ @@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
+ @@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
+
+
+ track.skip_uploader = true
+
+ original_ogg_preview_url = track["preview_url"]
+ original_mp3_preview_url = track["preview_mp3_url"]
+
+ # and finally update the JamTrackTrack with the new info
+ track["preview_url"] = track.preview_filename(ogg_md5, 'ogg')
+ track["preview_length"] = File.new(ogg_44100).size
+ # and finally update the JamTrackTrack with the new info
+ track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3')
+ track["preview_mp3_length"] = File.new(mp3_44100).size
+ track["preview_start_time"] = 0
+
+ if !track.save
+ finish("save_master_preview", track.errors.to_s)
+ return false
+ end
+
+ # if all that worked, now delete old previews, if present
+ begin
+ public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"]
+ public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
+ rescue
+ puts "UNABLE TO CLEANUP OLD PREVIEW URL"
+ end
+ rescue Exception => e
+ finish("sync_master_preview_exception", e.to_s)
+ return false
+ end
+
+
+ return true
+
+ end
+
+ def fetch_all_files(s3_path)
+ JamTrackImporter::s3_manager.list_files(s3_path)
+ end
+
+ def fetch_wav_files(s3_path)
+ files = fetch_all_files(s3_path)
+ files.select { |file| file.end_with?('.wav') }
+ end
+
+ def synchronize(jam_track, metadata, metalocation, options)
+
+ # metalocation should be audio/original artist/song name/meta.yml
+
+ metadata ||= {}
+
+ parsed_metalocation = parse_metalocation(metalocation)
+
+ return unless parsed_metalocation
+
+ original_artist = parsed_metalocation[1]
+ name = parsed_metalocation[2]
+
+ success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name)
+
+ return unless success
+
+ synchronized_audio = synchronize_audio(jam_track, metadata, "audio/#{original_artist}/#{name}", options[:skip_audio_upload])
+
+ return unless synchronized_audio
+
+ created_plan = synchronize_recurly(jam_track)
+ if created_plan
+ finish("success", nil)
+ end
+
+ end
+
+ def synchronize_recurly(jam_track)
+ begin
+ recurly = RecurlyClient.new
+ # no longer create JamTrack plans: VRFS-3028
+ # recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
+ rescue RecurlyClientError => x
+ finish('recurly_create_plan', x.errors.to_s)
+ return false
+ end
+
+ true
+ end
+
+ class << self
+
+ def s3_manager
+ @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+ end
+
+ def private_s3_manager
+ @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+ end
+
+
+ def dry_run
+ s3_manager.list_directories('audio').each do |original_artist|
+ @@log.debug("searching through artist directory '#{original_artist}'")
+
+ songs = s3_manager.list_directories(original_artist)
+ songs.each do |song|
+ @@log.debug("searching through song directory' #{song}'")
+
+ metalocation = "#{song}meta.yml"
+
+ metadata = load_metalocation(metalocation)
+
+ jam_track_importer = JamTrackImporter.new
+
+ jam_track_importer.dry_run(metadata, metalocation)
+ end
+ end
+
+ end
+
+ def synchronize_jamtrack_master_preview(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ master_track = jam_track.master_track
+
+ if master_track
+ Dir.mktmpdir do |tmp_dir|
+ ogg_44100 = File.join(tmp_dir, 'input.ogg')
+ private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100)
+ ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
+ if importer.synchronize_master_preview(master_track, tmp_dir, ogg_44100, ogg_44100_digest)
+ importer.finish("success", nil)
+ end
+ end
+ else
+ importer.finish('no_master_track', nil)
+ end
+
+ importer
+ end
+
+ def synchronize_jamtrack_master_previews
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_jamtrack_master_preview(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "jam_track_exists"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
+ def synchronize_duration(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ master_track = jam_track.master_track
+ if master_track
+ Dir.mktmpdir do |tmp_dir|
+ ogg_44100 = File.join(tmp_dir, 'input.ogg')
+ private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100)
+
+ if importer.synchronize_duration(jam_track, ogg_44100)
+ jam_track.save!
+ importer.finish("success", nil)
+ end
+ end
+ else
+ importer.finish('no_duration', nil)
+ end
+
+ importer
+ end
+
+ def synchronize_durations
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_duration(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "jam_track_exists"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
+ def download_master(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ Dir.mkdir('tmp') unless Dir.exists?('tmp')
+ Dir.mkdir('tmp/jam_track_masters') unless Dir.exists?('tmp/jam_track_masters')
+
+ master_track = jam_track.master_track
+ if master_track
+ ogg_44100 = File.join('tmp/jam_track_masters', "#{jam_track.original_artist} - #{jam_track.name}.ogg")
+ private_s3_manager.download(master_track.url_by_sample_rate(44), ogg_44100)
+ end
+ importer
+ end
+
+ def download_masters
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << download_master(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to download.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
+ def synchronize_all(options)
+ importers = []
+
+ s3_manager.list_directories('audio').each do |original_artist|
+ @@log.debug("searching through artist directory '#{original_artist}'")
+
+ songs = s3_manager.list_directories(original_artist)
+ songs.each do |song|
+ @@log.debug("searching through song directory' #{song}'")
+
+ metalocation = "#{song}meta.yml"
+
+ importer = synchronize_from_meta(metalocation, options)
+ importers << importer
+ end
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "jam_track_exists"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
+
+ def jam_track_dry_run(metalocation)
+ # see if we can find a JamTrack with this metalocation
+ jam_track = JamTrack.find_by_metalocation(metalocation)
+
+ meta = load_metalocation(metalocation)
+
+ if jam_track
+ @@log.debug("jamtrack #{jam_track.name} located by metalocation")
+ jam_track.dry_run(meta, metalocation)
+ else
+ jam_track = JamTrack.new
+ jam_track.dry_run(meta, metalocation)
+ end
+ end
+
+ def load_metalocation(metalocation)
+ begin
+ data = s3_manager.read_all(metalocation)
+ return YAML.load(data)
+ rescue AWS::S3::Errors::NoSuchKey
+ return nil
+ end
+ end
+
+ def create_from_metalocation(meta, metalocation, options = {skip_audio_upload: false})
+ jam_track = JamTrack.new
+ sync_from_metadata(jam_track, meta, metalocation, options)
+ end
+
+ def update_from_metalocation(jam_track, meta, metalocation, options)
+ sync_from_metadata(jam_track, meta, metalocation, options)
+ end
+
+ def sync_from_metadata(jam_track, meta, metalocation, options)
+ jam_track_importer = JamTrackImporter.new
+
+ JamTrack.transaction do
+ #begin
+ jam_track_importer.synchronize(jam_track, meta, metalocation, options)
+ #rescue Exception => e
+ # jam_track_importer.finish("unhandled_exception", e.to_s)
+ #end
+
+ if jam_track_importer.reason != "success"
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ jam_track_importer
+ end
+
+ def synchronize_from_meta(metalocation, options)
+ # see if we can find a JamTrack with this metalocation
+ jam_track = JamTrack.find_by_metalocation(metalocation)
+
+ meta = load_metalocation(metalocation)
+
+ jam_track_importer = nil
+ if jam_track
+ @@log.debug("jamtrack #{jam_track.name} located by metalocation")
+ jam_track_importer = update_from_metalocation(jam_track, meta, metalocation, options)
+ else
+ jam_track_importer = create_from_metalocation(meta, metalocation, options)
+ end
+
+ if jam_track_importer.reason == "success"
+ @@log.info("#{jam_track_importer.name} successfully imported")
+ else
+ @@log.error("#{jam_track_importer.name} failed to import.")
+ @@log.error("#{jam_track_importer.name} reason=#{jam_track_importer.reason}")
+ @@log.error("#{jam_track_importer.name} detail=#{jam_track_importer.detail}")
+ end
+
+ jam_track_importer
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb
index ee7318118..9322c3dbd 100644
--- a/ruby/lib/jam_ruby/jam_tracks_manager.rb
+++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb
@@ -11,27 +11,33 @@ module JamRuby
@@log = Logging.logger[JamTracksManager]
+ include JamRuby::S3ManagerMixin
+
class << self
- def save_jam_track_jkz(user, jam_track)
+
+
+ def save_jam_track_jkz(user, jam_track, sample_rate=48)
jam_track_right = jam_track.right_for_user(user)
raise ArgumentError if jam_track_right.nil?
- save_jam_track_right_jkz(jam_track_right)
+ save_jam_track_right_jkz(jam_track_right, sample_rate)
end
- def save_jam_track_right_jkz(jam_track_right)
+ def save_jam_track_right_jkz(jam_track_right, sample_rate=48)
jam_track = jam_track_right.jam_track
- #py_root = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "jamtracks"))
py_root = APP_CONFIG.jamtracks_dir
Dir.mktmpdir do |tmp_dir|
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
+
+ next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ
+
# use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata
- nm = jam_track_track.id + File.extname(jam_track_track.filename)
+ nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate))
track_filename = File.join(tmp_dir, nm)
- track_url = jam_track_track.sign_url
+ track_url = jam_track_track.sign_url(120, sample_rate)
+ @@log.info("downloading #{track_url} to #{track_filename}")
copy_url_to_file(track_url, track_filename)
- copy_url_to_file(track_url, File.join(".", nm))
- jam_file_opts << " -i '#{track_filename}+#{jam_track_track.part}'"
+ jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}"
end
#puts "LS + " + `ls -la '#{tmp_dir}'`
@@ -39,10 +45,11 @@ module JamRuby
title=jam_track.name
output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz")
py_file = File.join(py_root, "jkcreate.py")
+ version = jam_track.version
@@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})"
-
+
# From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819:
- cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}'"
+ cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output_jkz)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}"
Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
pid = wait_thr.pid
exit_status = wait_thr.value
@@ -51,11 +58,14 @@ module JamRuby
#puts "stdout: #{out}, stderr: #{err}"
raise ArgumentError, "Error calling python script: #{err}" if err.present?
raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file"))
- jam_track_right[:url]
-
+
#raise ArgumentError, "output_jkz is empty #{output_jkz}" unless File.exists?(output_jkz)
-
- jam_track_right.url.store!(File.open(output_jkz, "rb"))
+ if sample_rate==48
+ jam_track_right.url_48.store!(File.open(output_jkz, "rb"))
+ else
+ jam_track_right.url_44.store!(File.open(output_jkz, "rb"))
+ end
+
jam_track_right.signed=true
jam_track_right.downloaded_since_sign=false
jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem")
@@ -73,7 +83,7 @@ module JamRuby
http.request request do |response|
response_code = response.code.to_i
unless response_code >= 200 && response_code <= 299
- puts "Response from server was #{response_code} / #{response.message}"
+ @@log.info "Response from server was #{response_code} / #{response.message}"
raise "bad status code: #{response_code}. body: #{response.body}"
end
response.read_body do |chunk|
diff --git a/ruby/lib/jam_ruby/jmep_manager.rb b/ruby/lib/jam_ruby/jmep_manager.rb
new file mode 100644
index 000000000..a4d0215bc
--- /dev/null
+++ b/ruby/lib/jam_ruby/jmep_manager.rb
@@ -0,0 +1,55 @@
+require 'json'
+require 'tempfile'
+require 'open3'
+require 'fileutils'
+require 'open-uri'
+
+module JamRuby
+
+ # Interact with external python tools to create jmep json
+ class JmepManager
+
+ @@log = Logging.logger[JmepManager]
+
+ class << self
+
+ def execute(jmep_text)
+
+ json = nil
+
+ if jmep_text.blank?
+ return nil
+ end
+
+ py_root = APP_CONFIG.jmep_dir
+ Dir.mktmpdir do |tmp_dir|
+
+ output_json = File.join(tmp_dir, "jmep.json")
+ input_text = File.join(tmp_dir, "jmep.txt")
+
+ # put JMEP text into input file
+ File.open(input_text, 'w') { |file| file.write(jmep_text) }
+
+ py_file = File.join(py_root, "jmepgen.py")
+ @@log.info "Executing python source in #{py_file}, outputting to #{output_json})"
+
+ # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819:
+ cli = "python #{py_file} -i '#{input_text}' -o '#{output_json}'"
+ Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
+ pid = wait_thr.pid
+ exit_status = wait_thr.value
+ err = stderr.read(1000)
+ out = stdout.read(1000)
+
+ raise ArgumentError, "#{out} #{err}" if exit_status != 0
+
+ json = File.read(output_json)
+ end
+ end
+
+ json
+ end
+
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb
index 3b6a6bf77..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)
@@ -88,6 +99,24 @@ module JamRuby
end
end
+ def read_all(key)
+ s = StringIO.new
+ s3_bucket.objects[key].read do |data|
+ s.write(data)
+ end
+ s.string
+ end
+
+ def list_files(prefix)
+ tree = s3_bucket.as_tree(prefix: prefix)
+ tree.children.select(&:leaf?).collect(&:key)
+ end
+
+ def list_directories(prefix)
+ tree = s3_bucket.as_tree(prefix: prefix)
+ tree.children.select(&:branch?).collect(&:prefix)
+ end
+
def exists?(filename)
s3_bucket.objects[filename].exists?
end
diff --git a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb
index 61781aaab..c294afe6d 100644
--- a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb
+++ b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb
@@ -10,8 +10,8 @@ module JamRuby
end
- def s3_manager(options={:public => false})
- @s3_manager ||= S3Manager.new(options[:public] ? app_config.aws_bucket_public : app_config.aws_bucket, app_config.aws_access_key_id, app_config.aws_secret_access_key)
+ def s3_manager(options={:bucket => nil, :public => false})
+ @s3_manager ||= S3Manager.new(options[:bucket] ? options[:bucket] : (options[:public] ? app_config.aws_bucket_public : app_config.aws_bucket), app_config.aws_access_key_id, app_config.aws_secret_access_key)
end
end
end
\ No newline at end of file
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/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb
index c28ae8de2..2addd2266 100644
--- a/ruby/lib/jam_ruby/models/active_music_session.rb
+++ b/ruby/lib/jam_ruby/models/active_music_session.rb
@@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions'
- attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording
+ attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome, :jam_track_id
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
@@ -15,6 +15,9 @@ module JamRuby
belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions
belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id"
+ belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id"
+ belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id"
+
has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id'
has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id'
belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id
@@ -27,6 +30,10 @@ module JamRuby
validate :creator_is_musician
validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track
+ validate :validate_opening_backing_track, :if => :opening_backing_track
+
+ # not sure if this is helpful since if one opens, it always stays open
+ validate :validate_opening_metronome, :if => :opening_metronome
after_create :started_session
@@ -73,22 +80,47 @@ module JamRuby
if is_jam_track_open?
errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
+
+ if is_backing_track_open?
+ errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
+ end
+
+ if is_metronome_open?
+ errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN)
+ end
end
def validate_opening_jam_track
+ validate_other_audio(:jam_track)
+ end
+
+ def validate_opening_backing_track
+ validate_other_audio(:backing_track)
+ end
+
+ def validate_opening_metronome
+ validate_other_audio(:metronome)
+ end
+
+ def validate_other_audio(error_key)
+ # validate that there is no backing track already open in this session
+ if backing_track_path_was.present?
+ errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
+ end
+
# validate that there is no jam track already open in this session
- unless jam_track_id_was.nil?
- errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
+ if jam_track_id_was.present?
+ errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
# validate that there is no recording being made
if is_recording?
- errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
+ errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
end
# validate that there is no recording being played back to the session
if is_playing_recording?
- errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
+ errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
end
end
@@ -593,6 +625,14 @@ module JamRuby
!self.jam_track.nil?
end
+ def is_backing_track_open?
+ self.backing_track_path.present?
+ end
+
+ def is_metronome_open?
+ self.metronome_active.present?
+ end
+
# is this music session currently recording?
def is_recording?
recordings.where(:duration => nil).count > 0
@@ -742,6 +782,35 @@ module JamRuby
self.save
end
+ # @param backing_track_path is a relative path:
+ def open_backing_track(user, backing_track_path)
+ self.backing_track_path = backing_track_path
+ self.backing_track_initiator = user
+ self.opening_backing_track = true
+ self.save
+ self.opening_backing_track = false
+ end
+
+ def close_backing_track
+ self.backing_track_path = nil
+ self.backing_track_initiator = nil
+ self.save
+ end
+
+ def open_metronome(user)
+ self.metronome_active = true
+ self.metronome_initiator = user
+ self.opening_metronome = true
+ self.save
+ self.opening_metronome = false
+ end
+
+ def close_metronome
+ self.metronome_active = false
+ self.metronome_initiator = nil
+ self.save
+ end
+
def self.sync(session_history)
music_session = MusicSession.find_by_id(session_history.id)
diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb
new file mode 100644
index 000000000..0a843185a
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/anonymous_user.rb
@@ -0,0 +1,29 @@
+# this was added to support the idea of an anonymous user interacting with our site; needed by the ShoppingCart
+# over time it might make sense to beef this up and to use it conistently in anonymous interactions
+
+module JamRuby
+ class AnonymousUser
+
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def shopping_carts
+ ShoppingCart.where(anonymous_user_id: @id)
+ end
+
+ def destroy_all_shopping_carts
+ ShoppingCart.destroy_all(anonymous_user_id: @id)
+ end
+
+ def admin
+ false
+ end
+
+ def has_redeemable_jamtrack
+ true
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/backing_track.rb b/ruby/lib/jam_ruby/models/backing_track.rb
new file mode 100644
index 000000000..fc5e6517b
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/backing_track.rb
@@ -0,0 +1,19 @@
+module JamRuby
+ class BackingTrack < ActiveRecord::Base
+
+ self.table_name = "backing_tracks"
+ self.primary_key = 'id'
+
+ default_scope order('created_at ASC')
+
+ belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id'
+ validates :connection, presence: true
+ validates :client_track_id, presence: true
+ validates :filename, presence: true
+
+ def user
+ self.connection.user
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb
index 8a813f9ce..5a5378b91 100644
--- a/ruby/lib/jam_ruby/models/connection.rb
+++ b/ruby/lib/jam_ruby/models/connection.rb
@@ -18,15 +18,18 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id
has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
+ has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
+ validates :metronome_open, :inclusion => {:in => [true, false]}
validates :as_musician, :inclusion => {:in => [true, false, nil]}
validates :client_type, :inclusion => {:in => CLIENT_TYPES}
- validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
+ validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true
validate :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present
- after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
+ # this is no longer required with the new no-input profile
+ #after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
after_create :did_create
after_save :report_add_participant
@@ -60,11 +63,11 @@ module JamRuby
def state_message
case self.aasm_state.to_sym
when CONNECT_STATE
- 'Connected'
- when STALE_STATE
- 'Stale'
+ 'Connected'
+ when STALE_STATE
+ 'Stale'
else
- 'Idle'
+ 'Idle'
end
end
@@ -83,7 +86,7 @@ module JamRuby
def joining_session?
joining_session
end
-
+
def can_join_music_session
# puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed?
@@ -181,8 +184,8 @@ module JamRuby
end
def associate_tracks(tracks)
+ self.tracks.clear()
unless tracks.nil?
- self.tracks.clear()
tracks.each do |track|
t = Track.new
t.instrument = Instrument.find(track["instrument_id"])
diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb
index 7f383f5dc..f05643c86 100644
--- a/ruby/lib/jam_ruby/models/jam_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track.rb
@@ -10,73 +10,175 @@ module JamRuby
PRODUCT_TYPE = 'JamTrack'
- mount_uploader :url, JamTrackUploader
+ @@log = Logging.logger[JamTrack]
+ attr_accessor :uploading_preview
attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type,
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
- :jam_track_tap_ins_attributes, :available, as: :admin
+ :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
+ validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
validates :description, length: {maximum: 1000}
- validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/
- validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES}
+ validates :time_signature, inclusion: {in: [nil] + [''] + TIME_SIGNATURES} # the empty string is needed because of activeadmin
validates :status, inclusion: {in: [nil] + STATUS}
validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE}
validates :original_artist, length: {maximum: 200}
validates :songwriter, length: {maximum: 1000}
validates :publisher, length: {maximum: 1000}
- validates :pro, inclusion: {in: [nil] + PRO}
validates :sales_region, inclusion: {in: [nil] + SALES_REGION}
validates_format_of :price, with: /^\d+\.*\d{0,2}$/
- validates :initial_play_silence, numericality: true, :allow_nil => true
-
+ validates :version, presence: true
+ validates :pro_ascap, inclusion: {in: [true, false]}
+ validates :pro_bmi, inclusion: {in: [true, false]}
+ validates :pro_sesac, inclusion: {in: [true, false]}
+ validates :public_performance_royalty, inclusion: {in: [nil, true, false]}
validates :reproduction_royalty, inclusion: {in: [nil, true, false]}
validates :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}$/
- validates_format_of :pro_royalty_amount, with: /^\d+\.*\d{0,3}$/
-
- before_save :sanitize_active_admin
belongs_to :genre, class_name: "JamRuby::Genre"
belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id'
- has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'position ASC'
+ has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC'
has_many :jam_track_tap_ins, :class_name => "JamRuby::JamTrackTapIn", order: 'offset_time ASC'
- has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id"
+ has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" # '
+
has_many :owners, :through => :jam_track_rights, :class_name => "JamRuby::User", :source => :user
has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession"
+ has_many :recordings, :class_name => "JamRuby::Recording"
+
+ # VRFS-2916 jam_tracks.id is varchar: REMOVE
+ # has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy
+ # VRFS-2916 jam_tracks.id is varchar: ADD
+ has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
+
accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true
accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true
- class << self
- def index(options, user)
- limit = options[:limit]
- limit ||= 20
- limit = limit.to_i
+ def duplicate_positions?
+ counter = {}
+ jam_track_tracks.each do |track|
+ count = counter[track.position]
+ if count.nil?
+ count = 0
+ end
+ counter[track.position] = count + 1
+ end
+
+ duplicate = false
+ counter.each do|position, count|
+ if count > 1
+ duplicate = true
+ break
+ end
+ end
+ duplicate
+ end
+
+ def missing_previews?
+ missing_preview = false
+ self.jam_track_tracks.each do |track|
+ unless track.has_preview?
+ missing_preview = true
+ break
+ end
+ end
+ missing_preview
+ end
+
+ def onboard_warnings
+ warnings = []
+ warnings << 'POSITIONS' if duplicate_positions?
+ warnings << 'PREVIEWS'if missing_previews?
+ warnings << 'DURATION' if duration.nil?
+ warnings.join(',')
+ end
+
+ def band_jam_track_count
+ JamTrack.where(original_artist: original_artist).count
+ end
+
+ class << self
+ # @return array[artist_name(string)]
+ def all_artists
+ JamTrack.select("original_artist").
+ group("original_artist").
+ collect{|jam_track|jam_track.original_artist}
+ end
+
+ # @return array[JamTrack] for given artist_name
+ def tracks_for_artist(artist_name)
+ JamTrack.where("original_artist=?", artist_name).all
+ end
+
+ def index(options, user)
+ if options[:page]
+ page = options[:page].to_i
+ per_page = options[:per_page].to_i
+
+ if per_page == 0
+ # try and see if limit was specified
+ limit = options[:limit]
+ limit ||= 20
+ limit = limit.to_i
+ else
+ limit = per_page
+ end
+
+ start = (page -1 )* per_page
+ limit = per_page
+ else
+ limit = options[:limit]
+ limit ||= 20
+ limit = limit.to_i
+
+ start = options[:start].presence
+ start = start.to_i || 0
+
+ page = 1 + start/limit
+ per_page = limit
+ end
- start = options[:start].presence
- start = start.to_i || 0
query = JamTrack.joins(:jam_track_tracks)
- .paginate(page: 1 + start/limit, per_page: limit)
+ .paginate(page: page, per_page: per_page)
if options[:show_purchased_only]
query = query.joins(:jam_track_rights)
query = query.where("jam_track_rights.user_id = ?", user.id)
end
+
+ if options[:artist].present?
+ query = query.where("original_artist=?", options[:artist])
+ end
+
+ if options[:id].present?
+ query = query.where("jam_tracks.id=?", options[:id])
+ end
+
+ if options[:group_artist]
+ query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id")
+ query = query.group("original_artist")
+ query = query.order('jam_tracks.original_artist')
+ else
+ query = query.group("jam_tracks.id")
+ query = query.order('jam_tracks.name')
+ end
- query = query.where("jam_tracks.available = ?", true) unless user.admin
+ query = query.where("jam_tracks.status = ?", 'Production') unless user.admin
query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank?
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank?
- query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
- query = query.group("jam_tracks.id")
- query = query.order('jam_tracks.name')
+ query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
+
if query.length == 0
[query, nil]
@@ -87,69 +189,19 @@ module JamRuby
end
end
end
-
- # create storage directory that will house this jam_track, as well as
- def store_dir
- "jam_tracks/#{id}"
- end
- # create name of the file
- def filename
- "#{name}.jkz"
- end
- # creates a short-lived URL that has access to the object.
- # the idea is that this is used when a user who has the rights to this tries to download this JamTrack
- # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
- # but the url is short lived enough so that it wouldn't be easily shared
- def sign_url(expiration_time = 120)
- s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/jkz', :secure => false})
+ def master_track
+ JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first
end
-
def can_download?(user)
owners.include?(user)
end
def right_for_user(user)
jam_track_rights.where("user_id=?", user).first
- end
-
- def self.list_downloads(user, limit = 100, since = 0)
- since = 0 unless since || since == '' # guard against nil
- downloads = []
-
- user.jam_track_rights
- .limit(limit)
- .where('jam_track_rights.id > ?', since)
- .each do |jam_track_right|
- downloads << {
- :type => "jam_track",
- :id => jam_track_right.id.to_s,
- :jam_track_id => jam_track_right.jam_track_id,
- :length => jam_track_right.length,
- :md5 => jam_track_right.md5,
- :url => jam_track_right.url,
- :created_at => jam_track_right.created_at,
- :next => jam_track_right.id
- }
- end
-
- next_id = downloads[-1][:next] if downloads.length > 0
- next_id = since if next_id.nil? # echo back to the client the same value they passed in, if there are no results
-
- {
- 'downloads' => downloads,
- 'next' => next_id.to_s
- }
end
-
- private
-
- def sanitize_active_admin
- self.genre_id = nil if self.genre_id == ''
- self.licensor_id = nil if self.licensor_id == ''
- end
end
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb
index affee58a5..f5abb1353 100644
--- a/ruby/lib/jam_ruby/models/jam_track_right.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_right.rb
@@ -3,19 +3,24 @@ module JamRuby
# describes what users have rights to which tracks
class JamTrackRight < ActiveRecord::Base
include JamRuby::S3ManagerMixin
- attr_accessible :user, :jam_track, :user_id, :jam_track_id, :url, :md5, :length, :download_count
+ attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count
+ attr_accessible :user_id, :jam_track_id, as: :admin
+ attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
validates :user, presence:true
validates :jam_track, presence:true
+ validates :is_test_purchase, inclusion: {in: [true, false]}
+
validate :verify_download_count
after_save :after_save
validates_uniqueness_of :user_id, scope: :jam_track_id
# Uploads the JKZ:
- mount_uploader :url, JamTrackRightUploader
+ mount_uploader :url_48, JamTrackRightUploader
+ mount_uploader :url_44, JamTrackRightUploader
before_destroy :delete_s3_files
MAX_JAM_TRACK_DOWNLOADS = 1000
@@ -30,7 +35,7 @@ module JamRuby
end
def store_dir
- "#{jam_track.store_dir}/rights"
+ "jam_track_rights/#{created_at.strftime('%m-%d-%Y')}/#{user_id}-#{id}"
end
# create name of the file
@@ -59,12 +64,17 @@ module JamRuby
else
raise "Error sending notification #{self.errors}"
end
-
end
- def finish_sign(length, md5)
+
+ def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now
- self.length = length
- self.md5 = md5
+ if bitrate==48
+ self.length_48 = length
+ self.md5_48 = md5
+ else
+ self.length_44 = length
+ self.md5_44 = md5
+ end
self.signed = true
self.error_count = 0
self.error_reason = nil
@@ -81,36 +91,50 @@ module JamRuby
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
- def sign_url(expiration_time = 120)
- s3_manager.sign_url(self[:url], {:expires => expiration_time, :secure => false})
+ def sign_url(expiration_time = 120, bitrate=48)
+ field_name = (bitrate==48) ? "url_48" : "url_44"
+ s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => false})
end
def delete_s3_files
- remove_url!
+ remove_url_48!
+ remove_url_44!
end
- def enqueue
+
+ def enqueue(sample_rate=48)
begin
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil)
- Resque.enqueue(JamTracksBuilder, self.id)
+ Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e
+ puts "e: #{e}"
# implies redis is down. we don't update started_at by bailing out here
false
end
end
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
- def enqueue_if_needed
+ def enqueue_if_needed(sample_rate=48)
state = signing_state
-
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
else
- enqueue
+ enqueue(sample_rate)
true
end
end
+
+
+ # @return true if signed && file exists for the sample_rate specifed:
+ def ready?(sample_rate=48)
+ if sample_rate==48
+ self.signed && self.url_48.present? && self.url_48.file.exists?
+ else
+ self.signed && self.url_44.present? && self.url_44.file.exists?
+ end
+ end
+
# returns easy to digest state field
# SIGNED - the package is ready to be downloaded
# ERROR - the package was built unsuccessfully
@@ -123,8 +147,6 @@ module JamRuby
state = nil
if signed
state = 'SIGNED'
- elsif error_count > 0
- state = 'ERROR'
elsif signing_started_at
if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time
state = 'SIGNING_TIMEOUT'
@@ -137,6 +159,8 @@ module JamRuby
else
state = 'QUEUED'
end
+ elsif error_count > 0
+ state = 'ERROR'
else
state = 'QUIET' # needs to be poked to go build
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_tap_in.rb b/ruby/lib/jam_ruby/models/jam_track_tap_in.rb
index 0bddeb8c6..fa0e63eda 100644
--- a/ruby/lib/jam_ruby/models/jam_track_tap_in.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_tap_in.rb
@@ -35,7 +35,6 @@ module JamRuby
else
raise "format of offset time must be MM:SS:MLS"
end
-
end
end
end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb
index 978ed29ab..41514b2f8 100644
--- a/ruby/lib/jam_ruby/models/jam_track_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_track.rb
@@ -3,42 +3,90 @@ 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{Master Track}
+ TRACK_TYPE = %w{Track Master}
- mount_uploader :url, JamTrackTrackUploader
+ @@log = Logging.logger[JamTrackTrack]
- attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, :url, as: :admin
+
+ # Because JamTrackImporter imports audio files now, and because also the mere presence of this causes serious issues when updating the model (because reset of url_44 to something bogus), I've removed these
+ #mount_uploader :url_48, JamTrackTrackUploader
+ #mount_uploader :url_44, JamTrackTrackUploader
+
+ attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin
+ attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin
+
+ attr_accessor :original_audio_s3_path, :skip_uploader
validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000}
- validates :part, length: {maximum: 20}
+ validates :part, length: {maximum: 25}
validates :track_type, inclusion: {in: TRACK_TYPE }
- validates_uniqueness_of :position, scope: :jam_track_id
- validates_uniqueness_of :part, scope: :jam_track_id
+ validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true
+ validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id]
# validates :jam_track, presence: true
belongs_to :instrument, class_name: "JamRuby::Instrument"
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
+ has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy
+
# create storage directory that will house this jam_track, as well as
def store_dir
- "#{jam_track.store_dir}/tracks"
+ "jam_track_tracks"
end
# create name of the file
- def filename
- track_type == 'Master' ? 'master.ogg' : "#{part}.ogg"
+ def filename(original_name)
+ "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
end
+ # create name of the preview file.
+ # md5-'ed because we cache forever
+ def preview_filename(md5, ext='ogg')
+ original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}"
+ "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
+ end
+
+ def has_preview?
+ !self["preview_url"].nil? && !self['preview_mp3_url'].nil?
+ end
+
+ # generates a URL that points to a public version of the preview
+ def preview_public_url(media_type='ogg')
+ url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url]
+ if url
+ s3_public_manager.public_url(url,{ :secure => false})
+ else
+ nil
+ end
+ end
+
+ def manually_uploaded_filename(mounted_as)
+ if track_type == 'Master'
+ filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg")
+ else
+ filename("#{jam_track.name} Stem - #{instrument.description}-#{part}-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg")
+ end
+ end
+
+ def master?
+ track_type == 'Master'
+ end
+
+ def url_by_sample_rate(sample_rate=48)
+ field_name = (sample_rate==48) ? "url_48" : "url_44"
+ self[field_name]
+ end
# creates a short-lived URL that has access to the object.
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
- def sign_url(expiration_time = 120)
- s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
+ def sign_url(expiration_time = 120, sample_rate=48)
+ s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
-
+
def can_download?(user)
# I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download?
jam_track.owners.include?(user)
diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb
index 8e8ab2ace..d5c7bd364 100644
--- a/ruby/lib/jam_ruby/models/mix.rb
+++ b/ruby/lib/jam_ruby/models/mix.rb
@@ -136,11 +136,62 @@ module JamRuby
def manifest
one_day = 60 * 60 * 24
+ jam_track_offset = 0
+
+ if recording.timeline
+ recording_timeline_data = JSON.parse(recording.timeline)
+
+ recording_start_time = recording_timeline_data["recording_start_time"]
+ jam_track_play_start_time = recording_timeline_data["jam_track_play_start_time"]
+ jam_track_recording_start_play_offset = recording_timeline_data["jam_track_recording_start_play_offset"]
+
+ jam_track_offset = -jam_track_recording_start_play_offset
+ end
+
manifest = { "files" => [], "timeline" => [] }
mix_params = []
+
+
+ # this 'pick limiter' logic will ensure that we set a limiter on the 1st recorded_track we come across.
+ pick_limiter = false
+ if recording.is_jamtrack_recording?
+ # we only use the limiter feature if this is a JamTrack recording
+ # by setting this to true, the 1st recorded_track in the database will be the limiter
+ pick_limiter = true
+ end
+
recording.recorded_tracks.each do |recorded_track|
- manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
- mix_params << { "level" => 100, "balance" => 0 }
+ manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0, limiter:pick_limiter }
+ pick_limiter = false
+ mix_params << { "level" => 1.0, "balance" => 0 }
+ end
+
+ recording.recorded_backing_tracks.each do |recorded_backing_track|
+ manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
+ mix_params << { "level" => 1.0, "balance" => 0 }
+ end
+
+ recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
+ manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => jam_track_offset }
+ # let's look for level info from the client
+ level = 1.0 # default value - means no effect
+ if recorded_jam_track_track.timeline
+
+ timeline_data = JSON.parse(recorded_jam_track_track.timeline)
+
+ # always take the 1st entry for now
+ first = timeline_data[0]
+
+ if first["mute"]
+ # mute equates to no noise
+ level = 0.0
+ else
+ # otherwise grab the left channel...
+ level = first["vol_l"]
+ end
+ end
+
+ mix_params << { "level" => level, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb
index a29d0b2f9..e2954d9f3 100644
--- a/ruby/lib/jam_ruby/models/music_session.rb
+++ b/ruby/lib/jam_ruby/models/music_session.rb
@@ -307,7 +307,7 @@ module JamRuby
filter_approved = only_approved ? 'AND rrrs.chosen = true' : ''
MusicSession.where(%Q{music_sessions.canceled = FALSE AND
- music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND
+ (music_sessions.create_type is NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}') AND
(music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND
music_sessions.id in (
select distinct(rs.music_session_id)
diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb
index 430c897a1..bca0ecccc 100644
--- a/ruby/lib/jam_ruby/models/music_session_user_history.rb
+++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb
@@ -43,7 +43,7 @@ module JamRuby
session_user_history.music_session_id = music_session_id
session_user_history.user_id = user_id
session_user_history.client_id = client_id
- session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|")
+ session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") if tracks
session_user_history.save
end
diff --git a/ruby/lib/jam_ruby/models/playable_play.rb b/ruby/lib/jam_ruby/models/playable_play.rb
index 4631bc4db..a04018689 100644
--- a/ruby/lib/jam_ruby/models/playable_play.rb
+++ b/ruby/lib/jam_ruby/models/playable_play.rb
@@ -2,9 +2,27 @@ module JamRuby
class PlayablePlay < ActiveRecord::Base
self.table_name = "playable_plays"
- belongs_to :playable, :polymorphic => :true, :counter_cache => :play_count
+ belongs_to :playable, :polymorphic => :true
+ # VRFS-2916 jam_tracks.id is varchar: REMOVE
+ #belongs_to :jam_track, :foreign_key => :jam_track_id
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "player_id"
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id"
+ validate do
+ # VRFS-2916 jam_tracks.id is varchar: REMOVE
+ #if !playable_id && !jam_track_id
+ # self.errors[:base] << 'No playable instance detected'
+ #end
+
+ # VRFS-2916 jam_tracks.id is varchar: ADD
+ if !playable_id
+ self.errors[:base] << 'No playable instance detected'
+ end
+
+ if !user
+ self.errors[:base] << 'No user detected'
+ end
+ end
+
end
end
diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track.rb b/ruby/lib/jam_ruby/models/recorded_backing_track.rb
new file mode 100644
index 000000000..0826d160c
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recorded_backing_track.rb
@@ -0,0 +1,196 @@
+module JamRuby
+ # BackingTrack analog to JamRuby::RecordedTrack
+ class RecordedBackingTrack < ActiveRecord::Base
+
+ include JamRuby::S3ManagerMixin
+
+ attr_accessor :marking_complete
+ attr_writer :current_user
+
+ belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks
+ validates :filename, :presence => true
+
+ validates :client_id, :presence => true # not a connection relation on purpose
+ validates :backing_track_id, :presence => true # not a track relation on purpose
+ validates :client_track_id, :presence => true
+ validates :md5, :presence => true, :if => :upload_starting?
+ validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely...
+ validates :user, presence: true
+ validates :download_count, presence: true
+
+ before_destroy :delete_s3_files
+ validate :validate_fully_uploaded
+ validate :validate_part_complete
+ validate :validate_too_many_upload_failures
+ validate :verify_download_count
+
+ def self.create_from_backing_track(backing_track, recording)
+ recorded_backing_track = self.new
+ recorded_backing_track.recording = recording
+ recorded_backing_track.client_id = backing_track.connection.client_id
+ recorded_backing_track.backing_track_id = backing_track.id
+ recorded_backing_track.client_track_id = "R" + backing_track.client_track_id # Matches behavior in RecordingManager.cpp#getWavComment
+ recorded_backing_track.user = backing_track.connection.user
+ recorded_backing_track.filename = backing_track.filename
+ recorded_backing_track.next_part_to_upload = 0
+ recorded_backing_track.file_offset = 0
+ recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id)
+ recorded_backing_track.save
+ recorded_backing_track
+ end
+
+ def sign_url(expiration_time = 120)
+ s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
+ end
+
+ def can_download?(some_user)
+ claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user }
+
+ if claimed_recording
+ !claimed_recording.discarded
+ else
+ false
+ end
+ end
+
+ def too_many_upload_failures?
+ upload_failures >= APP_CONFIG.max_track_upload_failures
+ end
+
+ def too_many_downloads?
+ (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
+ end
+
+ def upload_starting?
+ next_part_to_upload_was == 0 && next_part_to_upload == 1
+ end
+
+ def validate_too_many_upload_failures
+ if upload_failures >= APP_CONFIG.max_track_upload_failures
+ errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED)
+ end
+ end
+
+ def validate_fully_uploaded
+ if marking_complete && fully_uploaded && fully_uploaded_was
+ errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED)
+ end
+ end
+
+ def validate_part_complete
+
+ # if we see a transition from is_part_uploading from true to false, we validate
+ if is_part_uploading_was && !is_part_uploading
+ if next_part_to_upload_was + 1 != next_part_to_upload
+ errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED)
+ end
+
+ if file_offset > length
+ errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH)
+ end
+ elsif next_part_to_upload_was + 1 == next_part_to_upload
+ # this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start
+ if next_part_to_upload_was != 0
+ # we see that the part number was ticked--but was is_part_upload set to true before this transition?
+ if !is_part_uploading_was && !is_part_uploading
+ errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED)
+ end
+ end
+ end
+ end
+
+ def verify_download_count
+ if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
+ errors.add(:download_count, "must be less than or equal to 100")
+ end
+ end
+
+
+ def upload_start(length, md5)
+ #self.upload_id set by the observer
+ self.next_part_to_upload = 1
+ self.length = length
+ self.md5 = md5
+ save
+ end
+
+ # if for some reason the server thinks the client can't carry on with the upload,
+ # this resets everything to the initial state
+ def reset_upload
+ self.upload_failures = self.upload_failures + 1
+ self.part_failures = 0
+ self.file_offset = 0
+ self.next_part_to_upload = 0
+ self.upload_id = nil
+ self.md5 = nil
+ self.length = 0
+ self.fully_uploaded = false
+ self.is_part_uploading = false
+ save :validate => false # skip validation because we need this to always work
+ end
+
+ def upload_next_part(length, md5)
+ self.marking_complete = true
+ if next_part_to_upload == 0
+ upload_start(length, md5)
+ end
+ self.is_part_uploading = true
+ save
+ end
+
+ def upload_sign(content_md5)
+ s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id)
+ end
+
+ def upload_part_complete(part, offset)
+ # validated by :validate_part_complete
+ self.marking_complete = true
+ self.is_part_uploading = false
+ self.next_part_to_upload = self.next_part_to_upload + 1
+ self.file_offset = offset.to_i
+ self.part_failures = 0
+ save
+ end
+
+ def upload_complete
+ # validate from happening twice by :validate_fully_uploaded
+ self.fully_uploaded = true
+ self.marking_complete = true
+ save
+ end
+
+ def increment_part_failures(part_failure_before_error)
+ self.part_failures = part_failure_before_error + 1
+ RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'")
+ end
+
+ def stored_filename
+ # construct a path from s3
+ RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id)
+ end
+
+ def update_download_count(count=1)
+ self.download_count = self.download_count + count
+ self.last_downloaded_at = Time.now
+ end
+
+ def delete_s3_files
+ s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url])
+ end
+
+ def mark_silent
+ destroy
+
+ # check if we have all the files we need, now that the recorded_backing_track is out of the way
+ recording.preconditions_for_mix?
+ end
+
+ private
+
+ def self.construct_filename(created_at, recording_id, client_track_id)
+ raise "unknown ID" unless client_track_id
+ "recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg"
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb
new file mode 100644
index 000000000..1a2b8f291
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb
@@ -0,0 +1,91 @@
+module JamRuby
+ class RecordedBackingTrackObserver < ActiveRecord::Observer
+
+ # if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here
+ observe JamRuby::RecordedBackingTrack
+
+ def before_validation(recorded_backing_tracks)
+
+ # if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded
+ if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading
+ begin
+ aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1)
+ # calling size on a part that does not exist will throw an exception... that's what we want
+ aws_part.size
+ rescue SocketError => e
+ raise # this should cause a 500 error, which is what we want. The client will retry later on 500.
+ rescue Exception => e
+ recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
+ rescue RuntimeError => e
+ recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
+ rescue
+ recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
+ end
+
+ end
+
+ # if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together
+ if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded
+
+ multipart_success = false
+ begin
+ recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id)
+ multipart_success = true
+ rescue SocketError => e
+ raise # this should cause a 500 error, which is what we want. The client will retry later.
+ rescue Exception => e
+ #recorded_track.reload
+ recorded_backing_tracks.reset_upload
+ recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD)
+ end
+
+ # unlike RecordedTracks, only the person who uploaded can download it, so no need to notify
+
+ # tell all users that a download is available, except for the user who just uploaded
+ # recorded_backing_tracks.recording.users.each do |user|
+ #Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user
+ # end
+
+ end
+ end
+
+ def after_commit(recorded_backing_track)
+
+ end
+
+ # here we tick upload failure counts, or revert the state of the model, as needed
+ def after_rollback(recorded_backing_track)
+ # if fully uploaded, don't increment failures
+ if recorded_backing_track.fully_uploaded
+ return
+ end
+
+ # increment part failures if there is a part currently being uploaded
+ if recorded_backing_track.is_part_uploading_was
+ #recorded_track.reload # we don't want anything else that the user set to get applied
+ recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was)
+ if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures
+ # save upload id before we abort this bad boy
+ upload_id = recorded_backing_track.upload_id
+ begin
+ recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id)
+ rescue => e
+ puts e.inspect
+ end
+ recorded_backing_track.reset_upload
+ if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures
+ # do anything?
+ end
+ end
+ end
+
+ end
+
+ def before_save(recorded_backing_track)
+ # if we are on the 1st part, then we need to make sure we can save the upload_id
+ if recorded_backing_track.next_part_to_upload == 1
+ recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url])
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb
new file mode 100644
index 000000000..53bad104c
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb
@@ -0,0 +1,22 @@
+module JamRuby
+ # BackingTrack analog to JamRuby::RecordedTrack
+ class RecordedJamTrackTrack < ActiveRecord::Base
+
+ belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_jam_track_tracks
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_jam_track_tracks
+ belongs_to :jam_track_track, :class_name => "JamRuby::JamTrackTrack", :inverse_of => :recorded_jam_track_tracks
+
+ validates :user, presence: true
+ validates :jam_track_track, presence:true
+
+ def self.create_from_jam_track_track(jam_track_track, recording, user)
+ recorded_jam_track_track = self.new
+ recorded_jam_track_track.recording = recording
+ recorded_jam_track_track.jam_track_track = jam_track_track
+ recorded_jam_track_track.user = user
+ recorded_jam_track_track.save
+ recorded_jam_track_track
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index 49f4219f1..b7c683747 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -3,7 +3,7 @@ module JamRuby
@@log = Logging.logger[Recording]
- attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin
+ attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, :jam_track_id, as: :admin
has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy
@@ -11,6 +11,8 @@ module JamRuby
has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy
+ has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy
+ has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy
has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
@@ -19,6 +21,9 @@ module JamRuby
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id'
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id
+ belongs_to :non_active_music_session, :class_name => "JamRuby::MusicSession", foreign_key: :music_session_id
+ belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id'
+ belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id'
accepts_nested_attributes_for :recorded_tracks, :mixes, :claimed_recordings, allow_destroy: true
@@ -45,6 +50,10 @@ module JamRuby
self.comments.size
end
+ def is_jamtrack_recording?
+ !jam_track_id.nil?
+ end
+
def high_quality_mix?
has_final_mix
end
@@ -179,8 +188,16 @@ module JamRuby
recorded_tracks.where(:user_id => user.id)
end
+ def recorded_backing_tracks_for_user(user)
+ unless self.users.exists?(user)
+ raise PermissionError, "user was not in this session"
+ end
+ recorded_backing_tracks.where(:user_id => user.id)
+ end
+
+
def has_access?(user)
- users.exists?(user)
+ users.exists?(user) || plays.where("player_id=?", user).count != 0
end
# Start recording a session.
@@ -209,7 +226,21 @@ module JamRuby
connection.video_sources.each do |video|
recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording)
end
+
+ connection.backing_tracks.each do |backing_track|
+ recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording)
+ end
end
+
+ if music_session.jam_track
+ music_session.jam_track.jam_track_tracks.each do |jam_track_track|
+ recording.recorded_jam_track_tracks << RecordedJamTrackTrack.create_from_jam_track_track(jam_track_track, recording, owner) if jam_track_track.track_type == 'Track'
+ end
+ recording.jam_track = music_session.jam_track
+ recording.jam_track_initiator = music_session.jam_track_initiator
+ end
+
+ recording.save
end
end
@@ -321,8 +352,7 @@ module JamRuby
}
)
end
-
- latest_recorded_track = downloads[-1][:next] if downloads.length > 0
+ latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0
Mix.joins(:recording).joins(:recording => :claimed_recordings)
.order('mixes.id')
@@ -345,16 +375,31 @@ module JamRuby
}
)
end
+ latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0
- latest_mix = downloads[-1][:next] if downloads.length > 0
-
- if !latest_mix.nil? && !latest_recorded_track.nil?
- next_date = [latest_mix, latest_recorded_track].max
- elsif latest_mix.nil?
- next_date = latest_recorded_track
- else
- next_date = latest_mix
+ RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings)
+ .order('recorded_backing_tracks.id')
+ .where('recorded_backing_tracks.fully_uploaded = TRUE')
+ .where('recorded_backing_tracks.id > ?', since)
+ .where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back
+ .where('all_discarded = false')
+ .where('deleted = false')
+ .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track|
+ downloads.push(
+ {
+ :type => "recorded_backing_track",
+ :id => recorded_backing_track.client_track_id,
+ :recording_id => recorded_backing_track.recording_id,
+ :length => recorded_backing_track.length,
+ :md5 => recorded_backing_track.md5,
+ :url => recorded_backing_track[:url],
+ :next => recorded_backing_track.id
+ }
+ )
end
+ latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0
+
+ next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max
if next_date.nil?
next_date = since # echo back to the client the same value they passed in, if there are no results
@@ -417,6 +462,20 @@ module JamRuby
Arel::Nodes::As.new('stream_mix', Arel.sql('item_type'))
]).reorder("")
+ # Select fields for quick mix. Note that it must include
+ # the same number of fields as the track or video in order for
+ # the union to work:
+ backing_track_arel = RecordedBackingTrack.select([
+ :id,
+ :recording_id,
+ :user_id,
+ :url,
+ :fully_uploaded,
+ :upload_failures,
+ :client_track_id,
+ Arel::Nodes::As.new('backing_track', Arel.sql('item_type'))
+ ]).reorder("")
+
# Glue them together:
union = track_arel.union(vid_arel)
@@ -439,7 +498,25 @@ module JamRuby
])
# And repeat:
- union_all = arel.union(quick_mix_arel)
+ union_quick = arel.union(quick_mix_arel)
+ utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick)
+ arel = arel.from(utable_quick)
+
+ arel = arel.except(:select)
+ arel = arel.select([
+ "recorded_items_quick.id",
+ :recording_id,
+ :user_id,
+ :url,
+ :fully_uploaded,
+ :upload_failures,
+ :client_track_id,
+ :item_type
+ ])
+
+
+ # And repeat for backing track:
+ union_all = arel.union(backing_track_arel)
utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all)
arel = arel.from(utable_all)
@@ -455,7 +532,6 @@ module JamRuby
:item_type
])
-
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
@@ -492,6 +568,13 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
+ elsif recorded_item.item_type == 'backing_track'
+ uploads << ({
+ :type => "recorded_backing_track",
+ :recording_id => recorded_item.recording_id,
+ :client_track_id => recorded_item.client_track_id,
+ :next => recorded_item.id
+ })
else
end
@@ -513,6 +596,11 @@ module JamRuby
recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
+
+ recorded_backing_tracks.each do |recorded_backing_track|
+ return false unless recorded_backing_track.fully_uploaded
+ end
+
true
end
@@ -608,6 +696,25 @@ module JamRuby
self.save(:validate => false)
end
+ def add_timeline(timeline)
+ global = timeline["global"]
+ raise JamArgumentError, "global must be specified" unless global
+
+ tracks = timeline["tracks"]
+ raise JamArgumentError, "tracks must be specified" unless tracks
+
+ Recording.where(id: self.id).update_all(timeline: global.to_json)
+
+ jam_tracks = tracks.select {|track| track["type"] == "jam_track"}
+ jam_tracks.each do |client_jam_track|
+ RecordedJamTrackTrack.where(recording_id: id, jam_track_track_id: client_jam_track["id"]).update_all(timeline: client_jam_track["timeline"].to_json)
+ end
+ end
+
+ def self.popular_recordings(limit = 100)
+ Recording.select('recordings.id').joins('inner join claimed_recordings ON claimed_recordings.recording_id = recordings.id AND claimed_recordings.is_public = TRUE').where(all_discarded: false).where(is_done: true).where(deleted: false).order('play_count DESC').limit(limit).group('recordings.id')
+ end
+
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
new file mode 100644
index 000000000..d94dca2e0
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
@@ -0,0 +1,125 @@
+module JamRuby
+ class RecurlyTransactionWebHook < ActiveRecord::Base
+
+ belongs_to :user, class_name: 'JamRuby::User'
+ belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions
+ belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions
+
+ validates :recurly_transaction_id, presence: true
+ validates :action, presence: true
+ validates :status, presence: true
+ validates :amount_in_cents, numericality: {only_integer: true}
+ validates :user, presence: true
+
+
+ SUCCESSFUL_PAYMENT = 'payment'
+ FAILED_PAYMENT = 'failed_payment'
+ REFUND = 'refund'
+ VOID = 'void'
+
+ def is_credit_type?
+ transaction_type == REFUND || transaction_type == VOID
+ end
+
+ def is_voided?
+ transaction_type == VOID
+ end
+
+ def is_refund?
+ transaction_type == REFUND
+ end
+
+ def self.is_transaction_web_hook?(document)
+
+ return false if document.root.nil?
+ case document.root.name
+ when 'successful_payment_notification'
+ true
+ when 'successful_refund_notification'
+ true
+ when 'failed_payment_notification'
+ true
+ when 'void_payment_notification'
+ true
+ else
+ false
+ end
+ end
+
+ # see spec for examples of XML
+ def self.create_from_xml(document)
+
+ transaction = RecurlyTransactionWebHook.new
+
+ case document.root.name
+ when 'successful_payment_notification'
+ transaction.transaction_type = SUCCESSFUL_PAYMENT
+ when 'successful_refund_notification'
+ transaction.transaction_type = REFUND
+ when 'failed_payment_notification'
+ transaction.transaction_type = FAILED_PAYMENT
+ when 'void_payment_notification'
+ transaction.transaction_type = VOID
+ else
+ raise 'unknown document type ' + document.root.name
+ end
+
+ transaction.recurly_transaction_id = document.at_css('transaction id').content
+ transaction.user_id = document.at_css('account account_code').content
+ transaction.subscription_id = document.at_css('subscription_id').content
+ transaction.invoice_id = document.at_css('invoice_id').content
+ transaction.invoice_number_prefix = document.at_css('invoice_number_prefix').content
+ transaction.invoice_number = document.at_css('invoice_number').content
+ transaction.action = document.at_css('action').content
+ transaction.status = document.at_css('status').content
+ transaction.transaction_at = Time.parse(document.at_css('date').content)
+ transaction.amount_in_cents = document.at_css('amount_in_cents').content
+ transaction.reference = document.at_css('reference').content
+ transaction.message = document.at_css('message').content
+
+ transaction.save!
+
+ # now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
+
+ if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
+ sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
+
+ if sale && sale.is_jam_track_sale?
+ if sale.sale_line_items.length == 1
+ if sale.recurly_total_in_cents == transaction.amount_in_cents
+ jam_track = sale.sale_line_items[0].product
+ jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
+ if jam_track_right
+ jam_track_right.destroy
+ AdminMailer.alerts({
+ subject:"NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
+ body: "A void event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
+ }).deliver
+ else
+ AdminMailer.alerts({
+ subject:"NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
+ body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
+ }).deliver
+ end
+
+ else
+ AdminMailer.alerts({
+ subject:"ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
+ body: "We received a refund notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
+ }).deliver
+ end
+
+
+ else
+ AdminMailer.alerts({
+ subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
+ body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
+ }).deliver
+ end
+ end
+
+ end
+ transaction
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb
new file mode 100644
index 000000000..732bd45bb
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/sale.rb
@@ -0,0 +1,343 @@
+module JamRuby
+
+ # a sale is created every time someone tries to buy something
+ class Sale < ActiveRecord::Base
+
+ JAMTRACK_SALE = 'jamtrack'
+
+ belongs_to :user, class_name: 'JamRuby::User'
+ has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
+
+ has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id'
+
+ validates :order_total, numericality: {only_integer: false}
+ validates :user, presence: true
+
+ @@log = Logging.logger[Sale]
+
+ def self.index(user, params = {})
+
+ limit = params[:per_page]
+ limit ||= 20
+ limit = limit.to_i
+
+ query = Sale.limit(limit)
+ .includes([:recurly_transactions, :sale_line_items])
+ .where('sales.user_id' => user.id)
+ .order('sales.created_at DESC')
+
+ current_page = params[:page].nil? ? 1 : params[:page].to_i
+ next_page = current_page + 1
+
+ # will_paginate gem
+ query = query.paginate(:page => current_page, :per_page => limit)
+
+ if query.length == 0 # no more results
+ { query: query, next_page: nil}
+ elsif query.length < limit # no more results
+ { query: query, next_page: nil}
+ else
+ { query: query, next_page: next_page }
+ end
+
+ end
+
+ def state
+ original_total = self.recurly_total_in_cents
+
+ is_voided = false
+ refund_total = 0
+
+ recurly_transactions.each do |transaction|
+ if transaction.is_voided?
+ is_voided = true
+ else
+
+ end
+
+ if transaction.is_refund?
+ refund_total = refund_total + transaction.amount_in_cents
+ end
+ end
+
+ # if refund_total is > 0, then you have a refund.
+ # if voided is true, then in theory the whole thing has been refunded
+ {
+ voided: is_voided,
+ original_total: original_total,
+ refund_total: refund_total
+ }
+ end
+
+ def self.preview_invoice(current_user, shopping_carts)
+
+ line_items = {jam_tracks: []}
+ shopping_carts_jam_tracks = []
+ shopping_carts_subscriptions = []
+ shopping_carts.each do |shopping_cart|
+
+ if shopping_cart.is_jam_track?
+ shopping_carts_jam_tracks << shopping_cart
+ else
+ # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
+ shopping_carts_subscriptions << shopping_cart
+ end
+ end
+
+ jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
+ line_items[:jam_tracks] = jam_track_items if jam_track_items
+
+ # TODO: process shopping_carts_subscriptions
+
+ line_items
+ end
+
+ # place_order will create one or more sales based on the contents of shopping_carts for the current user
+ # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
+ # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
+ # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
+ def self.place_order(current_user, shopping_carts)
+
+ sales = []
+ shopping_carts_jam_tracks = []
+ shopping_carts_subscriptions = []
+ shopping_carts.each do |shopping_cart|
+
+ if shopping_cart.is_jam_track?
+ shopping_carts_jam_tracks << shopping_cart
+ else
+ # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
+ shopping_carts_subscriptions << shopping_cart
+ end
+ end
+
+ jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
+ sales << jam_track_sale if jam_track_sale
+
+ # TODO: process shopping_carts_subscriptions
+
+ sales
+ end
+
+ def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
+ ### XXX TODO;
+
+ # we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
+
+ # if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
+ # just a pain to implement
+ end
+
+ # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
+ # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
+ def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
+
+ client = RecurlyClient.new
+
+ sale = nil
+ Sale.transaction do
+ sale = create_jam_track_sale(current_user)
+
+ if sale.valid?
+ account = client.get_account(current_user)
+ if account.present?
+
+ purge_pending_adjustments(account)
+
+ created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
+
+ # now invoice the sale ... almost done
+
+ begin
+ invoice = account.invoice!
+ sale.recurly_invoice_id = invoice.uuid
+ sale.recurly_invoice_number = invoice.invoice_number
+
+ # now slap in all the real tax/purchase totals
+ sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
+ sale.recurly_tax_in_cents = invoice.tax_in_cents
+ sale.recurly_total_in_cents = invoice.total_in_cents
+ sale.recurly_currency = invoice.currency
+
+ # and resolve against sale_line_items
+ sale.sale_line_items.each do |sale_line_item|
+ found_line_item = false
+ invoice.line_items.each do |line_item|
+ if line_item.uuid == sale_line_item.recurly_adjustment_uuid
+ sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
+ sale_line_item.recurly_total_in_cents =line_item.total_in_cents
+ sale_line_item.recurly_currency = line_item.currency
+ sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
+ found_line_item = true
+ break
+ end
+
+ end
+
+ if !found_line_item
+ @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
+ puts "CANT FIND LINE ITEM"
+ end
+ end
+
+ unless sale.save
+ raise RecurlyClientError, "Invalid sale (at end)."
+ end
+ rescue Recurly::Resource::Invalid => e
+ # this exception is thrown by invoice! if the invoice is invalid
+ sale.rollback_adjustments(current_user, created_adjustments)
+ sale = nil
+ raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
+ end
+ else
+ raise RecurlyClientError, "Could not find account to place order."
+ end
+ else
+ raise RecurlyClientError, "Invalid sale."
+ end
+ end
+ sale
+ end
+
+ def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
+
+ created_adjustments = []
+
+ begin
+ shopping_carts_jam_tracks.each do |shopping_cart|
+ process_jam_track(current_user, shopping_cart, account, created_adjustments)
+ end
+ rescue Recurly::Error, NoMethodError => x
+ # rollback any adjustments created if error
+ rollback_adjustments(user, created_adjustments)
+ raise RecurlyClientError, x.to_s
+ rescue Exception => e
+ # rollback any adjustments created if error
+ rollback_adjustments(user, created_adjustments)
+ raise e
+ end
+
+ created_adjustments
+ end
+
+
+ def process_jam_track(current_user, shopping_cart, account, created_adjustments)
+ recurly_adjustment_uuid = nil
+ recurly_adjustment_credit_uuid = nil
+
+ # we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
+ shopping_cart.reload
+
+ # get the JamTrack in this shopping cart
+ jam_track = shopping_cart.cart_product
+
+ if jam_track.right_for_user(current_user)
+ # if the user already owns the JamTrack, we should just skip this cart item, and destroy it
+ # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
+ ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
+ return
+ end
+
+ # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
+ adjustments = shopping_cart.create_adjustment_attributes(current_user)
+
+ adjustments.each do |adjustment|
+
+ # create the adjustment at Recurly (this may not look like it, but it is a REST API)
+ created_adjustment = account.adjustments.new(adjustment)
+ created_adjustment.save
+
+ # if the adjustment could not be made, bail
+ raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?
+
+ # keep track of adjustments we created for this order, in case we have to roll them back
+ created_adjustments << created_adjustment
+
+ if ShoppingCart.is_product_purchase?(adjustment)
+ # this was a normal product adjustment, so track it as such
+ recurly_adjustment_uuid = created_adjustment.uuid
+ else
+ # this was a 'credit' adjustment, so track it as such
+ recurly_adjustment_credit_uuid = created_adjustment.uuid
+ end
+ end
+
+ # create one sale line item for every jam track
+ sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
+
+ # if the sale line item is invalid, blow up the transaction
+ unless sale_line_item.valid?
+ @log.error("sale item invalid! #{sale_line_item.errors.inspect}")
+ puts("sale item invalid! #{sale_line_item.errors.inspect}")
+ Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
+ raise RecurlyClientError.new(sale_line_item.errors)
+ end
+
+ # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
+ jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
+ jam_track_right.redeemed = shopping_cart.free?
+ end
+
+ # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
+ User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if shopping_cart.free?
+
+ # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
+ if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
+ jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
+ jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
+ unless jam_track_right.save
+ raise RecurlyClientError.new(jam_track_right.errors)
+ end
+ end
+
+ # delete the shopping cart; it's been dealt with
+ shopping_cart.destroy if shopping_cart
+
+ # blow up the transaction if the JamTrackRight did not get created
+ raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
+ end
+
+
+ def rollback_adjustments(current_user, adjustments)
+ begin
+ adjustments.each { |adjustment| adjustment.destroy }
+ rescue Exception => e
+ AdminMailer.alerts({
+ subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
+ body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
+ }).deliver
+
+ end
+ end
+
+ def self.purge_pending_adjustments(account)
+ account.adjustments.pending.find_each do |adjustment|
+ # we only pre-emptively destroy pending adjustments if they appear to be created by the server
+ adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
+ end
+ end
+
+ def is_jam_track_sale?
+ sale_type == JAMTRACK_SALE
+ end
+
+ def self.create_jam_track_sale(user)
+ sale = Sale.new
+ sale.user = user
+ sale.sale_type = JAMTRACK_SALE
+ sale.order_total = 0
+ sale.save
+ sale
+ end
+
+ # this checks just jamtrack sales appropriately
+ def self.check_integrity_of_jam_track_sales
+ Sale.select([:total, :voided]).find_by_sql(
+ "SELECT COUNT(sales.id) AS total,
+ COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
+ FROM sales
+ LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
+ WHERE sale_type = '#{JAMTRACK_SALE}'")
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb
new file mode 100644
index 000000000..2022318b0
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/sale_line_item.rb
@@ -0,0 +1,84 @@
+module JamRuby
+ class SaleLineItem < ActiveRecord::Base
+
+ JAMBLASTER = 'JamBlaster'
+ JAMCLOUD = 'JamCloud'
+ JAMTRACK = 'JamTrack'
+
+ belongs_to :sale, class_name: 'JamRuby::Sale'
+ belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
+ belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
+ has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid'
+
+ validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]}
+ validates :unit_price, numericality: {only_integer: false}
+ validates :quantity, numericality: {only_integer: true}
+ validates :free, numericality: {only_integer: true}
+ validates :sales_tax, numericality: {only_integer: false}, allow_nil: true
+ validates :shipping_handling, numericality: {only_integer: false}
+ validates :recurly_plan_code, presence:true
+ validates :sale, presence:true
+
+ def product
+ if product_type == JAMTRACK
+ JamTrack.find_by_id(product_id)
+ else
+ raise 'unsupported product type'
+ end
+ end
+
+ def product_info
+ item = product
+ { name: product.name } if item
+ end
+
+ def state
+ voided = false
+ refunded = false
+ failed = false
+ succeeded = false
+
+ recurly_transactions.each do |transaction|
+ if transaction.transaction_type == RecurlyTransactionWebHook::VOID
+ voided = true
+ elsif transaction.transaction_type == RecurlyTransactionWebHook::REFUND
+ refunded = true
+ elsif transaction.transaction_type == RecurlyTransactionWebHook::FAILED_PAYMENT
+ failed = true
+ elsif transaction.transaction_type == RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT
+ succeeded = true
+ end
+ end
+
+ {
+ void: voided,
+ refund: refunded,
+ fail: failed,
+ success: succeeded
+ }
+ end
+
+
+ def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
+ product_info = shopping_cart.product_info
+
+ sale.order_total = sale.order_total + product_info[:real_price]
+
+ sale_line_item = SaleLineItem.new
+ sale_line_item.product_type = shopping_cart.cart_type
+ sale_line_item.unit_price = product_info[:price]
+ sale_line_item.quantity = product_info[:quantity]
+ sale_line_item.free = product_info[:marked_for_redeem]
+ sale_line_item.sales_tax = nil
+ sale_line_item.shipping_handling = 0
+ sale_line_item.recurly_plan_code = product_info[:plan_code]
+ sale_line_item.product_id = shopping_cart.cart_id
+ sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid
+ sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid
+ sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
+ sale.sale_line_items << sale_line_item
+ sale_line_item.save
+ sale_line_item
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb
index da1e567f6..ed74d2490 100644
--- a/ruby/lib/jam_ruby/models/shopping_cart.rb
+++ b/ruby/lib/jam_ruby/models/shopping_cart.rb
@@ -1,34 +1,170 @@
module JamRuby
class ShoppingCart < ActiveRecord::Base
+ # just a normal purchase; used on the description field of a recurly adjustment
+ PURCHASE_NORMAL = 'purchase-normal'
+ # a free purchase; used on the description field of a recurly adjustment
+ PURCHASE_FREE = 'purchase-free'
+ # a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment
+ PURCHASE_FREE_CREDIT = 'purchase-free-credit'
+
+ PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT]
+
attr_accessible :quantity, :cart_type, :product_info
+ validates_uniqueness_of :cart_id, scope: :cart_type
+
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
validates :cart_id, presence: true
validates :cart_type, presence: true
validates :cart_class_name, presence: true
+ validates :marked_for_redeem, numericality: {only_integer: true}
default_scope order('created_at DESC')
def product_info
product = self.cart_product
- {name: product.name, price: product.price, product_id: cart_id} unless product.nil?
+ {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil?
end
+ # multiply quantity by price
+ def total_price(product)
+ quantity * product.price
+ end
+
+ # multiply (quantity - redeemable) by price
+ def real_price(product)
+ (quantity - marked_for_redeem) * product.price
+ end
+
+
def cart_product
- self.cart_class_name.classify.constantize.find_by_id self.cart_id unless self.cart_class_name.blank?
+ self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
end
- def self.create user, product, quantity = 1
+ def redeem(mark_redeem)
+ self.marked_for_redeem = mark_redeem ? 1 : 0
+ end
+
+ def free?
+ marked_for_redeem == quantity
+ end
+
+ def self.create user, product, quantity = 1, mark_redeem = false
cart = ShoppingCart.new
- cart.user = user
+ if user.is_a?(User)
+ cart.user = user
+ else
+ cart.anonymous_user_id = user.id
+ end
+
cart.cart_type = product.class::PRODUCT_TYPE
cart.cart_class_name = product.class.name
cart.cart_id = product.id
cart.quantity = quantity
+ cart.redeem(mark_redeem)
cart.save
cart
end
+
+ def is_jam_track?
+ cart_type == JamTrack::PRODUCT_TYPE
+ end
+
+
+ # returns an array of adjustments for the shopping cart
+ def create_adjustment_attributes(current_user)
+ raise "not a jam track" unless is_jam_track?
+
+ info = self.product_info
+
+ if free?
+
+ # create the credit, then the pseudo charge
+ [
+ {
+ accounting_code: PURCHASE_FREE_CREDIT,
+ currency: 'USD',
+ unit_amount_in_cents: -(info[:total_price] * 100).to_i,
+ description: "JamTrack: " + info[:name] + " (Credit)",
+ tax_exempt: true
+ },
+ {
+ accounting_code: PURCHASE_FREE,
+ currency: 'USD',
+ unit_amount_in_cents: (info[:total_price] * 100).to_i,
+ description: "JamTrack: " + info[:name],
+ tax_exempt: true
+ }
+ ]
+ else
+ [
+ {
+ accounting_code: PURCHASE_NORMAL,
+ currency: 'USD',
+ unit_amount_in_cents: (info[:total_price] * 100).to_i,
+ description: "JamTrack: " + info[:name],
+ tax_exempt: false
+ }
+ ]
+ end
+ end
+
+ def self.is_product_purchase?(adjustment)
+ (adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT)
+ end
+
+ # recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment)
+ # this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).'
+ def self.is_server_pending_adjustment?(recurly_adjustment)
+ recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT))
+ end
+
+ # if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked.
+ # if no shpping carts have been marked, then mark it redeemable
+ # should be wrapped in a TRANSACTION
+ def self.user_has_redeemable_jam_track?(any_user)
+ mark_redeem = false
+ if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack
+ mark_redeem = true # start out assuming we can redeem...
+ any_user.shopping_carts.each do |shopping_cart|
+ # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true
+ if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0
+ mark_redeem = false
+ break
+ end
+ end
+ end
+ mark_redeem
+ end
+
+ # adds a jam_track to cart, checking for promotions
+ def self.add_jam_track_to_cart(any_user, jam_track)
+ cart = nil
+ ShoppingCart.transaction do
+ mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
+ cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
+ end
+ cart
+ end
+
+ # deletes a jam track from the shopping cart, updating redeem flag as necessary
+ def self.remove_jam_track_from_cart(any_user, cart)
+ ShoppingCart.transaction do
+ cart.destroy
+ # check if we should move the redemption
+ mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
+
+ carts = any_user.shopping_carts
+
+ # if we find any carts on the account, mark one redeemable
+ if mark_redeem && carts.length > 0
+ carts[0].redeem(mark_redeem)
+ carts[0].save
+ end
+ end
+
+ end
end
end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb
new file mode 100644
index 000000000..b415d45a4
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/signup_hint.rb
@@ -0,0 +1,36 @@
+module JamRuby
+
+ # some times someone comes to signup as a new user, but there is context to preserve.
+ # the AnyUser cookie is one way that we can track the user from pre-signup to post-signup
+ # anyway, once the signup is done, we check to see if there is a SignupHint, and if so,
+ # we use it to figure out what to do with the user after they signup
+ class SignupHint < ActiveRecord::Base
+
+
+ belongs_to :user, class_name: 'JamRuby::User'
+
+ validates :redirect_location, length: {maximum: 1000}
+ validates :want_jamblaster, inclusion: {in: [nil, true, false]}
+
+ def self.refresh_by_anoymous_user(anonymous_user, options = {})
+
+ hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id)
+
+ unless hint
+ hint = SignupHint.new
+ end
+
+ hint.anonymous_user_id = anonymous_user.id
+ hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location)
+ hint.want_jamblaster = options[:want_jamblaster] if options.has_key?(:want_jamblaster)
+ hint.expires_at = 15.minutes.from_now
+ hint.save
+ hint
+ end
+
+ def self.delete_old
+ SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb
index 10b7ecb40..e430edb2d 100644
--- a/ruby/lib/jam_ruby/models/track.rb
+++ b/ruby/lib/jam_ruby/models/track.rb
@@ -55,80 +55,99 @@ module JamRuby
return query
end
+ def self.diff_track(track_class, existing_tracks, new_tracks, &blk)
+ result = []
+ if new_tracks.length == 0
+ existing_tracks.delete_all
+ else
+
+ # we will prune from this as we find matching tracks
+ to_delete = Set.new(existing_tracks)
+ to_add = Array.new(new_tracks)
+
+ existing_tracks.each do |existing_track|
+ new_tracks.each do |new_track|
+
+ if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id
+ to_delete.delete(existing_track)
+ to_add.delete(new_track)
+
+ blk.call(existing_track, new_track)
+
+ result.push(existing_track)
+
+ if existing_track.save
+ next
+ else
+ result = existing_track
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ end
+
+
+ to_add.each do |new_track|
+ existing_track = track_class.new
+
+ blk.call(existing_track, new_track)
+
+ if existing_track.save
+ result.push(existing_track)
+ else
+ result = existing_track
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ to_delete.each do |delete_me|
+ delete_me.delete
+ end
+ end
+ result
+ end
# this is a bit different from a normal track synchronization in that the client just sends up all tracks,
# ... some may already exist
- def self.sync(clientId, tracks)
- result = []
+ def self.sync(clientId, tracks, backing_tracks = [], metronome_open = false)
+ result = {}
+
+ backing_tracks = [] unless backing_tracks
Track.transaction do
connection = Connection.find_by_client_id!(clientId)
+ # synchronize metronome_open on connection
+ if connection.metronome_open != metronome_open
+ Connection.where(:id => connection.id).update_all(:metronome_open => metronome_open)
+ end
+
# each time tracks are synced we have to update the entry in music_sessions_user_history
msh = MusicSessionUserHistory.find_by_client_id!(clientId)
instruments = []
- if tracks.length == 0
- connection.tracks.delete_all
- else
- connection_tracks = connection.tracks
+ tracks.each do |track|
+ instruments << track[:instrument_id]
+ end
- # we will prune from this as we find matching tracks
- to_delete = Set.new(connection_tracks)
- to_add = Array.new(tracks)
+ result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info|
+ track_record.connection = connection
+ track_record.client_track_id = track_info[:client_track_id]
+ track_record.client_resource_id = track_info[:client_resource_id]
+ track_record.instrument_id = track_info[:instrument_id]
+ track_record.sound = track_info[:sound]
+ end
- tracks.each do |track|
- instruments << track[:instrument_id]
- end
+ result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info|
+ track_record.connection = connection
+ track_record.client_track_id = track_info[:client_track_id]
+ track_record.client_resource_id = track_info[:client_resource_id]
+ track_record.filename = track_info[:filename]
+ end
- connection_tracks.each do |connection_track|
- tracks.each do |track|
-
- if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id
- to_delete.delete(connection_track)
- to_add.delete(track)
- # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
- connection_track.instrument_id = track[:instrument_id]
- connection_track.sound = track[:sound]
- connection_track.client_track_id = track[:client_track_id]
- connection_track.client_resource_id = track[:client_resource_id]
-
- result.push(connection_track)
-
- if connection_track.save
- next
- else
- result = connection_track
- raise ActiveRecord::Rollback
- end
-
- end
- end
- end
-
- msh.instruments = instruments.join("|")
- if !msh.save
- raise ActiveRecord::Rollback
- end
-
- to_add.each do |track|
- connection_track = Track.new
- connection_track.connection = connection
- connection_track.instrument_id = track[:instrument_id]
- connection_track.sound = track[:sound]
- connection_track.client_track_id = track[:client_track_id]
- connection_track.client_resource_id = track[:client_resource_id]
- if connection_track.save
- result.push(connection_track)
- else
- result = connection_track
- raise ActiveRecord::Rollback
- end
- end
-
- to_delete.each do |delete_me|
- delete_me.delete
- end
+ msh.instruments = instruments.join("|")
+ if !msh.save
+ raise ActiveRecord::Rollback
end
end
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 30bde5246..be020d4b3 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -71,6 +71,11 @@ module JamRuby
has_many :playing_claimed_recordings, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :claimed_recording_initiator
has_many :playing_jam_tracks, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :jam_track_initiator
+ # VRFS-2916 jam_tracks.id is varchar: REMOVE
+ # has_many :jam_tracks_played, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id', :conditions => "jam_track_id IS NOT NULL"
+ # VRFS-2916 jam_tracks.id is varchar: ADD
+ has_many :jam_tracks_played, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id', :conditions => ["playable_type = 'JamRuby::JamTrack'"]
+
# self.id = user_id in likes table
has_many :likings, :class_name => "JamRuby::Like", :inverse_of => :user, :dependent => :destroy
@@ -122,7 +127,12 @@ module JamRuby
# saved tracks
has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user
+ has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user
has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user
+ has_many :recorded_jam_track_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedJamTrackTrack", :inverse_of => :user
+
+ # jam track recordings started
+ has_many :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id', :class_name => "JamRuby::Recording", :inverse_of => :jam_track_initiator
# invited users
has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser"
@@ -149,6 +159,9 @@ module JamRuby
# score history
has_many :from_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'from_user_id'
has_many :to_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'to_user_id'
+ has_many :sales, :class_name => 'JamRuby::Sale', dependent: :destroy
+ has_many :recurly_transaction_web_hooks, :class_name => 'JamRuby::RecurlyTransactionWebHook', dependent: :destroy
+
# This causes the authenticate method to be generated (among other stuff)
#has_secure_password
@@ -166,6 +179,8 @@ module JamRuby
validates_confirmation_of :password, :if => :should_validate_password?
validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false }
+ validates :reuse_card, :inclusion => {:in => [true, false]}
+ validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]}
validates :subscribe_email, :inclusion => {:in => [nil, true, false]}
validates :musician, :inclusion => {:in => [true, false]}
validates :show_whats_next, :inclusion => {:in => [nil, true, false]}
@@ -356,6 +371,14 @@ module JamRuby
MusicSession.scheduled_rsvp(self, true).length
end
+ def purchased_jamtracks_count
+ self.purchased_jam_tracks.count
+ end
+
+ def sales_count
+ self.sales.count
+ end
+
def joined_score
return nil unless has_attribute?(:score)
a = read_attribute(:score)
@@ -852,6 +875,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
@@ -923,6 +951,8 @@ module JamRuby
signup_confirm_url = options[:signup_confirm_url]
affiliate_referral_id = options[:affiliate_referral_id]
recaptcha_failed = options[:recaptcha_failed]
+ any_user = options[:any_user]
+ reuse_card = options[:reuse_card]
user = User.new
@@ -933,6 +963,7 @@ module JamRuby
user.subscribe_email = true
user.terms_of_service = terms_of_service
user.musician = musician
+ user.reuse_card unless reuse_card.nil?
# FIXME: Setting random password for social network logins. This
# is because we have validations all over the place on this.
@@ -977,6 +1008,9 @@ module JamRuby
user.photo_url = photo_url
+ # copy over the shopping cart to the new user, if a shopping cart is provided
+ user.shopping_carts = any_user.shopping_carts if any_user
+
unless fb_signup.nil?
user.update_fb_authorization(fb_signup)
@@ -1498,6 +1532,33 @@ module JamRuby
stats['audio_latency_avg'] = result['last_jam_audio_latency_avg'].to_f
stats
end
+
+ def destroy_all_shopping_carts
+ ShoppingCart.where("user_id=?", self).destroy_all
+ end
+
+ def unsubscribe_token
+ self.class.create_access_token(self)
+ end
+
+ # Verifier based on our application secret
+ def self.verifier
+ ActiveSupport::MessageVerifier.new(APP_CONFIG.secret_token)
+ end
+
+ # Get a user from a token
+ def self.read_access_token(signature)
+ uid = self.verifier.verify(signature)
+ User.find_by_id uid
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ end
+
+ # Class method for token generation
+ def self.create_access_token(user)
+ verifier.generate(user.id)
+ end
+
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
diff --git a/ruby/lib/jam_ruby/models/user_sync.rb b/ruby/lib/jam_ruby/models/user_sync.rb
index 8cdeac13b..1e915b953 100644
--- a/ruby/lib/jam_ruby/models/user_sync.rb
+++ b/ruby/lib/jam_ruby/models/user_sync.rb
@@ -4,6 +4,7 @@ module JamRuby
belongs_to :recorded_track
belongs_to :mix
belongs_to :quick_mix
+ belongs_to :recorded_backing_track
def self.show(id, user_id)
self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first
@@ -22,7 +23,7 @@ module JamRuby
raise 'no user id specified' if user_id.blank?
query = UserSync
- .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[])
+ .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[])
.joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id")
.where(user_id: user_id)
.where(%Q{
diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb
new file mode 100644
index 000000000..ec88cdaa5
--- /dev/null
+++ b/ruby/lib/jam_ruby/recurly_client.rb
@@ -0,0 +1,238 @@
+require 'recurly'
+module JamRuby
+ class RecurlyClient
+ def initialize()
+ @log = Logging.logger[self]
+ end
+
+ def create_account(current_user, billing_info)
+ options = account_hash(current_user, billing_info)
+ account = nil
+ begin
+ #puts "Recurly.api_key: #{Recurly.api_key}"
+ account = Recurly::Account.create(options)
+ raise RecurlyClientError.new(account.errors) if account.errors.any?
+ rescue Recurly::Error, NoMethodError => x
+ #puts "Error: #{x} : #{Kernel.caller}"
+ raise RecurlyClientError, x.to_s
+ else
+ if account
+ current_user.update_attribute(:recurly_code, account.account_code)
+ end
+ end
+ 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)
+ begin
+ account.destroy
+ rescue Recurly::Error, NoMethodError => x
+ raise RecurlyClientError, x.to_s
+ end
+ else
+ raise RecurlyClientError, "Could not find account to delete."
+ end
+ account
+ end
+
+ def get_account(current_user)
+ current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil
+ rescue Recurly::Error => x
+ raise RecurlyClientError, x.to_s
+ end
+
+ def update_account(current_user, billing_info=nil)
+ account = get_account(current_user)
+ if(account.present?)
+ options = account_hash(current_user, billing_info)
+ begin
+ account.update_attributes(options)
+ rescue Recurly::Error, NoMethodError => x
+ raise RecurlyClientError, x.to_s
+ end
+ end
+ account
+ end
+
+ def payment_history(current_user, options ={})
+
+ limit = params[:limit]
+ limit ||= 20
+ limit = limit.to_i
+
+ cursor = options[:cursor]
+
+ payments = []
+ account = get_account(current_user)
+ if(account.present?)
+ begin
+
+ account.transaction.paginate(per_page:limit, cursor:cursor).each do |transaction|
+ # XXX this isn't correct because we create 0 dollar transactions too (for free stuff)
+ #if transaction.amount_in_cents > 0 # Account creation adds a transaction record
+ payments << {
+ :created_at => transaction.created_at,
+ :amount_in_cents => transaction.amount_in_cents,
+ :status => transaction.status,
+ :payment_method => transaction.payment_method,
+ :reference => transaction.reference,
+ :plan_code => transaction.plan_code
+ }
+ #end
+ end
+ rescue Recurly::Error, NoMethodError => x
+ raise RecurlyClientError, x.to_s
+ end
+ end
+ payments
+ end
+
+ def update_billing_info(current_user, billing_info=nil)
+ account = get_account(current_user)
+ if (account.present?)
+ begin
+ account.billing_info = billing_info
+ account.billing_info.save
+ rescue Recurly::Error, NoMethodError => x
+ raise RecurlyClientError, x.to_s
+ end
+
+ raise RecurlyClientError.new(account.errors) if account.errors.any?
+ else
+ raise RecurlyClientError, "Could not find account to update billing info."
+ end
+ account
+ end
+
+ def refund_user_subscription(current_user, jam_track)
+ jam_track_right=JamRuby::JamTrackRight.where("user_id=? AND jam_track_id=?", current_user.id, jam_track.id).first
+ if jam_track_right
+ refund_subscription(jam_track_right)
+ else
+ raise RecurlyClientError, "The user #{current_user} does not have a subscription to #{jam_track}"
+ end
+ end
+
+ def refund_subscription(jam_track_right)
+ account = get_account(jam_track_right.user)
+ if (account.present?)
+ terminated = false
+ begin
+ jam_track = jam_track_right.jam_track
+ account.subscriptions.find_each do |subscription|
+ #puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}"
+ if(subscription.plan.plan_code == jam_track.plan_code)
+ subscription.terminate(:full)
+ raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
+ terminated = true
+ end
+ end
+
+ if terminated
+ jam_track_right.destroy()
+ else
+ raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund."
+ end
+
+ rescue Recurly::Error, NoMethodError => x
+ raise RecurlyClientError, x.to_s
+ end
+
+ else
+ raise RecurlyClientError, "Could not find account to refund order."
+ end
+ account
+ end
+
+ def find_jam_track_plan(jam_track)
+ plan = nil
+ begin
+ plan = Recurly::Plan.find(jam_track.plan_code)
+ rescue Recurly::Resource::NotFound
+ end
+ plan
+ end
+
+ def create_jam_track_plan(jam_track)
+ plan = Recurly::Plan.create(accounting_code: "",
+ bypass_hosted_confirmation: false,
+ cancel_url: nil,
+ description: jam_track.description,
+ display_donation_amounts: false,
+ display_phone_number: false,
+ display_quantity: false,
+ name: "JamTrack: #{jam_track.name}",
+ payment_page_css: nil,
+ payment_page_tos_link: nil,
+ plan_code: jam_track.plan_code,
+ plan_interval_length: 1,
+ plan_interval_unit: "months",
+ setup_fee_in_cents: Recurly::Money.new(:USD => 0), #
+ success_url: "",
+ tax_exempt: false,
+ total_billing_cycles: 1,
+ trial_interval_length: 0,
+ trial_interval_unit: "days",
+ unit_amount_in_cents: Recurly::Money.new(:USD => 1_99),
+ unit_name: "unit"
+ )
+ raise RecurlyClientError.new(plan.errors) if plan.errors.any?
+ end
+
+
+ def find_or_create_account(current_user, billing_info)
+ account = get_account(current_user)
+
+ if(account.nil?)
+ account = create_account(current_user, billing_info)
+ else
+ update_billing_info(current_user, billing_info)
+ end
+ account
+ end
+
+ private
+ def account_hash(current_user, billing_info)
+ options = {
+ account_code: current_user.id,
+ email: current_user.email,
+ first_name: current_user.first_name,
+ last_name: current_user.last_name,
+ address: {
+ city: current_user.city,
+ state: current_user.state,
+ country: current_user.country
+ }
+ }
+
+ options[:billing_info] = billing_info if billing_info
+ options
+ end
+ end # class
+
+ class RecurlyClientError < Exception
+ attr_accessor :errors
+ def initialize(data)
+ if data.respond_to?('has_key?')
+ self.errors = data
+ else
+ self.errors = {:message=>data.to_s}
+ end
+ end # initialize
+
+ def to_s
+ s=super
+ s << ", errors: #{errors.inspect}" if self.errors.any?
+ s
+ end
+
+ end # RecurlyClientError
+end # module
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/resque/audiomixer.rb b/ruby/lib/jam_ruby/resque/audiomixer.rb
index e0a3b065e..fa987b69f 100644
--- a/ruby/lib/jam_ruby/resque/audiomixer.rb
+++ b/ruby/lib/jam_ruby/resque/audiomixer.rb
@@ -260,6 +260,10 @@ module JamRuby
end
@manifest = symbolize_keys(mix.manifest)
+ @@log.debug("manifest")
+ @@log.debug("--------")
+ @@log.debug(JSON.pretty_generate(@manifest))
+
@manifest[:mix_id] = mix_id # slip in the mix_id so that the job can add it to the ogg comments
# sanity check the manifest
diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
index 19d836462..395d12084 100644
--- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
+++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
@@ -7,25 +7,24 @@ require 'digest/md5'
module JamRuby
class JamTracksBuilder
extend JamRuby::ResqueStats
-
+ attr_accessor :jam_track_right_id, :bitrate
@queue = :jam_tracks_builder
def log
@log || Logging.logger[JamTracksBuilder]
end
- attr_accessor :jam_track_right_id
-
- def self.perform(jam_track_right_id)
+ def self.perform(jam_track_right_id, bitrate=48)
jam_track_builder = JamTracksBuilder.new()
jam_track_builder.jam_track_right_id = jam_track_right_id
+ jam_track_builder.bitrate=bitrate
jam_track_builder.run
end
def run
+ self.bitrate ||= 48
begin
- log.info("jam_track_builder job starting. jam_track_right_id #{jam_track_right_id}")
-
+ log.info("jam_track_builder job starting. jam_track_right_id #{jam_track_right_id}, bitrate: #{self.bitrate}")
begin
@jam_track_right = JamTrackRight.find(jam_track_right_id)
@@ -37,16 +36,14 @@ module JamRuby
# track that it's started ( and avoid db validations )
JamTrackRight.where(:id => @jam_track_right.id).update_all(:signing_started_at => Time.now, :should_retry => false)
+ JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate)
- JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right)
-
- length = @jam_track_right.url.size()
+ # If bitrate is 48 (the default), use that URL. Otherwise, use 44kHz:
+ length = (self.bitrate==48) ? @jam_track_right.url_48.size() : @jam_track_right.url_44.size()
md5 = Digest::MD5.new
+ @jam_track_right.finish_sign(length, md5.to_s, self.bitrate)
- @jam_track_right.finish_sign(length, md5.to_s)
-
- log.info "Signed jamtrack to #{@jam_track_right[:url]}"
-
+ log.info "Signed #{self.bitrate}kHz jamtrack to #{@jam_track_right[:url]}"
rescue Exception => e
# record the error in the database
post_error(e)
diff --git a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb
index ddfb6b28f..f46946886 100644
--- a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb
+++ b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb
@@ -11,6 +11,7 @@ module JamRuby
@@log.debug("waking up")
FacebookSignup.delete_old
+ SignupHint.delete_old
@@log.debug("done")
end
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index bd52cc17f..1f4e3c662 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -18,6 +18,7 @@ FactoryGirl.define do
musician true
terms_of_service true
last_jam_audio_latency 5
+ reuse_card true
#u.association :musician_instrument, factory: :musician_instrument, user: u
@@ -232,6 +233,11 @@ FactoryGirl.define do
sequence(:client_resource_id) { |n| "resource_id#{n}"}
end
+ factory :backing_track, :class => JamRuby::BackingTrack do
+ sequence(:client_track_id) { |n| "client_track_id#{n}"}
+ filename 'foo.mp3'
+ end
+
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
@@ -250,6 +256,19 @@ FactoryGirl.define do
association :recording, factory: :recording
end
+ factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do
+ sequence(:client_id) { |n| "client_id-#{n}"}
+ sequence(:backing_track_id) { |n| "track_id-#{n}"}
+ sequence(:client_track_id) { |n| "client_track_id-#{n}"}
+ sequence(:filename) { |n| "filename-{#n}"}
+ sequence(:url) { |n| "/recordings/blah/#{n}"}
+ md5 'abc'
+ length 1
+ fully_uploaded true
+ association :user, factory: :user
+ association :recording, factory: :recording
+ end
+
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}
fully_uploaded true
@@ -258,6 +277,12 @@ FactoryGirl.define do
association :recording, factory: :recording
end
+ factory :recorded_jam_track_track, :class => JamRuby::RecordedJamTrackTrack do
+ association :user, factory: :user
+ association :recording, factory: :recording
+ association :jam_track_track, factory: :jam_track_track
+ end
+
factory :instrument, :class => JamRuby::Instrument do
description { |n| "Instrument #{n}" }
end
@@ -559,6 +584,7 @@ FactoryGirl.define do
end
factory :playable_play, :class => JamRuby::PlayablePlay do
+ association :user, factory: :user
end
factory :recording_like, :class => JamRuby::RecordingLiker do
@@ -700,22 +726,19 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
- bpm 100.1
time_signature '4/4'
status 'Production'
recording_type 'Cover'
sequence(:original_artist) { |n| "original-artist-#{n}" }
sequence(:songwriter) { |n| "songwriter-#{n}" }
sequence(:publisher) { |n| "publisher-#{n}" }
- pro 'ASCAP'
sales_region 'United States'
price 1.99
reproduction_royalty true
public_performance_royalty true
reproduction_royalty_amount 0.999
licensor_royalty_amount 0.999
- pro_royalty_amount 0.999
- available true
+ sequence(:plan_code) { |n| "jamtrack-#{n}" }
genre JamRuby::Genre.first
association :licensor, factory: :jam_track_licensor
@@ -746,4 +769,30 @@ FactoryGirl.define do
bpm 120
tap_in_count 3
end
+
+ factory :sale, :class => JamRuby::Sale do
+ order_total 0
+ association :user, factory:user
+ end
+
+ factory :recurly_transaction_web_hook, :class => JamRuby::RecurlyTransactionWebHook do
+
+ transaction_type JamRuby::RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT
+ sequence(:recurly_transaction_id ) { |n| "recurly-transaction-id-#{n}" }
+ sequence(:subscription_id ) { |n| "subscription-id-#{n}" }
+ sequence(:invoice_id ) { |n| "invoice-id-#{n}" }
+ sequence(:invoice_number ) { |n| 1000 + n }
+ invoice_number_prefix nil
+ action 'purchase'
+ status 'success'
+ transaction_at Time.now
+ amount_in_cents 199
+ reference 100000
+ message 'meh'
+ association :user, factory: :user
+
+ factory :recurly_transaction_web_hook_failed do
+ transaction_type JamRuby::RecurlyTransactionWebHook::FAILED_PAYMENT
+ end
+ end
end
diff --git a/ruby/spec/files/off.ogg b/ruby/spec/files/off.ogg
new file mode 100644
index 000000000..743d6e3aa
Binary files /dev/null and b/ruby/spec/files/off.ogg differ
diff --git a/ruby/spec/jam_ruby/jam_track_importer_spec.rb b/ruby/spec/jam_ruby/jam_track_importer_spec.rb
new file mode 100644
index 000000000..5a0a89cf4
--- /dev/null
+++ b/ruby/spec/jam_ruby/jam_track_importer_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe JamTrackImporter do
+
+ let(:s3_manager) { S3Manager.new(app_config.aws_bucket_jamtracks, app_config.aws_access_key_id, app_config.aws_secret_access_key) }
+
+ let(:sample_yml) {
+ {
+ "name" => "Back in Black",
+ "plan_code" => "jamtrack-acdc-backinblack",
+ "recording_type" => 'Cover',
+ "pro" => 'ASCAP',
+ "genre" => 'rock'
+
+ }
+ }
+ describe "load_metalocation" do
+
+ include UsesTempFiles
+
+ metafile = 'meta.yml'
+ in_directory_with_file(metafile)
+
+ before(:each) do
+ content_for_file(YAML.dump(sample_yml))
+ end
+
+ it "no meta" do
+ s3_metalocation = 'audio/Artist 1/Bogus Place/meta.yml'
+ JamTrackImporter.load_metalocation(s3_metalocation).should be_nil
+ end
+
+ it "successfully" do
+ s3_metalocation = 'audio/Artist 1/Song 1/meta.yml'
+ s3_manager.upload(s3_metalocation, metafile)
+
+ JamTrackImporter.load_metalocation(s3_metalocation).should eq(sample_yml)
+ end
+ end
+
+ describe "synchronize" do
+ let(:jam_track) { JamTrack.new }
+ let(:importer) { JamTrackImporter.new }
+ let(:minimum_meta) { nil }
+ let(:metalocation) { 'audio/Artist 1/Song 1/meta.yml' }
+ let(:options) {{ skip_audio_upload:true }}
+
+ it "bare minimum specification" do
+ importer.synchronize_metadata(jam_track, minimum_meta, metalocation, 'Artist 1', 'Song 1')
+
+ jam_track.plan_code.should eq('jamtrack-artist1-song1')
+ jam_track.name.should eq("Song 1")
+ jam_track.description.should == "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the Artist 1 song \"Song 1\"."
+ jam_track.time_signature.should be_nil
+ jam_track.status.should eq('Staging')
+ jam_track.recording_type.should eq('Cover')
+ jam_track.original_artist.should eq('Artist 1')
+ jam_track.songwriter.should be_nil
+ jam_track.publisher.should be_nil
+ jam_track.sales_region.should eq('United States')
+ jam_track.price.should eq(1.99)
+ end
+ end
+
+ describe "parse_wav" do
+ it "Guitar" do
+ result = JamTrackImporter.new.parse_wav('blah/Ready for Love Stem - Guitar - Main.wav')
+ result[:instrument].should eq('electric guitar')
+ result[:part].should eq('Main')
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb b/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb
new file mode 100644
index 000000000..3e70a7c95
--- /dev/null
+++ b/ruby/spec/jam_ruby/jam_tracks_manager_spec.rb
@@ -0,0 +1,6 @@
+require 'spec_helper'
+
+describe JamTracksManager do
+
+
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb
index 392d70473..c117a65c2 100644
--- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb
+++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb
@@ -745,6 +745,29 @@ describe ActiveMusicSession do
@music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN]
end
+
+ it "disallow a claimed recording to be started when backing track is open" do
+ # open the backing track
+ @backing_track = "foo.mp3"
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_false
+
+ # and try to open a recording for playback
+ @music_session.claimed_recording_start(@user1, @claimed_recording)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
+ end
+
+ it "disallow a claimed recording to be started when metronome is open" do
+ # open the metronome
+ @music_session.open_metronome(@user1)
+ @music_session.errors.any?.should be_false
+
+ # and try to open a recording for playback
+ @music_session.claimed_recording_start(@user1, @claimed_recording)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN]
+ end
end
end
@@ -830,5 +853,134 @@ describe ActiveMusicSession do
music_sessions[0].connections[0].tracks.should have(1).items
end
end
+
+ describe "open_backing_track" do
+ before(:each) do
+ @user1 = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user1)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
+ # @music_session.connections << @connection
+ @music_session.save!
+ @connection.join_the_session(@music_session, true, nil, @user1, 10)
+ @backing_track = "foo/bar.mp3"
+ end
+
+ it "allow a backing track to be associated" do
+ # simple success case; just open the backing track and observe the state of the session is correct
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_false
+ @music_session.reload
+ @music_session.backing_track_path.should == @backing_track
+ @music_session.backing_track_initiator.should == @user1
+ end
+
+ it "allow a backing track to be closed" do
+ # simple success case; close an opened backing track and observe the state of the session is correct
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_false
+ @music_session.close_backing_track
+ @music_session.errors.any?.should be_false
+ @music_session.reload
+ @music_session.backing_track_path.should be_nil
+ @music_session.backing_track_initiator.should be_nil
+ end
+
+ it "disallow a backing track to be opened when another is already opened" do
+ # if a backing track is open, don't allow another to be opened
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_false
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
+ end
+
+ it "disallow a backing track to be opened when recording is ongoing" do
+ @recording = Recording.start(@music_session, @user1)
+ @music_session.errors.any?.should be_false
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
+ end
+
+ it "disallow a backing track to be opened when recording is playing back" do
+ # create a recording, and open it for play back
+ @recording = Recording.start(@music_session, @user1)
+ @recording.errors.any?.should be_false
+ @recording.stop
+ @recording.reload
+ @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
+ @claimed_recording.errors.any?.should be_false
+ @music_session.claimed_recording_start(@user1, @claimed_recording)
+ @music_session.errors.any?.should be_false
+
+ # while it's open, try to open a jam track
+ @music_session.open_backing_track(@user1, @backing_track)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
+ end
+
+ end
+
+ describe "open_metronome" do
+ before(:each) do
+ @user1 = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user1)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
+ # @music_session.connections << @connection
+ @music_session.save!
+ @connection.join_the_session(@music_session, true, nil, @user1, 10)
+ end
+
+ it "allow a metronome to be activated" do
+ # simple success case; just open the metronome and observe the state of the session is correct
+ @music_session.open_metronome(@user1)
+ @music_session.errors.any?.should be_false
+ @music_session.reload
+ @music_session.metronome_active.should == true
+ @music_session.metronome_initiator.should == @user1
+ end
+
+ it "allow a metronome to be closed" do
+ # simple success case; close an opened metronome and observe the state of the session is correct
+ @music_session.open_metronome(@user1)
+ @music_session.errors.any?.should be_false
+ @music_session.close_metronome
+ @music_session.errors.any?.should be_false
+ @music_session.reload
+ @music_session.metronome_active.should be_false
+ @music_session.metronome_initiator.should be_nil
+ end
+
+ it "disallow a metronome to be opened when recording is ongoing" do
+ @recording = Recording.start(@music_session, @user1)
+ @music_session.errors.any?.should be_false
+ @music_session.open_metronome(@user1)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
+ end
+
+ it "disallow a metronome to be opened when recording is playing back" do
+ # create a recording, and open it for play back
+ @recording = Recording.start(@music_session, @user1)
+ @recording.errors.any?.should be_false
+ @recording.stop
+ @recording.reload
+ @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
+ @claimed_recording.errors.any?.should be_false
+ @music_session.claimed_recording_start(@user1, @claimed_recording)
+ @music_session.errors.any?.should be_false
+
+ # while it's open, try to open a jam track
+ @music_session.open_metronome(@user1)
+ @music_session.errors.any?.should be_true
+ @music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
+ end
+
+ end
+
end
diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
index b92bfa04b..530535f8c 100644
--- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb
@@ -17,14 +17,6 @@ describe JamTrackRight do
end
- it "lists" do
- jam_track_right = FactoryGirl.create(:jam_track_right)
- jam_tracks = JamTrack.list_downloads(jam_track_right.user)
- jam_tracks.should have_key('downloads')
- jam_tracks.should have_key('next')
- jam_tracks['downloads'].should have(1).items
- end
-
describe "validations" do
it "one purchase per user/jam_track combo" do
user = FactoryGirl.create(:user)
@@ -66,27 +58,28 @@ describe JamTrackRight do
user = FactoryGirl.create(:user)
jam_track_track = FactoryGirl.create(:jam_track_track)
jam_track = jam_track_track.jam_track
-
- uploader = JamTrackTrackUploader.new(jam_track_track, :url)
- uploader.store!(File.open(ogg_path, 'rb'))
+
+ s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+
+ s3.upload(jam_track_track.manually_uploaded_filename(:url_48), ogg_path)
+ jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48)
jam_track_track.save!
-
- jam_track_track[:url].should == jam_track_track.store_dir + '/' + jam_track_track.filename
+
+ jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48)
# verify it's on S3
- s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
- s3.exists?(jam_track_track[:url]).should be_true
- s3.length(jam_track_track[:url]).should == File.size?(ogg_path)
+ s3.exists?(jam_track_track[:url_48]).should be_true
+ s3.length(jam_track_track[:url_48]).should == File.size?(ogg_path)
jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track)
#expect {
JamRuby::JamTracksManager.save_jam_track_jkz(user, jam_track)
#}.to_not raise_error(ArgumentError)
jam_track_right.reload
- jam_track_right[:url].should == jam_track_right.store_dir + '/' + jam_track_right.filename
+ jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename
# verify it's on S3
- url = jam_track_right[:url]
+ url = jam_track_right[:url_48]
s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
s3.exists?(url).should be_true
s3.length(url).should > File.size?(ogg_path)
@@ -105,7 +98,7 @@ describe JamTrackRight do
end
it "bogus key" do
- JamTrackRight.list_keys(user, [2112]).should eq([])
+ JamTrackRight.list_keys(user, ['2112']).should eq([])
end
it "valid track with no rights to it by querying user" do
diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb
index 0c3ab5936..10cec40cd 100644
--- a/ruby/spec/jam_ruby/models/jam_track_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb
@@ -14,6 +14,30 @@ describe JamTrack do
jam_track.licensor.jam_tracks.should == [jam_track]
end
+ describe 'plays' do
+ it "creates played instance properly" do
+ @jam_track = FactoryGirl.create(:jam_track)
+ play = PlayablePlay.new
+
+ # VRFS-2916 jam_tracks.id is varchar: REMOVE
+ # play.jam_track = @jam_track
+ # VRFS-2916 jam_tracks.id is varchar: ADD
+ play.playable = @jam_track
+
+ play.user = user
+ play.save!
+ expect(@jam_track.plays.count).to eq(1)
+ expect(@jam_track.plays[0].user.id).to eq(user.id)
+ expect(user.jam_tracks_played.count).to eq(1)
+ end
+ it "handles played errors" do
+ play = PlayablePlay.new
+ play.user = user
+ play.save
+ expect(play.errors.count).to eq(1)
+ end
+ end
+
describe "index" do
it "empty query" do
query, pager = JamTrack.index({}, user)
@@ -55,25 +79,6 @@ describe JamTrack do
end
describe "validations" do
- describe "bpm" do
- it "1" do
- FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true
- end
-
- it "100" do
- FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true
- end
-
- it "100.1" do
- FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true
- end
-
- it "100.12" do
- jam_track = FactoryGirl.build(:jam_track, bpm: 100.12)
- jam_track.valid?.should be_false
- jam_track.errors[:bpm].should == ['is invalid']
- end
- end
describe "price" do
@@ -136,44 +141,5 @@ describe JamTrack do
end
end
end
-
- describe "upload/download" do
- JKA_NAME = 'blah.jkz'
-
- in_directory_with_file(JKA_NAME)
-
- before(:all) do
- original_storage = JamTrackUploader.storage = :fog
- end
-
- after(:all) do
- JamTrackUploader.storage = @original_storage
- end
-
- before(:each) do
- content_for_file('abc')
- end
-
- it "uploads to s3 with correct name, and then downloads via signed URL" do
- jam_track = FactoryGirl.create(:jam_track)
- uploader = JamTrackUploader.new(jam_track, :url)
- uploader.store!(File.open(JKA_NAME)) # uploads file
- jam_track.save!
-
- # verify that the uploader stores the correct path
- jam_track[:url].should == jam_track.store_dir + '/' + jam_track.filename
-
- # verify it's on S3
- s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
- s3.exists?(jam_track[:url]).should be_true
- s3.length(jam_track[:url]).should == 'abc'.length
-
- # download it via signed URL, and check contents
- url = jam_track.sign_url
- downloaded_contents = open(url).read
- downloaded_contents.should == 'abc'
- end
-
- end
end
diff --git a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb
index 6cc5c3773..6fb4343f6 100644
--- a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb
@@ -16,7 +16,7 @@ describe JamTrackTrack do
jam_track_track_1 = FactoryGirl.create(:jam_track_track, position: 1, jam_track: jam_track)
jam_track_track_2 = FactoryGirl.build(:jam_track_track, position: 1, jam_track: jam_track)
jam_track_track_2.valid?.should == false
- jam_track_track_2.errors[:position].should == ['has already been taken']
+ #jam_track_track_2.errors[:position].should == ['has already been taken']
end
it "jam_track required" do
@@ -46,18 +46,20 @@ describe JamTrackTrack do
end
it "uploads to s3 with correct name, and then downloads via signed URL" do
+ s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+
jam_track_track = FactoryGirl.create(:jam_track_track)
- uploader = JamTrackTrackUploader.new(jam_track_track, :url)
- uploader.store!(File.open(TRACK_NAME)) # uploads file
+ s3.upload(jam_track_track.manually_uploaded_filename(:url_48), TRACK_NAME)
+ jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48)
jam_track_track.save!
+ jam_track_track.reload
# verify that the uploader stores the correct path
- jam_track_track[:url].should == jam_track_track.store_dir + '/' + jam_track_track.filename
+ jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48)
# verify it's on S3
- s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
- s3.exists?(jam_track_track[:url]).should be_true
- s3.length(jam_track_track[:url]).should == 'abc'.length
+ s3.exists?(jam_track_track[:url_48]).should be_true
+ s3.length(jam_track_track[:url_48]).should == 'abc'.length
# download it via signed URL, and check contents
url = jam_track_track.sign_url
diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb
index 52163bcc4..ecb2a9e4c 100644
--- a/ruby/spec/jam_ruby/models/latency_tester_spec.rb
+++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb
@@ -39,7 +39,7 @@ describe LatencyTester do
latency_tester.connection.aasm_state = Connection::STALE_STATE.to_s
latency_tester.save!
- set_updated_at(latency_tester.connection, 1.days.ago)
+ set_updated_at(latency_tester.connection, 6.hours.ago)
params[:client_id] = latency_tester.connection.client_id
@@ -49,7 +49,7 @@ describe LatencyTester do
# state should have refreshed from stale to connected
found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s
# updated_at needs to be poked on connection to keep stale non-stale
- (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 24 # 1 day
+ (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 6 # 6hours
end
end
end
diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb
index 6273855d9..e152fed55 100644
--- a/ruby/spec/jam_ruby/models/music_session_spec.rb
+++ b/ruby/spec/jam_ruby/models/music_session_spec.rb
@@ -854,6 +854,13 @@ describe MusicSession do
music_session_1.rsvp_slots[0].rsvp_requests_rsvp_slots[0].save!
MusicSession.scheduled_rsvp(creator_1, true).should == []
end
+
+ it "create_type = nil will still return RSVPs" do
+ music_session_1.create_type = nil
+ music_session_1.save!
+
+ MusicSession.scheduled_rsvp(creator_1, true).should == [music_session_1]
+ end
end
end
diff --git a/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb
new file mode 100644
index 000000000..de65e22e1
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+require 'rest-client'
+
+describe RecordedBackingTrack do
+
+ include UsesTempFiles
+
+ before do
+ @user = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
+ @recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
+ end
+
+ it "should copy from a regular track properly" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+
+ @recorded_backing_track.user.id.should == @backing_track.connection.user.id
+ @recorded_backing_track.filename.should == @backing_track.filename
+ @recorded_backing_track.next_part_to_upload.should == 0
+ @recorded_backing_track.fully_uploaded.should == false
+ @recorded_backing_track.client_id = @connection.client_id
+ @recorded_backing_track.backing_track_id = @backing_track.id
+ end
+
+ it "should update the next part to upload properly" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.upload_part_complete(1, 1000)
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)"
+ @recorded_backing_track.errors[:md5][0].should == "can't be blank"
+ end
+
+ it "properly finds a recorded track given its upload filename" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.save.should be_true
+ RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track
+ end
+
+ it "gets a url for the track" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.errors.any?.should be_false
+ @recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg"
+ end
+
+ it "signs url" do
+ stub_const("APP_CONFIG", app_config)
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.sign_url.should_not be_nil
+ end
+
+ it "can not be downloaded if no claimed recording" do
+ user2 = FactoryGirl.create(:user)
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.can_download?(user2).should be_false
+ @recorded_backing_track.can_download?(@user).should be_false
+ end
+
+ it "can be downloaded if there is a claimed recording" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false
+ @recorded_backing_track.can_download?(@user).should be_true
+ end
+
+
+ describe "aws-based operations", :aws => true do
+
+ def put_file_to_aws(signed_data, contents)
+
+ begin
+ RestClient.put( signed_data[:url],
+ contents,
+ {
+ :'Content-Type' => 'audio/ogg',
+ :Date => signed_data[:datetime],
+ :'Content-MD5' => signed_data[:md5],
+ :Authorization => signed_data[:authorization]
+ })
+ rescue => e
+ puts e.response
+ raise e
+ end
+
+ end
+ # create a test file
+ upload_file='some_file.ogg'
+ in_directory_with_file(upload_file)
+
+ upload_file_contents="ogg binary stuff in here"
+ md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp
+ test_config = app_config
+ s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
+
+
+ before do
+ stub_const("APP_CONFIG", app_config)
+ # this block of code will fully upload a sample file to s3
+ content_for_file(upload_file_contents)
+ s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging
+
+
+ end
+
+ it "cant mark a part complete without having started it" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.upload_start(1000, "abc")
+ @recorded_backing_track.upload_part_complete(1, 1000)
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED
+ end
+
+ it "no parts" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.upload_start(1000, "abc")
+ @recorded_backing_track.upload_next_part(1000, "abc")
+ @recorded_backing_track.errors.any?.should be_false
+ @recorded_backing_track.upload_part_complete(1, 1000)
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS
+ end
+
+ it "enough part failures reset the upload" do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.upload_start(File.size(upload_file), md5)
+ @recorded_backing_track.upload_next_part(File.size(upload_file), md5)
+ @recorded_backing_track.errors.any?.should be_false
+ APP_CONFIG.max_track_part_upload_failures.times do |i|
+ @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
+ @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
+ part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
+ expected_is_part_uploading = !part_failure_rollover
+ expected_part_failures = part_failure_rollover ? 0 : i + 1
+ @recorded_backing_track.reload
+ @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
+ @recorded_backing_track.part_failures.should == expected_part_failures
+ end
+
+ @recorded_backing_track.reload
+ @recorded_backing_track.upload_failures.should == 1
+ @recorded_backing_track.file_offset.should == 0
+ @recorded_backing_track.next_part_to_upload.should == 0
+ @recorded_backing_track.upload_id.should be_nil
+ @recorded_backing_track.md5.should be_nil
+ @recorded_backing_track.length.should == 0
+ end
+
+ it "enough upload failures fails the upload forever" do
+ APP_CONFIG.stub(:max_track_upload_failures).and_return(1)
+ APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2)
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ APP_CONFIG.max_track_upload_failures.times do |j|
+ @recorded_backing_track.upload_start(File.size(upload_file), md5)
+ @recorded_backing_track.upload_next_part(File.size(upload_file), md5)
+ @recorded_backing_track.errors.any?.should be_false
+ APP_CONFIG.max_track_part_upload_failures.times do |i|
+ @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
+ @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
+ part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
+ expected_is_part_uploading = part_failure_rollover ? false : true
+ expected_part_failures = part_failure_rollover ? 0 : i + 1
+ @recorded_backing_track.reload
+ @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
+ @recorded_backing_track.part_failures.should == expected_part_failures
+ end
+ @recorded_backing_track.upload_failures.should == j + 1
+ end
+
+ @recorded_backing_track.reload
+ @recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures
+ @recorded_backing_track.file_offset.should == 0
+ @recorded_backing_track.next_part_to_upload.should == 0
+ @recorded_backing_track.upload_id.should be_nil
+ @recorded_backing_track.md5.should be_nil
+ @recorded_backing_track.length.should == 0
+
+ # try to poke it and get the right kind of error back
+ @recorded_backing_track.upload_next_part(File.size(upload_file), md5)
+ @recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED]
+ end
+
+ describe "correctly uploaded a file" do
+
+ before do
+ @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
+ @recorded_backing_track.upload_start(File.size(upload_file), md5)
+ @recorded_backing_track.upload_next_part(File.size(upload_file), md5)
+ signed_data = @recorded_backing_track.upload_sign(md5)
+ @response = put_file_to_aws(signed_data, upload_file_contents)
+ @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
+ @recorded_backing_track.errors.any?.should be_false
+ @recorded_backing_track.upload_complete
+ @recorded_backing_track.errors.any?.should be_false
+ @recorded_backing_track.marking_complete = false
+ end
+
+ it "can download an updated file" do
+ @response = RestClient.get @recorded_backing_track.sign_url
+ @response.body.should == upload_file_contents
+ end
+
+ it "can't mark completely uploaded twice" do
+ @recorded_backing_track.upload_complete
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
+ @recorded_backing_track.part_failures.should == 0
+ end
+
+ it "can't ask for a next part if fully uploaded" do
+ @recorded_backing_track.upload_next_part(File.size(upload_file), md5)
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
+ @recorded_backing_track.part_failures.should == 0
+ end
+
+ it "can't ask for mark part complete if fully uploaded" do
+ @recorded_backing_track.upload_part_complete(1, 1000)
+ @recorded_backing_track.errors.any?.should be_true
+ @recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
+ @recorded_backing_track.part_failures.should == 0
+ end
+ end
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb
index 303080174..3de7d09e1 100644
--- a/ruby/spec/jam_ruby/models/recording_spec.rb
+++ b/ruby/spec/jam_ruby/models/recording_spec.rb
@@ -15,6 +15,29 @@ describe Recording do
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
end
+ describe "popular_recordings" do
+ it "empty" do
+ Recording.popular_recordings.length.should eq(0)
+ end
+
+ it "one public recording" do
+ claim = FactoryGirl.create(:claimed_recording)
+
+ claim.recording.is_done = true
+ claim.recording.save!
+ recordings = Recording.popular_recordings
+ recordings.length.should eq(1)
+ recordings[0].id.should eq(claim.recording.id)
+ end
+
+ it "one private recording" do
+ claim = FactoryGirl.create(:claimed_recording, is_public: true)
+
+ recordings = Recording.popular_recordings
+ recordings.length.should eq(0)
+ end
+ end
+
describe "cleanup_excessive_storage" do
sample_audio='sample.file'
@@ -211,6 +234,20 @@ describe Recording do
user1_recorded_tracks[0].discard = true
user1_recorded_tracks[0].save!
end
+
+ it "should allow finding of backing tracks" do
+ user2 = FactoryGirl.create(:user)
+ connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session)
+ track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument)
+ backing_track = FactoryGirl.create(:backing_track, :connection => connection2)
+
+
+ @recording = Recording.start(@music_session, @user)
+ @recording.recorded_backing_tracks_for_user(@user).length.should eq(0)
+ user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2)
+ user2_recorded_tracks.length.should == 1
+ user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0]
+ end
it "should set up the recording properly when recording is started with 1 user in the session" do
@music_session.is_recording?.should be_false
@@ -547,6 +584,8 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
+ @backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
+
# We should have 2 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(3).items
@@ -1057,6 +1096,34 @@ describe Recording do
RecordedVideo.find_by_id(video.id).should_not be_nil
end
end
+
+ describe "add_timeline" do
+
+ let!(:recorded_jam_track_track) {FactoryGirl.create(:recorded_jam_track_track)}
+ let(:recording) {recorded_jam_track_track.recording}
+ let(:timeline_data) {{"sample" => "data"}}
+ let(:good_timeline) { {
+ "global" => {"recording_start_time" => 0, "jam_track_play_start_time" => 0, "jam_track_recording_start_play_offset" => 0},
+ "tracks" => [
+ {
+ "id" => recorded_jam_track_track.jam_track_track.id,
+ "timeline" => timeline_data,
+ "type" => "jam_track"
+ }
+ ]
+ }
+ }
+
+ it "applies timeline data correctly" do
+ recording.add_timeline good_timeline
+ recorded_jam_track_track.reload
+ JSON.parse(recorded_jam_track_track.timeline).should eq(timeline_data)
+ end
+
+ it "fails if no tracks data" do
+ expect { recording.add_timeline({}) }.to raise_error(JamRuby::JamArgumentError)
+ end
+ end
end
diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb
new file mode 100644
index 000000000..ba89aee20
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb
@@ -0,0 +1,260 @@
+require 'spec_helper'
+
+# verifies that all webhooks work, except for the failed_payment_notification hook, since I don't have an example of it.
+# because the other 3 types work, I feel pretty confident it will work
+
+# testing with CURL:
+# curl -X POST -d @filename.txt http://localhost:3000/api/recurly/webhook --header "Content-Type:text/xml" --user monkeytoesspeartoss:frizzyfloppymushface
+# where @filename.txt is either empty (creates no row), or the contents of one of the create_from_xml tests below (replacing the account_code with a real user_id in our system)
+
+describe RecurlyTransactionWebHook do
+
+
+ let(:refund_xml) {'
+
+
+ 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d
+
+ sethcall@gmail.com
+ Seth
+ Call
+
+
+
+ 2de439790e8fceb7fc385a4a89b89883
+ 2da71ad9c657adf9fe618e4f058c78bb
+
+ 1033
+ 2da71ad97c826a7b784c264ac59c04de
+ refund
+ 2015-04-01T14:41:40Z
+ 216
+ success
+ Successful test transaction
+ 3819545
+ subscription
+
+ Street address and postal code match.
+
+
+ true
+ true
+ false
+
+'
+ }
+
+ let(:void_xml) {
+'
+
+
+ 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d
+
+ sethcall@gmail.com
+ Seth
+ Call
+
+
+
+ 2de4370332f709c768313d4f47a9af1d
+ 2da71ad9c657adf9fe618e4f058c78bb
+
+ 1033
+ 2da71ad97c826a7b784c264ac59c04de
+ refund
+ 2015-04-01T14:38:59Z
+ 216
+ void
+ Successful test transaction
+ 3183996
+ subscription
+
+ Street address and postal code match.
+
+
+ true
+ false
+ false
+
+'
+ }
+
+ let(:success_xml) {
+'
+
+
+ 56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d
+
+ seth@jamkazam.com
+ Seth
+ Call
+
+
+
+ 2de4448533db12d6d92b4c4b4e90a4f1
+ 2de44484fa4528b504555f43ac8bf42f
+
+ 1037
+ 2de44484b460d95863799a431383b165
+ purchase
+ 2015-04-01T14:53:44Z
+ 216
+ success
+ Successful test transaction
+ 6249355
+ subscription
+
+ Street address and postal code match.
+
+
+ true
+ true
+ true
+
+'
+ }
+ describe "sales integrity maintanence" do
+
+ before(:each) do
+ @user = FactoryGirl.create(:user, id: '56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d')
+ end
+
+ it "deletes jam_track_right when refunded" do
+
+ sale = Sale.create_jam_track_sale(@user)
+ sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
+ sale.recurly_total_in_cents = 216
+ sale.save!
+ # create a jam_track right, which should be whacked as soon as we craete the web hook
+ jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'bleh')
+
+ shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
+ SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
+
+ document = Nokogiri::XML(refund_xml)
+
+ RecurlyTransactionWebHook.create_from_xml(document)
+
+ JamTrackRight.find_by_id(jam_track_right.id).should be_nil
+ end
+
+ it "deletes jam_track_right when voided" do
+
+ sale = Sale.create_jam_track_sale(@user)
+ sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
+ sale.recurly_total_in_cents = 216
+ sale.save!
+ # create a jam_track right, which should be whacked as soon as we craete the web hook
+ jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'blah')
+
+ shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
+ SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
+
+ document = Nokogiri::XML(void_xml)
+
+ RecurlyTransactionWebHook.create_from_xml(document)
+
+ JamTrackRight.find_by_id(jam_track_right.id).should be_nil
+ end
+ end
+
+
+ describe "is_transaction_web_hook?" do
+
+ it "successful payment" do
+ document = Nokogiri::XML('')
+ RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true
+ end
+
+ it "successful refund" do
+ document = Nokogiri::XML('')
+ RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true
+ end
+
+ it "failed payment" do
+ document = Nokogiri::XML('')
+ RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true
+ end
+
+ it "void" do
+ document = Nokogiri::XML('')
+ RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_true
+ end
+
+ it "not a transaction web hook" do
+ document = Nokogiri::XML('')
+ RecurlyTransactionWebHook.is_transaction_web_hook?(document).should be_false
+ end
+ end
+ describe "create_from_xml" do
+
+ before(:each) do
+ @user = FactoryGirl.create(:user, id: '56d5b2c6-2a4b-46e4-a984-ec1fbe83a50d')
+ end
+
+ it "successful payment" do
+
+ document = Nokogiri::XML(success_xml)
+
+ transaction = RecurlyTransactionWebHook.create_from_xml(document)
+ transaction.valid?.should be_true
+ transaction.user.should eq(@user)
+ transaction.transaction_type.should eq('payment')
+ transaction.subscription_id.should eq('2de44484b460d95863799a431383b165')
+ transaction.invoice_id.should eq('2de44484fa4528b504555f43ac8bf42f')
+ transaction.invoice_number_prefix.should eq('')
+ transaction.invoice_number.should eq(1037)
+ transaction.recurly_transaction_id.should eq('2de4448533db12d6d92b4c4b4e90a4f1')
+ transaction.action.should eq('purchase')
+ transaction.transaction_at.should eq(Time.parse('2015-04-01T14:53:44Z'))
+ transaction.amount_in_cents.should eq(216)
+ transaction.status.should eq('success')
+ transaction.message.should eq('Successful test transaction')
+ transaction.reference.should eq('6249355')
+ end
+
+ it "successful refund" do
+ document = Nokogiri::XML(refund_xml)
+
+ transaction = RecurlyTransactionWebHook.create_from_xml(document)
+ transaction.valid?.should be_true
+ transaction.user.should eq(@user)
+ transaction.transaction_type.should eq('refund')
+ transaction.subscription_id.should eq('2da71ad97c826a7b784c264ac59c04de')
+ transaction.invoice_id.should eq('2da71ad9c657adf9fe618e4f058c78bb')
+ transaction.invoice_number_prefix.should eq('')
+ transaction.invoice_number.should eq(1033)
+ transaction.recurly_transaction_id.should eq('2de439790e8fceb7fc385a4a89b89883')
+ transaction.action.should eq('refund')
+ transaction.transaction_at.should eq(Time.parse('2015-04-01T14:41:40Z'))
+ transaction.amount_in_cents.should eq(216)
+ transaction.status.should eq('success')
+ transaction.message.should eq('Successful test transaction')
+ transaction.reference.should eq('3819545')
+
+ end
+
+ it "successful void" do
+ document = Nokogiri::XML(void_xml)
+
+ transaction = RecurlyTransactionWebHook.create_from_xml(document)
+ transaction.valid?.should be_true
+ transaction.user.should eq(@user)
+ transaction.transaction_type.should eq('void')
+ transaction.subscription_id.should eq('2da71ad97c826a7b784c264ac59c04de')
+ transaction.invoice_id.should eq('2da71ad9c657adf9fe618e4f058c78bb')
+ transaction.invoice_number_prefix.should eq('')
+ transaction.invoice_number.should eq(1033)
+ transaction.recurly_transaction_id.should eq('2de4370332f709c768313d4f47a9af1d')
+ transaction.action.should eq('refund')
+ transaction.transaction_at.should eq(Time.parse('2015-04-01T14:38:59Z'))
+ transaction.amount_in_cents.should eq(216)
+ transaction.status.should eq('void')
+ transaction.message.should eq('Successful test transaction')
+ transaction.reference.should eq('3183996')
+ end
+ end
+end
+
+# https://github.com/killbilling/recurly-java-library/blob/master/src/main/java/com/ning/billing/recurly/model/push/payment/FailedPaymentNotification.java
+# failed_payment_notification
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb
new file mode 100644
index 000000000..334166734
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb
@@ -0,0 +1,41 @@
+
+require 'spec_helper'
+
+describe SaleLineItem do
+
+ let(:user) {FactoryGirl.create(:user)}
+ let(:user2) {FactoryGirl.create(:user)}
+ let(:jam_track) {FactoryGirl.create(:jam_track)}
+
+ describe "associations" do
+
+ it "can find associated recurly transaction web hook" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
+ transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
+
+ sale_line_item.reload
+ sale_line_item.recurly_transactions.should eq([transaction])
+ end
+ end
+
+
+ describe "state" do
+
+ it "success" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
+ transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
+
+ sale_line_item.reload
+ sale_line_item.state.should eq({
+ void: false,
+ refund: false,
+ fail: false,
+ success: true
+ })
+ end
+ end
+end
diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb
new file mode 100644
index 000000000..b08aad36d
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/sale_spec.rb
@@ -0,0 +1,329 @@
+require 'spec_helper'
+
+describe Sale do
+
+ let(:user) {FactoryGirl.create(:user)}
+ let(:user2) {FactoryGirl.create(:user)}
+ let(:jam_track) {FactoryGirl.create(:jam_track)}
+
+ describe "index" do
+ it "empty" do
+ result = Sale.index(user)
+ result[:query].length.should eq(0)
+ result[:next].should eq(nil)
+ end
+
+ it "one" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
+
+ result = Sale.index(user)
+ result[:query].length.should eq(1)
+ result[:next].should eq(nil)
+ end
+
+ it "user filtered correctly" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
+
+ result = Sale.index(user)
+ result[:query].length.should eq(1)
+ result[:next].should eq(nil)
+
+ sale2 = Sale.create_jam_track_sale(user2)
+ shopping_cart = ShoppingCart.create(user2, jam_track)
+ sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil)
+
+ result = Sale.index(user)
+ result[:query].length.should eq(1)
+ result[:next].should eq(nil)
+ end
+ end
+
+
+ describe "place_order" do
+
+ let(:user) {FactoryGirl.create(:user)}
+ let(:jamtrack) { FactoryGirl.create(:jam_track) }
+ let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i }
+ let(:client) { RecurlyClient.new }
+ let(:billing_info) {
+ info = {}
+ info[:first_name] = user.first_name
+ info[:last_name] = user.last_name
+ info[:address1] = 'Test Address 1'
+ info[:address2] = 'Test Address 2'
+ info[:city] = user.city
+ info[:state] = user.state
+ info[:country] = user.country
+ info[:zip] = '12345'
+ info[:number] = '4111-1111-1111-1111'
+ info[:month] = '08'
+ info[:year] = '2017'
+ info[:verification_value] = '111'
+ info
+ }
+
+ after(:each) do
+ if user.recurly_code
+ account = Recurly::Account.find(user.recurly_code)
+ if account.present?
+ account.destroy
+ end
+ end
+ end
+
+
+ it "for a free jam track" do
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, true
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart])
+
+ user.reload
+ user.sales.length.should eq(1)
+
+ sales.should eq(user.sales)
+ sale = sales[0]
+ sale.recurly_invoice_id.should_not be_nil
+
+ sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
+ sale.recurly_tax_in_cents.should eq(0)
+ sale.recurly_total_in_cents.should eq(0)
+ sale.recurly_currency.should eq('USD')
+ sale.order_total.should eq(0)
+ sale.sale_line_items.length.should == 1
+ sale_line_item = sale.sale_line_items[0]
+ sale_line_item.recurly_tax_in_cents.should eq(0)
+ sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
+ sale_line_item.recurly_currency.should eq('USD')
+ sale_line_item.recurly_discount_in_cents.should eq(0)
+ sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
+ sale_line_item.unit_price.should eq(jamtrack.price)
+ sale_line_item.quantity.should eq(1)
+ sale_line_item.free.should eq(1)
+ sale_line_item.sales_tax.should be_nil
+ sale_line_item.shipping_handling.should eq(0)
+ sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
+ sale_line_item.product_id.should eq(jamtrack.id)
+ sale_line_item.recurly_subscription_uuid.should be_nil
+ sale_line_item.recurly_adjustment_uuid.should_not be_nil
+ sale_line_item.recurly_adjustment_credit_uuid.should_not be_nil
+ sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
+ sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid)
+
+ # verify subscription is in Recurly
+ recurly_account = client.get_account(user)
+ adjustments = recurly_account.adjustments
+ adjustments.should_not be_nil
+ adjustments.should have(2).items
+ free_purchase= adjustments[0]
+ free_purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
+ free_purchase.accounting_code.should eq(ShoppingCart::PURCHASE_FREE)
+ free_purchase.description.should eq("JamTrack: " + jamtrack.name)
+ free_purchase.state.should eq('invoiced')
+ free_purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
+
+ free_credit = adjustments[1]
+ free_credit.unit_amount_in_cents.should eq(-(jamtrack.price * 100).to_i)
+ free_credit.accounting_code.should eq(ShoppingCart::PURCHASE_FREE_CREDIT)
+ free_credit.description.should eq("JamTrack: " + jamtrack.name + " (Credit)")
+ free_credit.state.should eq('invoiced')
+ free_credit.uuid.should eq(sale_line_item.recurly_adjustment_credit_uuid)
+
+ invoices = recurly_account.invoices
+ invoices.should have(1).items
+ invoice = invoices[0]
+ invoice.uuid.should eq(sale.recurly_invoice_id)
+ invoice.line_items.should have(2).items # should have both adjustments associated
+ invoice.line_items[0].should eq(free_credit)
+ invoice.line_items[1].should eq(free_purchase)
+ invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
+ invoice.total_in_cents.should eq(0)
+ invoice.state.should eq('collected')
+
+ # verify jam_track_rights data
+ user.jam_track_rights.should_not be_nil
+ user.jam_track_rights.should have(1).items
+ user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
+ user.jam_track_rights.last.redeemed.should be_true
+ user.has_redeemable_jamtrack.should be_false
+ end
+
+ it "for a normally priced jam track" do
+ user.has_redeemable_jamtrack = false
+ user.save!
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, false
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart])
+
+ user.reload
+ user.sales.length.should eq(1)
+
+ sales.should eq(user.sales)
+ sale = sales[0]
+ sale.recurly_invoice_id.should_not be_nil
+
+ sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
+ sale.recurly_tax_in_cents.should eq(0)
+ sale.recurly_total_in_cents.should eq(jam_track_price_in_cents)
+ sale.recurly_currency.should eq('USD')
+
+ sale.order_total.should eq(jamtrack.price)
+ sale.sale_line_items.length.should == 1
+ sale_line_item = sale.sale_line_items[0]
+ # validate we are storing pricing info from recurly
+ sale_line_item.recurly_tax_in_cents.should eq(0)
+ sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
+ sale_line_item.recurly_currency.should eq('USD')
+ sale_line_item.recurly_discount_in_cents.should eq(0)
+ sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
+ sale_line_item.unit_price.should eq(jamtrack.price)
+ sale_line_item.quantity.should eq(1)
+ sale_line_item.free.should eq(0)
+ sale_line_item.sales_tax.should be_nil
+ sale_line_item.shipping_handling.should eq(0)
+ sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
+ sale_line_item.product_id.should eq(jamtrack.id)
+ sale_line_item.recurly_subscription_uuid.should be_nil
+ sale_line_item.recurly_adjustment_uuid.should_not be_nil
+ sale_line_item.recurly_adjustment_credit_uuid.should be_nil
+ sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
+
+ # verify subscription is in Recurly
+ recurly_account = client.get_account(user)
+ adjustments = recurly_account.adjustments
+ adjustments.should_not be_nil
+ adjustments.should have(1).items
+ purchase= adjustments[0]
+ purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
+ purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL)
+ purchase.description.should eq("JamTrack: " + jamtrack.name)
+ purchase.state.should eq('invoiced')
+ purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
+
+ invoices = recurly_account.invoices
+ invoices.should have(1).items
+ invoice = invoices[0]
+ invoice.uuid.should eq(sale.recurly_invoice_id)
+ invoice.line_items.should have(1).items # should have single adjustment associated
+ invoice.line_items[0].should eq(purchase)
+ invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
+ invoice.total_in_cents.should eq((jamtrack.price * 100).to_i)
+ invoice.state.should eq('collected')
+
+ # verify jam_track_rights data
+ user.jam_track_rights.should_not be_nil
+ user.jam_track_rights.should have(1).items
+ user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
+ user.jam_track_rights.last.redeemed.should be_false
+ user.has_redeemable_jamtrack.should be_false
+ end
+
+ it "for a jamtrack already owned" do
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, true
+ client.find_or_create_account(user, billing_info)
+
+ sales = Sale.place_order(user, [shopping_cart])
+
+ user.reload
+ user.sales.length.should eq(1)
+
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, false
+ sales = Sale.place_order(user, [shopping_cart])
+ sales.should have(0).items
+ # also, verify that no earlier adjustments were affected
+ recurly_account = client.get_account(user)
+ adjustments = recurly_account.adjustments
+ adjustments.should have(2).items
+ end
+
+ # this test counts on the fact that two adjustments are made when buying a free JamTrack
+ # so if we make the second adjustment invalid from Recurly's standpoint, then
+ # we can see if the first one is ultimately destroyed
+ it "rolls back created adjustments if error" do
+
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, true
+
+ # grab the real response; we will modify it to make a nil accounting code
+ adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
+ client.find_or_create_account(user, billing_info)
+
+ adjustment_attrs[1][:unit_amount_in_cents] = nil # invalid amount
+ ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs)
+
+ expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError)
+
+ user.reload
+ user.sales.should have(0).items
+
+ recurly_account = client.get_account(user)
+ recurly_account.adjustments.should have(0).items
+ end
+
+ it "rolls back adjustments created before the order" do
+ shopping_cart = ShoppingCart.create user, jamtrack, 1, true
+ client.find_or_create_account(user, billing_info)
+
+ # create a single adjustment on the account
+ adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
+ recurly_account = client.get_account(user)
+ adjustment = recurly_account.adjustments.new (adjustment_attrs[0])
+ adjustment.save
+ adjustment.errors.any?.should be_false
+
+ sales = Sale.place_order(user, [shopping_cart])
+
+ user.reload
+
+ recurly_account = client.get_account(user)
+ adjustments = recurly_account.adjustments
+ adjustments.should have(2).items # two adjustments are created for a free jamtrack; that should be all there is
+ end
+ end
+
+ describe "check_integrity_of_jam_track_sales" do
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:jam_track) { FactoryGirl.create(:jam_track) }
+
+ it "empty" do
+ check_integrity = Sale.check_integrity_of_jam_track_sales
+ check_integrity.length.should eq(1)
+ r = check_integrity[0]
+ r.total.to_i.should eq(0)
+ r.voided.to_i.should eq(0)
+ end
+
+ it "one succeeded sale" do
+ sale = Sale.create_jam_track_sale(user)
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
+
+ check_integrity = Sale.check_integrity_of_jam_track_sales
+ r = check_integrity[0]
+ r.total.to_i.should eq(1)
+ r.voided.to_i.should eq(0)
+ end
+
+
+ it "one voided sale" do
+ sale = Sale.create_jam_track_sale(user)
+ sale.recurly_invoice_id = 'some_recurly_invoice_id'
+ sale.save!
+ shopping_cart = ShoppingCart.create(user, jam_track)
+ SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
+ FactoryGirl.create(:recurly_transaction_web_hook, transaction_type: RecurlyTransactionWebHook::VOID, invoice_id: 'some_recurly_invoice_id')
+
+ check_integrity = Sale.check_integrity_of_jam_track_sales
+ r = check_integrity[0]
+ r.total.to_i.should eq(1)
+ r.voided.to_i.should eq(1)
+ end
+
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
index db3f4d75c..13f6777bc 100644
--- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
+++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb
@@ -4,6 +4,7 @@ describe ShoppingCart do
let(:user) { FactoryGirl.create(:user) }
let(:jam_track) {FactoryGirl.create(:jam_track) }
+ let(:jam_track2) {FactoryGirl.create(:jam_track) }
before(:each) do
ShoppingCart.delete_all
@@ -20,4 +21,49 @@ describe ShoppingCart do
user.shopping_carts[0].quantity.should == 1
end
+ it "should not add duplicate JamTrack to ShoppingCart" do
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart1.should_not be_nil
+ cart1.errors.any?.should be_false
+ user.reload
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart2.errors.any?.should be_true
+
+ end
+
+ describe "redeemable behavior" do
+ it "adds redeemable item to shopping cart" do
+
+ user.has_redeemable_jamtrack.should be_true
+
+ # first item added to shopping cart should be marked for redemption
+ cart = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart.marked_for_redeem.should eq(1)
+
+ # but the second item should not
+
+ user.reload
+
+ cart = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
+ cart.marked_for_redeem.should eq(0)
+ end
+
+ it "removes redeemable item to shopping cart" do
+
+ user.has_redeemable_jamtrack.should be_true
+ cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
+ cart1.should_not be_nil
+ user.reload
+ cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
+ cart2.should_not be_nil
+
+ cart1.marked_for_redeem.should eq(1)
+ cart2.marked_for_redeem.should eq(0)
+ ShoppingCart.remove_jam_track_from_cart(user, jam_track)
+
+ user.shopping_carts.length.should eq(1)
+ cart2.reload
+ cart1.marked_for_redeem.should eq(1)
+ end
+ end
end
diff --git a/ruby/spec/jam_ruby/models/signup_hint_spec.rb b/ruby/spec/jam_ruby/models/signup_hint_spec.rb
new file mode 100644
index 000000000..1ec79efef
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/signup_hint_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe SignupHint do
+
+ let(:user) {AnonymousUser.new(SecureRandom.uuid)}
+
+ describe "refresh_by_anoymous_user" do
+ it "creates" do
+ hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'})
+ hint.errors.any?.should be_false
+ hint.redirect_location.should eq('abc')
+ hint.want_jamblaster.should be_false
+ end
+
+ it "updated" do
+ SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'})
+
+ hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: nil, want_jamblaster: true})
+ hint.errors.any?.should be_false
+ hint.redirect_location.should be_nil
+ hint.want_jamblaster.should be_true
+ end
+ end
+end
diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb
index 1e03d123b..4db0f59a5 100644
--- a/ruby/spec/jam_ruby/models/track_spec.rb
+++ b/ruby/spec/jam_ruby/models/track_spec.rb
@@ -7,8 +7,10 @@ describe Track do
let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) }
let (:track) { FactoryGirl.create(:track, :connection => connection)}
let (:track2) { FactoryGirl.create(:track, :connection => connection)}
+ let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)}
let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) }
let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
+ let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} }
before(:each) do
msuh.touch
@@ -16,7 +18,8 @@ describe Track do
describe "sync" do
it "create one track" do
- tracks = Track.sync(connection.client_id, [track_hash])
+ result = Track.sync(connection.client_id, [track_hash])
+ tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@@ -25,7 +28,8 @@ describe Track do
end
it "create two tracks" do
- tracks = Track.sync(connection.client_id, [track_hash, track_hash])
+ result = Track.sync(connection.client_id, [track_hash, track_hash])
+ tracks = result[:tracks]
tracks.length.should == 2
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@@ -40,7 +44,8 @@ describe Track do
it "delete only track" do
track.id.should_not be_nil
connection.tracks.length.should == 1
- tracks = Track.sync(connection.client_id, [])
+ result = Track.sync(connection.client_id, [])
+ tracks = result[:tracks]
tracks.length.should == 0
end
@@ -49,7 +54,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
- tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@@ -62,7 +68,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
- tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@@ -75,7 +82,8 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
- tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@@ -87,7 +95,8 @@ describe Track do
it "updates a single track using .client_track_id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
- tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@@ -99,11 +108,85 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
- tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
+ result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
+ tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
end
+
+ describe "backing tracks" do
+ it "create one track and one backing track" do
+ result = Track.sync(connection.client_id, [track_hash], [backing_track_hash])
+ tracks = result[:tracks]
+ tracks.length.should == 1
+ track = tracks[0]
+ track.client_track_id.should == track_hash[:client_track_id]
+ track.sound = track_hash[:sound]
+ track.instrument.should == Instrument.find('drums')
+
+ backing_tracks = result[:backing_tracks]
+ backing_tracks.length.should == 1
+ track = backing_tracks[0]
+ track.client_track_id.should == backing_track_hash[:client_track_id]
+ end
+
+ it "delete only backing_track" do
+ track.id.should_not be_nil
+ backing_track.id.should_not be_nil
+ connection.tracks.length.should == 1
+ connection.backing_tracks.length.should == 1
+ result = Track.sync(connection.client_id,
+ [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
+ [])
+ tracks = result[:tracks]
+ tracks.length.should == 1
+ found = tracks[0]
+ expect(found.id).to eq track.id
+ expect(found.updated_at.to_i).to eq track.updated_at.to_i
+
+ backing_tracks = result[:backing_tracks]
+ backing_tracks.length.should == 0
+ end
+
+ it "does not touch updated_at when nothing changes" do
+ track.id.should_not be_nil
+ backing_track.id.should_not be_nil
+ connection.tracks.length.should == 1
+ set_updated_at(track, 1.days.ago)
+ set_updated_at(backing_track, 1.days.ago)
+ result = Track.sync(connection.client_id,
+ [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
+ [{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}])
+ tracks = result[:tracks]
+ tracks.length.should == 1
+ found = tracks[0]
+ expect(found.id).to eq track.id
+ expect(found.updated_at.to_i).to eq track.updated_at.to_i
+
+ backing_tracks = result[:backing_tracks]
+ backing_tracks.length.should == 1
+ found = backing_tracks[0]
+ expect(found.id).to eq backing_track.id
+ expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i
+ end
+ end
+
+ describe "metronome_open" do
+ it "sets metronome_open to true" do
+ result = Track.sync(connection.client_id, [track_hash], [], true)
+ connection.reload
+ connection.metronome_open.should be_true
+ end
+
+ it "sets metronome_open to false" do
+ connection.metronome_open = true
+ connection.save!
+ result = Track.sync(connection.client_id, [track_hash], [], false)
+ connection.reload
+ connection.metronome_open.should be_false
+ end
+ end
end
end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb
index 6d51c9b6e..1d6b0f40f 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/ruby/spec/jam_ruby/models/user_sync_spec.rb b/ruby/spec/jam_ruby/models/user_sync_spec.rb
index 2bea2d766..b015e77f5 100644
--- a/ruby/spec/jam_ruby/models/user_sync_spec.rb
+++ b/ruby/spec/jam_ruby/models/user_sync_spec.rb
@@ -20,6 +20,49 @@ describe UserSync do
data[:next].should be_nil
end
+ describe "backing_tracks" do
+
+ let!(:recording1) {
+ recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1)
+ recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false)
+ recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false)
+ recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false)
+ recording.save!
+ recording.reload
+ recording
+ }
+
+ let(:sorted_tracks) {
+ Array.new(recording1.recorded_tracks).sort! {|a, b|
+ if a.created_at == b.created_at
+ a.id <=> b.id
+ else
+ a.created_at <=> b.created_at
+ end
+ }
+ }
+
+ # backing tracks should only list download, or upload, for the person who opened it, for legal reasons
+ it "lists backing track for opener" do
+ data = UserSync.index({user_id: user1.id})
+ data[:next].should be_nil
+ user_syncs = data[:query]
+ user_syncs.count.should eq(3)
+ user_syncs[0].recorded_track.should == sorted_tracks[0]
+ user_syncs[1].recorded_track.should == sorted_tracks[1]
+ user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0]
+ end
+
+ it "does not list backing track for non-opener" do
+ data = UserSync.index({user_id: user2.id})
+ data[:next].should be_nil
+ user_syncs = data[:query]
+ user_syncs.count.should eq(2)
+ user_syncs[0].recorded_track.should == sorted_tracks[0]
+ user_syncs[1].recorded_track.should == sorted_tracks[1]
+ end
+ end
+
it "one mix and quick mix" do
mix = FactoryGirl.create(:mix)
mix.recording.duration = 1
diff --git a/web/spec/managers/recurly_client_spec.rb b/ruby/spec/jam_ruby/recurly_client_spec.rb
similarity index 73%
rename from web/spec/managers/recurly_client_spec.rb
rename to ruby/spec/jam_ruby/recurly_client_spec.rb
index 8b1825cba..108fe1600 100644
--- a/web/spec/managers/recurly_client_spec.rb
+++ b/ruby/spec/jam_ruby/recurly_client_spec.rb
@@ -1,12 +1,11 @@
require 'spec_helper'
-require "recurly_client"
+require "jam_ruby/recurly_client"
describe RecurlyClient do
- let(:jamtrack) { FactoryGirl.create(:jam_track) }
- #let(:client) { RecurlyClient.new }
+ let(:jamtrack) { FactoryGirl.create(:jam_track, plan_code: 'jamtrack-acdc-backinblack') }
before :all do
@client = RecurlyClient.new
- @jamtrack = FactoryGirl.create(:jam_track)
+ @jamtrack = FactoryGirl.create(:jam_track, plan_code: 'jamtrack-acdc-backinblack')
end
before(:each) do
@@ -87,21 +86,27 @@ describe RecurlyClient do
found.state.should eq('closed')
end
- it "can place order" do
- @client.find_or_create_account(@user, @billing_info)
- expect{@client.place_order(@user, @jamtrack)}.not_to raise_error()
- subs = @client.get_account(@user).subscriptions
- subs.should_not be_nil
- subs.should have(1).items
- @user.jam_track_rights.should_not be_nil
- @user.jam_track_rights.should have(1).items
- @user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id)
- end
+=begin
+ it "can refund subscription" do
+ sale = Sale.create(@user)
+ shopping_cart = ShoppingCart.create @user, @jamtrack, 1
+ @client.find_or_create_account(@user, @billing_info)
+
+ # Place order:
+ expect{@client.place_order(@user, @jamtrack, shopping_cart, sale)}.not_to raise_error()
+ active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'}
+ @jamtrack.reload
+ @jamtrack.jam_track_rights.should have(1).items
- it "detects error on double order" do
- @client.find_or_create_account(@user, @billing_info)
- expect{@client.place_order(@user, @jamtrack)}.not_to raise_error()
- expect{@client.place_order(@user, @jamtrack)}.to raise_error(RecurlyClientError)
+ # Refund:
+ expect{@client.refund_user_subscription(@user, @jamtrack)}.not_to raise_error()
+ active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'}
+ active_subs.should have(0).items
+
+ @jamtrack.reload
+ @jamtrack.jam_track_rights.should have(0).items
end
+=end
+
end
diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb
new file mode 100644
index 000000000..7ead35e3a
--- /dev/null
+++ b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe JamTracksBuilder do
+ include UsesTempFiles
+ include CarrierWave::Test::Matchers
+
+ before (:all) do
+ @user = FactoryGirl.create(:user)
+ @jam_track = FactoryGirl.create(:jam_track)
+ original_storage = JamTrackTrackUploader.storage = :fog
+ original_storage = JamTrackRightUploader.storage = :fog
+ @s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
+ end
+
+ after(:all) do
+ JamTrackRightUploader.storage = @original_storage
+ JamTrackTrackUploader.storage = @original_storage
+ end
+
+ before(:each) do
+ @s3.delete_folder('jam_tracks')
+ end
+
+ it "should build" do
+ ogg_path = File.join('spec', 'files', 'on.ogg')
+ user = FactoryGirl.create(:user)
+ jam_track_track = FactoryGirl.create(:jam_track_track)
+ jam_track = jam_track_track.jam_track
+
+ @s3.upload(jam_track_track.manually_uploaded_filename(:url_48), ogg_path)
+ jam_track_track[:url_48] = jam_track_track.manually_uploaded_filename(:url_48)
+ jam_track_track.save!
+
+ jam_track_track[:url_48].should == jam_track_track.manually_uploaded_filename(:url_48)
+
+ # verify it's on S3
+ @s3.exists?(jam_track_track[:url_48]).should be_true
+ @s3.length(jam_track_track[:url_48]).should == File.size?(ogg_path)
+ jam_track_track[:url_44].should be_nil
+
+ # Check right
+ jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track)
+ jam_track_right[:url_48].should be_nil
+ jam_track_right[:url_44].should be_nil
+ JamTracksBuilder.perform(jam_track_right.id, 48)
+ jam_track_right.reload
+ jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename
+ jam_track_track[:url_44].should be_nil
+ end
+
+ describe "with bitrate 44" do
+ it "should build" do
+ ogg_path = File.join('spec', 'files', 'on.ogg')
+ user = FactoryGirl.create(:user)
+
+ # Should build bitrate 44 and only bitrate 44:
+ jam_track_track = FactoryGirl.create(:jam_track_track)
+ jam_track = jam_track_track.jam_track
+
+ # uploader = JamTrackTrackUploader.new(jam_track_track, :url_44)
+ # uploader.store!(File.open(ogg_path, 'rb'))
+ @s3.upload(jam_track_track.manually_uploaded_filename(:url_44), ogg_path)
+ jam_track_track[:url_44] = jam_track_track.manually_uploaded_filename(:url_44)
+ jam_track_track.save!
+
+ jam_track_track[:url_44].should == jam_track_track.manually_uploaded_filename(:url_44)
+
+ # verify it's on S3
+ @s3.exists?(jam_track_track[:url_44]).should be_true
+ @s3.length(jam_track_track[:url_44]).should == File.size?(ogg_path)
+ jam_track_track[:url_48].should be_nil
+
+ # Check right
+ jam_track_right = JamTrackRight.create(:user=>user, :jam_track=>jam_track)
+ jam_track_right[:url_44].should be_nil
+ jam_track_right[:url_48].should be_nil
+ JamTracksBuilder.perform(jam_track_right.id, 44)
+ jam_track_right.reload
+ jam_track_right[:url_44].should == jam_track_right.store_dir + '/' + jam_track_right.filename
+ jam_track_right.url_44.should_not be_nil
+ jam_track_track[:url_48].should be_nil
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb
index 36cb5a067..1c900bc52 100644
--- a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb
+++ b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb
@@ -26,20 +26,20 @@ describe JamTracksCleaner do
jam_track_right.signed=true
jam_track_right
- jam_track_right.url.store!(File.open(RIGHT_NAME))
+ jam_track_right.url_48.store!(File.open(RIGHT_NAME))
jam_track_right.downloaded_since_sign=true
jam_track_right.save!
- jam_track_right[:url].should == jam_track_right.store_dir + '/' + jam_track_right.filename
+ jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename
jam_track_right.reload
# Should exist after uploading:
- url = jam_track_right[:url]
+ url = jam_track_right[:url_48]
s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
url.should_not be_nil
s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
- s3.exists?(jam_track_right[:url]).should be_true
+ s3.exists?(jam_track_right[:url_48]).should be_true
JamRuby::JamTracksCleaner.perform
s3.exists?(url).should be_true
diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb
index 62b1472c6..c3d041060 100644
--- a/ruby/spec/mailers/user_mailer_spec.rb
+++ b/ruby/spec/mailers/user_mailer_spec.rb
@@ -12,6 +12,7 @@ describe UserMailer do
let(:user) { FactoryGirl.create(:user) }
before(:each) do
+ stub_const("APP_CONFIG", app_config)
UserMailer.deliveries.clear
end
diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb
index 890a195c0..16ac84514 100644
--- a/ruby/spec/spec_helper.rb
+++ b/ruby/spec/spec_helper.rb
@@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance
ActiveRecord::Base.add_observer UserObserver.instance
ActiveRecord::Base.add_observer FeedbackObserver.instance
ActiveRecord::Base.add_observer RecordedTrackObserver.instance
+ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance
ActiveRecord::Base.add_observer QuickMixObserver.instance
#RecordedTrack.observers.disable :all # only a few tests want this observer active
diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb
index fda7ec00a..b22dd4467 100644
--- a/ruby/spec/support/utilities.rb
+++ b/ruby/spec/support/utilities.rb
@@ -3,10 +3,22 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws
def app_config
klass = Class.new do
+ def email_alerts_alias
+ 'alerts@jamkazam.com'
+ end
+
+ def email_generic_from
+ 'nobody@jamkazam.com'
+ end
+
def aws_bucket
JAMKAZAM_TESTING_BUCKET
end
+ def aws_bucket_jamtracks
+ 'jamkazam-jamtracks-test'
+ end
+
def aws_access_key_id
'AKIAJESQY24TOT542UHQ'
end
@@ -162,6 +174,18 @@ def app_config
20 # 20 seconds
end
+ def one_free_jamtrack_per_user
+ true
+ end
+
+ def secret_token
+ 'foobar'
+ end
+
+ def unsubscribe_token
+ 'blah'
+ end
+
private
def audiomixer_workspace_path
@@ -231,4 +255,4 @@ end
def friend(user1, user2)
FactoryGirl.create(:friendship, user: user1, friend: user2)
FactoryGirl.create(:friendship, user: user2, friend: user1)
-end
\ No newline at end of file
+end
diff --git a/web/Gemfile b/web/Gemfile
index e5c3f1583..8aa825f40 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -51,11 +51,12 @@ gem 'twitter'
gem 'fb_graph', '2.5.9'
gem 'sendgrid', '1.2.0'
gem 'filepicker-rails', '0.1.0'
-gem 'aws-sdk' #, '1.29.1'
+gem 'aws-sdk', '~> 1'
gem 'aasm', '3.0.16'
gem 'carrierwave', '0.9.0'
gem 'carrierwave_direct'
gem 'fog'
+gem 'jquery-payment-rails'
gem 'haml-rails'
gem 'unf' #optional fog dependency
gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError)
@@ -75,7 +76,7 @@ gem 'netaddr'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
-gem 'rest_client'
+gem 'rest-client'
gem 'iso-639'
gem 'language_list'
gem 'rubyzip'
@@ -86,6 +87,7 @@ gem 'recurly'
gem 'guard', '2.7.3'
gem 'influxdb', '0.1.8'
gem 'influxdb-rails', '0.1.10'
+gem 'sitemap_generator'
group :development, :test do
gem 'rspec-rails', '2.14.2'
diff --git a/web/README.md b/web/README.md
index ae45c560b..975118c93 100644
--- a/web/README.md
+++ b/web/README.md
@@ -1,15 +1,5 @@
-TODO:
-====
-
Jasmine Javascript Unit Tests
=============================
-1. Ensure you have the jasmine Gem installed;
-$ bundle
-
-2. Start the jasmine server (defaults to :8888)
-$ rake jasmine
-
-Open browser to localhost:8888
-
+Open browser to localhost:3000/teaspoon
diff --git a/web/Rakefile b/web/Rakefile
index f4019acfc..882005f42 100644
--- a/web/Rakefile
+++ b/web/Rakefile
@@ -6,6 +6,7 @@
#require 'resque/scheduler/tasks'
require 'resque/tasks'
require 'resque/scheduler/tasks'
+require 'sitemap_generator/tasks'
require File.expand_path('../config/application', __FILE__)
SampleApp::Application.load_tasks
diff --git a/web/app/assets/images/content/bkg_home_guitar.jpg b/web/app/assets/images/content/bkg_home_guitar.jpg
new file mode 100644
index 000000000..b67797dd8
Binary files /dev/null and b/web/app/assets/images/content/bkg_home_guitar.jpg differ
diff --git a/web/app/assets/images/content/bkg_home_guitar_x.jpg b/web/app/assets/images/content/bkg_home_guitar_x.jpg
new file mode 100644
index 000000000..e76ec6a83
Binary files /dev/null and b/web/app/assets/images/content/bkg_home_guitar_x.jpg differ
diff --git a/web/app/assets/images/content/checkmark.png b/web/app/assets/images/content/checkmark.png
index 8ce5e42ee..628f05a1f 100644
Binary files a/web/app/assets/images/content/checkmark.png and b/web/app/assets/images/content/checkmark.png differ
diff --git a/web/app/assets/images/content/icon_metronome.png b/web/app/assets/images/content/icon_metronome.png
new file mode 100644
index 000000000..2e8d963b2
Binary files /dev/null and b/web/app/assets/images/content/icon_metronome.png differ
diff --git a/web/app/assets/images/content/icon_metronome_small.png b/web/app/assets/images/content/icon_metronome_small.png
new file mode 100644
index 000000000..b24f11fdb
Binary files /dev/null and b/web/app/assets/images/content/icon_metronome_small.png differ
diff --git a/web/app/assets/images/content/icon_shopping_cart.png b/web/app/assets/images/content/icon_shopping_cart.png
index 24bf9b09f..511fe7c77 100644
Binary files a/web/app/assets/images/content/icon_shopping_cart.png and b/web/app/assets/images/content/icon_shopping_cart.png differ
diff --git a/web/app/assets/images/content/shopping-cart.png b/web/app/assets/images/content/shopping-cart.png
index eef1a4c69..62e6cc2fb 100644
Binary files a/web/app/assets/images/content/shopping-cart.png and b/web/app/assets/images/content/shopping-cart.png differ
diff --git a/web/app/assets/images/up_arrow.png b/web/app/assets/images/up_arrow.png
new file mode 100644
index 000000000..6f3c95d68
Binary files /dev/null and b/web/app/assets/images/up_arrow.png differ
diff --git a/web/app/assets/images/web/back-us-kickstarter.png b/web/app/assets/images/web/back-us-kickstarter.png
new file mode 100644
index 000000000..d2883c8a3
Binary files /dev/null and b/web/app/assets/images/web/back-us-kickstarter.png differ
diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png
new file mode 100644
index 000000000..8650fe322
Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamblaster.png differ
diff --git a/web/app/assets/images/web/button_cta_jamtrack.png b/web/app/assets/images/web/button_cta_jamtrack.png
new file mode 100644
index 000000000..998d89b75
Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamtrack.png differ
diff --git a/web/app/assets/images/web/button_cta_platform.png b/web/app/assets/images/web/button_cta_platform.png
new file mode 100644
index 000000000..dba8d499b
Binary files /dev/null and b/web/app/assets/images/web/button_cta_platform.png differ
diff --git a/web/app/assets/images/web/carousel_community.png b/web/app/assets/images/web/carousel_community.png
new file mode 100644
index 000000000..ace54546b
Binary files /dev/null and b/web/app/assets/images/web/carousel_community.png differ
diff --git a/web/app/assets/images/web/carousel_jamblaster.png b/web/app/assets/images/web/carousel_jamblaster.png
new file mode 100644
index 000000000..636ede273
Binary files /dev/null and b/web/app/assets/images/web/carousel_jamblaster.png differ
diff --git a/web/app/assets/images/web/carousel_overview.png b/web/app/assets/images/web/carousel_overview.png
new file mode 100644
index 000000000..f8493c1f4
Binary files /dev/null and b/web/app/assets/images/web/carousel_overview.png differ
diff --git a/web/app/assets/images/web/thumbnail_buzz.jpg b/web/app/assets/images/web/thumbnail_buzz.jpg
new file mode 100644
index 000000000..737a17873
Binary files /dev/null and b/web/app/assets/images/web/thumbnail_buzz.jpg differ
diff --git a/web/app/assets/images/web/thumbnail_jamblaster.jpg b/web/app/assets/images/web/thumbnail_jamblaster.jpg
new file mode 100644
index 000000000..7eef7d267
Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamblaster.jpg differ
diff --git a/web/app/assets/images/web/thumbnail_jamtracks.jpg b/web/app/assets/images/web/thumbnail_jamtracks.jpg
new file mode 100644
index 000000000..98bb27db5
Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamtracks.jpg differ
diff --git a/web/app/assets/images/web/thumbnail_platform.jpg b/web/app/assets/images/web/thumbnail_platform.jpg
new file mode 100644
index 000000000..8adacef59
Binary files /dev/null and b/web/app/assets/images/web/thumbnail_platform.jpg differ
diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js
index 73997c0a7..61b889eb2 100644
--- a/web/app/assets/javascripts/JamServer.js
+++ b/web/app/assets/javascripts/JamServer.js
@@ -529,10 +529,12 @@
if(server.connecting) {
logger.error("server.connect should never be called if we are already connecting. cancelling.")
+ // XXX should return connectDeferred, but needs to be tested/vetted
return;
}
if(server.connected) {
logger.error("server.connect should never be called if we are already connected. cancelling.")
+ // XXX should return connectDeferred, but needs to be tested/vetted
return;
}
@@ -678,7 +680,12 @@
logger.info("server.send(" + jsMessage + ")");
}
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
- server.socket.send(jsMessage);
+ try {
+ server.socket.send(jsMessage);
+ }
+ catch(err) {
+ logger.warn("error when sending on websocket: " + err)
+ }
} else {
logger.warn("Dropped message because server connection is closed.");
}
diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js
index 92edf171a..a3b75f9b9 100644
--- a/web/app/assets/javascripts/accounts.js
+++ b/web/app/assets/javascripts/accounts.js
@@ -35,6 +35,10 @@
}
}
+ function licenseDetail(userDetail) {
+ return (userDetail.purchased_jamtracks_count==0) ? "You don't currently own any JamTracks" : 'You currently own a license to use ' + userDetail.purchased_jamtracks_count + " JamTracks"
+ }
+
function populateAccount(userDetail) {
var validProfiles = prettyPrintAudioProfiles(context.JK.getGoodConfigMap());
@@ -49,15 +53,18 @@
var $template = $(context._.template($('#template-account-main').html(), {
email: userDetail.email,
name: userDetail.name,
+ licenseDetail: licenseDetail(userDetail),
location : userDetail.location,
session : sessionSummary,
+ paymentMethod: "mastercard",
instruments : prettyPrintInstruments(userDetail.instruments),
photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url),
validProfiles : validProfiles,
invalidProfiles : invalidProfiles,
isNativeClient: gon.isNativeClient,
musician: context.JK.currentUserMusician,
- webcamName: webcam
+ webcamName: webcam,
+ sales_count: userDetail.sales_count
} , { variable: 'data' }));
$('#account-content-scroller').html($template);
@@ -103,8 +110,10 @@
// events for main screen
function events() {
- // wire up main panel clicks
+ // wire up main panel clicks:
$('#account-content-scroller').on('click', '#account-scheduled-sessions-link', function(evt) { evt.stopPropagation(); navToScheduledSessions(); return false; } );
+ $('#account-content-scroller').on('click', '#account-my-jamtracks-link', function(evt) { evt.stopPropagation(); navToMyJamTracks(); return false; } );
+
$('#account-content-scroller').on('click', '#account-edit-identity-link', function(evt) { evt.stopPropagation(); navToEditIdentity(); return false; } );
$('#account-content-scroller').on('click', '#account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } );
$('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } );
@@ -112,12 +121,18 @@
$('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } );
$('#account-content-scroller').on('click', '#account-edit-video-link', function(evt) { evt.stopPropagation(); navToEditVideo(); return false; } );
$('#account-content-scroller').on('avatar_changed', '#profile-avatar', function(evt, newAvatarUrl) { evt.stopPropagation(); updateAvatar(newAvatarUrl); return false; })
+
+ // License dialog:
+ $("#account-content-scroller").on('click', '#account-view-license-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-license-dialog'); return false; } );
+ $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } );
}
function renderAccount() {
+ app.user().done(function() {
rest.getUserDetail()
- .done(populateAccount)
- .error(app.ajaxError)
+ .done(populateAccount)
+ .error(app.ajaxError)
+ })
}
function navToScheduledSessions() {
@@ -125,6 +140,11 @@
window.location = '/client#/account/sessions'
}
+ function navToMyJamTracks() {
+ resetForm();
+ window.location = '/client#/account/jamtracks'
+ }
+
function navToEditIdentity() {
resetForm()
window.location = '/client#/account/identity'
@@ -136,7 +156,7 @@
}
function navToEditSubscriptions() {
-
+ window.location = '/client#/account/profile'
}
function navToEditPayments() {
@@ -153,6 +173,10 @@
window.location = "/client#/account/video"
}
+ function navToPaymentHistory() {
+ window.location = '/client#/account/paymentHistory'
+ }
+
// handle update avatar event
function updateAvatar(avatar_url) {
var photoUrl = context.JK.resolveAvatarUrl(avatar_url);
diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
new file mode 100644
index 000000000..f7bdef10f
--- /dev/null
+++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
@@ -0,0 +1,93 @@
+$ = jQuery
+context = window
+context.JK ||= {}
+
+context.JK.AccountJamTracks = class AccountJamTracks
+ constructor: (@app) ->
+ @rest = context.JK.Rest()
+ @client = context.jamClient
+ @logger = context.JK.logger
+ @screen = null
+ @userId = context.JK.currentUserId;
+
+ initialize:() =>
+ screenBindings =
+ 'beforeShow': @beforeShow
+ 'afterShow': @afterShow
+ @app.bindScreen('account/jamtracks', screenBindings)
+ @screen = $('#account-jamtracks')
+
+ beforeShow:() =>
+ rest.getPurchasedJamTracks({})
+ .done(@populateJamTracks)
+ .fail(@app.ajaxError);
+
+ afterShow:() =>
+
+ populateJamTracks:(data) =>
+ if (data.jamtracks? && data.jamtracks.length > 0)
+ @screen.find(".no-jamtracks-found").addClass("hidden")
+ @appendJamTracks context._.template($('#template-account-jamtrack').html(), {jamtracks:data.jamtracks}, { variable: 'data' })
+ @screen.find('.jamtrack-solo-session').on 'click', @soloSession
+ @screen.find('.jamtrack-group-session').on 'click', @groupSession
+ else
+ @screen.find(".no-jamtracks-found").removeClass("hidden")
+
+ appendJamTracks:(template) =>
+ $('#account-my-jamtracks table tbody').replaceWith template
+
+ soloSession:(e) =>
+ #context.location="client#/createSession"
+ jamRow = $(e.target).parents("tr")
+ @createSession(jamRow.data(), true)
+
+ groupSession:(e) =>
+ #context.location="client#/createSession"
+ jamRow = $(e.target).parents("tr")
+ @createSession(jamRow.data(), false)
+
+ createSession:(sessionData, solo) =>
+ tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient)
+
+ if (context.JK.guardAgainstBrowser(@app))
+ data = {}
+ data.client_id = @app.clientId
+ #data.description = $('#description').val()
+ data.description = "Jam Track Session"
+ data.as_musician = true
+ data.legal_terms = true
+ data.intellectual_property = true
+ data.approval_required = false
+ data.musician_access = !solo
+ data.fan_access = false
+ data.fan_chat = false
+ data.genre = [sessionData.genre]
+ data.genres = [sessionData.genre]
+ # data.genres = context.JK.GenreSelectorHelper.getSelectedGenres('#create-session-genre')
+ # data.musician_access = if $('#musician-access option:selected').val() == 'true' then true else false
+ # data.approval_required = if $('input[name=\'musician-access-option\']:checked').val() == 'true' then true else false
+ # data.fan_access = if $('#fan-access option:selected').val() == 'true' then true else false
+ # data.fan_chat = if $('input[name=\'fan-chat-option\']:checked').val() == 'true' then true else false
+ # if $('#band-list option:selected').val() != ''
+ # data.band = $('#band-list option:selected').val()
+ data.audio_latency = context.jamClient.FTUEGetExpectedLatency().latency
+ data.tracks = tracks
+
+ rest.legacyCreateSession(data).done((response) =>
+ newSessionId = response.id
+ context.location = '/client#/session/' + newSessionId
+ # Re-loading the session settings will cause the form to reset with the right stuff in it.
+ # This is an extra xhr call, but it keeps things to a single codepath
+ loadSessionSettings()
+ context.JK.GA.trackSessionCount data.musician_access, data.fan_access, invitationCount
+ context.JK.GA.trackSessionMusicians context.JK.GA.SessionCreationTypes.create
+ ).fail (jqXHR) =>
+ handled = false
+ if jqXHR.status = 422
+ response = JSON.parse(jqXHR.responseText)
+ if response['errors'] and response['errors']['tracks'] and response['errors']['tracks'][0] == 'Please select at least one track'
+ @app.notifyAlert 'No Inputs Configured', $('You will need to reconfigure your audio device.')
+ handled = true
+ if !handled
+ @app.notifyServerError jqXHR, 'Unable to Create Session'
+
\ No newline at end of file
diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee
new file mode 100644
index 000000000..05fc089d7
--- /dev/null
+++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee
@@ -0,0 +1,179 @@
+$ = jQuery
+context = window
+context.JK ||= {}
+
+context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen
+ LIMIT = 20
+
+ constructor: (@app) ->
+ @logger = context.JK.logger
+ @rest = context.JK.Rest()
+ @screen = null
+ @scroller = null
+ @genre = null
+ @artist = null
+ @instrument = null
+ @availability = null
+ @nextPager = null
+ @noMoreSales = null
+ @currentPage = 0
+ @next = null
+ @tbody = null
+ @rowTemplate = null
+
+ beforeShow:(data) =>
+
+
+ afterShow:(data) =>
+ @refresh()
+
+ events:() =>
+ @backBtn.on('click', @onBack)
+
+ onBack:() =>
+ window.location = '/client#/account'
+ return false
+
+ clearResults:() =>
+ @currentPage = 0
+ @tbody.empty()
+ @noMoreSales.hide()
+ @next = null
+
+
+
+ refresh:() =>
+ @currentQuery = this.buildQuery()
+ @rest.getSalesHistory(@currentQuery)
+ .done(@salesHistoryDone)
+ .fail(@salesHistoryFail)
+
+
+ renderPayments:(response) =>
+ if response.entries? && response.entries.length > 0
+ for sale in response.entries
+ amt = sale.recurly_total_in_cents
+ amt = 0 if !amt?
+
+ original_total = sale.state.original_total
+ refund_total = sale.state.refund_total
+
+ refund_state = null
+ if original_total != 0 # the enclosed logic does not work for free purchases
+ if refund_total == original_total
+ refund_state = 'refunded'
+ else if refund_total != 0 and refund_total < original_total
+ refund_state = 'partial refund'
+
+
+ displayAmount = (amt/100).toFixed(2)
+ status = 'paid'
+
+ if sale.state.voided
+ status = 'voided'
+ displayAmount = (0).toFixed(2)
+ else if refund_state?
+ status = refund_state
+ displayAmount = (amt/100).toFixed(2) + " (refunded: #{(refund_total/100).toFixed(2)})"
+
+ description = []
+ for line_item in sale.line_items
+ description.push(line_item.product_info?.name)
+
+ payment = {
+ date: context.JK.formatDate(sale.created_at, true)
+ amount: displayAmount
+ status: status
+ payment_method: 'Credit Card',
+ description: description.join(', ')
+ }
+
+ tr = $(context._.template(@rowTemplate, payment, { variable: 'data' }));
+ @tbody.append(tr);
+ else
+ tr = "
No payments found
"
+ @tbody.append(tr);
+
+ salesHistoryDone:(response) =>
+
+ # Turn in to HTML rows and append:
+ #@tbody.html("")
+ @next = response.next_page
+ @renderPayments(response)
+ if response.next_page == null
+ # if we less results than asked for, end searching
+ @scroller.infinitescroll 'pause'
+ @logger.debug("end of history")
+ if @currentPage > 0
+ @noMoreSales.show()
+ # there are bugs with infinitescroll not removing the 'loading'.
+ # it's most noticeable at the end of the list, so whack all such entries
+ $('.infinite-scroll-loader').remove()
+ else
+ @currentPage++
+ this.buildQuery()
+ this.registerInfiniteScroll()
+
+
+ salesHistoryFail:(jqXHR)=>
+ @noMoreSales.show()
+ @app.notifyServerError jqXHR, 'Payment History Unavailable'
+
+ defaultQuery:() =>
+ query =
+ per_page: LIMIT
+ page: @currentPage+1
+ if @next
+ query.since = @next
+ query
+
+ buildQuery:() =>
+ @currentQuery = this.defaultQuery()
+
+
+ registerInfiniteScroll:() =>
+ that = this
+ @scroller.infinitescroll {
+ behavior: 'local'
+ navSelector: '#account-payment-history .btn-next-pager'
+ nextSelector: '#account-payment-history .btn-next-pager'
+ binder: @scroller
+ dataType: 'json'
+ appendCallback: false
+ prefill: false
+ bufferPx: 100
+ loading:
+ msg: $('
Loading ...
')
+ img: '/assets/shared/spinner.gif'
+ path: (page) =>
+ '/api/sales?' + $.param(that.buildQuery())
+
+ }, (json, opts) =>
+ this.salesHistoryDone(json)
+ @scroller.infinitescroll 'resume'
+
+ initialize:() =>
+ screenBindings =
+ 'beforeShow': this.beforeShow
+ 'afterShow': this.afterShow
+ @app.bindScreen 'account/paymentHistory', screenBindings
+ @screen = $('#account-payment-history')
+ @scroller = @screen.find('.content-body-scroller')
+ @nextPager = @screen.find('a.btn-next-pager')
+ @noMoreSales = @screen.find('.end-of-payments-list')
+ @tbody = @screen.find("table.payment-table tbody")
+ @rowTemplate = $('#template-payment-history-row').html()
+ @backBtn = @screen.find('.back')
+
+ if @screen.length == 0
+ throw new Error('@screen must be specified')
+ if @scroller.length == 0
+ throw new Error('@scroller must be specified')
+ if @tbody.length == 0
+ throw new Error('@tbody must be specified')
+ if @noMoreSales.length == 0
+ throw new Error('@noMoreSales must be specified')
+
+ this.events()
+
+
diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js
index 037c2c1b7..bc065cf22 100644
--- a/web/app/assets/javascripts/application.js
+++ b/web/app/assets/javascripts/application.js
@@ -35,6 +35,8 @@
//= require jquery.browser
//= 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/checkout_order.js b/web/app/assets/javascripts/checkout_order.js
new file mode 100644
index 000000000..fbe41c665
--- /dev/null
+++ b/web/app/assets/javascripts/checkout_order.js
@@ -0,0 +1,364 @@
+(function (context, $) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.CheckoutOrderScreen = function (app) {
+
+ var EVENTS = context.JK.EVENTS;
+ var logger = context.JK.logger;
+ var rest = context.JK.Rest();
+ var jamTrackUtils = context.JK.JamTrackUtils;
+ var checkoutUtils = context.JK.CheckoutUtilsInstance;
+
+ var $screen = null;
+ var $navigation = null;
+ var $templateOrderContent = null;
+ var $templatePurchasedJamTrack = null;
+ var $orderPanel = null;
+ var $thanksPanel = null;
+ var $jamTrackInBrowser = null;
+ var $purchasedJamTrack = null;
+ var $purchasedJamTrackHeader = null;
+ var $purchasedJamTracks = null;
+ var $orderContent = null;
+ var userDetail = null;
+ var step = null;
+ var downloadJamTracks = [];
+ var purchasedJamTracks = null;
+ var purchasedJamTrackIterator = 0;
+ var $backBtn = null;
+ var $orderPrompt = null;
+ var $emptyCartPrompt = null;
+ var $noAccountInfoPrompt = null;
+
+
+ function beforeShow() {
+ beforeShowOrder();
+ }
+
+
+ function afterShow(data) {
+
+ }
+
+
+ function beforeHide() {
+ if(downloadJamTracks) {
+ context._.each(downloadJamTracks, function(downloadJamTrack) {
+ downloadJamTrack.destroy();
+ downloadJamTrack.root.remove();
+ })
+
+ downloadJamTracks = [];
+ }
+ purchasedJamTracks = null;
+ purchasedJamTrackIterator = 0;
+ }
+
+ function beforeShowOrder() {
+ $orderPrompt.addClass('hidden')
+ $emptyCartPrompt.addClass('hidden')
+ $noAccountInfoPrompt.addClass('hidden')
+ $orderPanel.removeClass("hidden")
+ $thanksPanel.addClass("hidden")
+ $screen.find(".place-order").addClass('disabled').off('click', placeOrder)
+ $("#order_error").text('').addClass("hidden")
+ step = 3;
+ renderNavigation();
+ populateOrderPage();
+ }
+
+
+ function populateOrderPage() {
+ clearOrderPage();
+
+ rest.getShoppingCarts()
+ .done(function(carts) {
+ rest.getBillingInfo()
+ .done(function(billingInfo) {
+ renderOrderPage(carts, billingInfo)
+ })
+ .fail(function(jqXHR) {
+ if(jqXHR.status == 404) {
+ // no account for this user
+ $noAccountInfoPrompt.removeClass('hidden')
+ app.notify({ title: "No account information",
+ text: "Please restart the checkout process." },
+ null,
+ true);
+ }
+ })
+ })
+ .fail(app.ajaxError);
+ }
+
+
+ function renderOrderPage(carts, recurlyAccountInfo) {
+ logger.debug("rendering order page")
+ var data = {}
+
+ var sub_total = 0.0
+ var taxes = 0.0
+ $.each(carts, function(index, cart) {
+ sub_total += parseFloat(cart.product_info.real_price)
+ });
+ if(carts.length == 0) {
+ data.grand_total = '-.--'
+ data.sub_total = '-.--'
+ data.taxes = '-.--'
+ data.shipping_handling = '-.--'
+ }
+ else {
+ data.grand_total = 'Calculating...'
+ data.sub_total = '$' + sub_total.toFixed(2)
+ data.taxes = 'Calculating...'
+ data.shipping_handling = '$0.00'
+ }
+
+ data.carts = carts
+ data.billing_info = recurlyAccountInfo.billing_info
+ data.shipping_info = recurlyAccountInfo.address
+
+ data.shipping_as_billing = true; //jamTrackUtils.compareAddress(data.billing_info, data.shipping_info);
+
+ var orderContentHtml = $(
+ context._.template(
+ $templateOrderContent.html(),
+ data,
+ {variable: 'data'}
+ )
+ )
+
+ $orderContent.append(orderContentHtml)
+ $orderPanel.find(".change-payment-info").on('click', moveToPaymentInfo)
+
+ var $placeOrder = $screen.find(".place-order")
+ if(carts.length == 0) {
+ $orderPrompt.addClass('hidden')
+ $emptyCartPrompt.removeClass('hidden')
+ $noAccountInfoPrompt.addClass('hidden')
+ $placeOrder.addClass('disabled')
+ }
+ else {
+ logger.debug("cart has " + carts.length + " items in it")
+ $orderPrompt.removeClass('hidden')
+ $emptyCartPrompt.addClass('hidden')
+ $noAccountInfoPrompt.addClass('hidden')
+ $placeOrder.removeClass('disabled').on('click', placeOrder)
+
+ var planPricing = {}
+
+
+ var priceElement = $screen.find('.order-right-page .plan.jamtrack')
+
+ if(priceElement.length == 0) {
+ logger.error("unable to find price element for jamtrack");
+ app.notify({title: "Error Encountered", text: "Unable to find plan info for jam track"})
+ return false;
+ }
+
+ logger.debug("creating recurly pricing element for plan: " + gon.recurly_tax_estimate_jam_track_plan)
+
+ var effectiveQuantity = 0
+
+ context._.each(carts, function(cart) {
+ effectiveQuantity += cart.product_info.quantity - cart.product_info.marked_for_redeem
+ })
+
+ var pricing = context.recurly.Pricing();
+ pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan;
+ pricing.resolved = false;
+ pricing.effective_quantity = 1
+
+ // this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it.
+ pricing.on('change', function(price) {
+
+ var totalTax = 0;
+ var totalPrice = 0;
+
+ var unitTax = Number(pricing.price.now.tax) * effectiveQuantity;
+ totalTax += unitTax;
+
+ var totalUnitPrice = Number(pricing.price.now.total) * effectiveQuantity;
+ totalPrice += totalUnitPrice;
+
+ $screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
+ $screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
+ })
+
+ pricing.attach(priceElement.eq(0))
+ }
+ }
+
+ function moveToPaymentInfo() {
+ context.location = '/client#/checkoutPayment';
+ return false;
+ }
+
+ function placeOrder(e) {
+ e.preventDefault();
+ $screen.find(".place-order").off('click').addClass('disabled')
+ $("#order_error").text('').addClass("hidden")
+ rest.placeOrder()
+ .done(moveToThanks)
+ .fail(orderErrorHandling);
+ }
+
+
+ function orderErrorHandling(xhr, ajaxOptions, thrownError) {
+ if (xhr && xhr.responseJSON) {
+ var message = "Error submitting payment: "
+ $.each(xhr.responseJSON.errors, function (key, error) {
+ message += key + ": " + error
+ })
+ $("#order_error").text(message).removeClass("hidden")
+ }
+ else {
+ $("#order_error").text(xhr.responseText).removeClass("hidden")
+ }
+ $screen.find(".place-order").on('click', placeOrder).removeClass('disabled')
+ }
+
+ function moveToThanks(purchaseResponse) {
+ checkoutUtils.deletePreserveBillingInfo()
+ $("#order_error").addClass("hidden")
+ $orderPanel.addClass("hidden")
+ $thanksPanel.removeClass("hidden")
+ jamTrackUtils.checkShoppingCart()
+ handleJamTracksPurchased(purchaseResponse.jam_tracks)
+ }
+
+ function handleJamTracksPurchased(jamTracks) {
+ // were any JamTracks purchased?
+ var jamTracksPurchased = jamTracks && jamTracks.length > 0;
+ if(jamTracksPurchased) {
+ if(gon.isNativeClient) {
+ startDownloadJamTracks(jamTracks)
+ }
+ else {
+ $jamTrackInBrowser.removeClass('hidden');
+ }
+ }
+ }
+
+ function startDownloadJamTracks(jamTracks) {
+ // there can be multiple purchased JamTracks, so we cycle through them
+
+ purchasedJamTracks = jamTracks;
+
+ // populate list of jamtracks purchased, that we will iterate through graphically
+ context._.each(jamTracks, function(jamTrack) {
+ var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small');
+ var $purchasedJamTrack = $(context._.template(
+ $templatePurchasedJamTrack.html(),
+ jamTrack,
+ {variable: 'data'}
+ ));
+
+ $purchasedJamTracks.append($purchasedJamTrack)
+
+ // show it on the page
+ $purchasedJamTrack.append(downloadJamTrack.root)
+
+ downloadJamTracks.push(downloadJamTrack)
+ })
+
+ iteratePurchasedJamTracks();
+ }
+
+ function iteratePurchasedJamTracks() {
+ if(purchasedJamTrackIterator < purchasedJamTracks.length ) {
+ var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++];
+
+ // make sure the 'purchasing JamTrack' section can be seen
+ $purchasedJamTrack.removeClass('hidden');
+
+ // the widget indicates when it gets to any transition; we can hide it once it reaches completion
+ $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) {
+
+ if(data.state == downloadJamTrack.states.synchronized) {
+ logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;")
+ //downloadJamTrack.root.remove();
+ downloadJamTrack.destroy();
+
+ // go to the next JamTrack
+ iteratePurchasedJamTracks()
+ }
+ })
+
+ logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing")
+
+ // kick off the download JamTrack process
+ downloadJamTrack.init()
+
+ // XXX style-test code
+ // downloadJamTrack.transitionError("package-error", "The server failed to create your package.")
+
+ }
+ else {
+ logger.debug("done iterating over purchased JamTracks")
+ $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.')
+ }
+ }
+
+ function clearOrderPage() {
+ $orderContent.empty();
+ }
+
+ function renderNavigation() {
+ $navigation.html("");
+ var navigationHtml = $(
+ context._.template(
+ $('#template-checkout-navigation').html(),
+ {current: step},
+ {variable: 'data'}
+ )
+ );
+
+ $navigation.append(navigationHtml);
+ }
+
+ function events() {
+ $backBtn.on('click', function(e) {
+ e.preventDefault();
+
+ context.location = '/client#/checkoutPayment'
+ })
+ }
+
+ function initialize() {
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow,
+ 'beforeHide': beforeHide
+ };
+ app.bindScreen('checkoutOrder', screenBindings);
+
+ $screen = $("#checkoutOrderScreen");
+ $navigation = $screen.find(".checkout-navigation-bar");
+ $templateOrderContent = $("#template-order-content");
+ $templatePurchasedJamTrack = $('#template-purchased-jam-track');
+ $orderPanel = $screen.find(".order-panel");
+ $thanksPanel = $screen.find(".thanks-panel");
+ $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser");
+ $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track");
+ $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header");
+ $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list")
+ $backBtn = $screen.find('.back');
+ $orderPrompt = $screen.find('.order-prompt');
+ $emptyCartPrompt = $screen.find('.empty-cart-prompt');
+ $noAccountInfoPrompt = $screen.find('.no-account-info-prompt');
+ $orderContent = $orderPanel.find(".order-content");
+
+ if ($screen.length == 0) throw "$screen must be specified";
+ if ($navigation.length == 0) throw "$navigation must be specified";
+
+ events();
+ }
+
+ this.initialize = initialize;
+
+ return this;
+ }
+})
+(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js
new file mode 100644
index 000000000..10afdb70d
--- /dev/null
+++ b/web/app/assets/javascripts/checkout_payment.js
@@ -0,0 +1,706 @@
+(function(context,$) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.CheckoutPaymentScreen = function(app) {
+
+ var EVENTS = context.JK.EVENTS;
+ var logger = context.JK.logger;
+ var jamTrackUtils = context.JK.JamTrackUtils;
+ var checkoutUtils = context.JK.CheckoutUtilsInstance;
+
+ var $screen = null;
+ var $navigation = null;
+ var $billingInfo = null;
+ var $shippingInfo = null;
+ var $paymentMethod = null;
+ var $shippingAddress = null;
+ var $shippingAsBilling = null;
+ var $paymentInfoPanel = null;
+ var $accountSignup = null;
+ var userDetail = null;
+ var step = null;
+ var billing_info = null;
+ var shipping_info = null;
+ var shipping_as_billing = null;
+ var $reuseExistingCard = null;
+ var $reuseExistingCardChk = null;
+ var $existingCardEndsWith = null;
+ var $newCardInfo = null;
+ var selectCountry = null;
+ var selectCountryLoaded = false;
+ var $freeJamTrackPrompt = null;
+ var $noFreeJamTrackPrompt = null;
+ var $alreadyEnteredJamTrackPrompt = null;
+
+ function afterShow() {
+
+ beforeShowPaymentInfo();
+ }
+
+ function beforeShowPaymentInfo() {
+ step = 2;
+ renderNavigation();
+ renderAccountInfo();
+ }
+
+
+ function renderAccountInfo() {
+
+ $paymentInfoPanel.addClass('hidden')
+ $reuseExistingCard.addClass('hidden');
+ $newCardInfo.removeClass('hidden');
+ $freeJamTrackPrompt.addClass('hidden');
+ $noFreeJamTrackPrompt.addClass('hidden');
+ $alreadyEnteredJamTrackPrompt.addClass('hidden')
+ $("#payment_error").addClass('hidden').text('')
+
+
+ if(checkoutUtils.shouldPreserveBillingInfo()) {
+ logger.debug("showing 'user has already set up billing info' because 'preserve billing' session is active")
+ checkoutUtils.refreshPreserveBillingInfo()
+ $alreadyEnteredJamTrackPrompt.removeClass('hidden')
+ return
+ }
+
+ $paymentInfoPanel.removeClass('hidden')
+
+
+ var selectCountryReady = selectCountry.ready();
+ if(!selectCountryReady) {
+ // one time init of country dropdown
+ selectCountryReady = selectCountry.load('US', null, null);
+ }
+
+ selectCountryReady.done(function() {
+ var user = rest.getUserDetail()
+ if(user) {
+ user.done(populateAccountInfo).error(app.ajaxError);
+ }
+ else {
+ $reuseExistingCardChk.iCheck('uncheck').attr('checked', false)
+ if(gon.global.one_free_jamtrack_per_user) {
+ $freeJamTrackPrompt.removeClass('hidden')
+ }
+ else {
+ $noFreeJamTrackPrompt.removeClass('hidden')
+ }
+ }
+ })
+ }
+
+ function populateAccountInfo(user) {
+ userDetail = user;
+
+ $reuseExistingCardChk.iCheck(userDetail.reuse_card && userDetail.has_recurly_account ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card)
+
+ // show appropriate prompt text based on whether user has a free jamtrack
+ if(user.free_jamtrack) {
+ $freeJamTrackPrompt.removeClass('hidden')
+ }
+ else {
+ $noFreeJamTrackPrompt.removeClass('hidden')
+ }
+
+ if (userDetail.has_recurly_account) {
+
+ rest.getBillingInfo()
+ .done(function(response) {
+
+ if(userDetail.reuse_card) {
+ $reuseExistingCard.removeClass('hidden');
+ toggleReuseExistingCard.call($reuseExistingCardChk)
+ $existingCardEndsWith.text(response.billing_info.last_four);
+ }
+
+ var isSameAsShipping = true // jamTrackUtils.compareAddress(response.billing_info, response.address);
+
+ $shippingAsBilling.iCheck(isSameAsShipping ? 'check' : 'uncheck').attr('checked', isSameAsShipping)
+
+ $billingInfo.find("#billing-first-name").val(response.billing_info.first_name);
+ $billingInfo.find("#billing-last-name").val(response.billing_info.last_name);
+ $billingInfo.find("#billing-address1").val(response.billing_info.address1);
+ $billingInfo.find("#billing-address2").val(response.billing_info.address2);
+ $billingInfo.find("#billing-city").val(response.billing_info.city);
+ $billingInfo.find("#billing-state").val(response.billing_info.state);
+ $billingInfo.find("#billing-zip").val(response.billing_info.zip);
+ $billingInfo.find("#billing-country").val(response.billing_info.country);
+
+ //$shippingAddress.find("#shipping-first-name").val(response.billing_info.first_name);
+ //$shippingAddress.find("#shipping-last-name").val(response.billing_info.last_name);
+ //$shippingAddress.find("#shipping-address1").val(response.address.address1);
+ //$shippingAddress.find("#shipping-address2").val(response.address.address2);
+ //$shippingAddress.find("#shipping-city").val(response.address.city);
+ //$shippingAddress.find("#shipping-state").val(response.address.state);
+ //$shippingAddress.find("#shipping-zip").val(response.address.zip);
+ //$shippingAddress.find("#shipping-country").val(response.address.country);
+
+ })
+ .error(app.ajaxError);
+ }
+ else {
+ $billingInfo.find("#billing-first-name").val(userDetail.first_name);
+ $billingInfo.find("#billing-last-name").val(userDetail.last_name);
+ $billingInfo.find("#billing-city").val(userDetail.city);
+ $billingInfo.find("#billing-state").val(userDetail.state);
+ $billingInfo.find("#billing-country").val(userDetail.country);
+
+ $shippingAddress.find("#shipping-first-name").val(userDetail.first_name);
+ $shippingAddress.find("#shipping-last-name").val(userDetail.last_name);
+ $shippingAddress.find("#shipping-city").val(userDetail.city);
+ $shippingAddress.find("#shipping-state").val(userDetail.state);
+ $shippingAddress.find("#shipping-country").val(userDetail.country);
+ }
+ }
+
+ function beforeShow(data) {
+ }
+
+ function beforeHide() {
+
+ }
+
+ function beforeHide() {
+
+ }
+
+ // TODO: Refactor: this function is long and fraught with many return points.
+ function next(e) {
+
+ // check if we are showing the 'change payment info' pass; if so, just move on to checkoutOrder
+ if($alreadyEnteredJamTrackPrompt.is(':visible')) {
+ logger.debug("skipping payment logic ")
+ context.location = '/client#/checkoutOrder'
+ return false;
+ }
+ $paymentInfoPanel.find('.error-text').remove();
+ $paymentInfoPanel.find('.error').removeClass('error');
+ e.preventDefault();
+ $("#payment_error").addClass("hidden").text('')
+
+ var reuse_card_this_time = $reuseExistingCardChk.is(':checked');
+ var reuse_card_next_time = $paymentMethod.find('#save-card').is(':checked');
+
+ // validation
+ var billing_first_name = $billingInfo.find("#billing-first-name").val();
+ var billing_last_name = $billingInfo.find("#billing-last-name").val();
+ var billing_address1 = $billingInfo.find("#billing-address1").val();
+ var billing_address2 = $billingInfo.find("#billing-address2").val();
+ var billing_city = $billingInfo.find("#billing-city").val();
+ var billing_state = $billingInfo.find("#billing-state").val();
+ var billing_zip = $billingInfo.find("#billing-zip").val();
+ var billing_country = $billingInfo.find("#billing-country").val();
+
+ var billingInfoValid = true;
+ if (!billing_first_name) {
+ $billingInfo.find('#divBillingFirstName .error-text').remove();
+ $billingInfo.find('#divBillingFirstName').addClass("error").addClass("transparent");
+ $billingInfo.find('#billing-first-name').after("
+ {% if (data.sales_count == 0) { %}
+ You have made no purchases.
+ {% } else { %}
+ You have made {{data.sales_count}} purchase{{data.sales_count == 1 ? '' : 's'}}.
+ {% } %}
+
<% unless @welcome_page %>
- <%= content_tag(:div, content_tag(:h1,'Play music together over the Internet as if in the same room'), :class => "landing-tag") %>
- <%= content_tag(:div,'',:class => "clearall") %>
+
+
Live music platform & social network for musicians
diff --git a/web/app/views/shared/_recurly.html.slim b/web/app/views/shared/_recurly.html.slim
new file mode 100644
index 000000000..d22bdabbd
--- /dev/null
+++ b/web/app/views/shared/_recurly.html.slim
@@ -0,0 +1,4 @@
+script src="https://js.recurly.com/v3/recurly.js"
+
+javascript:
+ recurly.configure(gon.global.recurly_public_api_key)
\ No newline at end of file
diff --git a/web/app/views/spikes/download_jam_track.html.slim b/web/app/views/spikes/download_jam_track.html.slim
new file mode 100644
index 000000000..ade3744b5
--- /dev/null
+++ b/web/app/views/spikes/download_jam_track.html.slim
@@ -0,0 +1,44 @@
+= javascript_include_tag "download_jamtrack"
+= render "clients/download_jamtrack_templates"
+= stylesheet_link_tag "client/downloadJamTrack"
+
+- provide(:title, 'Download Jam Track Widget')
+
+.content-wrapper
+ h2 Jam Track State Widget
+
+ h3 Possible States
+ ul
+ li synchronized
+ li no_client
+ li packaging
+ li downloading
+ li keying
+ li initial
+ li errored
+ #widget
+
+javascript:
+ var initialized = false;
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+ window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() {
+ if(initialized) {
+ return;
+ }
+ initialized = true
+
+ setTimeout(function() {
+ window.downloadJamTrack = new JK.DownloadJamTrack(data.app, {id: gon.jamTrackId, jam_track_right_id: gon.jamTrackRightId, name: 'Back in Black'}, gon.size)
+ downloadJamTrack.init()
+ $('#widget').append(window.downloadJamTrack.root)
+
+ if (gon.switchState == 'errored') {
+ downloadJamTrack.transitionError("package-error", "The server failed to create your package.")
+ }
+ else if (gon.switchState) {
+ downloadJamTrack.transition(downloadJamTrack.states[gon.switchState]);
+ }
+ }, 1)
+
+ })
+ })
\ No newline at end of file
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/app/views/spikes/recording_source.html.slim b/web/app/views/spikes/recording_source.html.slim
new file mode 100644
index 000000000..ba7c0fb5b
--- /dev/null
+++ b/web/app/views/spikes/recording_source.html.slim
@@ -0,0 +1,19 @@
+= javascript_include_tag "site_validator"
+div style="width:50%"
+ = render "clients/site_validator", site_type: params[:site_type]
+= stylesheet_link_tag "client/site_validator"
+
+= select_tag "site_type", options_for_select(Utils::RECORDING_SOURCES, params[:site_type])
+
+javascript:
+ var initialized = false;
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+ setTimeout(function() {
+ window.site_validator = new JK.SiteValidator('#{params[:site_type] || 'rec_youtube'}');
+ site_validator.init();
+ $('#validate_input_'+'#{params[:site_type] || 'url'}').val('jonathankolyer');
+ }, 1)
+ });
+ $('#site_type').change(function(){
+ location.href = 'recording_source?site_type='+$(this).val();
+ });
diff --git a/web/app/views/spikes/site_validate.html.slim b/web/app/views/spikes/site_validate.html.slim
new file mode 100644
index 000000000..ccd9d8749
--- /dev/null
+++ b/web/app/views/spikes/site_validate.html.slim
@@ -0,0 +1,19 @@
+= javascript_include_tag "site_validator"
+div style="width:50%"
+ = render "clients/site_validator", site_type: params[:site_type] || 'url'
+= stylesheet_link_tag "client/site_validator"
+
+= select_tag "site_type", options_for_select(Utils::SITE_TYPES, params[:site_type] || 'url')
+
+javascript:
+ var initialized = false;
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+ setTimeout(function() {
+ window.site_validator = new JK.SiteValidator('#{params[:site_type] || 'url'}');
+ site_validator.init();
+ $('#validate_input_'+'#{params[:site_type] || 'url'}').val('jonathankolyer');
+ }, 1)
+ });
+ $('#site_type').change(function(){
+ location.href = 'site_validate?site_type='+$(this).val();
+ });
diff --git a/web/app/views/users/_download_templates.html.erb b/web/app/views/users/_download_templates.html.erb
deleted file mode 100644
index 6b8fad6e4..000000000
--- a/web/app/views/users/_download_templates.html.erb
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/web/app/views/users/_download_templates.html.slim b/web/app/views/users/_download_templates.html.slim
new file mode 100644
index 000000000..21d886117
--- /dev/null
+++ b/web/app/views/users/_download_templates.html.slim
@@ -0,0 +1,46 @@
+
+script type="text/template" id="client-download-blurb-contents"
+ .downloads
+
+ a href="{{data.uri}}" class="current-os-download" data-platform="{{data.platform}}"
+
+ .downloads-container
+
+ h5 SYSTEM REQUIREMENTS:
+ | {% if(data.platform == "Win32") { %}
+ ul.windows-requirements
+ li Windows 7 or 8, 64-bit (32-bit not supported)
+ li Dual core processor or higher
+ li 75MB hard disk space for app
+ li External audio interface recommended (but you can start with built-in mic and & headphone jack)
+ li Ethernet port for real-time online sessions (WiFi not recommended)
+ li Broadband Internet service with 1Mbps uplink bandwidth for real-time online sessions
+ | {% } else if(data.platform == "MacOSX") { %}
+ ul.mac-requirements
+ li Mac OS X 10.7 or higher, 64-bit
+ li Dual-core processor or higher
+ li 75MB hard disk space for app
+ li External audio interface recommended (but you can start with built-in mic and & headphone jack)
+ li Ethernet port for real-time online sessions (WiFi not recommended)
+ li Broadband Internet service with 1Mbps uplink bandwidth for real-time online sessions
+ | {% } else { %}
+ ul.linux-requirements
+ li Linux is not yet supported
+ | {% } %}
+
+
+
+ .hidden.hidden-images
+ = image_tag("content/button_download_mac.png", :alt => "download mac", :size => "348x92", "data-purpose" => "mac")
+ = image_tag("content/button_download_windows.png", :alt => "download windows", :size => "348x92", "data-purpose" => "windows")
+ = image_tag("content/button_download_linux.png", :alt => "download linux", :size => "348x92", "data-purpose" => "linux")
+
+script type="text/template" id="client-download-select-others"
+ .download-box
+ .download-others
+ a.choose-other-platform href="#" data-order="1" data-platform="{{data.platform1}}"
+ | Need a different version?
+ br
+ | Click here for to get JamKazam
+ br
+ | for {{data.platformDisplay1}}
\ No newline at end of file
diff --git a/web/app/views/users/_downloads.html.erb b/web/app/views/users/_downloads.html.erb
deleted file mode 100644
index 0fe47521a..000000000
--- a/web/app/views/users/_downloads.html.erb
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-<% content_for :after_black_bar do %>
-
-
SYSTEM REQUIREMENTS:
-
A short summary of requirements follows. For a more detailed explanation of system requirements, please review our Minimum System Requirements knowledgebase article.
-
-
Windows 64-bit operating system (Win 7 & 8 tested, Win XP and Vista like to work but not officially supported
-
Dual-core processor or higher
-
Ethernet port for Internet (we strongly advise that you not use Wi-Fi)
-
74MB hard disk space for app, plus any space needed for recordings
-
Audio interface (best to use an audio interface device that gets your music into your computer, else can use built-in mic & headphones on your computer to get started)
-
Broadband Internet service with 1Mbps uplink bandwidth
-
-
-
Mac OS X 64-bit operating system 10.7 or higher
-
Dual-core processor or higher
-
Ethernet port for Internet (we strongly advise that you not use Wi-Fi)
-
74MB hard disk space for app, plus any space needed for recordings
-
Audio interface (best to use an audio interface device that gets your music into your computer, else can use built-in mic & headphones on your computer to get started)
-
Broadband Internet service with 1Mbps uplink bandwidth
-
-
-
Linux is not yet supported
-
-
-<%end%>
-
-<%= render "users/download_templates" %>
-
-
diff --git a/web/app/views/users/_downloads.html.slim b/web/app/views/users/_downloads.html.slim
new file mode 100644
index 000000000..6c3977e40
--- /dev/null
+++ b/web/app/views/users/_downloads.html.slim
@@ -0,0 +1,64 @@
+// used by congrats_musician, and downloads
+- provide(:page_name, 'downloads')
+- provide(:title, 'Download')
+
+.w100
+ .download-app
+ .spinner-large
+
+ h2.create-account-header
+ .badge-number 2
+ | Download the free JamKazam app
+
+ .download-content
+ .download-entreaty
+
+ p You need the JamKazam application to:
+ ul
+ li Play music with others in real time on the JamKazam platform
+ li Make audio recordings and share them via Facebook or URL
+ li Make video recordings and share them via YouTube or URL
+ li Live broadcast your sessions to family, friends, and fans
+ li Have full control over your JamTracks multi-track recordings
+
+ p.click-to-download Click the button below to download the JamKazam application installer.
+ .downloads-blurb
+
+ .jamtracks
+
+ h2.shop-jamtracks
+ .badge-number 3
+ | Get your free JamTrack
+ span.special-value
+ | ($1.99 value)
+
+
+ .jamtrack-content
+ .jamtrack-entreaty
+
+ p JamTracks are multi-track pro recordings you can use to:
+ ul
+ li Solo any part to hear and learn it
+ li Mute the part 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 Go online to play real time sessions, with others playing parts
+ p
+ | Watch the video below to learn more. Then click the button to shop
+ | for your first JamTrack - free! Add it to your shopping cart, and we'll
+ | make it free during the checkout process. Free offer good for 1 week only!
+ .video-container
+ iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen
+
+ a.go-jamtrack-shopping href="/client#/jamtrackBrowse" rel="external"
+ | Shop for free
+ br
+ | JamTrack now!
+
+ br clear="all"
+
+
+
+ = render "users/download_templates"
+
+
diff --git a/web/app/views/users/already_signed_up.html.erb b/web/app/views/users/already_signed_up.html.erb
index e0cf7635a..cff7f1479 100644
--- a/web/app/views/users/already_signed_up.html.erb
+++ b/web/app/views/users/already_signed_up.html.erb
@@ -1,4 +1,5 @@
<% provide(:title, 'Already Signed Up') %>
+<% provide(:description, 'You have already signed up with JamKazam') %>
diff --git a/web/app/views/users/congratulations_fan.html.erb b/web/app/views/users/congratulations_fan.html.erb
index 7c0735c9d..77a714e10 100644
--- a/web/app/views/users/congratulations_fan.html.erb
+++ b/web/app/views/users/congratulations_fan.html.erb
@@ -1,13 +1,7 @@
<% provide(:title, 'Congratulations') %>
+<% provide(:description, 'Congratulations on becoming a new JamKazam member!') %>
+<%= render "users/downloads" %>
-
-
Congratulations!
-
-
You have successfully registered as a JamKazam fan.
-<% else %>
- <%= render "users/downloads" %>
-<% end %>
-
+<% provide(:description, 'Congratulations on becoming a new JamKazam member!') %>
+<%= render "users/downloads" %>
diff --git a/web/app/views/users/downloads.html.erb b/web/app/views/users/downloads.html.erb
index ddd7d5f7c..021927b78 100644
--- a/web/app/views/users/downloads.html.erb
+++ b/web/app/views/users/downloads.html.erb
@@ -1,7 +1,8 @@
<% provide(:title, 'Downloads') %>
+<% provide(:description, 'Download the JamKazam app for Windows or Mac to play music online with others.') %>
<%= render "users/downloads" %>
\ No newline at end of file
diff --git a/web/app/views/users/finalize_update_email.html.erb b/web/app/views/users/finalize_update_email.html.erb
index df7f19506..98ac7add7 100644
--- a/web/app/views/users/finalize_update_email.html.erb
+++ b/web/app/views/users/finalize_update_email.html.erb
@@ -1,4 +1,6 @@
<% provide(:title, 'Email Change Confirmation') %>
+<% provide(:description, 'Your email has been changed successfully') %>
+
diff --git a/web/app/views/users/home.html.slim b/web/app/views/users/home.html.slim
new file mode 100644
index 000000000..d253ee8d8
--- /dev/null
+++ b/web/app/views/users/home.html.slim
@@ -0,0 +1,64 @@
+- provide(:page_name, 'home')
+- provide(:description, 'Play music with others online in real time. Play with multitrack audio of your favorite music. Musician community.')
+
+.home-column
+ = link_to image_tag("web/thumbnail_jamtracks.jpg", :alt => "JamTracks explanatory video"), '#', class: "jamtracks-video video-item", 'data-video-header' => 'JamTracks', 'data-video-url' => 'http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1'
+ h3 Complete, Multi-Track Backing Tracks
+
+ p
+ strong 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.
+
+ = link_to image_tag("web/button_cta_jamtrack.png", width: 234, height:57), '/client#/jamtrackBrowse', class: 'cta-button jamtracks'
+ br clear="all"
+ .extra-links
+ .learn-more
+ a.learn-more-jamtracks href='/products/jamtracks' learn more
+
+.home-column
+ = link_to image_tag("web/thumbnail_platform.jpg", :alt => "JamKazam explanatory video!"), '#', class: "platform-video video-item", 'data-video-header' => 'JamKazam Platform', 'data-video-url' => 'http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1'
+ h3 Online Music Collaboration Platform
+
+ p
+ strong 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.
+
+ = link_to image_tag("web/button_cta_platform.png", width: 234, height: 57), '/signup', class: 'cta-button platform'
+ .extra-links
+ span.learn-more.shared
+ a.learn-more-platform href='/products/platform' learn more
+ span.sign-in-holder.shared
+ a.sign-in href='/signin' sign in
+
+ br clear="all"
+
+.home-column.last
+ = link_to image_tag("web/thumbnail_jamblaster.jpg", :alt => "JamBlaster explanatory video!"), '#', class: "jamblaster-video video-item", 'data-video-header' => 'JamBlaster', 'data-video-url' => 'http://www.youtube.com/embed/gAJAIHMyois?autoplay=1'
+ h3 Ultra Low-Latency Audio Interface
+
+ p
+ | The
+ strong JamBlaster
+ | is a device designed from the ground up to meet the requirements of online music play, vastly extending the range over which musicians can play together across the Internet.
+
+ = link_to image_tag("web/button_cta_jamblaster.png", width: 234, height: 57), '/products/jamblaster', class: 'cta-button jamblaster'
+ .extra-links
+ .learn-more
+ a.learn-more-jamblaster href='/products/jamblaster' learn more
+ br clear="all"
+
+br clear="all"
+
+- content_for :after_black_bar do
+ .latest-promo
+ = render :partial => "latest"
+ .endorsement-promo
+ .home-buzz
+ h2 What Musicians in the JamKazam Community Are Saying
+ = link_to image_tag("web/thumbnail_buzz.jpg", :alt => "JamKazam Endorsements!", width:300), '#', class: "endorsements-video video-item", 'data-video-header' => 'JamKazam Community', 'data-video-url' => 'http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1'
+
+
+ br clear="all"
+
+javascript:
+ window.JK.HomePage();
diff --git a/web/app/views/users/isp.html.erb b/web/app/views/users/isp.html.erb
index 8e25edb01..c3cdfa524 100644
--- a/web/app/views/users/isp.html.erb
+++ b/web/app/views/users/isp.html.erb
@@ -1,4 +1,5 @@
<%= content_for(:title) { 'Internet Latency Test' }%>
+<% provide(:description, 'Test your internet latency using a custom Java-based tool') %>
JamKazam Internet Latency Test: Select your Internet Service Provider
Java must be installed to run this tool. Please select logo corresponding to your ISP, or if you're not sure, choose "Other". OSX not currently supported.
diff --git a/web/app/views/users/new.html.erb b/web/app/views/users/new.html.erb
index 09a4e9f08..83a2c5c0c 100644
--- a/web/app/views/users/new.html.erb
+++ b/web/app/views/users/new.html.erb
@@ -1,8 +1,9 @@
<% provide(:page_name, 'register') %>
<% provide(:title, 'Register') %>
+<% provide(:description, 'Sign up for your JamKazam account to play music with others online in real time and network with musicians.') %>