* merged
This commit is contained in:
parent
503e46ed74
commit
42a2abe99c
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue