This commit is contained in:
Seth Call 2015-02-15 22:01:06 -06:00
parent 503e46ed74
commit 42a2abe99c
32 changed files with 1245 additions and 86 deletions

View File

@ -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

View File

@ -1 +1,2 @@
ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL;
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;

View File

@ -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;

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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({

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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)

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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}"}

View File

@ -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