diff --git a/db/manifest b/db/manifest index 6483cb000..0e81a7c5f 100755 --- a/db/manifest +++ b/db/manifest @@ -251,3 +251,4 @@ performance_samples.sql user_presences.sql recorded_backing_tracks_add_filename.sql discard_scores_optimized.sql +user_syncs_include_backing_tracks.sql \ No newline at end of file diff --git a/db/up/recorded_backing_tracks_add_filename.sql b/db/up/recorded_backing_tracks_add_filename.sql index af959b9d5..e686700b7 100644 --- a/db/up/recorded_backing_tracks_add_filename.sql +++ b/db/up/recorded_backing_tracks_add_filename.sql @@ -1 +1,2 @@ -ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL; \ No newline at end of file +ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL; +ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/db/up/user_syncs_include_backing_tracks.sql b/db/up/user_syncs_include_backing_tracks.sql new file mode 100644 index 000000000..165617abf --- /dev/null +++ b/db/up/user_syncs_include_backing_tracks.sql @@ -0,0 +1,47 @@ + +DROP VIEW user_syncs; + +CREATE VIEW user_syncs AS + SELECT DISTINCT b.id AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + b.id AS unified_id, + a.user_id AS user_id, + b.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id + UNION ALL + SELECT CAST(NULL AS BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + a.id AS recorded_backing_track_id, + a.id AS unified_id, + a.user_id AS user_id, + a.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + mixes.id AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + mixes.id AS unified_id, + claimed_recordings.user_id AS user_id, + NULL as fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + quick_mixes.id AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + quick_mixes.id AS unified_id, + quick_mixes.user_id, + quick_mixes.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE; diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 0f14d015d..b94d7d9a2 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -134,6 +134,7 @@ require "jam_ruby/models/recording" require "jam_ruby/models/recording_comment" require "jam_ruby/models/recording_liker" require "jam_ruby/models/recorded_backing_track" +require "jam_ruby/models/recorded_backing_track_observer" require "jam_ruby/models/recorded_track" require "jam_ruby/models/recorded_track_observer" require "jam_ruby/models/recorded_video" diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 8e8ab2ace..83557f4f7 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -138,11 +138,17 @@ module JamRuby manifest = { "files" => [], "timeline" => [] } mix_params = [] + 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 } 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" => 100, "balance" => 0 } + end + manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } manifest["output"] = { "codec" => "vorbis" } manifest["recording_id"] = self.recording.id diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track.rb b/ruby/lib/jam_ruby/models/recorded_backing_track.rb index 4d8d5ff35..f37659d3e 100644 --- a/ruby/lib/jam_ruby/models/recorded_backing_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_backing_track.rb @@ -1,10 +1,30 @@ module JamRuby # BackingTrack analog to JamRuby::RecordedTrack - class RecordedBackingTrack < ActiveRecord::Base + 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 @@ -20,6 +40,145 @@ module JamRuby 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 + private def self.construct_filename(created_at, recording_id, client_track_id) 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_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index e11a76bf0..2a6a828d1 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -228,7 +228,7 @@ module JamRuby 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}/track-#{client_track_id}.ogg" + "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/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index cf1597ef3..52e4643af 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -363,6 +363,7 @@ module JamRuby .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| @@ -453,7 +454,7 @@ module JamRuby :url, :fully_uploaded, :upload_failures, - Arel::Nodes::As.new('', Arel.sql('backing_track_track_id')), + :client_track_id, Arel::Nodes::As.new('backing_track', Arel.sql('item_type')) ]).reorder("") @@ -551,8 +552,9 @@ module JamRuby }) elsif recorded_item.item_type == 'backing_track' uploads << ({ - :type => "backing_track", + :type => "recorded_backing_track", :recording_id => recorded_item.recording_id, + :client_track_id => recorded_item.client_track_id, :next => recorded_item.id }) else 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/spec/factories.rb b/ruby/spec/factories.rb index 717ff789e..8d897d4ac 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -232,16 +232,16 @@ 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}"} end - factory :backing_track, :class => JamRuby::BackingTrack do - sequence(:client_track_id) { |n| "client_track_id#{n}"} - filename 'foo.mp3' - end - factory :recorded_track, :class => JamRuby::RecordedTrack do instrument JamRuby::Instrument.first sound 'stereo' @@ -255,6 +255,20 @@ 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 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/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/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/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 27e1bc3e8..d08911d7c 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -112,6 +112,10 @@ // tell the server we are about to start a recording rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) .done(function(response) { + + // update session info + context.JK.CurrentSessionModel.updateSession(response); + var recordingId = $(this).attr('data-recording-id'); var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 85e400115..feb1f2e28 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -890,66 +890,94 @@ logger.warn("some tracks are open that we don't know how to show") } - } - + // this method is pretty complicated because it forks on a key bit of state: + // sessionModel.isPlayingRecording() + // a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks) + // than a backing track opend ad-hoc (connection.backing_tracks) function renderBackingTracks(backingTrackMixers) { - var backingTrack = sessionModel.backingTrack() - var backingTrackPath = backingTrack ? backingTrack.path : null - var name = backingTrackPath - console.log("Opening backing track ", backingTrackPath, backingTrack) - // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) - // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener - var isOpener = backingTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup; + var backingTracks = [] + if(sessionModel.isPlayingRecording()) { + // only return managed mixers for recorded backing tracks + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return mixer.managed}) + backingTracks = sessionModel.recordedBackingTracks(); + } + else { + // only return un-managed (ad-hoc) mixers for normal backing tracks + backingTracks = sessionModel.backingTracks(); + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return !mixer.managed}) + if(backingTrackMixers.length > 1) { + app.notify({ + title: "Multiple Backing Tracks Encounterd", + text: "Only one backing track can be open a time.", + icon_url: "/assets/content/icon_alert_big.png" + }); + return false; + } + } - - // using the server's info in conjuction with the client's, draw the recording tracks - if(backingTrackPath && backingTrackMixers.length > 0) { - var backingTrack = {path: backingTrackPath} - //backingTrackPath sessionModel.getCurrentSession().backing_track_path - - $('.session-recording-name').text(name); - var noCorrespondingTracks = false; - var mixer = backingTrackMixers[0] - var preMasteredClass = ""; + var noCorrespondingTracks = false; + $.each(backingTrackMixers, function(index, mixer) { + + console.log("hunting for backing tracks:", backingTrackMixers, backingTracks) // find the track or tracks that correspond to the mixer var correspondingTracks = [] - correspondingTracks.push(backingTrack); - - if(correspondingTracks.length == 0) { + + var noCorrespondingTracks = false; + if(sessionModel.isPlayingRecording()) { + $.each(backingTracks, function (i, backingTrack) { + if(mixer.persisted_track_id == backingTrack.client_track_id) { + correspondingTracks.push(backingTrack) + } + }); + } + else + { + // if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours + correspondingTracks.push(backingTracks[0]) + } + + if (correspondingTracks.length == 0) { noCorrespondingTracks = true; app.notify({ - title: "Unable to Open BackingTrack", + title: "Unable to Open Backing Track", text: "Could not correlate server and client tracks", - icon_url: "/assets/content/icon_alert_big.png"}); + icon_url: "/assets/content/icon_alert_big.png" + }); return false; } - // prune found recorded tracks - // backingTracks = $.grep(backingTracks, function(value) { - // return $.inArray(value, correspondingTracks) < 0; - // }); + // now we have backing track and mixer in hand; we can render + var backingTrack = correspondingTracks[0] - var oneOfTheTracks = correspondingTracks[0]; - var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener + var isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup; + + if(!sessionModel.isPlayingRecording()) { + // if a recording is being played back, do not set this header, because renderRecordedTracks already did + // ugly. + $('.session-recording-name').text(backingTrack.filename); + } + + var instrumentIcon = context.JK.getInstrumentIcon45(backingTrack.instrument_id); var photoUrl = "/assets/content/icon_recording.png"; - // Default trackData to participant + no Mixer state. var trackData = { - trackId: oneOfTheTracks.id, - clientId: oneOfTheTracks.client_id, - name: name, + trackId: backingTrack.id, + clientId: backingTrack.client_id, + name: backingTrack.filename, instrumentIcon: instrumentIcon, avatar: photoUrl, latency: "good", gainPercent: 0, muteClass: 'muted', - showLoop: true, + showLoop: !sessionModel.isPlayingRecording(), mixerId: "", - avatarClass : 'avatar-recording', + avatarClass: 'avatar-recording', preMasteredClass: "" }; @@ -965,12 +993,12 @@ trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) - if(sessionModel.isPersonalMixMode() || !isOpener) { + if (sessionModel.isPersonalMixMode() || !isOpener) { trackData.mediaControlsDisabled = true; trackData.mediaTrackOpener = isOpener; } - _addRecordingTrack(trackData); - }// if + _addRecordingTrack(trackData); + }); } function renderJamTracks(jamTrackMixers) { @@ -2357,8 +2385,10 @@ function closeRecording() { rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) - .done(function() { - sessionModel.refreshCurrentSession(true); + .done(function(response) { + //sessionModel.refreshCurrentSession(true); + // update session info + context.JK.CurrentSessionModel.updateSession(response); }) .fail(function(jqXHR) { app.notify({ diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 7f2ce0776..f2143684d 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -69,7 +69,7 @@ function isPlayingRecording() { // this is the server's state; there is no guarantee that the local tracks // requested from the backend will have corresponding track information - return currentSession && currentSession.claimed_recording; + return !!(currentSession && currentSession.claimed_recording); } function recordedTracks() { @@ -81,6 +81,28 @@ } } + function recordedBackingTracks() { + if(currentSession && currentSession.claimed_recording) { + return currentSession.claimed_recording.recording.recorded_backing_tracks + } + else { + return null; + } + } + + function backingTracks() { + var backingTracks = [] + // this may be wrong if we loosen the idea that only one person can have a backing track open. + // but for now, the 1st person we find with a backing track open is all there is to find... + context._.each(participants(), function(participant) { + if(participant.backing_tracks.length > 0) { + backingTracks = participant.backing_tracks; + return false; // break + } + }) + return backingTracks; + } + function jamTracks() { if(currentSession && currentSession.jam_track) { return currentSession.jam_track.tracks @@ -346,6 +368,25 @@ } } + function updateSession(response) { + updateSessionInfo(response, null, true); + } + + function updateSessionInfo(response, callback, force) { + if(force === true || currentTrackChanges < response.track_changes_counter) { + logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) + currentTrackChanges = response.track_changes_counter; + sendClientParticipantChanges(currentSession, response); + updateCurrentSession(response); + if(callback != null) { + callback(); + } + } + else { + logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); + } + } + /** * Reload the session data from the REST server, calling * the provided callback when complete. @@ -369,18 +410,7 @@ type: "GET", url: url, success: function(response) { - if(force === true || currentTrackChanges < response.track_changes_counter) { - logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) - currentTrackChanges = response.track_changes_counter; - sendClientParticipantChanges(currentSession, response); - updateCurrentSession(response); - if(callback != null) { - callback(); - } - } - else { - logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); - } + updateSessionInfo(response, callback, force); }, error: function(jqXHR) { if(jqXHR.status != 404) { @@ -572,12 +602,10 @@ var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); // backingTracks can be passed in as an optimization, so that we don't hit the backend excessively - if(backingTracks === undefined) { - backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient); + if(backingTracks === undefined ) { + backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient); } - console.log('backingTracks', backingTracks); - // create a trackSync request based on backend data var syncTrackRequest = {}; syncTrackRequest.client_id = app.clientId; @@ -777,6 +805,8 @@ this.id = id; this.start = start; this.backingTrack = backingTrack; + this.backingTracks = backingTracks; + this.recordedBackingTracks = recordedBackingTracks; this.metronomeActive = metronomeActive; this.setUserTracks = setUserTracks; this.recordedTracks = recordedTracks; @@ -785,6 +815,7 @@ this.joinSession = joinSession; this.leaveCurrentSession = leaveCurrentSession; this.refreshCurrentSession = refreshCurrentSession; + this.updateSession = updateSession; this.subscribe = subscribe; this.participantForClientId = participantForClientId; this.isPlayingRecording = isPlayingRecording; diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 3215359e2..3ba40ffb3 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer @list = @root.find('.list') @logList = @root.find('.log-list') @templateRecordedTrack = $('#template-sync-viewer-recorded-track') + @templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track') @templateStreamMix = $('#template-sync-viewer-stream-mix') @templateMix = $('#template-sync-viewer-mix') @templateNoSyncs = $('#template-sync-viewer-no-syncs') @templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details') @templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track') + @templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track') @templateHoverMix = $('#template-sync-viewer-hover-mix') @templateDownloadReset = $('#template-sync-viewer-download-progress-reset') @templateUploadReset = $('#template-sync-viewer-upload-progress-reset') @templateGenericCommand = $('#template-sync-viewer-generic-command') @templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command') + @templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command') @templateLogItem = $('#template-sync-viewer-log-item') @tabSelectors = @root.find('.dialog-tabs .tab') @tabs = @root.find('.tab-content') @@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer them_upload_soon: 'them-upload-soon' missing: 'missing', me_uploaded: 'me-uploaded', - them_uploaded: 'them-uploaded' + them_uploaded: 'them-uploaded', + not_mine: 'not-mine' } @clientStates = { unknown: 'unknown', @@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer hq: 'hq', sq: 'sq', missing: 'missing', - discarded: 'discarded' + discarded: 'discarded', + not_mine: 'not-mine' } throw "no sync-viewer" if not @root.exists() @@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer $clientRetry.hide() $uploadRetry.hide() + updateBackingTrackState: ($track) => + clientInfo = $track.data('client-info') + serverInfo = $track.data('server-info') + myTrack = serverInfo.user.id == context.JK.currentUserId + + # determine client state + clientStateMsg = 'UNKNOWN' + clientStateClass = 'unknown' + clientState = @clientStates.unknown + + if serverInfo.mine + if serverInfo.download.should_download + if serverInfo.download.too_many_downloads + clientStateMsg = 'EXCESS DOWNLOADS' + clientStateClass = 'error' + clientState = @clientStates.too_many_uploads + else + if clientInfo? + if clientInfo.local_state == 'HQ' + clientStateMsg = 'HIGHEST QUALITY' + clientStateClass = 'hq' + clientState = @clientStates.hq + else if clientInfo.local_state == 'MISSING' + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'DISCARDED' + clientStateClass = 'discarded' + clientState = @clientStates.discarded + else + clientStateMsg = 'NOT MINE' + clientStateClass = 'not_mine' + clientState = @clientStates.not_mine + + # determine upload state + uploadStateMsg = 'UNKNOWN' + uploadStateClass = 'unknown' + uploadState = @uploadStates.unknown + + if serverInfo.mine + if !serverInfo.fully_uploaded + if serverInfo.upload.too_many_upload_failures + uploadStateMsg = 'UPLOAD FAILURE' + uploadStateClass = 'error' + uploadState = @uploadStates.too_many_upload_failures + else + if myTrack + if clientInfo? + if clientInfo.local_state == 'HQ' + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.me_upload_soon + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.them_upload_soon + else + uploadStateMsg = 'UPLOADED' + uploadStateClass = 'uploaded' + if myTrack + uploadState = @uploadStates.me_uploaded + else + uploadState = @uploadStates.them_uploaded + else + uploadStateMsg = 'NOT MINE' + uploadStateClass = 'not_mine' + uploadState = @uploadStates.not_mine + + + $clientState = $track.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + $clientStateProgress = $clientState.find('.progress') + $uploadState = $track.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + $uploadStateProgress = $uploadState.find('.progress') + + $clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass) + $clientStateMsg.text(clientStateMsg) + $clientStateProgress.css('width', '0') + $uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass) + $uploadStateMsg.text(uploadStateMsg) + $uploadStateProgress.css('width', '0') + + # this allows us to make styling decisions based on the combination of both client and upload state. + $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}") + + $clientRetry = $clientState.find('.retry') + $uploadRetry = $uploadState.find('.retry') + + if gon.isNativeClient + # handle client state + + # only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already + if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + $clientRetry.show() + else + $clientRetry.hide() + + # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it + if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon) + $uploadRetry.show() + else + $uploadRetry.hide() + else + $clientRetry.hide() + $uploadRetry.hide() + + associateClientInfo: (recording) => for clientInfo in recording.local_tracks $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") $track.data('client-info', clientInfo) $track.data('total-size', recording.size) + for clientInfo in recording.backing_tracks + $track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") + $track.data('client-info', clientInfo) + $track.data('total-size', recording.size) + $track = @list.find(".mix[data-recording-id='#{recording.recording_id}']") $track.data('client-info', recording.mix) $track.data('total-size', recording.size) @@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer uploadStateClass: uploadStateClass} {variable: 'data'}) - onHoverOfStateIndicator: () -> + displayBackingTrackHover: ($recordedTrack) => + $clientState = $recordedTrack.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + clientStateClass = $clientState.data('custom-class') + clientState = $clientState.attr('data-state') + clientInfo = $recordedTrack.data('client-info') + + $uploadState = $recordedTrack.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + uploadStateClass = $uploadState.data('custom-class') + uploadState = $uploadState.attr('data-state') + serverInfo = $recordedTrack.data('server-info') + + # decide on special case strings first + + summary = '' + if clientState == @clientStates.not_mine && @uploadStates.them_uploaded + # this is not our backing track + summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you." + else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon + # this is not our backing track + summary = "#{serverInfo.user.name} has not yet uploaded their backing track." + else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded + # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon! + summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing." + else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.discarded + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded ) + summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made." + + clientStateDefinition = switch clientState + when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed." + when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track." + when @clientStates.missing then "MISSING means you do not have this backing track anymore." + when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over." + when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track." + else 'There is no help for this state' + + uploadStateDefinition = switch uploadState + when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted." + when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this track soon." + when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this track soon." + when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this track." + when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this track." + when @uploadStates.missing then "MISSING means your JamKazam application does not have this track, and the server does not either." + when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track." + + context._.template(@templateHoverRecordedBackingTrack.html(), + {summary: summary, + clientStateDefinition: clientStateDefinition, + uploadStateDefinition: uploadStateDefinition, + clientStateMsg: $clientStateMsg.text(), + uploadStateMsg: $uploadStateMsg.text(), + clientStateClass: clientStateClass, + uploadStateClass: uploadStateClass} + {variable: 'data'}) + + onTrackHoverOfStateIndicator: () -> $recordedTrack = $(this).closest('.recorded-track.sync') self = $recordedTrack.data('sync-viewer') self.displayTrackHover($recordedTrack) + onBackingTrackHoverOfStateIndicator: () -> + $recordedTrack = $(this).closest('.recorded-backing-track.sync') + self = $recordedTrack.data('sync-viewer') + self.displayBackingTrackHover($recordedTrack) + onStreamMixHover: () -> $streamMix = $(this).closest('.stream-mix.sync') self = $streamMix.data('sync-viewer') @@ -548,8 +745,8 @@ context.JK.SyncViewer = class SyncViewer $uploadStateRetry.click(this.retryUploadRecordedTrack) context.JK.bindHoverEvents($track) context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); - context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) - context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) + context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) + context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) $clientState.addClass('is-native-client') if gon.isNativeClient $uploadState.addClass('is-native-client') if gon.isNativeClient $track diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index f75ba85a3..a08885af3 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -33,14 +33,20 @@ getBackingTracks: function(jamClient) { var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4); + console.log("mediaTracks", mediaTracks) var backingTracks = [] context._.each(mediaTracks, function(mediaTrack) { - var track = {}; - track.client_track_id = mediaTrack.id; - track.client_resource_id = mediaTrack.rid; - track.filename = mediaTrack.id; - backingTracks.push(track); + // the check for 'not managed' means this is not a track opened by a recording, basically + // we do not try and sync these sorts of backing tracks to the server, because they + // are already encompassed by + if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) { + var track = {}; + track.client_track_id = mediaTrack.persisted_track_id; + track.client_resource_id = mediaTrack.rid; + track.filename = mediaTrack.filename; + backingTracks.push(track); + } }) return backingTracks; diff --git a/web/app/assets/stylesheets/client/help.css.scss b/web/app/assets/stylesheets/client/help.css.scss index 4755d711f..512988936 100644 --- a/web/app/assets/stylesheets/client/help.css.scss +++ b/web/app/assets/stylesheets/client/help.css.scss @@ -45,7 +45,7 @@ body.jam, body.web, .dialog{ } } - .help-hover-recorded-tracks, .help-hover-stream-mix { + .help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks { font-size:12px; padding:5px; diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 6244e1e17..842a9d361 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ] before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] + before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ] before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ] before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ] @@ -43,7 +44,7 @@ class ApiRecordingsController < ApiController @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) end - def download + def download # track raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user) @recorded_track.current_user = current_user @@ -58,6 +59,21 @@ class ApiRecordingsController < ApiController end end + def backing_track_download + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user) + + @recorded_backing_track.current_user = current_user + @recorded_backing_track.update_download_count + + @recorded_backing_track.valid? + if !@recorded_backing_track.errors.any? + @recorded_backing_track.save! + redirect_to @recorded_backing_track.sign_url + else + render :json => { :message => "download limit surpassed" }, :status => 404 + end + end + def start music_session = ActiveMusicSession.find(params[:music_session_id]) @@ -227,6 +243,61 @@ class ApiRecordingsController < ApiController end end + def backing_track_upload_next_part + length = params[:length] + md5 = params[:md5] + + @recorded_backing_track.upload_next_part(length, md5) + + if @recorded_backing_track.errors.any? + + response.status = :unprocessable_entity + # this is not typical, but please don't change this line unless you are sure it won't break anything + # this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's + # state doesn't cause errors to shoot out like normal. + render :json => { :errors => @recorded_backing_track.errors }, :status => 422 + else + result = { + :part => @recorded_backing_track.next_part_to_upload, + :offset => @recorded_backing_track.file_offset.to_s + } + + render :json => result, :status => 200 + end + + end + + def backing_track_upload_sign + render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200 + end + + def backing_track_upload_part_complete + part = params[:part] + offset = params[:offset] + + @recorded_backing_track.upload_part_complete(part, offset) + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + else + render :json => {}, :status => 200 + end + end + + def backing_track_upload_complete + @recorded_backing_track.upload_complete + @recorded_backing_track.recording.upload_complete + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + return + else + render :json => {}, :status => 200 + end + end + # POST /api/recordings/:id/videos/:video_id/upload_sign def video_upload_sign length = params[:length] @@ -314,6 +385,11 @@ class ApiRecordingsController < ApiController raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user) end + def lookup_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user) + end + def lookup_stream_mix @quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id) raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user) diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index dbc1c7fd9..b038a0102 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -2,6 +2,8 @@ # I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata # depictable. +# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState + object @claimed_recording attributes :id, :name, :description, :is_public, :genre_id, :discarded @@ -36,6 +38,18 @@ child(:recording => :recording) { } } + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url + } + + node :mine do |user| + user == current_user + end + } + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 1d87b55c1..cd277c3fe 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -54,6 +54,10 @@ else child(:tracks => :tracks) { attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at } + + child(:backing_tracks => :backing_tracks) { + attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at + } } child({:invitations => :invitations}) { @@ -114,6 +118,14 @@ else attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url } } + + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + } + } } } diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index 1fe574026..f3c817b07 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -27,6 +27,12 @@ child(:recorded_tracks => :recorded_tracks) { end } +child(:recorded_backing_tracks => :recorded_backing_tracks) { + node do |recorded_backing_track| + partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track) + end +} + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_recordings/show_recorded_backing_track.rabl b/web/app/views/api_recordings/show_recorded_backing_track.rabl new file mode 100644 index 000000000..1c9e1a078 --- /dev/null +++ b/web/app/views/api_recordings/show_recorded_backing_track.rabl @@ -0,0 +1,7 @@ +object @recorded_backing_track + +attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename + +child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url +} \ No newline at end of file diff --git a/web/app/views/api_user_syncs/show.rabl b/web/app/views/api_user_syncs/show.rabl index 0c63ba010..730120276 100644 --- a/web/app/views/api_user_syncs/show.rabl +++ b/web/app/views/api_user_syncs/show.rabl @@ -18,7 +18,6 @@ glue :recorded_track do partial("api_recordings/show", :object => recorded_track.recording) end - node :upload do |recorded_track| { should_upload: true, @@ -35,6 +34,45 @@ glue :recorded_track do end + +glue :recorded_backing_track do + + @object.current_user = current_user + + node :type do |i| + 'recorded_backing_track' + end + + attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename + + node :user do |recorded_backing_track| + partial("api_users/show_minimal", :object => recorded_backing_track.user) + end + + node :recording do |recorded_backing_track| + partial("api_recordings/show", :object => recorded_backing_track.recording) + end + + node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + + node :upload do |recorded_backing_track| + { + should_upload: true, + too_many_upload_failures: recorded_backing_track.too_many_upload_failures? + } + end + + node :download do |recorded_backing_track| + { + should_download: recorded_backing_track.can_download?(current_user), + too_many_downloads: recorded_backing_track.too_many_downloads? + } + end + +end + glue :mix do @object.current_user = current_user diff --git a/web/app/views/clients/_sync_viewer_templates.html.slim b/web/app/views/clients/_sync_viewer_templates.html.slim index 4c08f9ab9..92f0953bd 100644 --- a/web/app/views/clients/_sync_viewer_templates.html.slim +++ b/web/app/views/clients/_sync_viewer_templates.html.slim @@ -59,6 +59,23 @@ script type="text/template" id='template-sync-viewer-recorded-track' a.retry href='#' = image_tag('content/icon_resync.png', width:12, height: 14) +script type="text/template" id='template-sync-viewer-recorded-backing-track' + .recorded-track.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}" data-client-id="{{data.client_id}}" data-client-track-id="{{data.client_track_id}}" data-track-id="{{data.backing_track_id}}" data-fully-uploaded="{{data.fully_uploaded}}" + .type + span.text BACKING + a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician" + img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}" + .client-state.bar + .progress + span.msg + a.retry href='#' + = image_tag('content/icon_resync.png', width:12, height: 14) + .upload-state.bar + .progress + span.msg + a.retry href='#' + = image_tag('content/icon_resync.png', width:12, height: 14) + script type="text/template" id='template-sync-viewer-stream-mix' .stream-mix.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}" @@ -128,6 +145,33 @@ script type="text/template" id="template-sync-viewer-hover-recorded-track" | {{data.summary}} | {% } %} +script type="text/template" id="template-sync-viewer-hover-recorded-backing-track" + .help-hover-recorded-backing-tracks + + .client-box + .client-state-info + span.special-text is the file on your system? + .client-state class="{{data.clientStateClass}}" + span.msg + | {{data.clientStateMsg}} + .client-state-definition.sync-definition + | {{data.clientStateDefinition}} + .upload-box + .upload-state-info + span.special-text is it uploaded? + .upload-state class="{{data.uploadStateClass}}" + span.msg + | {{data.uploadStateMsg}} + .upload-state-definition.sync-definition + | {{data.uploadStateDefinition}} + br clear="both" + + | {% if(data.summary) { %} + .summary + .title what's next? + | {{data.summary}} + | {% } %} + script type="text/template" id="template-sync-viewer-hover-stream-mix" .help-hover-stream-mix @@ -198,6 +242,15 @@ script type="text/template" id="template-sync-viewer-recorded-track-command" img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}" img.instrument-icon data-instrument-id="{{data.instrument_id}}" hoveraction="instrument" src="{{JK.getInstrumentIconMap24()[data.instrument_id].asset}}" +script type="text/template" id="template-sync-viewer-recorded-backing-track-command" + .recorded-backing-track.sync + .type + .progress + span.text + | {{data.action}} BACKING TRACK + a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician" + img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}" + script type="text/template" id="template-sync-viewer-log-item" .log class="success-{{data.success}}" .command diff --git a/web/config/application.rb b/web/config/application.rb index 7ffd31ed7..1d76163fd 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -39,7 +39,7 @@ if defined?(Bundler) # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver" + config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver", "JamRuby::RecordedBackingTrackObserver" # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. diff --git a/web/config/routes.rb b/web/config/routes.rb index 9cb0ee9da..d50d69a4c 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -456,13 +456,20 @@ SampleApp::Application.routes.draw do match '/recordings/:id/tracks/:track_id/upload_sign' => 'api_recordings#upload_sign', :via => :get match '/recordings/:id/tracks/:track_id/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :post match '/recordings/:id/tracks/:track_id/upload_complete' => 'api_recordings#upload_complete', :via => :post - match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get # Recordings - stream_mix match '/recordings/:id/stream_mix/upload_sign' => 'api_recordings#upload_sign_stream_mix', :via => :get match '/recordings/:id/stream_mix/upload_part_complete' => 'api_recordings#upload_part_complete_stream_mix', :via => :post match '/recordings/:id/stream_mix/upload_complete' => 'api_recordings#upload_complete_stream_mix', :via => :post - + match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get + + # Recordings - backing tracks + match '/recordings/:id/backing_tracks/:track_id/download' => 'api_recordings#backing_track_download', :via => :get, :as => 'api_recordings_download' + match '/recordings/:id/backing_tracks/:track_id/upload_next_part' => 'api_recordings#backing_track_upload_next_part', :via => :get + match '/recordings/:id/backing_tracks/:track_id/upload_sign' => 'api_recordings#backing_track_upload_sign', :via => :get + match '/recordings/:id/backing_tracks/:track_id/upload_part_complete' => 'api_recordings#backing_track_upload_part_complete', :via => :post + match '/recordings/:id/backing_tracks/:track_id/upload_complete' => 'api_recordings#backing_track_upload_complete', :via => :post + # Recordings - recorded_videos match '/recordings/:id/tracks/:video_id/upload_sign' => 'api_recordings#video_upload_sign', :via => :get match '/recordings/:id/videos/:video_id/upload_start' => 'api_recordings#video_upload_start', :via => :post diff --git a/web/spec/controllers/api_recordings_controller_spec.rb b/web/spec/controllers/api_recordings_controller_spec.rb index e0691cb2d..f09a6130d 100644 --- a/web/spec/controllers/api_recordings_controller_spec.rb +++ b/web/spec/controllers/api_recordings_controller_spec.rb @@ -10,6 +10,7 @@ describe ApiRecordingsController do @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) controller.current_user = @user end @@ -100,7 +101,65 @@ describe ApiRecordingsController do end end - describe "download" do + describe "download track" do + let(:mix) { FactoryGirl.create(:mix) } + + it "should only allow a user to download a track if they have claimed the recording" do + post :start, { :format => 'json', :music_session_id => @music_session.id } + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + post :stop, { :format => 'json', :id => recording.id } + response.should be_success + end + + + it "is possible" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + controller.current_user = mix.recording.owner + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 1 + + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 2 + end + + + it "prevents download after limit is reached" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + controller.current_user = recorded_track.user + get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 404 + JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed" + end + + + it "lets admins surpass limit" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + recorded_track.user.admin = true + recorded_track.user.save! + + controller.current_user = recorded_track.user + get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + recorded_track.reload + recorded_track.download_count.should == 101 + end + end + + describe "download backing track" do let(:mix) { FactoryGirl.create(:mix) } it "should only allow a user to download a track if they have claimed the recording" do diff --git a/web/spec/factories.rb b/web/spec/factories.rb index bf0e44083..eeb17a8c0 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -274,6 +274,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}"} @@ -303,6 +308,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(:recording_id) { |n| "recording_id-#{n}"} sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} diff --git a/web/spec/spec_helper.rb b/web/spec/spec_helper.rb index 09606e087..fdb947180 100644 --- a/web/spec/spec_helper.rb +++ b/web/spec/spec_helper.rb @@ -192,6 +192,12 @@ bputs "before register capybara" end config.before(:all) do + # to reduce frequency of timeout on initial test + # https://github.com/teampoltergeist/poltergeist/issues/294#issuecomment-72746472 + if self.respond_to? :visit + visit '/assets/application.css' + visit '/assets/application.js' + end end config.before(:each) do