* many recording features/improvements: VRFS-2151, VRFS-1551, VRFS-2360, VRFS-2290, VRFS-2002, VRFS-2181

This commit is contained in:
Seth Call 2014-10-22 23:10:49 -05:00
parent b9b1fbb313
commit fe7edbba5d
92 changed files with 4995 additions and 10569 deletions

View File

@ -1,6 +1,6 @@
<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_batch_emails_path : "#{Gon.global.prefix}/admin/batch_emails/#{resource.id}") do |f| %>
<%= f.inputs do %>
<%= f.input(:from_email, :label => "From Email", :input_html => {:maxlength => 64}) %>
<%= f.input(:from_email, :label => "From Email", :input_html => {:maxlength => 64}, :hint => 'JamKazam <noreply@jamkazam.com>') %>
<%= f.input(:subject, :label => "Subject", :input_html => {:maxlength => 128}) %>
<%= f.input(:test_emails, :label => "Test Emails", :input_html => {:maxlength => 1024, :size => '3x3'}) %>
<%= f.input(:body, :label => "Body", :input_html => {:maxlength => 60000, :size => '10x20'}) %>

View File

@ -223,3 +223,4 @@ recorded_videos.sql
emails_from_update2.sql
add_youtube_flag_to_claimed_recordings.sql
add_session_create_type.sql
user_syncs_and_quick_mix.sql

View File

@ -0,0 +1,68 @@
ALTER TABLE recordings ADD COLUMN has_stream_mix BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE recordings ADD COLUMN has_final_mix BOOLEAN DEFAULT FALSE NOT NULL;
UPDATE recordings SET has_final_mix = TRUE WHERE (SELECT count(mixes.id) FROM mixes WHERE recording_id = recordings.id) > 0;
CREATE TABLE quick_mixes (
id BIGINT PRIMARY KEY,
next_part_to_upload INTEGER NOT NULL DEFAULT 0,
fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE,
upload_id VARCHAR(1024),
file_offset BIGINT DEFAULT 0,
is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE,
upload_failures INTEGER DEFAULT 0,
part_failures INTEGER DEFAULT 0,
ogg_md5 VARCHAR(100),
ogg_length INTEGER,
ogg_url VARCHAR(1000),
mp3_md5 VARCHAR(100),
mp3_length INTEGER,
mp3_url VARCHAR(1000),
error_count INTEGER NOT NULL DEFAULT 0,
error_reason TEXT,
error_detail TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP,
completed_at TIMESTAMP,
completed BOOLEAN NOT NULL DEFAULT FALSE,
should_retry BOOLEAN NOT NULL DEFAULT FALSE,
cleaned BOOLEAN NOT NULL DEFAULT FALSE,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
recording_id VARCHAR(64) REFERENCES recordings(id) ON DELETE CASCADE
);
ALTER TABLE quick_mixes ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');
CREATE VIEW user_syncs AS
SELECT b.id AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) as quick_mix_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 INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
mixes.id AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_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
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,
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;
ALTER TABLE recordings ADD COLUMN first_quick_mix_id BIGINT REFERENCES quick_mixes(id) ON DELETE SET NULL;

View File

@ -38,6 +38,7 @@ require "jam_ruby/lib/em_helper"
require "jam_ruby/lib/nav"
require "jam_ruby/lib/html_sanitize"
require "jam_ruby/resque/audiomixer"
require "jam_ruby/resque/quick_mixer"
require "jam_ruby/resque/icecast_config_writer"
require "jam_ruby/resque/resque_hooks"
require "jam_ruby/resque/scheduled/audiomixer_retry"
@ -122,6 +123,8 @@ require "jam_ruby/models/recording_comment"
require "jam_ruby/models/recording_liker"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/recorded_track_observer"
require "jam_ruby/models/quick_mix"
require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token"
require "jam_ruby/models/mix"
require "jam_ruby/models/claimed_recording"
@ -174,6 +177,7 @@ require "jam_ruby/models/chat_message"
require "jam_ruby/models/generic_state"
require "jam_ruby/models/score_history"
require "jam_ruby/models/jam_company"
require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source"
require "jam_ruby/models/recorded_video"

View File

@ -64,6 +64,10 @@ module JamRuby
s3_bucket.objects[upload_filename].multipart_uploads[upload_id].parts[part]
end
def exists?(filename)
s3_bucket.objects[filename].exists?
end
def delete(filename)
s3_bucket.objects[filename].delete
end
@ -76,6 +80,14 @@ module JamRuby
s3_bucket.objects.with_prefix(folder).delete_all
end
def download(key, filename)
File.open(filename, "wb") do |f|
s3_bucket.objects[key].read do |data|
f.write(data)
end
end
end
private
def s3_bucket

View File

@ -49,8 +49,8 @@ module JamRuby
mix.is_skip_mount_uploader = true
mix.recording = recording
mix.save
mix[:ogg_url] = construct_filename(mix.created_at, recording.id, mix.id, type='ogg')
mix[:mp3_url] = construct_filename(mix.created_at, recording.id, mix.id, type='mp3')
mix[:ogg_url] = construct_filename(recording.created_at, recording.id, mix.id, type='ogg')
mix[:mp3_url] = construct_filename(recording.created_at, recording.id, mix.id, type='mp3')
if mix.save
mix.enqueue
end
@ -68,19 +68,53 @@ module JamRuby
end
# avoid db validations
Mix.where(:id => self.id).update_all(:started_at => Time.now)
Mix.where(:id => self.id).update_all(:started_at => Time.now, :should_retry => false)
true
end
def can_download?(some_user)
!ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording_id).nil?
claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id)
if claimed_recording
!claimed_recording.discarded
else
false
end
end
def mix_timeout?
Time.now - started_at > 60 * 30 # 30 minutes to mix is more than enough
end
def state
return 'mixed' if completed
return 'stream-mix' if recording.has_stream_mix
return 'waiting-to-mix' if started_at.nil?
return 'error' if error_count > 0 || mix_timeout?
return 'mixing'
end
def error
return nil if state != 'error'
return {error_count: error_count, error_reason: error_reason, error_detail: error_detail} if error_count > 0
return {error_count: 1, error_reason: 'mix-timeout', error_detail: started_at} if mix_timeout?
return {error_count: 1, error_reason: 'unknown', error_detail: 'unknown'}
end
def too_many_downloads?
(self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
end
def errored(reason, detail)
self.started_at = nil
self.error_reason = reason
self.error_detail = detail
self.error_count = self.error_count + 1
if self.error_count <= 3
self.should_retry = true
end
save
end
@ -94,6 +128,8 @@ module JamRuby
if save
Notification.send_recording_master_mix_complete(recording)
end
Recording.where(:id => self.recording.id).update_all(:has_final_mix => true)
end
# valid for 1 day; because the s3 urls eventually expire
@ -154,7 +190,7 @@ module JamRuby
def filename(type='ogg')
# construct a path for s3
Mix.construct_filename(self.created_at, self.recording_id, self.id, type)
Mix.construct_filename(recording.created_at, self.recording_id, self.id, type)
end
def update_download_count(count=1)

View File

@ -0,0 +1,269 @@
module JamRuby
class QuickMix < ActiveRecord::Base
include S3ManagerMixin
MAX_MIX_TIME = 7200 # 2 hours
attr_accessible :ogg_url, :should_retry, as: :admin
attr_accessor :marking_complete, :is_skip_mount_uploader
attr_writer :current_user
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :quick_mixes
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :quick_mixes, :foreign_key => 'recording_id'
validates :ogg_md5, :presence => true, :if => :upload_starting?
validates :ogg_length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely...
validates :user, presence: true
validate :validate_fully_uploaded
validate :validate_part_complete
validate :validate_too_many_upload_failures
before_destroy :delete_s3_files
skip_callback :save, :before, :store_picture!, if: :is_skip_mount_uploader
def too_many_upload_failures?
upload_failures >= APP_CONFIG.max_track_upload_failures
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 > ogg_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 sanitize_active_admin
self.user_id = nil if self.user_id == ''
end
def upload_start(length, md5)
#self.upload_id set by the observer
self.next_part_to_upload = 1
self.ogg_length = length
self.ogg_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.ogg_md5 = nil
self.ogg_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[:ogg_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
enqueue
save
end
def increment_part_failures(part_failure_before_error)
self.part_failures = part_failure_before_error + 1
QuickMix.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'")
end
def self.create(recording, user)
raise if recording.nil?
mix = QuickMix.new
mix.is_skip_mount_uploader = true
mix.recording = recording
mix.user = user
mix.save
mix[:ogg_url] = construct_filename(mix.created_at, recording.id, mix.id, type='ogg')
mix[:mp3_url] = construct_filename(mix.created_at, recording.id, mix.id, type='mp3')
mix.save
mix.is_skip_mount_uploader = false
mix
end
def enqueue
begin
Resque.enqueue(QuickMixer, self.id, self.sign_put(3600 * 24, 'mp3'))
rescue Exception => e
# implies redis is down. we don't update started_at by bailing out here
false
end
# avoid db validations
QuickMix.where(:id => self.id).update_all(:started_at => Time.now, :should_retry => false)
true
end
def mix_timeout?
Time.now - started_at > 60 * 30 # 30 minutes to mix is more than enough
end
def state
return 'mixed' if completed
return 'waiting-to-mix' if started_at.nil?
return 'error' if error_count > 0 || mix_timeout?
return 'mixing'
end
def error
return nil if state != 'error'
return {error_count: error_count, error_reason: error_reason, error_detail: error_detail} if error_count > 0
return {error_count: 1, error_reason: 'mix-timeout', error_detail: started_at} if mix_timeout?
return {error_count: 1, error_reason: 'unknown', error_detail: 'unknown'}
end
def errored(reason, detail)
self.started_at = nil
self.error_reason = reason
self.error_detail = detail
self.error_count = self.error_count + 1
if self.error_count <= 3
self.should_retry = true
end
save
end
def finish(mp3_length, mp3_md5)
self.completed_at = Time.now
self.mp3_length = mp3_length
self.mp3_md5 = mp3_md5
self.completed = true
save!
Recording.where(:id => self.recording.id).update_all(:has_stream_mix => true)
# only update first_quick_mix_id pointer if this is the 1st quick mix to complete for this recording
Recording.where(:id => self.recording.id).update_all(:first_quick_mix_id => self.id) if recording.first_quick_mix_id.nil?
end
def s3_url(type='ogg')
if type == 'ogg'
s3_manager.s3_url(self[:ogg_url])
else
s3_manager.s3_url(self[:mp3_url])
end
end
def is_completed
completed
end
# if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed
def resolve_url(url_field, mime_type, expiration_time)
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false})
end
def sign_url(expiration_time = 120, type='ogg')
type ||= 'ogg'
# expire link in 1 minute--the expectation is that a client is immediately following this link
if type == 'ogg'
resolve_url(:ogg_url, 'audio/ogg', expiration_time)
else
resolve_url(:mp3_url, 'audio/mpeg', expiration_time)
end
end
def sign_put(expiration_time = 3600 * 24, type='ogg')
type ||= 'ogg'
if type == 'ogg'
s3_manager.sign_url(self[:ogg_url], {:expires => expiration_time, :content_type => 'audio/ogg', :secure => false}, :put)
else
s3_manager.sign_url(self[:mp3_url], {:expires => expiration_time, :content_type => 'audio/mpeg', :secure => false}, :put)
end
end
def self.cleanup_excessive_storage
QuickMix
.joins(:recording)
.includes(:recording)
.where("cleaned = FALSE AND completed = TRUE AND NOW() - completed_at > '7 days'::INTERVAL AND (has_final_mix = TRUE OR (has_stream_mix = TRUE AND quick_mixes.id IN (SELECT qm.id FROM quick_mixes qm WHERE qm.recording_id = recordings.id AND (recordings.first_quick_mix_id IS NULL OR recordings.first_quick_mix_id != qm.id))))").limit(1000).each do |quick_mix|
quick_mix.delete_s3_files
QuickMix.where(:id => quick_mix.id).update_all(:cleaned => true)
if quick_mix.recording.first_quick_mix_id == quick_mix.id
Recording.where(:id => quick_mix.recording.id).update_all(:has_stream_mix => false, :first_quick_mix_id => nil)
end
end
end
def filename(type='ogg')
# construct a path for s3
QuickMix.construct_filename(self.created_at, self.recording_id, self.id, type)
end
def delete_s3_files
s3_manager.delete(filename(type='ogg')) if self[:ogg_url] && s3_manager.exists?(filename(type='ogg'))
s3_manager.delete(filename(type='mp3')) if self[:mp3_url] && s3_manager.exists?(filename(type='mp3'))
end
def self.construct_filename(created_at, recording_id, id, type='ogg')
raise "unknown ID" unless id
"recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/stream-mix-#{id}.#{type}"
end
end
end

View File

@ -0,0 +1,83 @@
module JamRuby
class QuickMixObserver < 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::QuickMix
def before_validation(quick_mix)
# if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded
if quick_mix.is_part_uploading_was && !quick_mix.is_part_uploading
begin
aws_part = quick_mix.s3_manager.multiple_upload_find_part(quick_mix[:ogg_url], quick_mix.upload_id, quick_mix.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
quick_mix.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue RuntimeError => e
quick_mix.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue
quick_mix.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 quick_mix.marking_complete && !quick_mix.fully_uploaded_was && quick_mix.fully_uploaded
multipart_success = false
begin
quick_mix.s3_manager.multipart_upload_complete(quick_mix[:ogg_url], quick_mix.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
#quick_mix.reload
quick_mix.reset_upload
quick_mix.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD)
end
end
end
def after_commit(quick_mix)
end
# here we tick upload failure counts, or revert the state of the model, as needed
def after_rollback(quick_mix)
# if fully uploaded, don't increment failures
if quick_mix.fully_uploaded
return
end
# increment part failures if there is a part currently being uploaded
if quick_mix.is_part_uploading_was
#quick_mix.reload # we don't want anything else that the user set to get applied
quick_mix.increment_part_failures(quick_mix.part_failures_was)
if quick_mix.part_failures >= APP_CONFIG.max_track_part_upload_failures
# save upload id before we abort this bad boy
upload_id = quick_mix.upload_id
begin
quick_mix.s3_manager.multipart_upload_abort(quick_mix[:ogg_url], upload_id)
rescue => e
puts e.inspect
end
quick_mix.reset_upload
if quick_mix.upload_failures >= APP_CONFIG.max_track_upload_failures
# do anything?
end
end
end
end
def before_save(quick_mix)
# if we are on the 1st part, then we need to make sure we can save the upload_id
if quick_mix.next_part_to_upload == 1
quick_mix.upload_id = quick_mix.s3_manager.multipart_upload_start(quick_mix[:ogg_url])
end
end
end
end

View File

@ -61,7 +61,21 @@ module JamRuby
end
def can_download?(some_user)
!ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id).nil?
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?
@ -126,7 +140,7 @@ module JamRuby
recorded_track.file_offset = 0
recorded_track.is_skip_mount_uploader = true
recorded_track.save
recorded_track.url = construct_filename(recorded_track.created_at, recording.id, track.client_track_id)
recorded_track.url = construct_filename(recording.created_at, recording.id, track.client_track_id)
recorded_track.save
recorded_track.is_skip_mount_uploader = false
recorded_track
@ -152,7 +166,6 @@ module JamRuby
self.file_offset = 0
self.next_part_to_upload = 0
self.upload_id = nil
self.file_offset
self.md5 = nil
self.length = 0
self.fully_uploaded = false
@ -161,6 +174,7 @@ module JamRuby
end
def upload_next_part(length, md5)
self.marking_complete = true
if next_part_to_upload == 0
upload_start(length, md5)
end
@ -174,6 +188,7 @@ module JamRuby
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
@ -195,7 +210,7 @@ module JamRuby
def filename
# construct a path from s3
RecordedTrack.construct_filename(self.created_at, self.recording.id, self.client_track_id)
RecordedTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id)
end
def update_download_count(count=1)

View File

@ -3,11 +3,16 @@ module JamRuby
self.primary_key = 'id'
@@log = Logging.logger[Recording]
attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin
has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy
has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy
has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy
has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy
@ -44,10 +49,34 @@ module JamRuby
self.comments.size
end
def has_mix?
self.mixes.length > 0 && self.mixes.first.completed
def high_quality_mix?
has_final_mix
end
def has_mix?
# this is used by the UI to know whether it can show a play button. We prefer a real mix, but a stream mix will do
return true if high_quality_mix?
has_stream_mix
end
# this should be a has-one relationship. until this, this is easiest way to get from recording > mix
def mix
self.mixes[0] if self.mixes.length > 0
end
def mix_state
mix.state if mix
end
def mix_error
mix.error if mix
end
def stream_mix
quick_mixes.find{|quick_mix| quick_mix.completed && !quick_mix.cleaned }
end
# this can probably be done more efficiently, but David needs this asap for a video
def grouped_tracks
tracks = []
@ -149,6 +178,11 @@ module JamRuby
if recording.save
GoogleAnalyticsEvent.report_band_recording(recording.band)
# make quick mixes *before* the audio/video tracks, because this will give them precedence in list_uploads
music_session.users.uniq.each do |user|
QuickMix.create(recording, user)
end
music_session.connections.each do |connection|
connection.tracks.each do |track|
recording.recorded_tracks << RecordedTrack.create_from_track(track, recording)
@ -179,6 +213,7 @@ module JamRuby
end
# Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording.
def claim(user, name, description, genre, is_public, upload_to_youtube=false)
upload_to_youtube = !!upload_to_youtube # Correct where nil is borking save
@ -337,37 +372,74 @@ module JamRuby
Arel::Nodes::As.new('video', Arel.sql('item_type'))
]).reorder("")
# Select fields for quick mix. Note that it must include
# the same number of fields as the track or video in order for
# the union to work:
quick_mix_arel = QuickMix.select([
:id,
:recording_id,
:user_id,
:ogg_url,
:fully_uploaded,
:upload_failures,
Arel::Nodes::As.new('', Arel.sql('quick_mix_track_id')),
Arel::Nodes::As.new('stream_mix', Arel.sql('item_type'))
]).reorder("")
# Glue them together:
union = track_arel.union(vid_arel)
# Create a table alias from the union so we can get back to arel:
utable = RecordedTrack.arel_table.create_table_alias(union, :recorded_items)
utable = Arel::Nodes::TableAlias.new(union, :recorded_items)
arel = track_arel.from(utable)
arel = arel.except(:select)
# Remove the implicit select created by .from. It
# contains an ambigious "id" field:
arel = arel.except(:select)
arel = arel.select([
:id,
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
"recorded_items.id",
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
# And repeat:
union_all = arel.union(quick_mix_arel)
utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all)
arel = arel.from(utable_all)
arel = arel.except(:select)
arel = arel.select([
"recorded_items_all.id",
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN (SELECT id as rec_id, all_discarded, duration FROM recordings) recs ON rec_id=recorded_items.recording_id") \
.where('recorded_items.user_id' => user.id) \
.where('recorded_items.fully_uploaded = ?', false) \
.where('recorded_items.id > ?', since) \
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
.where('recorded_items_all.fully_uploaded = ?', false) \
.where('recorded_items_all.id > ?', since) \
.where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") \
.where("duration IS NOT NULL") \
.where('all_discarded = false') \
.order('recorded_items.id') \
.order('recorded_items_all.id') \
.limit(limit)
# Load into array:
arel.each do |recorded_item|
if(recorded_item.item_type=='video')
if recorded_item.item_type=='video'
# A video:
uploads << ({
:type => "recorded_video",
@ -375,7 +447,7 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
else
elsif recorded_item.item_type == 'track'
# A track:
uploads << ({
:type => "recorded_track",
@ -383,7 +455,16 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
elsif recorded_item.item_type == 'stream_mix'
uploads << ({
:type => "stream_mix",
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
else
end
end
next_value = uploads.length > 0 ? uploads[-1][:next].to_s : nil
@ -397,18 +478,22 @@ module JamRuby
}
end
def preconditions_for_mix?
recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
true
end
# Check to see if all files have been uploaded. If so, kick off a mix.
def upload_complete
# Don't allow multiple mixes for now.
raise JamRuby::JamArgumentError unless self.mixes.length == 0
return if self.mixes.length > 0
# FIXME: There's a possible race condition here. If two users complete
# uploads at the same time, we'll schedule 2 mixes.
recorded_tracks.each do |recorded_track|
return unless recorded_track.fully_uploaded
end
self.mixes << Mix.schedule(self)
self.mixes << Mix.schedule(self) if preconditions_for_mix?
save
end
@ -423,7 +508,14 @@ module JamRuby
claimed_recordings.first
end
private
# returns a ClaimedRecording that the user did not discard
def claim_for_user(user)
return nil unless user
claim = claimed_recordings.find{|claimed_recording| claimed_recording.user == user }
claim unless claim && claim.discarded
end
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR

View File

@ -276,6 +276,7 @@ module JamRuby
rel, page = self.relation_pagination(rel, params)
rel = rel.includes([:instruments, :followings, :friends])
# XXX: DOES THIS MEAN ALL MATCHING USERS ARE RETURNED?
objs = rel.all
srch = Search.new

View File

@ -104,6 +104,7 @@ module JamRuby
# saved tracks
has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user
has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user
# invited users
has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser"

View File

@ -0,0 +1,46 @@
module JamRuby
class UserSync < ActiveRecord::Base
belongs_to :recorded_track
belongs_to :mix
belongs_to :quick_mix
def self.show(id, user_id)
self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first
end
def self.index(params)
user_id = params[:user_id]
limit = params[:limit].to_i
limit = limit == 0 ? 20 : limit
offset = params[:offset].to_i
id = params[:id]
recording_id = params[:recording_id]
page = (offset / limit) + 1 # one-based madness
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:[]).where(user_id: user_id).paginate(:page => page, :per_page => limit).order('created_at DESC, unified_id')
# allow selection of single user_sync, by ID
unless id.blank?
query = query.where(unified_id: id)
end
unless recording_id.blank?
query = query.where(recording_id: recording_id)
end
if query.length == 0
{ query:query, next: nil}
elsif query.length < limit
{ query:query, next: nil}
else
{ query:query, next: offset + limit}
end
end
end
end

View File

@ -30,7 +30,7 @@ module JamRuby
end
def self.queue_jobs_needing_retry
Mix.find_each(:conditions => 'should_retry = TRUE or started_at is NULL', :batch_size => 100) do |mix|
Mix.find_each(:conditions => "completed = FALSE AND (should_retry = TRUE OR (started_at IS NOT NULL AND NOW() - started_at > '1 hour'::INTERVAL))", :batch_size => 100) do |mix|
mix.enqueue
end
end
@ -130,6 +130,8 @@ module JamRuby
# update manifest so that audiomixer writes here
@manifest[:output][:filename] = @output_ogg_filename
# this is not used by audiomixer today; since today the Ruby code handles this
@manifest[:output][:filename_mp3] = @output_mp3_filename
@@log.debug("output ogg file: #{@output_ogg_filename}, output mp3 file: #{@output_mp3_filename}")
end
@ -228,8 +230,8 @@ module JamRuby
end
mix.errored(error_reason, error_detail)
rescue
@@log.error "unable to post back to the database the error"
rescue Exception => e
@@log.error "unable to post back to the database the error #{e}"
end
end

View File

@ -0,0 +1,148 @@
require 'json'
require 'resque'
require 'resque-retry'
require 'net/http'
require 'digest/md5'
module JamRuby
# executes a mix of tracks, creating a final output mix
class QuickMixer
@queue = :quick_mixer
@@log = Logging.logger[QuickMixer]
attr_accessor :quick_mix_id, :quick_mix, :output_filename, :error_out_filename,
:postback_mp3_url, :error_reason, :error_detail
def self.perform(quick_mix_id, postback_mp3_url)
JamWebEventMachine.run_wait_stop do
audiomixer = QuickMixer.new
audiomixer.postback_mp3_url = postback_mp3_url
audiomixer.quick_mix_id = quick_mix_id
audiomixer.run
end
end
def self.find_jobs_needing_retry &blk
QuickMix.find_each(:conditions => "completed = FALSE AND (should_retry = TRUE OR (started_at IS NOT NULL AND NOW() - started_at > '1 hour'::INTERVAL))", :batch_size => 100) do |mix|
blk.call(mix)
end
end
def self.queue_jobs_needing_retry
find_jobs_needing_retry do |mix|
mix.enqueue
end
end
def initialize
@s3_manager = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
def run
@@log.info("quickmixer job starting. quick_mix_id #{quick_mix_id}")
@quick_mix = QuickMix.find(quick_mix_id)
begin
# bailout check
if @quick_mix.completed
@@log.debug("quick mix is already completed. bailing")
return
end
fetch_audio_files
create_mp3(@input_ogg_filename, @output_mp3_filename)
postback
post_success(@quick_mix)
@@log.info("audiomixer job successful. mix_id #{quick_mix_id}")
rescue Exception => e
puts "EEOUOU #{e}"
post_error(@quick_mix, e)
raise
end
end
def postback
@@log.debug("posting mp3 mix to #{@postback_mp3_url}")
uri = URI.parse(@postback_mp3_url)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Put.new(uri.request_uri)
response = nil
File.open(@output_mp3_filename,"r") do |f|
request.body_stream=f
request["Content-Type"] = "audio/mpeg"
request.add_field('Content-Length', File.size(@output_mp3_filename))
response = http.request(request)
end
response_code = response.code.to_i
unless response_code >= 200 && response_code <= 299
@error_reason = "postback-mp3-mix-to-s3"
raise "unable to put to url: #{@postback_mp3_url}, status: #{response.code}, body: #{response.body}"
end
end
def post_success(mix)
mp3_length = File.size(@output_mp3_filename)
mp3_md5 = Digest::MD5.new
File.open(@output_mp3_filename, 'rb').each {|line| mp3_md5.update(line)}
mix.finish(mp3_length, mp3_md5.to_s)
end
def post_error(mix, e)
begin
# if error_reason is null, assume this is an unhandled error
unless @error_reason
@error_reason = "unhandled-job-exception"
@error_detail = e.to_s
end
mix.errored(error_reason, error_detail)
rescue Exception => e
@@log.error "unable to post back to the database the error #{e}"
end
end
def fetch_audio_files
@input_ogg_filename = Dir::Tmpname.make_tmpname( ["#{Dir.tmpdir}/quick_mixer_#{@quick_mix.id}}", '.ogg'], nil)
@output_mp3_filename = Dir::Tmpname.make_tmpname( ["#{Dir.tmpdir}/quick_mixer_#{@quick_mix.id}}", '.mp3'], nil)
@s3_manager.download(@quick_mix[:ogg_url], @input_ogg_filename)
end
private
def create_mp3(input_ogg_filename, output_mp3_filename)
ffmpeg_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{input_ogg_filename}\" -ab 128k -metadata JamRecordingId=#{@quick_mix.recording_id} -metadata JamQuickMixId=#{@quick_mix_id} -metadata JamType=QuickMix \"#{output_mp3_filename}\""
system(ffmpeg_cmd)
unless $? == 0
@error_reason = 'ffmpeg-failed'
@error_detail = $?.to_s
error_msg = "ffmpeg failed status=#{$?} error_reason=#{@error_reason} error_detail=#{@error_detail}"
@@log.info(error_msg)
raise error_msg
end
end
end
end

View File

@ -6,7 +6,7 @@ require 'digest/md5'
module JamRuby
# periodically scheduled to find jobs that need retrying
# periodically scheduled to find jobs that need retrying, and cleanup activities
class AudioMixerRetry
extend Resque::Plugins::LonelyJob
@ -21,6 +21,8 @@ module JamRuby
def self.perform
AudioMixer.queue_jobs_needing_retry
QuickMixer.queue_jobs_needing_retry
Recording.cleanup_excessive_storage
end
end

View File

@ -308,6 +308,30 @@ FactoryGirl.define do
}
end
factory :quick_mix, :class => JamRuby::QuickMix do
ignore do
autowire true
end
started_at nil
completed_at nil
ogg_md5 nil
ogg_length 0
ogg_url nil
mp3_md5 nil
mp3_length 0
mp3_url nil
completed false
before(:create) {|mix, evaluator|
if evaluator.autowire
user = FactoryGirl.create(:user)
mix.user = user
mix.recording = FactoryGirl.create(:recording_with_track, owner: user)
mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording)
end
}
end
factory :invited_user, :class => JamRuby::InvitedUser do
sequence(:email) { |n| "user#{n}@someservice.com" }
autofriend false

View File

@ -0,0 +1,212 @@
require 'spec_helper'
require 'rest-client'
describe QuickMix 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)
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
end
it "should copy from a regular track properly" do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.user.id.should == @track.connection.user.id
@quick_mix.next_part_to_upload.should == 0
@quick_mix.fully_uploaded.should == false
end
it "should update the next part to upload properly" do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.upload_part_complete(1, 1000)
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:ogg_length][0].should == "is too short (minimum is 1 characters)"
@quick_mix.errors[:ogg_md5][0].should == "can't be blank"
end
it "gets a url for the track" do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.errors.any?.should be_false
@quick_mix[:ogg_url].should == "recordings/#{@recording.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/stream-mix-#{@quick_mix.id}.ogg"
@quick_mix[:mp3_url].should == "recordings/#{@recording.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/stream-mix-#{@quick_mix.id}.mp3"
end
it "signs url" do
stub_const("APP_CONFIG", app_config)
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.sign_url.should_not be_nil
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
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.upload_start(1000, "abc")
@quick_mix.upload_part_complete(1, 1000)
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED
end
it "no parts" do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.upload_start(1000, "abc")
@quick_mix.upload_next_part(1000, "abc")
@quick_mix.errors.any?.should be_false
@quick_mix.upload_part_complete(1, 1000)
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS
end
it "enough part failures reset the upload" do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.upload_start(File.size(upload_file), md5)
@quick_mix.upload_next_part(File.size(upload_file), md5)
@quick_mix.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@quick_mix.upload_part_complete(@quick_mix.next_part_to_upload, File.size(upload_file))
@quick_mix.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
@quick_mix.reload
@quick_mix.is_part_uploading.should == expected_is_part_uploading
@quick_mix.part_failures.should == expected_part_failures
end
@quick_mix.reload
@quick_mix.upload_failures.should == 1
@quick_mix.file_offset.should == 0
@quick_mix.next_part_to_upload.should == 0
@quick_mix.upload_id.should be_nil
@quick_mix.ogg_md5.should be_nil
@quick_mix.ogg_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)
@quick_mix = QuickMix.create(@recording, @user)
APP_CONFIG.max_track_upload_failures.times do |j|
@quick_mix.upload_start(File.size(upload_file), md5)
@quick_mix.upload_next_part(File.size(upload_file), md5)
@quick_mix.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@quick_mix.upload_part_complete(@quick_mix.next_part_to_upload, File.size(upload_file))
@quick_mix.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
@quick_mix.reload
@quick_mix.is_part_uploading.should == expected_is_part_uploading
@quick_mix.part_failures.should == expected_part_failures
end
@quick_mix.upload_failures.should == j + 1
end
@quick_mix.reload
@quick_mix.upload_failures.should == APP_CONFIG.max_track_upload_failures
@quick_mix.file_offset.should == 0
@quick_mix.next_part_to_upload.should == 0
@quick_mix.upload_id.should be_nil
@quick_mix.ogg_md5.should be_nil
@quick_mix.ogg_length.should == 0
# try to poke it and get the right kind of error back
@quick_mix.upload_next_part(File.size(upload_file), md5)
@quick_mix.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED]
end
describe "correctly uploaded a file" do
before do
@quick_mix = QuickMix.create(@recording, @user)
@quick_mix.upload_start(File.size(upload_file), md5)
@quick_mix.upload_next_part(File.size(upload_file), md5)
signed_data = @quick_mix.upload_sign(md5)
@response = put_file_to_aws(signed_data, upload_file_contents)
@quick_mix.upload_part_complete(@quick_mix.next_part_to_upload, File.size(upload_file))
@quick_mix.errors.any?.should be_false
@quick_mix.upload_complete
@quick_mix.marking_complete = false # this is a side effect of using the same model throughout this process; controllers wouldn't see this
@quick_mix.finish(File.size(upload_file), md5)
@quick_mix.errors.any?.should be_false
# let's verify that .finish sets everything it should
@quick_mix.mp3_length.should == File.size(upload_file)
@quick_mix.mp3_md5.should == md5
@quick_mix.completed.should be_true
@quick_mix.recording.reload
@quick_mix.recording.first_quick_mix_id.should == @quick_mix.id
end
it "can download an updated file" do
@response = RestClient.get @quick_mix.sign_url
@response.body.should == upload_file_contents
end
it "can't mark completely uploaded twice" do
@quick_mix.upload_complete
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:fully_uploaded][0].should == "already set"
@quick_mix.part_failures.should == 0
end
it "can't ask for a next part if fully uploaded" do
@quick_mix.upload_next_part(File.size(upload_file), md5)
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:fully_uploaded][0].should == "already set"
@quick_mix.part_failures.should == 0
end
it "can't ask for mark part complete if fully uploaded" do
@quick_mix.upload_part_complete(1, 1000)
@quick_mix.errors.any?.should be_true
@quick_mix.errors[:fully_uploaded][0].should == "already set"
@quick_mix.part_failures.should == 0
end
end
end
end

View File

@ -192,6 +192,7 @@ describe RecordedTrack do
@recorded_track.errors.any?.should be_false
@recorded_track.upload_complete
@recorded_track.errors.any?.should be_false
@recorded_track.marking_complete = false
end
it "can download an updated file" do

View File

@ -2,6 +2,11 @@ require 'spec_helper'
describe Recording do
include UsesTempFiles
let(:s3_manager) { S3Manager.new(app_config.aws_bucket, app_config.aws_access_key_id, app_config.aws_secret_access_key) }
before do
@user = FactoryGirl.create(:user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@ -10,6 +15,186 @@ describe Recording do
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
end
describe "cleanup_excessive_storage" do
sample_audio='sample.file'
in_directory_with_file(sample_audio)
let(:user2) {FactoryGirl.create(:user)}
let(:quick_mix) { FactoryGirl.create(:quick_mix) }
let(:quick_mix2) {
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2)
recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user2, recording: recording)
recording.save!
FactoryGirl.create(:quick_mix, autowire: false, recording:recording, user: user2) }
let(:recording) { quick_mix.recording }
before(:each) do
content_for_file("ogg goodness")
end
it "cleans up nothing without failing" do
QuickMix.cleanup_excessive_storage
end
it "leaves untouched no completed mixes" do
recording.has_final_mix.should be_false
recording.has_stream_mix.should be_false
quick_mix.cleaned.should be_false
QuickMix.cleanup_excessive_storage
recording.reload
quick_mix.reload
recording.has_final_mix.should be_false
recording.has_stream_mix.should be_false
quick_mix.cleaned.should be_false
end
it "leaves untouched a completed quick mix if it's the only one and old enough" do
s3_manager.upload(quick_mix.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix.filename('mp3'), sample_audio)
quick_mix.completed = true
quick_mix[:ogg_url] = quick_mix.filename('ogg')
quick_mix[:mp3_url] = quick_mix.filename('mp3')
quick_mix.completed_at = 10.days.ago
quick_mix.save!
recording.has_stream_mix = true
recording.first_quick_mix_id = quick_mix.id
recording.save!
quick_mix.recording_id.should == recording.id
QuickMix.cleanup_excessive_storage
recording.reload
quick_mix.reload
recording.has_final_mix.should be_false
recording.has_stream_mix.should be_true
quick_mix.cleaned.should be_false
quick_mix[:ogg_url].should_not be_nil
quick_mix[:mp3_url].should_not be_nil
end
it "cleans up completed quick mix if their is a final mix" do
s3_manager.upload(quick_mix.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix.filename('mp3'), sample_audio)
quick_mix.completed = true
quick_mix[:ogg_url] = quick_mix.filename('ogg')
quick_mix[:mp3_url] = quick_mix.filename('mp3')
quick_mix.completed_at = 10.days.ago
quick_mix.save!
recording.has_final_mix = true
recording.has_stream_mix = true
recording.first_quick_mix_id = quick_mix.id
recording.save!
quick_mix.recording_id.should == recording.id
QuickMix.cleanup_excessive_storage
recording.reload
quick_mix.reload
recording.has_final_mix.should be_true
recording.has_stream_mix.should be_false
recording.first_quick_mix_id.should be_nil
quick_mix.cleaned.should be_true
quick_mix[:ogg_url].should_not be_nil
quick_mix[:mp3_url].should_not be_nil
s3_manager.exists?(quick_mix.filename('ogg')).should be_false
s3_manager.exists?(quick_mix.filename('mp3')).should be_false
end
it "cleans up one of two quick mixes" do
# quick_mix2 should get cleaned up, but quick_mix1 should be left alone because we need at least one if has_final_mix = false
quick_mix2.touch
s3_manager.upload(quick_mix.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix.filename('mp3'), sample_audio)
s3_manager.upload(quick_mix2.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix2.filename('mp3'), sample_audio)
quick_mix.completed = true
quick_mix[:ogg_url] = quick_mix.filename('ogg')
quick_mix[:mp3_url] = quick_mix.filename('mp3')
quick_mix.completed_at = 10.days.ago
quick_mix.save!
quick_mix2.completed = true
quick_mix2[:ogg_url] = quick_mix.filename('ogg')
quick_mix2[:mp3_url] = quick_mix.filename('mp3')
quick_mix2.completed_at = 10.days.ago
quick_mix2.save!
recording.has_stream_mix = true
recording.first_quick_mix_id = quick_mix.id
recording.save!
quick_mix.recording_id.should == recording.id
QuickMix.cleanup_excessive_storage
recording.reload
quick_mix.reload
quick_mix2.reload
recording.has_final_mix.should be_false
recording.has_stream_mix.should be_true
recording.first_quick_mix_id.should_not be_nil
quick_mix.cleaned.should be_false
quick_mix[:ogg_url].should_not be_nil
quick_mix[:mp3_url].should_not be_nil
s3_manager.exists?(quick_mix.filename('ogg')).should be_true
s3_manager.exists?(quick_mix.filename('mp3')).should be_true
quick_mix2.cleaned.should be_true
quick_mix2[:ogg_url].should_not be_nil
quick_mix2[:mp3_url].should_not be_nil
s3_manager.exists?(quick_mix2.filename('ogg')).should be_false
s3_manager.exists?(quick_mix2.filename('mp3')).should be_false
end
it "cleans up all quick mixes if there is a final mix" do
# quick_mix2 should get cleaned up, but quick_mix1 should be left alone because we need at least one if has_final_mix = false
quick_mix2.touch
s3_manager.upload(quick_mix.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix.filename('mp3'), sample_audio)
s3_manager.upload(quick_mix2.filename('ogg'), sample_audio)
s3_manager.upload(quick_mix2.filename('mp3'), sample_audio)
quick_mix.completed = true
quick_mix[:ogg_url] = quick_mix.filename('ogg')
quick_mix[:mp3_url] = quick_mix.filename('mp3')
quick_mix.completed_at = 10.days.ago
quick_mix.save!
quick_mix2.completed = true
quick_mix2[:ogg_url] = quick_mix.filename('ogg')
quick_mix2[:mp3_url] = quick_mix.filename('mp3')
quick_mix2.completed_at = 10.days.ago
quick_mix2.save!
recording.has_final_mix = true
recording.has_stream_mix = true
recording.first_quick_mix_id = quick_mix.id
recording.save!
quick_mix.recording_id.should == recording.id
QuickMix.cleanup_excessive_storage
recording.reload
quick_mix.reload
quick_mix2.reload
recording.has_final_mix.should be_true
recording.has_stream_mix.should be_false
recording.first_quick_mix_id.should be_nil
quick_mix.cleaned.should be_true
quick_mix[:ogg_url].should_not be_nil
quick_mix[:mp3_url].should_not be_nil
s3_manager.exists?(quick_mix.filename('ogg')).should be_false
s3_manager.exists?(quick_mix.filename('mp3')).should be_false
quick_mix2.cleaned.should be_true
quick_mix2[:ogg_url].should_not be_nil
quick_mix2[:mp3_url].should_not be_nil
s3_manager.exists?(quick_mix2.filename('ogg')).should be_false
s3_manager.exists?(quick_mix2.filename('mp3')).should be_false
end
end
it "should allow finding of recorded tracks" do
user2 = FactoryGirl.create(:user)
connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session)
@ -241,7 +426,7 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
uploads = Recording.list_uploads(@user)
uploads["uploads"].length.should == 1
uploads["uploads"].length.should == 2 # recorded_track and stream_mix
Recording.list_uploads(@user, 10, uploads["next"])["uploads"].length.should == 0
end
@ -336,11 +521,12 @@ describe Recording do
# We should have 2 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(2).items
uploads["uploads"][0][:type].should eq("recorded_track")
uploads["uploads"][1][:type].should eq("recorded_video")
uploads["uploads"][0].should include(:client_track_id)
uploads["uploads"][1].should include(:client_video_source_id)
uploads["uploads"].should have(3).items
uploads["uploads"][0][:type].should eq("stream_mix")
uploads["uploads"][1][:type].should eq("recorded_track")
uploads["uploads"][2][:type].should eq("recorded_video")
uploads["uploads"][1].should include(:client_track_id)
uploads["uploads"][2].should include(:client_video_source_id)
# Next page should have nothing:
uploads = Recording.list_uploads(@user, 10, uploads["next"])
@ -358,13 +544,19 @@ describe Recording do
# Limit to 1, so we can test pagination:
uploads = Recording.list_uploads(@user, 1)
uploads["uploads"].should have(1).items # First page
uploads["uploads"][0][:type].should == 'stream_mix'
# Second page:
uploads = Recording.list_uploads(@user, 1, uploads["next"])
uploads["uploads"].should have(1).items
uploads["uploads"][0][:type].should == 'recorded_track'
# Last page (should be empty):
uploads = Recording.list_uploads(@user, 10, uploads["next"])
uploads = Recording.list_uploads(@user, 1, uploads["next"])
uploads["uploads"].should have(1).items
uploads["uploads"][0][:type].should == 'recorded_video'
uploads = Recording.list_uploads(@user, 1, uploads["next"])
uploads["uploads"].should have(0).items
end
@ -384,15 +576,17 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
# We should have 2 items; a track and a video:
# We should have 3 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(2).items
uploads["uploads"][0][:type].should eq("recorded_track")
uploads["uploads"][1][:type].should eq("recorded_video")
uploads["uploads"][0].should include(:client_track_id)
uploads["uploads"][1].should include(:client_video_source_id)
uploads["uploads"][0][:client_track_id].should eq(@track.client_track_id)
uploads["uploads"][1][:client_video_source_id].should eq(video_source.client_video_source_id)
uploads["uploads"].should have(3).items
uploads["uploads"][0][:type].should eq("stream_mix")
uploads["uploads"][1][:type].should eq("recorded_track")
uploads["uploads"][2][:type].should eq("recorded_video")
uploads["uploads"][1].should include(:client_track_id)
uploads["uploads"][2].should include(:client_video_source_id)
uploads["uploads"][1][:client_track_id].should eq(@track.client_track_id)
uploads["uploads"][2][:client_video_source_id].should eq(video_source.client_video_source_id)
# Next page should have nothing:
uploads = Recording.list_uploads(@user, 10, uploads["next"])
@ -401,20 +595,49 @@ describe Recording do
# List uploads for user2. We should have 2 items; a track and a video:
# The track and video source IDs should be different than above:
uploads = Recording.list_uploads(user2)
uploads["uploads"].should have(2).items
uploads["uploads"][0][:type].should eq("recorded_track")
uploads["uploads"][1][:type].should eq("recorded_video")
uploads["uploads"][0].should include(:client_track_id)
uploads["uploads"][1].should include(:client_video_source_id)
uploads["uploads"][0][:client_track_id].should eq(track2.client_track_id)
uploads["uploads"][1][:client_video_source_id].should eq(video_source2.client_video_source_id)
uploads["uploads"].should have(3).items
uploads["uploads"][0][:type].should eq("stream_mix")
uploads["uploads"][1][:type].should eq("recorded_track")
uploads["uploads"][2][:type].should eq("recorded_video")
uploads["uploads"][1].should include(:client_track_id)
uploads["uploads"][2].should include(:client_video_source_id)
uploads["uploads"][1][:client_track_id].should eq(track2.client_track_id)
uploads["uploads"][2][:client_video_source_id].should eq(video_source2.client_video_source_id)
# Next page should have nothing:
uploads = Recording.list_uploads(@user, 10, uploads["next"])
uploads = Recording.list_uploads(user2, 10, uploads["next"])
uploads["uploads"].should have(0).items
end
it "stream_mix.fully_uploaded = true removes from list_uploads" do
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
# We should have 2 items; a track and a quick mix:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(2).items
uploads["uploads"][0][:type].should eq("stream_mix")
uploads["uploads"][1][:type].should eq("recorded_track")
@recording.quick_mixes[0].fully_uploaded = true
@recording.quick_mixes[0].save!
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(1).items
uploads["uploads"][0][:type].should eq("recorded_track")
@recording.recorded_tracks[0].fully_uploaded = true
@recording.recorded_tracks[0].save!
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(0).items
end
end

View File

@ -0,0 +1,226 @@
require 'spec_helper'
describe UserSync do
let(:user1) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
before(:each) do
end
it "empty results" do
UserSync.all.length.should == 0
end
describe "index" do
it "empty results" do
data = UserSync.index({user_id: user1})
data[:query].length.should == 0
data[:next].should be_nil
end
it "one mix" do
mix = FactoryGirl.create(:mix)
mix.recording.duration = 1
mix.recording.save!
data = UserSync.index({user_id: mix.recording.recorded_tracks[0].user.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == mix.recording.recorded_tracks[0]
user_syncs[0].mix.should be_nil
user_syncs[1].mix.should == mix
user_syncs[1].recorded_track.should be_nil
end
it "two mixes, one not belonging to querier" do
mix1 = FactoryGirl.create(:mix)
mix2 = FactoryGirl.create(:mix)
mix1.recording.duration = 1
mix1.recording.save!
mix2.recording.duration = 1
mix2.recording.save!
data = UserSync.index({user_id: mix1.recording.recorded_tracks[0].user.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == mix1.recording.recorded_tracks[0]
user_syncs[0].mix.should be_nil
user_syncs[1].mix.should == mix1
user_syncs[1].recorded_track.should be_nil
data = UserSync.index({user_id: mix2.recording.recorded_tracks[0].user.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == mix2.recording.recorded_tracks[0]
user_syncs[0].mix.should be_nil
user_syncs[1].mix.should == mix2
user_syncs[1].recorded_track.should be_nil
end
describe "one recording with two users" 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)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2)
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
}
}
it "no claimed_recordings" do
# both user1 and user2 should be told about each others tracks, because both will need to upload their tracks
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
it "recording isn't over" do
recording1.duration = nil
recording1.save!
# nothing should be returned, because the recording isn't over
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 0
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 0
end
it "one user decides to keep the recording" do
claimed_recording = FactoryGirl.create(:claimed_recording, user: user1, recording: recording1, discarded:false)
claimed_recording.recording.should == recording1
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
it "one user decides to discard the recording" do
FactoryGirl.create(:claimed_recording, user: user1, recording: recording1, discarded:true)
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
it "both users decide to discard the recording" do
recording1.all_discarded = true
recording1.save!
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 0
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.length.should == 0
end
end
end
describe "pagination" 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)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2)
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
}
}
it "tiny page size" do
# get the 1st track
data = UserSync.index({user_id: user1.id, offset: 0, limit: 1})
data[:next].should == 1
user_syncs = data[:query]
user_syncs.length.should == 1
user_syncs[0].recorded_track.should == sorted_tracks[0]
data[:query].total_entries.should == 2
# then get the second track, which happens to be on an edge, pagination wise
data = UserSync.index({user_id: user1.id, offset: 1, limit: 1})
data[:next].should == 2
user_syncs = data[:query]
user_syncs.length.should == 1
user_syncs[0].recorded_track.should == sorted_tracks[1]
data[:query].total_entries.should == 2
# so prove it's on the edge by asking for 2 items instead of 1.
data = UserSync.index({user_id: user1.id, offset: 0, limit: 3})
data[:next].should == nil
user_syncs = data[:query]
user_syncs.length.should == 2
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
data[:query].total_entries.should == 2
data = UserSync.index({user_id: user1.id, offset: 2, limit: 1})
data[:next].should == nil
user_syncs = data[:query]
user_syncs.length.should == 0
data[:query].total_entries.should == 2
end
end
end

View File

@ -1,7 +1,6 @@
require 'spec_helper'
require 'fileutils'
=begin
# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
describe AudioMixer do
@ -157,7 +156,7 @@ describe AudioMixer do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
# @music_session.connections << @connection
@music_session.save
@connection.join_the_session(@music_session, true, nil)
@connection.join_the_session(@music_session, true, [@track], @user, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.claim(@user, "name", "description", Genre.first, true)
@ -178,8 +177,10 @@ describe AudioMixer do
# stub out methods that are environmentally sensitive (so as to skip s3, and not run an actual audiomixer)
before(:each) do
AudioMixer.any_instance.stub(:execute) do |manifest_file|
output_filename = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_filename
output_ogg_filename = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_ogg_filename
output_mp3_filename = JSON.parse(File.read(manifest_file))['output']['filename_mp3']
FileUtils.touch output_mp3_filename
end
AudioMixer.any_instance.stub(:postback) # don't actually post resulting off file up
@ -194,18 +195,20 @@ describe AudioMixer do
@mix = Mix.schedule(@recording)
@mix.reload
@mix.started_at.should_not be_nil
AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24))
AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24, 'ogg'), @mix.sign_url(60 * 60 * 24, 'mp3'))
@mix.reload
@mix.completed.should be_true
@mix.length.should == 0
@mix.md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file
@mix.ogg_length.should == 0
@mix.ogg_md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file
@mix.mp3_length.should == 0
@mix.mp3_md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file
end
it "errored" do
local_files_manifest[:files][0][:filename] = '/some/path/to/nowhere'
Mix.any_instance.stub(:manifest).and_return(local_files_manifest)
@mix = Mix.schedule(@recording)
expect{ AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24)) }.to raise_error
expect{ AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24, 'ogg'), @mix.sign_url(60 * 60 * 24, 'mp3')) }.to raise_error
@mix.reload
@mix.completed.should be_false
@mix.error_count.should == 1
@ -283,8 +286,10 @@ describe AudioMixer do
@s3_sample = @s3_manager.sign_url(key, :secure => false)
AudioMixer.any_instance.stub(:execute) do |manifest_file|
output_filename = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_filename
output_filename_ogg = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_filename_ogg
output_filename_mp3 = JSON.parse(File.read(manifest_file))['output']['filename_mp3']
FileUtils.touch output_filename_mp3
end
end
@ -296,26 +301,12 @@ describe AudioMixer do
@mix.reload
@mix.completed.should be_true
@mix.length.should == 0
@mix.md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file, which is what the stubbed :execute does
end
it "fails" do
with_resque do
s3_manifest[:files][0][:filename] = @s3_manager.url('audiomixer/bogus.ogg', :secure => false) # take off some of the trailing chars of the url
Mix.any_instance.stub(:manifest).and_return(s3_manifest)
expect{ Mix.schedule(@recording) }.to raise_error
end
@mix = Mix.order('id desc').limit(1).first()
@mix.reload
@mix.completed.should be_false
@mix.error_reason.should == "unable to download"
#ResqueFailedJobMailer::Mailer.deliveries.count.should == 1
@mix.ogg_length.should == 0
@mix.ogg_md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file, which is what the stubbed :execute does
@mix.mp3_length.should == 0
@mix.mp3_md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file, which is what the stubbed :execute does
end
end
end
end
=end

View File

@ -0,0 +1,121 @@
require 'spec_helper'
require 'fileutils'
# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
describe AudioMixer do
include UsesTempFiles
let(:quick_mix) { FactoryGirl.create(:quick_mix, started_at: nil, should_retry: false) }
before(:each) do
stub_const("APP_CONFIG", app_config)
module EM
@next_tick_queue = []
end
MQRouter.client_exchange = double("client_exchange")
MQRouter.user_exchange = double("user_exchange")
MQRouter.client_exchange.should_receive(:publish).any_number_of_times
MQRouter.user_exchange.should_receive(:publish).any_number_of_times
end
def assert_found_job_count(expected)
count = 0
QuickMixer.find_jobs_needing_retry do
count = count + 1
end
count.should == expected
end
describe "queue_jobs_needing_retry" do
it "won't find anything if no quick mixes" do
assert_found_job_count(0)
end
it "won't find a quick mix that hasn't been started" do
quick_mix.touch
assert_found_job_count(0)
end
it "won't find a quick mix that has should_retry flagged" do
quick_mix.should_retry = true
quick_mix.save!
assert_found_job_count(1)
end
it "won't find a quick mix that has should_retry flagged" do
quick_mix.should_retry = false
quick_mix.started_at = 2.hours.ago
quick_mix.save!
assert_found_job_count(1)
end
end
# these tests try to run the job with minimal faking. Here's what we still fake:
# we don't run audiomixer. audiomixer is tested already
# we don't run redis and actual resque, because that's tested by resque/resque-spec
describe "full", :aws => true do
sample_ogg='sample.ogg'
in_directory_with_file(sample_ogg)
before(:each) do
content_for_file("ogg goodness")
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
# @music_session.connections << @connection
@music_session.save
@connection.join_the_session(@music_session, true, [@track], @user, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.claim(@user, "name", "description", Genre.first, true)
@recording.errors.any?.should be_false
@recording.quick_mixes.length.should == 1
@quick_mix = @recording.quick_mixes[0]
test_config = app_config
@s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
# put a file in s3
@s3_manager.upload(@quick_mix[:ogg_url], sample_ogg)
# create a signed url that the job will use to fetch it back down
@s3_sample = @s3_manager.sign_url(@quick_mix[:ogg_url], :secure => false)
QuickMixer.any_instance.stub(:create_mp3) do |input_file, output_file|
FileUtils.mv input_file, output_file
end
end
it "completes" do
with_resque do
#QuickMix.any_instance.stub(:manifest).and_return(s3_manifest)
@mix = @recording.quick_mixes[0]
@mix.enqueue
end
@mix.reload
@mix.completed.should be_true
@mix.mp3_length.should == "ogg goodness".length
@mix.mp3_md5.should == ::Digest::MD5.file(sample_ogg).hexdigest
# fetch back the mp3 file from s3, check that it's equivalent to the sample_ogg file (which we did a FileUtils.mv of to make a fake mp3)
mp3_fetched_from_s3 = Tempfile.new('foo')
@s3_manager.download(@quick_mix[:mp3_url], mp3_fetched_from_s3.path)
@mix.mp3_md5.should == ::Digest::MD5.file(mp3_fetched_from_s3).hexdigest
end
end
end

View File

@ -42,6 +42,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 QuickMixObserver.instance
#RecordedTrack.observers.disable :all # only a few tests want this observer active

View File

@ -34,6 +34,7 @@
//= require jquery.pulse
//= require jquery.browser
//= require jquery.custom-protocol
//= require jquery.exists
//= require jstz
//= require class
//= require AAC_underscore

View File

@ -0,0 +1,48 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.AllSyncsDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $dialog = null;
var $syncsHolder = null;
var syncViewer = null;
function registerEvents() {
}
function beforeShow() {
syncViewer.onShow()
}
function beforeHide() {
syncViewer.onHide()
}
function initialize(invitationDialogInstance) {
var dialogBindings = {
'beforeShow': beforeShow,
'beforeHide': beforeHide
};
app.bindDialog('all-syncs-dialog', dialogBindings);
$dialog = $('#all-syncs-dialog');
$syncsHolder = $dialog.find('.syncs')
syncViewer = new context.JK.SyncViewer(app);
syncViewer.init($syncsHolder);
$syncsHolder.append(syncViewer.root)
registerEvents();
};
this.initialize = initialize;
}
return this;
})(window, jQuery);

View File

@ -7,6 +7,7 @@
var rest = context.JK.Rest();
var playbackControls = null;
var recording = null; // deferred object
var $dialog = null;
function resetForm() {
@ -16,6 +17,7 @@
}
function beforeShow() {
$dialog.data('result', null);
if (recording == null) {
alert("recording data should not be null");
app.layout.closeDialog('recordingFinished');
@ -104,6 +106,7 @@
logger.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText);
})
.always(function () {
$dialog.data('result', {keep:false});
app.layout.closeDialog('recordingFinished')
registerDiscardRecordingHandlers(true);
})
@ -128,6 +131,7 @@
is_public: is_public
})
.done(function () {
$dialog.data('result', {keep:true});
app.layout.closeDialog('recordingFinished');
context.JK.GA.trackMakeRecording();
})
@ -231,6 +235,7 @@
app.bindDialog('recordingFinished', dialogBindings);
$dialog = $('#recording-finished-dialog')
playbackControls = new context.JK.PlaybackControls($('#recording-finished-dialog .recording-controls'));
registerStaticEvents();

View File

@ -1,64 +0,0 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.WhatsNextDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var invitationDialog = null;
function registerEvents() {
$('#whatsnext-dialog a.facebook-invite').on('click', function (e) {
invitationDialog.showFacebookDialog(e);
});
$('#whatsnext-dialog a.google-invite').on('click', function (e) {
invitationDialog.showGoogleDialog();
});
$('#whatsnext-dialog a.email-invite').on('click', function (e) {
invitationDialog.showEmailDialog();
});
}
function beforeShow() {
}
function beforeHide() {
var $dontShowWhatsNext = $('#show_whats_next');
if ($dontShowWhatsNext.is(':checked')) {
rest.updateUser({show_whats_next: false})
}
}
function initializeButtons() {
var dontShow = $('#show_whats_next');
context.JK.checkbox(dontShow);
}
function initialize(invitationDialogInstance) {
var dialogBindings = {
'beforeShow': beforeShow,
'beforeHide': beforeHide
};
app.bindDialog('whatsNext', dialogBindings);
registerEvents();
invitationDialog = invitationDialogInstance;
initializeButtons();
};
this.initialize = initialize;
}
return this;
})(window, jQuery);

View File

@ -336,6 +336,12 @@
dbg('AbortRecording');
fakeJamClientRecordings.AbortRecording(recordingId, errorReason, errorDetail);
}
function OnTrySyncCommand(cmd) {
}
function GetRecordingManagerState() {
return {running: false}
}
function TestASIOLatency(s) { dbg('TestASIOLatency' + JSON.stringify(arguments)); }
@ -808,6 +814,8 @@
// Javascript Bridge seems to camel-case
// Set the instance functions:
this.AbortRecording = AbortRecording;
this.OnTrySyncCommand = OnTrySyncCommand;
this.GetRecordingManagerState = GetRecordingManagerState;
this.GetASIODevices = GetASIODevices;
this.GetOS = GetOS;
this.GetOSAsString = GetOSAsString;

View File

@ -9,6 +9,7 @@
var rest = new context.JK.Rest();
var EVENTS = context.JK.EVENTS;
var ui = new context.JK.UIHelper(JK.app);
var recordingUtils = context.JK.RecordingUtils;
var userId = null;
var currentFeedPage = 0;
var feedBatchSize = 10;
@ -424,6 +425,9 @@
logger.error("a recording in the feed should always have one claimed_recording")
return;
}
// pump some useful data about mixing into the feed item
feed.mix_info = recordingUtils.createMixInfo({state: feed.mix_state})
var options = {
feed_item: feed,
candidate_claimed_recording: obtainCandidate(feed),
@ -433,6 +437,10 @@
var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'}));
var $controls = $feedItem.find('.recording-controls');
$controls.data('mix-state', feed.mix_info) // for recordingUtils helper methods
$controls.data('server-info', feed.mix) // for recordingUtils helper methods
$controls.data('view-context', 'feed')
$('.timeago', $feedItem).timeago();
context.JK.prettyPrintElements($('.duration', $feedItem));
context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem));
@ -501,6 +509,8 @@
}).show();
}
context.JK.helpBubble($feedItem.find('.help-launcher'), recordingUtils.onMixHover, {}, {width:'450px', closeWhenOthersOpen: true, positions:['top', 'left', 'bottom', 'right'], offsetParent: $screen.parent()})
// put the feed item on the page
renderFeed($feedItem);

View File

@ -6,10 +6,12 @@
context.JK.FeedItemRecording = function($parentElement, options){
var ui = new context.JK.UIHelper(JK.app);
var recordingUtils = context.JK.RecordingUtils;
var claimedRecordingId = $parentElement.attr('data-claimed-recording-id');
var recordingId = $parentElement.attr('id');
var mode = $parentElement.attr('data-mode');
var mixInfo = null;
var $feedItem = $parentElement;
var $name = $('.name', $feedItem);
@ -129,6 +131,12 @@
context.JK.bindHoverEvents($feedItem);
//context.JK.bindProfileClickEvents($feedItem);
mixInfo = recordingUtils.createMixInfo({state: options.mix_state})
$controls.data('mix-state', mixInfo) // for recordingUtils helper methods
$controls.data('view-context', 'feed')
$controls.addClass(mixInfo.mixStateClass)
$status.text(mixInfo.mixStateMsg)
}
initialize();

View File

@ -35,7 +35,11 @@
RSVP_CANCELED : 'rsvp_canceled',
USER_UPDATED : 'user_updated',
SESSION_STARTED : 'session_started',
SESSION_ENDED : 'session_stopped'
SESSION_ENDED : 'session_stopped',
FILE_MANAGER_CMD_START : 'file_manager_cmd_start',
FILE_MANAGER_CMD_STOP : 'file_manager_cmd_stop',
FILE_MANAGER_CMD_PROGRESS : 'file_manager_cmd_progress',
FILE_MANAGER_CMD_ASAP_UPDATE : 'file_manager_cmd_asap_update'
};
context.JK.ALERT_NAMES = {

View File

@ -840,7 +840,29 @@
})
}
function getUserSyncs(options) {
if(!options) { options = {}; }
var userId = getId(options)
return $.ajax({
type: 'GET',
dataType: "json",
url: "/api/users/" + userId + "/syncs?" + $.param(options),
processData:false
})
}
function getUserSync(options) {
if(!options) { options = {}; }
var userId = getId(options)
var userSyncId = options['user_sync_id']
return $.ajax({
type: 'GET',
dataType: "json",
url: "/api/users/" + userId + "/syncs/" + userSyncId,
processData:false
})
}
function sendFriendRequest(app, userId, callback) {
var url = "/api/users/" + context.JK.currentUserId + "/friend_requests";
@ -979,6 +1001,18 @@
})
}
function getRecordedTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId + "/tracks/" + trackId
});
}
function getRecording(options) {
var recordingId = options["id"];
@ -1221,6 +1255,7 @@
});
}
function initialize() {
return self;
}
@ -1281,6 +1316,8 @@
this.getMusicianInvites = getMusicianInvites;
this.postFeedback = postFeedback;
this.getFeeds = getFeeds;
this.getUserSyncs = getUserSyncs;
this.getUserSync = getUserSync;
this.serverHealthCheck = serverHealthCheck;
this.sendFriendRequest = sendFriendRequest;
this.getFriendRequest = getFriendRequest;
@ -1295,6 +1332,7 @@
this.startRecording = startRecording;
this.stopRecording = stopRecording;
this.getRecording = getRecording;
this.getRecordedTrack = getRecordedTrack;
this.getClaimedRecordings = getClaimedRecordings;
this.getClaimedRecording = getClaimedRecording;
this.updateClaimedRecording = updateClaimedRecording;

View File

@ -0,0 +1 @@
jQuery.fn.exists = function(){return this.length > 0;}

View File

@ -18,6 +18,7 @@
//= require jquery.custom-protocol
//= require jquery.ba-bbq
//= require jquery.icheck
//= require jquery.exists
//= require AAA_Log
//= require AAC_underscore
//= require alert

View File

@ -345,6 +345,7 @@
else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
$notification.find('#div-actions').hide();
logger.debug("context.jamClient.OnDownloadAvailable!")
context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
}

View File

@ -9,6 +9,8 @@
context.JK.Paginator = {
$templatePaginator: null,
/** returns a jquery object that encapsulates pagination markup.
* It's left to the caller to append it to the page as they like.
* @param pages the number of pages
@ -18,6 +20,16 @@
*/
create:function(totalEntries, perPage, currentPage, onPageSelected) {
if(this.$templatePaginator === null) {
this.$templatePaginator = $('#template-paginator')
if(this.$templatePaginator.length == 0) {
throw "no #template-paginator"
}
}
var $templatePaginator = this.$templatePaginator;
function calculatePages(total, perPageValue) {
return Math.ceil(total / perPageValue);
}
@ -32,13 +44,13 @@
onPageSelected(targetPage)
.done(function(data, textStatus, jqXHR) {
totalEntries = parseInt(jqXHR.getResponseHeader('total-entries'));
totalEntries = data.total_entries || parseInt(jqXHR.getResponseHeader('total-entries'));
pages = calculatePages(totalEntries, perPage);
options = { pages: pages,
currentPage: targetPage };
// recreate the pagination, and
var newPaginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' }));
var newPaginator = $(context._.template($templatePaginator.html(), options, { variable: 'data' }));
registerEvents(newPaginator);
paginator.replaceWith(newPaginator);
paginator = newPaginator;
@ -90,7 +102,7 @@
var options = { pages: pages,
currentPage: currentPage };
var paginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' }));
var paginator = $(context._.template($templatePaginator.html(), options, { variable: 'data' }));
registerEvents(paginator);

View File

@ -2,121 +2,178 @@
* Recording Manager viewer
* Although multiple instances could be made, only one should be
*/
(function(context, $) {
(function (context, $) {
"use strict";
"use strict";
context.JK = context.JK || {};
context.JK = context.JK || {};
context.JK.RecordingManager = function(){
context.JK.RecordingManager = function (app) {
var $parentElement = $('#recording-manager-viewer');
var $parentElement = $('#recording-manager-viewer');
var logger = context.JK.logger;
if($parentElement.length == 0) {
logger.debug("no $parentElement specified in RecordingManager");
}
var logger = context.JK.logger;
var EVENTS = context.JK.EVENTS;
var $downloadCommand = $('#recording-manager-download', $parentElement);
var $downloadPercent = $('#recording-manager-download .percent', $parentElement);
var $uploadCommand = $('#recording-manager-upload', $parentElement);
var $uploadPercent = $('#recording-manager-upload .percent', $parentElement);
var $convertCommand = $('#recording-manager-convert', $parentElement);
var $convertPercent = $('#recording-manager-convert .percent', $parentElement);
// keys come from backend
var lookup = {
SyncDownload: { command: $downloadCommand, percent: $downloadPercent },
SyncUpload: { command: $uploadCommand, percent: $uploadPercent },
SyncConvert: { command: $convertCommand, percent: $convertPercent }
}
var $self = $(this);
function renderStartCommand($command) {
$command.css('visibility', 'visible');
}
function renderEndCommand($command) {
$command.css('visibility', 'hidden');
}
function renderPercentage($percent, value) {
$percent.text(Math.round(value * 100));
}
function onStartCommand(id, type) {
var command = lookup[type];
if(!command) { return }
var existingCommandId = command.command.data('command-id');
if(existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
}
command.command.data('command-id', id);
renderStartCommand(command.command);
renderPercentage(command.percent, 0);
}
function onStopCommand(id, type, success, reason, detail) {
var command = lookup[type];
if(!command) { return }
var existingCommandId = command.command.data('command-id');
if(!existingCommandId) {
command.command.data('command-id', id);
renderStartCommand(command.command);
}
else if(existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
command.command.data('command-id', id);
renderStartCommand(command.command);
}
renderPercentage(command.percent, 1);
renderEndCommand(command.command);
command.command.data('command-id', null);
}
function onCommandProgress(id, type, progress) {
var command = lookup[type];
if(!command) { return }
var existingCommandId = command.command.data('command-id');
if(!existingCommandId) {
command.command.data('command-id', id);
renderStartCommand(command.command);
}
else if(existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
command.command.data('command-id', id);
renderStartCommand(command.command);
}
renderPercentage(command.percent, progress);
}
function onCommandsChanged(id, type) {
}
context.JK.RecordingManagerCommandStart = onStartCommand;
context.JK.RecordingManagerCommandStop = onStopCommand;
context.JK.RecordingManagerCommandProgress = onCommandProgress;
context.JK.RecordingManagerCommandsChanged = onCommandsChanged;
context.jamClient.RegisterRecordingManagerCallbacks(
"JK.RecordingManagerCommandStart",
"JK.RecordingManagerCommandProgress",
"JK.RecordingManagerCommandStop",
"JK.RecordingManagerCommandsChanged"
)
return this;
if ($parentElement.length == 0) {
logger.debug("no $parentElement specified in RecordingManager");
}
var $downloadCommand = $('#recording-manager-download', $parentElement);
var $downloadPercent = $('#recording-manager-download .percent', $parentElement);
var $uploadCommand = $('#recording-manager-upload', $parentElement);
var $uploadPercent = $('#recording-manager-upload .percent', $parentElement);
var $convertCommand = $('#recording-manager-convert', $parentElement);
var $convertPercent = $('#recording-manager-convert .percent', $parentElement);
var $fileManager = $('#recording-manager-launcher', $parentElement);
if($fileManager.length == 0) {throw "no file manager element"; }
$downloadCommand.data('command-type', 'download')
$uploadCommand.data('command-type', 'upload')
$convertCommand.data('command-type', 'convert')
// keys come from backend
var lookup = {
SyncDownload: { command: $downloadCommand, percent: $downloadPercent},
SyncUpload: { command: $uploadCommand, percent: $uploadPercent},
SyncConvert: { command: $convertCommand, percent: $convertPercent}
}
var $self = $(this);
if (context.jamClient.IsNativeClient()) {
$parentElement.addClass('native-client')
}
function renderStartCommand($command) {
$command.css('visibility', 'visible').addClass('running');
$parentElement.addClass('running')
$(document).triggerHandler(EVENTS.FILE_MANAGER_CMD_START, $command.data())
}
function renderEndCommand($command) {
$command.css('visibility', 'hidden').removeClass('running')
if ($parentElement.find('.recording-manager-command.running').length == 0) {
$parentElement.removeClass('running')
}
$(document).triggerHandler(EVENTS.FILE_MANAGER_CMD_STOP, $command.data())
}
function renderPercentage($command, $percent, value) {
var percentage = Math.round(value * 100)
if(percentage > 100) percentage = 100;
$percent.text(percentage);
$command.data('percentage', percentage)
$(document).triggerHandler(EVENTS.FILE_MANAGER_CMD_PROGRESS, $command.data())
}
function onStartCommand(id, type, metadata) {
var command = lookup[type];
if (!command) {
return
}
var existingCommandId = command.command.data('command-id');
if (existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
}
command.command.data('command-id', id)
command.command.data('command-metadata', metadata)
command.command.data('command-success', null);
command.command.data('command-reason', null);
command.command.data('command-detail', null);
renderStartCommand(command.command);
renderPercentage(command.command, command.percent, 0);
}
function onStopCommand(id, type, success, reason, detail) {
var command = lookup[type];
if (!command) {
return
}
var existingCommandId = command.command.data('command-id');
if (!existingCommandId) {
command.command.data('command-id', id);
renderStartCommand(command.command);
}
else if (existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
command.command.data('command-id', id);
renderStartCommand(command.command);
}
command.command.data('command-success', success);
command.command.data('command-reason', reason);
command.command.data('command-detail', detail);
renderPercentage(command.command, command.percent, 1);
renderEndCommand(command.command);
command.command.data('command-id', null);
}
function onCommandProgress(id, type, progress) {
var command = lookup[type];
if (!command) {
return
}
var existingCommandId = command.command.data('command-id');
if (!existingCommandId) {
command.command.data('command-id', id);
renderStartCommand(command.command);
}
else if (existingCommandId && existingCommandId != id) {
renderEndCommand(command.command);
command.command.data('command-id', id);
renderStartCommand(command.command);
}
renderPercentage(command.command, command.percent, progress);
}
function onCommandsChanged(id, type) {
}
function onAsapCommandStatus(id, type, metadata, reason) {
$(document).triggerHandler(EVENTS.FILE_MANAGER_CMD_ASAP_UPDATE, {commandMetadata: metadata, commandId: id, commandReason: reason})
}
function onClick() {
app.layout.showDialog('all-syncs-dialog')
return false;
}
$fileManager.click(onClick)
$convertCommand.click(onClick)
$downloadCommand.click(onClick)
$uploadCommand.click(onClick)
context.JK.RecordingManagerCommandStart = onStartCommand;
context.JK.RecordingManagerCommandStop = onStopCommand;
context.JK.RecordingManagerCommandProgress = onCommandProgress;
context.JK.RecordingManagerCommandsChanged = onCommandsChanged;
context.JK.RecordingManagerAsapCommandStatus = onAsapCommandStatus;
context.jamClient.RegisterRecordingManagerCallbacks(
"JK.RecordingManagerCommandStart",
"JK.RecordingManagerCommandProgress",
"JK.RecordingManagerCommandStop",
"JK.RecordingManagerCommandsChanged",
"JK.RecordingManagerAsapCommandStatus"
)
return this;
}
})(window, jQuery);

View File

@ -0,0 +1,141 @@
#
# Common utility functions.
#
$ = jQuery
context = window
context.JK ||= {};
class RecordingUtils
constructor: () ->
@logger = context.JK.logger
init: () =>
@templateHoverMix = $('#template-sync-viewer-hover-mix')
mixStateErrorDescription: (mix) =>
summary = ''
if mix.error.error_reason == 'mix-timeout'
summary = "The mix was started, but it has taken too long to for it to finish. The mix was started #{$.timeago(mix.error.error_detail)}. JamKazam employees have been notified of the issue."
else if mix.error.error_reason == 'unknown'
summary = "The reason the mix failed is unknown. JamKazam employees have been notified of the issue. "
else if mix.error.error_reason == 'unable to download'
summary = "The mixer could not fetch one of the tracks from JamKazam servers. JamKazam employees have been notified of the issue."
else if mix.error.error_reason == 'unable-parse-error-out'
summary = "The mixer failed, but it could not report what went wrong. JamKazam employees have been notified of the issue."
else if mix.error.error_reason == 'postback-ogg-mix-to-s3' or mix.error_reason == 'postback-mp3-mix-to-s3'
summary = "The mixer made the mix, but could not publish it. JamKazam employees have been notified of the issue."
else if mix.error.error_reason == 'unhandled-job-exception'
summary = "The mixer had an error while mixing. JamKazam employees have been notified of the issue."
else if mix.error.error_reason == 'unhandled-job-exception'
summary = "The mixer had an error while mixing. JamKazam employees have been notified of the issue."
summary
mixStateDefinition: (mixState) =>
definition = switch mixState
when 'still-uploading' then "STILL UPLOADING means the you or others in the recording have not uploaded enough information yet to make a mix."
when 'stream-mix' then "STREAM MIX means a user's real-time mix is available to listen to."
when 'discarded' then "DISCARDED means you chose to not keep this recording when the recording was over."
when 'mixed' then "MIXED means all tracks have been uploaded, and a final mix has been made."
when 'mixing' then "MIXING means all tracks have been uploaded, and the final mix is currently being made."
when 'waiting-to-mix' then "MIX QUEUED means all tracks have been uploaded, and the final mix should start being made very soon."
when 'error' then 'MIX FAILED means something went wrong with the mixer after all tracks were uploaded.'
else 'There is no help for this state'
createMixInfo: (mix) =>
mixStateMsg = 'UNKNOWN'
mixStateClass = 'unknown'
mixState = 'unknown'
if mix? and mix.state? and !mix.fake
# the '.download' property is only used for personalized views of a mix.
if !mix.download? mix.download.should_download
if mix.state == 'error'
mixStateMsg = 'MIX FAILED'
mixStateClass = 'error'
mixState = 'error'
else if mix.state == 'stream-mix'
mixStateMsg = 'STREAM MIX'
mixStateClass = 'stream-mix'
mixState = 'stream-mix'
else if mix.state == 'waiting-to-mix'
mixStateMsg = 'MIX QUEUED'
mixStateClass = 'waiting-to-mix'
mixState = 'waiting-to-mix'
else if mix.state == 'mixing'
mixStateMsg = 'MIXING'
mixStateClass = 'mixing'
mixState = 'mixing'
else if mix.state == 'mixed'
mixStateMsg = 'MIXED'
mixStateClass = 'mixed'
mixState = 'mixed'
else
mixStateMsg = mix.state
mixStateClass = 'unknown'
mixState = 'unknown'
else
mixStateMsg = 'DISCARDED'
mixStateClass = 'discarded'
mixState = 'discarded'
else
mixStateMsg = 'STILL UPLOADING'
mixStateClass = 'still-uploading'
mixState = 'still-uploading'
return {
mixStateMsg: mixStateMsg,
mixStateClass: mixStateClass,
mixState: mixState,
isError: mixState == 'error'
}
onMixHover: () ->
$mix = $(this).closest('.mix')
mixStateInfo = $mix.data('mix-state')
viewContext = $mix.data('view-context')
if !mixStateInfo? # to support feed and syncs both using this same code, check .mix-state too
$mixState = $mix.find('.mix-state')
mixStateInfo = $mixState.data('mix-state')
if !mixStateInfo?
logger.error('no mix-state anywhere', this)
throw 'no mix-state'
mixStateMsg = mixStateInfo.mixStateMsg
mixStateClass = mixStateInfo.mixStateClass
mixState = mixStateInfo.mixState
serverInfo = $mix.data('server-info')
# lie if this is a virtualized mix (i.e., mix is created after recording is made)
mixState = 'still-uploading' if !serverInfo? or serverInfo.fake
summary = ''
if mixState == 'still-uploading'
#summary = "No one in the recording has uploaded any files yet. Once tracks have been uploaded, a mix will be made."
else if mixState == 'discarded'
summary = "When this recording was made, you elected to not keep it. But at least one other person elected to keep it so your tracks are needed for the mix."
else if mixState == 'mixed'
#summary = 'All tracks have been uploaded, and a final mix has been made.'
else if mixState == 'stream-mix'
summary = 'A STREAM MIX the mix that someone heard in their headphones as the actual recording was made. In the case of a recording involving two or more people, it will contain any imperfections caused by the internet. Once the final, high-quality mix is available, we will automatically replace the stream mix with it.'
else if mixState == 'mixing'
#summary = 'All tracks have been uploaded, and the final mix is currently being made.'
else if mixState == 'waiting-to-mix'
#summary = 'All tracks have been uploaded, and the finale mix should start being made very soon.'
else if mixState == 'error'
summary = context.JK.RecordingUtils.mixStateErrorDescription(serverInfo)
mixStateDefinition = context.JK.RecordingUtils.mixStateDefinition(mixState)
context._.template(context.JK.RecordingUtils.templateHoverMix.html(), {summary: summary, mixStateDefinition: mixStateDefinition, mixStateMsg: mixStateMsg, mixStateClass: mixStateClass, viewContext: viewContext}, {variable: 'data'})
# global instance
context.JK.RecordingUtils = new RecordingUtils()

View File

@ -4,6 +4,7 @@
context.JK = context.JK || {};
context.JK.SessionScreen = function(app) {
var EVENTS = context.JK.EVENTS;
var gearUtils = context.JK.GearUtils;
var sessionUtils = context.JK.SessionUtils;
var logger = context.JK.logger;
@ -35,7 +36,8 @@
var rateSessionDialog = null;
var friendInput = null;
var sessionPageDone = null;
var $recordingManagerViewer = null;
var $screen = null;
var rest = context.JK.Rest();
var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there.
@ -1348,7 +1350,11 @@
rest.getRecording( {id: recordingId} )
.done(function(recording) {
recordingFinishedDialog.setRecording(recording);
app.layout.showDialog('recordingFinished');
app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(data.result && data.result.keep){
context.JK.prodBubble($recordingManagerViewer, 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $screen.parent()})
}
})
})
.fail(app.ajaxError);
}
@ -1462,6 +1468,8 @@
};
app.bindScreen('session', screenBindings);
$recordingManagerViewer = $('#recording-manager-viewer');
$screen = $('#session-screen');
// make sure no previous plays are still going on by accident
context.jamClient.SessionStopPlay();
if(context.jamClient.SessionRemoveAllPlayTracks) {

View File

@ -0,0 +1,892 @@
$ = jQuery
context = window
context.JK ||= {};
context.JK.SyncViewer = class SyncViewer
constructor: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
@recordingUtils = context.JK.RecordingUtils
@since = 0
@limit = 20
@showing = false
@downloadCommandId = null
@downloadMetadata = null
@uploadCommandId = null
@uploadMetadata = null;
init: () =>
@root = $($('#template-sync-viewer').html())
@inProgress = @root.find('.in-progress')
@downloadProgress = @inProgress.find('.download-progress')
@uploadProgress = @inProgress.find('.upload-progress')
@list = @root.find('.list')
@logList = @root.find('.log-list')
@templateRecordedTrack = $('#template-sync-viewer-recorded-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')
@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')
@templateLogItem = $('#template-sync-viewer-log-item')
@tabSelectors = @root.find('.dialog-tabs .tab')
@tabs = @root.find('.tab-content')
@logBadge = @tabSelectors.find('.badge')
@paginator = @root.find('.paginator-holder')
@uploadStates = {
unknown: 'unknown',
too_many_upload_failures: 'too-many-upload-failures',
me_upload_soon: 'me-upload-soon',
them_upload_soon: 'them-upload-soon'
missing: 'missing',
me_uploaded: 'me-uploaded',
them_uploaded: 'them-uploaded'
}
@clientStates = {
unknown: 'unknown',
too_many_uploads: 'too-many-downloads',
hq: 'hq',
sq: 'sq',
missing: 'missing',
discarded: 'discarded'
}
throw "no sync-viewer" if not @root.exists()
throw "no in-progress" if not @inProgress.exists()
throw "no list" if not @list.exists()
throw "no recorded track template" if not @templateRecordedTrack.exists()
throw "no stream mix template" if not @templateStreamMix.exists()
throw "no empty syncs template" if not @templateNoSyncs.exists()
$(document).on(@EVENTS.FILE_MANAGER_CMD_START, this.fileManagerCmdStart)
$(document).on(@EVENTS.FILE_MANAGER_CMD_STOP, this.fileManagerCmdStop)
$(document).on(@EVENTS.FILE_MANAGER_CMD_PROGRESS, this.fileManagerCmdProgress)
$(document).on(@EVENTS.FILE_MANAGER_CMD_ASAP_UPDATE, this.fileManagerAsapCommandStatus)
@tabSelectors.click((e) =>
$tabSelected = $(e.target).closest('a.tab')
purpose = $tabSelected.attr('purpose')
$otherTabSelectors = @tabSelectors.not('[purpose="' + purpose + '"]')
$otherTabs = @tabs.not('[purpose="' + purpose + '"]')
$tab = @tabs.filter('[purpose="' + purpose + '"]')
$otherTabs.hide();
$tab.show()
$otherTabSelectors.removeClass('selected');
$tabSelected.addClass('selected');
if purpose == 'log'
@logBadge.hide()
)
onShow: () =>
@showing = true
this.load()
onHide: () =>
@showing = false
#$(document).off(@EVENTS.FILE_MANAGER_CMD_START, this.fileManagerCmdStart)
#$(document).off(@EVENTS.FILE_MANAGER_CMD_STOP, this.fileManagerCmdStop)
#$(document).off(@EVENTS.FILE_MANAGER_CMD_PROGRESS, this.fileManagerCmdProgress)
getUserSyncs: (page) =>
@rest.getUserSyncs({since: page * @limit, limit: @limit})
.done(this.processUserSyncs)
load: () =>
@list.empty()
@since = 0
this.renderHeader()
this.getUserSyncs(0)
.done((response) =>
$paginator = context.JK.Paginator.create(response.total_entries, @limit, 0, this.getUserSyncs)
@paginator.empty().append($paginator);
)
renderHeader: () =>
recordingManagerState = context.jamClient.GetRecordingManagerState()
if recordingManagerState.running
@uploadProgress.removeClass('quiet paused busy')
@downloadProgress.removeClass('quiet paused busy')
if recordingManagerState.current_download
@downloadProgress.addClass('busy')
else
@downloadProgress.addClass('quiet')
@downloadProgress.find('.busy').empty()
if recordingManagerState.current_upload
@uploadProgress.addClass('busy')
else
@uploadProgress.addClass('quiet')
@uploadProgress.find('.busy').empty()
else
@downloadProgress.removeClass('quiet paused busy').addClass('paused')
@uploadProgress.removeClass('quiet paused busy').addClass('paused')
@downloadProgress.find('.busy').empty()
@uploadProgress.find('.busy').empty()
updateMixState: ($mix) =>
serverInfo = $mix.data('server-info')
mixInfo = @recordingUtils.createMixInfo(serverInfo)
$mixState = $mix.find('.mix-state')
$mixStateMsg = $mixState.find('.msg')
$mixStateProgress = $mixState.find('.progress')
$mixState.removeClass('still-uploading discarded unknown mixed mixing waiting-to-mix error stream-mix').addClass(mixInfo.mixStateClass).attr('data-state', mixInfo.mixState).data('mix-state', mixInfo)
$mixStateMsg.text(mixInfo.mixStateMsg)
$mixStateProgress.css('width', '0')
updateStreamMixState: ($streamMix) =>
clientInfo = $streamMix.data('client-info')
serverInfo = $streamMix.data('server-info')
# determine client state
clientStateMsg = 'UNKNOWN'
clientStateClass = 'unknown'
clientState = @clientStates.unknown
if clientInfo?
if clientInfo.local_state == 'COMPRESSED'
clientStateMsg = 'STREAM QUALITY'
clientStateClass = 'sq'
clientState = @clientStates.sq
else if clientInfo.local_state == 'UNCOMPRESSED'
clientStateMsg = 'STREAM QUALITY'
clientStateClass = 'sq'
clientState = @clientStates.sq
else if clientInfo.local_state == 'MISSING'
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
# determine upload state
uploadStateMsg = 'UNKNOWN'
uploadStateClass = 'unknown'
uploadState = @uploadStates.unknown
if !serverInfo.fully_uploaded
if serverInfo.upload.too_many_upload_failures
uploadStateMsg = 'UPLOAD FAILURE'
uploadStateClass = 'error'
uploadState = @uploadStates.too_many_upload_failures
else
if clientInfo?
if clientInfo.local_state == 'UNCOMPRESSED' or clientInfo.local_state == 'COMPRESSED'
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 = 'UPLOADED'
uploadStateClass = 'uploaded'
uploadState = @uploadStates.me_uploaded
$clientState = $streamMix.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
$clientStateProgress = $clientState.find('.progress')
$uploadState = $streamMix.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
$uploadStateProgress = $uploadState.find('.progress')
$clientState.removeClass('discarded missing sq hq unknown error').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').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
$uploadStateMsg.text(uploadStateMsg)
$uploadStateProgress.css('width', '0')
updateTrackState: ($track) =>
clientInfo = $track.data('client-info')
serverInfo = $track.data('server-info')
# determine client state
clientStateMsg = 'UNKNOWN'
clientStateClass = 'unknown'
clientState = @clientStates.unknown
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
clientStateMsg = 'STREAM QUALITY'
clientStateClass = 'sq'
clientState = @clientStates.sq
else
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateClass = 'DISCARDED'
clientStateClass = 'discarded'
clientState = @clientStates.discarded
# determine upload state
uploadStateMsg = 'UNKNOWN'
uploadStateClass = 'unknown'
uploadState = @uploadStates.unknown
if !serverInfo.fully_uploaded
if serverInfo.upload.too_many_upload_failures
uploadStateMsg = 'UPLOAD FAILURE'
uploadStateClass = 'error'
uploadState = @uploadStates.too_many_upload_failures
else
if serverInfo.user.id == context.JK.currentUserId
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 serverInfo.user.id == context.JK.currentUserId
uploadState = @uploadStates.me_uploaded
else
uploadState = @uploadStates.them_uploaded
$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 sq hq unknown error').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').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
$uploadStateMsg.text(uploadStateMsg)
$uploadStateProgress.css('width', '0')
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 = @list.find(".mix[data-recording-id='#{recording.recording_id}']")
$track.data('client-info', recording.mix)
$track = @list.find(".stream-mix[data-recording-id='#{recording.recording_id}']")
$track.data('client-info', recording.stream_mix)
displayStreamMixHover: ($streamMix) =>
$clientState = $streamMix.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
clientStateClass = $clientState.data('custom-class')
clientState = $clientState.attr('data-state')
clientInfo = $streamMix.data('client-info')
$uploadState = $streamMix.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
uploadStateClass = $uploadState.data('custom-class')
uploadState = $uploadState.attr('data-state')
serverInfo = $streamMix.data('server-info')
# decide on special case strings first
summary = ''
if clientState == @clientStates.sq && uploadState == @uploadStates.me_upload_soon
summary = "We will attempt to upload your stream mix so that others can hear the recording on the JamKazam site, even before the final mix is done. It will upload shortly."
else if clientState == @clientStates.sq && uploadState == @uploadStates.me_uploaded
# we have the SQ version, and the other user has uploaded the HQ version... it's coming soon!
summary = "We already uploaded your stream mix so that others can hear the recording on the JamKazam site, even before the final mix is done. Since it's uploaded, there is nothing else left to do with the stream mix... you're all done!"
else if clientState == @clientStates.missing
summary = "You do not have the stream mix on your computer anymore. This can happen if you change the computer that you run JamKazam on. It's important to note that once a final mix for the recording is available, there is no value in the stream mix."
clientStateDefinition = switch clientState
when @clientStates.sq then "The stream mix is always STREAM QUALITY, because it's the version of the recording that you heard in your earphones as you made the recording."
when @clientStates.missing then "MISSING means you do not have the stream mix anymore."
else 'There is no help for this state'
uploadStateDefinition = switch uploadState
when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this stream mix 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 stream mix soon."
when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this stream mix."
when @uploadStates.missing then "MISSING means your JamKazam application does not have this stream mix, and the server does not either."
context._.template(@templateHoverRecordedTrack.html(),
{summary: summary,
clientStateDefinition: clientStateDefinition,
uploadStateDefinition: uploadStateDefinition,
clientStateMsg: $clientStateMsg.text(),
uploadStateMsg: $uploadStateMsg.text(),
clientStateClass: clientStateClass,
uploadStateClass: uploadStateClass}
{variable: 'data'})
displayTrackHover: ($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.sq && uploadState == @uploadStates.them_upload_soon
# we have the SQ version, and the other user hasn't uploaded it yet
summary = "#{serverInfo.user.name} has not yet uploaded the high-quality version of this track. Once he or she does, JamKazam will download it and replace your stream-quality version."
else if clientState == @clientStates.missing && uploadState == @uploadStates.them_upload_soon
# we don't have any version of the track at all, and the other user hasn't uploaded it yet
summary = "#{serverInfo.user.name} has not yet uploaded the high-quality version of this track. Once he or she does, JamKazam will download it and this track will no longer be missing."
else if clientState == @clientStates.sq && uploadState == @uploadStates.them_uploaded
# we have the SQ version, and the other user has uploaded the HQ version... it's coming soon!
summary = "#{serverInfo.user.name} has uploaded the high-quality version of this track. JamKazam will soon download it and replace your stream-quality version."
else if clientState == @clientStates.missing && uploadState == @uploadStates.them_uploaded
# we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
summary = "#{serverInfo.user.name} has uploaded the high-quality version of this track. JamKazam will soon restore it and then this track will no longer be missing."
else if clientState == @clientStates.sq && uploadState == @uploadStates.me_uploaded
# we have the SQ version, 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 replace your stream-quality version."
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 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 tracks for the recording, because at least one other person decided to keep the recording and needs your tracks 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 tracks for the recording, because at least one other person decided to keep the recording and needs your tracks to make a high-quality mix."
else if clientState == @clientStates.hq and ( uploadState == @uploadStates.them_uploaded or 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 track has been downloaded a unusually large number of times. No more downloads are allowed."
when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this track, as recorded by the user that made it."
when @clientStates.sq then "STREAM QUALITY means you have the version of the track that you received over the internet in real-time."
when @clientStates.missing then "MISSING means you do not have this track anymore."
when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
else 'There is no help for this state'
uploadStateDefinition = switch uploadState
when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this 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."
context._.template(@templateHoverRecordedTrack.html(),
{summary: summary,
clientStateDefinition: clientStateDefinition,
uploadStateDefinition: uploadStateDefinition,
clientStateMsg: $clientStateMsg.text(),
uploadStateMsg: $uploadStateMsg.text(),
clientStateClass: clientStateClass,
uploadStateClass: uploadStateClass}
{variable: 'data'})
onHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayTrackHover($recordedTrack)
onStreamMixHover: () ->
$streamMix = $(this).closest('.stream-mix.sync')
self = $streamMix.data('sync-viewer')
self.displayStreamMixHover($streamMix)
resetDownloadProgress: () =>
@downloadProgress
resetUploadProgress: () =>
@uploadProgress
sendCommand: ($retry, cmd) =>
if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession()
context.JK.confirmBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')})
else
context.jamClient.OnTrySyncCommand(cmd)
context.JK.confirmBubble($retry, 'sync-viewer-retry', {}, {offsetParent: $retry.closest('.dialog')})
retryDownloadRecordedTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-track')
serverInfo = $track.data('server-info')
this.sendCommand($retry, {
type: 'recorded_track',
action: 'download'
queue: 'download',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
retryUploadRecordedTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-track')
serverInfo = $track.data('server-info')
this.sendCommand($retry, {
type: 'recorded_track',
action: 'upload'
queue: 'upload',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
createMix: (userSync) =>
recordingInfo = null
if userSync == 'fake'
recordingInfo = arguments[1]
userSync = { recording_id: recordingInfo.id, duration: recordingInfo.duration, fake:true }
$mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'}))
else
$mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'}))
$mix.data('server-info', userSync)
$mix.data('sync-viewer', this)
$mix.data('view-context', 'sync')
$mixState = $mix.find('.mix-state')
this.updateMixState($mix)
context.JK.hoverBubble($mixState, @recordingUtils.onMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['left'], trigger:['hoverIntent', 'none']})
$mix
createTrack: (userSync) =>
$track = $(context._.template(@templateRecordedTrack.html(), userSync, {variable: 'data'}))
$track.data('server-info', userSync)
$track.data('sync-viewer', this)
$clientState = $track.find('.client-state')
$uploadState = $track.find('.upload-state')
$clientState.find('.retry').click(this.retryDownloadRecordedTrack)
$uploadState.find('.retry').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']})
$clientState.addClass('is-native-client') if context.jamClient.IsNativeClient()
$uploadState.addClass('is-native-client') if context.jamClient.IsNativeClient()
$track
createStreamMix: (userSync) =>
$track = $(context._.template(@templateStreamMix.html(), userSync, {variable: 'data'}))
$track.data('server-info', userSync)
$track.data('sync-viewer', this)
$clientState = $track.find('.client-state')
$uploadState = $track.find('.upload-state')
$uploadState.find('.retry').click(this.retryUploadRecordedTrack)
context.JK.hoverBubble($clientState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if context.jamClient.IsNativeClient()
$uploadState.addClass('is-native-client') if context.jamClient.IsNativeClient()
$track
createRecordingWrapper: ($toWrap, recordingInfo) =>
recordingInfo.recording_landing_url = "/recordings/#{recordingInfo.id}"
$wrapperDetails = $(context._.template(@templateRecordingWrapperDetails.html(), recordingInfo, {variable: 'data'}))
$wrapper = $('<div class="recording-holder"></div>')
$toWrap.wrapAll($wrapper)
$wrapper = $toWrap.closest('.recording-holder')
$wrapperDetails.prependTo($wrapper)
$mix = $wrapper.find('.mix.sync')
if $mix.length == 0
# create a virtual mix so that the UI is consistent
$wrapper.append(this.createMix('fake', recordingInfo))
separateByRecording: () =>
$recordedTracks = @list.find('.sync')
currentRecordingId = null;
queue = $([]);
for recordedTrack in $recordedTracks
$recordedTrack = $(recordedTrack)
recordingId = $recordedTrack.attr('data-recording-id')
if recordingId != currentRecordingId
if queue.length > 0
this.createRecordingWrapper(queue, $(queue.get(0)).data('server-info').recording)
queue = $([])
currentRecordingId = recordingId
queue = queue.add(recordedTrack)
if queue.length > 0
this.createRecordingWrapper(queue, $(queue.get(0)).data('server-info').recording)
processUserSyncs: (response) =>
@list.empty()
# check if no entries
if @since == 0 and response.entries.length == 0
@list.append(context._.template(@templateNoSyncs.html(), {}, {variable: 'data'}))
else
recordings = {} # collect all unique recording
for userSync in response.entries
if userSync.type == 'recorded_track'
@list.append(this.createTrack(userSync))
else if userSync.type == 'mix'
@list.append(this.createMix(userSync))
else if userSync.type == 'stream_mix'
@list.append(this.createStreamMix(userSync))
recordings[userSync.recording_id] = userSync.recording
recordingsToResolve = []
# resolve each track against backend data:
for recording_id, recording of recordings
recordingsToResolve.push(recording)
clientRecordings = context.jamClient.GetLocalRecordingState(recordings: recordingsToResolve)
if clientRecordings.error?
alert(clientRecordings.error)
else
this.associateClientInfo(turp) for turp in clientRecordings.recordings
for track in @list.find('.recorded-track.sync')
this.updateTrackState($(track))
for streamMix in @list.find('.stream-mix.sync')
this.updateStreamMixState($(streamMix))
this.separateByRecording()
@since = response.next
resolveTrack: (commandMetadata) =>
recordingId = commandMetadata['recording_id']
clientTrackId = commandMetadata['track_id']
matchingTrack = @list.find(".recorded-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if matchingTrack.length == 0
return @rest.getRecordedTrack({recording_id: recordingId, track_id: clientTrackId})
else
deferred = $.Deferred();
deferred.resolve(matchingTrack.data('server-info'))
return deferred
renderFullUploadRecordedTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
$busy.empty().append($track)
@uploadProgress.find('.progress').css('width', '0%')
renderFullDownloadRecordedTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'}))
$busy = @downloadProgress.find('.busy')
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'download', commandMetadata)
deferred.done(this.renderFullDownloadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )
renderUploadRecordedTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'upload', commandMetadata)
deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )
renderGeneric: (commandId, category, commandMetadata) =>
commandMetadata.displayType = this.displayName(commandMetadata)
$generic = $(context._.template(@templateGenericCommand.html(), commandMetadata, {variable: 'data'}))
if category == 'download'
$busy = @downloadProgress.find('.busy')
$busy.empty().append($generic)
else if category == 'upload'
$busy = @uploadProgress.find('.busy')
$busy.empty().append($generic)
else
@logger.error("unknown category #{category}")
renderStartCommand: (commandId, commandType, commandMetadata) =>
#console.log("renderStartCommand", arguments)
unless commandMetadata?
managerState = context.jamClient.GetRecordingManagerState()
if commandType == 'download'
commandMetadata = managerState.current_download
else if commandType == 'upload'
commandMetadata = managerState.current_upload
else
@logger.error("unknown commandType #{commandType}")
unless commandMetadata?
# we still have no metadata. we have to give up
@logger.error("no metadata found for current command #{commandId} #{commandType}. bailing out")
return
if commandMetadata.queue == 'download'
@downloadCommandId = commandId
@downloadMetadata = commandMetadata
@downloadProgress.removeClass('quiet paused busy')
@downloadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'download', commandMetadata)
else if commandMetadata.queue == 'upload'
@uploadCommandId = commandId
@uploadMetadata = commandMetadata
@uploadProgress.removeClass('quiet paused busy')
@uploadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'upload', commandMetadata)
renderSingleRecording: (userSyncs) =>
return if userSyncs.entries.length == 0
clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSyncs.entries[0].recording])
for userSync in userSyncs.entries
if userSync.type == 'recorded_track'
$track = @list.find(".sync[data-id='#{userSync.id}']")
continue if $track.length == 0
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
else if userSync.type == 'mix'
# check if there is a virtual mix 1st; if so, update it
$mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']")
if $mix.length == 0
$mix = @list.find(".sync[data-id='#{userSync.id}']")
continue if $mix.length == 0
$newMix = this.createMix(userSync)
this.associateClientInfo(clientRecordings.recordings[0])
$mix.replaceWith($newMix)
else if userSync.type == 'stream_mix'
$streamMix = @list.find(".sync[data-id='#{userSync.id}']")
continue if $streamMix.length == 0
$streamMix.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateStreamMixState($streamMix)
updateSingleRecording: (recording_id) =>
@rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording)
updateSingleRecordedTrack: ($track) =>
serverInfo = $track.data('server-info')
@rest.getUserSync({user_sync_id: serverInfo.id})
.done((userSync) =>
# associate new server-info with this track
$track.data('server-info', userSync)
# associate new client-info with this track
clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSync.recording])
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
)
.fail(@app.ajaxError)
updateProgressOnSync: ($track, queue, percentage) =>
state = if queue == 'upload' then '.upload-state' else '.client-state'
$progress = $track.find("#{state} .progress")
$progress.css('width', percentage + '%')
renderFinishCommand: (commandId, data) =>
reason = data.commandReason
success = data.commandSuccess
if commandId == @downloadCommandId
this.logResult(@downloadMetadata, success, reason, false)
recordingId = @downloadMetadata['recording_id']
this.updateSingleRecording(recordingId) if recordingId?
else if commandId == @uploadCommandId
this.logResult(@uploadMetadata, success, reason, false)
recordingId = @uploadMetadata['recording_id']
this.updateSingleRecording(recordingId) if recordingId?
else
@logger.error("unknown commandId in renderFinishCommand")
# refresh the header when done. we need to leave this callback to let the command fully switch to off
#setTimeout(this.renderHeader, 1)
this.renderHeader()
renderPercentage: (commandId, commandType, percentage) =>
if commandId == @downloadCommandId
$progress = @downloadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @downloadMetadata.type == 'recorded_track'
clientTrackId = @downloadMetadata['track_id']
recordingId = @downloadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'download', percentage)
else if commandId == @uploadCommandId
$progress = @uploadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload'
clientTrackId = @uploadMetadata['track_id']
recordingId = @uploadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'upload', percentage)
else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload'
recordingId = @uploadMetadata['recording_id']
$matchingStreamMix = @list.find(".stream-mix.sync[data-recording-id='#{recordingId}']")
if $matchingStreamMix.length > 0
this.updateProgressOnSync($matchingStreamMix, 'upload', percentage)
else
@logger.error("unknown commandId in renderFinishCommand")
fileManagerCmdStart: (e, data) =>
#console.log("fileManagerCmdStart", data)
commandId = data['commandId']
commandType = data['commandType']
commandMetadata = data['commandMetadata']
category = commandType == 'download' ? 'download' : 'upload'
if category == 'download' && (@downloadCommandId != null && @downloadCommandId != commandId)
@logger.warn("received command-start for download but previous command did not send stop")
this.renderFinishCommand(commandId, category)
else if @uploadCommandId != null && @uploadCommandId != commandId
@logger.warn("received command-start for upload but previous command did not send stop")
this.renderFinishCommand(commandId, category)
this.renderStartCommand(commandId, commandType, commandMetadata)
fileManagerCmdStop: (e, data) =>
#console.log("fileManagerCmdStop", data)
commandId = data['commandId']
if commandId == @downloadCommandId
category = 'download'
this.renderFinishCommand(commandId, data)
@downloadCommandId = null
@downloadMetadata = null;
else if commandId == @uploadCommandId
category = 'upload'
this.renderFinishCommand(commandId, data)
@uploadCommandId = null
@uploadMetadata = null;
else
@logger.warn("received command-stop for unknown command: #{commandId} #{@downloadCommandId} #{@uploadCommandId}" )
fileManagerCmdProgress: (e, data) =>
#console.log("fileManagerCmdProgress", data)
commandId = data['commandId']
if commandId == @downloadCommandId
category = 'download'
this.renderPercentage(commandId, category, data.percentage)
else if commandId == @uploadCommandId
category = 'upload'
this.renderPercentage(commandId, category, data.percentage)
else
@logger.warn("received command-percentage for unknown command")
fileManagerAsapCommandStatus: (e, data) =>
this.logResult(data.commandMetadata, false, data.commandReason, true)
displayName: (metadata) =>
if metadata.type == 'recorded_track' && metadata.action == 'download'
return 'DOWNLOADING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'upload'
return 'UPLOADING TRACK'
else if metadata.type == 'mix' && metadata.action == 'download'
return 'DOWNLOADING MIX'
else if metadata.type == 'recorded_track' && metadata.action == 'convert'
return 'COMPRESSING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'delete'
return 'CLEANUP TRACK'
else if metadata.type == 'stream_mix' && metadata.action == 'upload'
return 'UPLOADING STREAM MIX'
else
return "#{metadata.action} #{metadata.type}".toUpperCase()
shownTab: () =>
@tabSelectors.filter('.selected')
# create a log in the Log tab
logResult: (metadata, success, reason, isAsap) =>
# if an error comes in, and the log tab is not already showing, increment the badge
if not success and (!@showing || this.shownTab().attr('purpose') != 'log')
@logBadge.css('display', 'inline-block')
if @showing # don't do animation unless user can see it
@logBadge.pulse({'background-color' : '#868686'}, {pulses: 2}, () => @logBadge.css('background-color', '#980006'))
displayReason = switch reason
when 'no-match-in-queue' then 'restart JamKazam'
when 'already-done' then 'ignored, already done'
when 'failed-convert' then 'failed previously'
else reason
displaySuccess = if success then 'yes' else 'no'
$log = context._.template(@templateLogItem.html(), {isAsap: isAsap, command: this.displayName(metadata), success: success, displaySuccess: displaySuccess, detail: displayReason, when: new Date()}, {variable: 'data'})
@logList.prepend($log)

View File

@ -95,6 +95,12 @@
instrumentIconMap256[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "256.png", name: instrumentId};
});
context.JK.helpBubbleFunctionHelper = function(originalFunc) {
var helpText = originalFunc.apply(this);
var holder = $('<div class="hover-bubble help-bubble"></div>');
holder.append(helpText);
return holder;
}
/**
* Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
* @param $element The element that should show the help when hovered
@ -111,18 +117,48 @@
}
$element.on('remove', function() {
$element.btOff();
$element.btOff(); // if the element goes away for some reason, get rid of the bubble too
})
var holder = null;
if (context._.isFunction(templateName)) {
holder = function wrapper() {
return context.JK.helpBubbleFunctionHelper.apply(this, [templateName])
}
}
else {
var $template = $('#template-help-' + templateName)
if($template.length == 0) {
var helpText = templateName;
}
else {
var helpText = context._.template($template.html(), data, { variable: 'data' });
}
var $template = $('#template-help-' + templateName);
if($template.length == 0) throw "no template by the name " + templateName;
var helpText = context._.template($template.html(), data, { variable: 'data' });
var holder = $('<div class="hover-bubble help-bubble"></div>');
holder.append(helpText);
context.JK.hoverBubble($element, helpText, options);
holder = $('<div class="hover-bubble help-bubble"></div>');
holder = holder.append(helpText).html()
}
context.JK.hoverBubble($element, holder, options);
}
/**
* Meant to show a little bubble to confirm something happened, in a way less obtrusive than a app.notify
* @param $element The element that should show the help when hovered
* @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb
* @param data (optional) data for your template, if applicable
* @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
*/
context.JK.confirmBubble = function($element, templateName, data, options) {
if(!options) options = {};
options.spikeGirth = 0;
options.spikeLength = 0;
options.shrinkToFit = true
options.duration = 3000
options.cornerRadius = 5
return context.JK.prodBubble($element, templateName, data, options);
}
/**
* Associates a help bubble immediately with the specified $element, using jquery.bt.js (BeautyTips)
* By 'prod' it means to literally prod the user, to make them aware of something important because they did something else
@ -203,7 +239,14 @@
options = defaultOpts;
}
$element.bt(text, options);
if(typeof text != 'string') {
options.contentSelector = text;
$element.bt(options);
}
else {
$element.bt(text, options);
}
}
context.JK.bindProfileClickEvents = function($parent, dialogsToClose) {
@ -524,7 +567,7 @@
}
context.JK.formatDateTime = function (dateString) {
var date = new Date(dateString);
var date = dateString instanceof Date ? dateString : new Date(dateString);
return context.JK.padString(date.getMonth() + 1, 2) + "/" + context.JK.padString(date.getDate(), 2) + "/" + date.getFullYear() + " - " + date.toLocaleTimeString();
}

View File

@ -19,6 +19,7 @@
//= require jquery.ba-bbq
//= require jquery.icheck
//= require jquery.bt
//= require jquery.exists
//= require AAA_Log
//= require AAC_underscore
//= require alert
@ -47,6 +48,7 @@
//= require ga
//= require jam_rest
//= require session_utils
//= require recording_utils
//= require helpBubbleHelper
//= require facebook_rest
//= require landing/init

View File

@ -60,4 +60,5 @@
*= require web/sessions
*= require jquery.Jcrop
*= require icheck/minimal/minimal
*= require users/syncViewer
*/

View File

@ -44,6 +44,12 @@ $latencyBadgePoor: #980006;
$latencyBadgeUnacceptable: #868686;
$latencyBadgeUnknown: #868686;
$good: #71a43b;
$unknown: #868686;
$poor: #980006;
$error: #980006;
$fair: #cc9900;
@mixin border_box_sizing {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
@ -65,6 +71,55 @@ $latencyBadgeUnknown: #868686;
background-clip: padding-box; /* stops bg color from leaking outside the border: */
}
/** IE10 and above only */
@mixin vertical-align-row {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-pack:space-around;
-webkit-justify-content:space-around;
-moz-justify-content:space-around;
-ms-flex-pack:space-around;
justify-content:space-around;
-webkit-flex-line-pack:center;
-ms-flex-line-pack:center;
-webkit-align-content:center;
align-content:center;
-webkit-flex-direction:row;
-moz-flex-direction:row;
-ms-flex-direction:row;
flex-direction:row;
}
@mixin vertical-align-column {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-pack:space-around;
-webkit-justify-content:space-around;
-moz-justify-content:space-around;
-ms-flex-pack:space-around;
justify-content:space-around;
-webkit-flex-line-pack:center;
-ms-flex-line-pack:center;
-webkit-align-content:center;
align-content:center;
-webkit-flex-direction:column;
-moz-flex-direction:column;
-ms-flex-direction:column;
flex-direction:column;
}
// Single side border-radius
@mixin border-top-radius($radius) {
@ -122,3 +177,115 @@ $latencyBadgeUnknown: #868686;
}
}
@mixin client-state-box {
.client-state {
position:relative;
text-align:center;
padding:3px;
@include border_box_sizing;
color:white;
&.unknown {
background-color: $error;
}
&.error {
background-color: $error;
&.is-native-client { .retry { display:inline-block; } }
}
&.hq {
background-color: $good;
}
&.sq {
background-color: $good;
&.is-native-client { .retry { display:inline-block; } }
}
&.missing {
background-color: $error;
&.is-native-client { .retry { display:inline-block; } }
}
&.discarded {
background-color: $unknown;
}
.retry {
display:none;
position:absolute;
line-height:28px;
top:4px;
right:8px;
z-index:1;
}
}
}
@mixin upload-state-box {
.upload-state {
position:relative;
text-align:center;
padding:3px;
@include border_box_sizing;
color:white;
&.unknown {
background-color: $unknown;
}
&.error {
background-color: $error;
&.is-native-client { .retry { display:inline-block; } }
}
&.missing {
background-color: $error;
}
&.upload-soon {
background-color: $fair;
&.is-native-client { .retry { display:inline-block; } }
}
&.uploaded {
background-color: $good;
}
.retry {
display:none;
position:absolute;
line-height:28px;
visibility: middle;
top:4px;
right:8px;
}
}
}
@mixin mix-state-box {
.mix-state {
position:relative;
text-align:center;
padding:3px;
@include border_box_sizing;
color:white;
&.still-uploading { background-color: $fair; }
&.discard {background-color: $unknown; }
&.unknown { background-color: $unknown; }
&.error { background-color: $error; }
&.mixed { background-color: $good; }
&.mixing {background-color: $good; }
&.waiting-to-mix {background-color: $good; }
&.stream-mix { background-color: $fair; }
}
}

View File

@ -1,4 +1,6 @@
.screen, body.web {
@import "client/common";
body.jam, body.web, .dialog{
.bt-wrapper {
.bt-content {
@ -43,6 +45,110 @@
}
}
.help-hover-recorded-tracks, .help-hover-stream-mix {
font-size:12px;
padding:5px;
@include border_box_sizing;
width:450px;
.sync-definition {
padding:0 10px;
}
.client-box {
float:left;
width:50%;
min-width:200px;
@include border_box_sizing;
}
.upload-box {
float:left;
width:50%;
min-width:200px;
@include border_box_sizing;
}
@include client-state-box;
.client-state {
width:100%;
margin:5px 0 10px;
font-size:16px;
}
@include upload-state-box;
.upload-state {
width: 100%;
margin:5px 0 10px;
font-size:16px;
}
.client-state-info, .upload-state-info {
text-align:center;
font-size:12px;
}
.client-state-definition {
margin-bottom:20px;
}
.upload-state-definition {
margin-bottom:20px;
}
.summary {
margin-bottom:10px;
.title {
text-align:center;
font-size:16px;
margin:10px 0 5px;
}
}
}
.help-hover-mix {
font-size:12px;
padding:5px;
@include border_box_sizing;
width:450px;
.mix-box {
float:left;
width:100%;
min-width:400px;
@include border_box_sizing;
}
@include mix-state-box;
.mix-state {
width:100%;
margin:5px 0 10px;
font-size:16px;
//background-color:black ! important;
}
.mix-state-info {
text-align:center;
font-size:12px;
}
.mix-state-definition {
margin-bottom:20px;
}
.summary {
margin-bottom:10px;
.title {
text-align:center;
font-size:16px;
margin:10px 0 5px;
}
}
}
}
}
}

View File

@ -2,12 +2,34 @@
color: #CCCCCC;
font-size: 11px;
margin: 0 auto;
margin: -5px auto 0 ;
position: absolute;
text-align: center;
left: 17%;
width: 50%;
z-index:-1;
// if it's the native client, then show the File Manager span. if it's not (normal browser) hide it.
// even if it's the native client, once a command is running, hide File Manager
&.native-client {
.recording-manager-launcher {
display:inline-block;
}
&.running {
.recording-manager-launcher {
display:none;
}
.recording-manager-command {
display:inline;
}
}
}
.recording-manager-launcher {
width:100%;
margin:5px 10px;
display:none;
cursor:pointer;
}
.recording-manager-command {
-webkit-box-sizing: border-box;
@ -16,7 +38,9 @@
box-sizing: border-box;
width:33%;
margin:5px 10px;
visibility: hidden;
display:none;
visible:hidden;
cursor:pointer;
.percent {
margin-left:3px;

View File

@ -428,6 +428,16 @@ small, .small {font-size:11px;}
margin:0;
}
}
.help-launcher {
font-weight:bold;
font-size:12px;
line-height:12px;
position:absolute;
top:3px;
right:3px;
cursor:pointer;
}
.white {color:#fff;}
.lightgrey {color:#ccc;}
.grey {color:#999;}

View File

@ -49,10 +49,11 @@
font-size:11px;
height:18px;
vertical-align:top;
margin-left:15px;
}
.recording-controls {
display:none;
position: absolute;
bottom: 0;
height: 25px;
@ -60,6 +61,9 @@
.play-button {
top:2px;
}
.recording-current {
top:3px ! important;
}
}
.playback-mode-buttons {
@ -675,17 +679,6 @@ table.vu td {
font-size:18px;
}
.recording-controls {
display:none;
.play-button {
outline:none;
}
.play-button img.pausebutton {
display:none;
}
}
.playback-mode-buttons {
display:none;
@ -762,3 +755,15 @@ table.vu td {
.rate-thumbsdown.selected {
background-image:url('/assets/content/icon_thumbsdown_big_on.png');
}
.recording-controls {
.play-button {
outline:none;
}
.play-button img.pausebutton {
display:none;
}
}

View File

@ -1,6 +1,6 @@
@import "client/common";
#whatsnext-dialog, #getting-started-dialog {
#getting-started-dialog {
height:auto;
width:auto;

View File

@ -0,0 +1,3 @@
#all-syncs-dialog {
height:660px;
}

View File

@ -0,0 +1,281 @@
@import "client/common";
.sync-viewer {
width: 100%;
.list {
height:400px;
overflow:auto;
border-style:solid;
border-width:0 0 1px 0;
border-color:#AAAAAA;
}
.knobs {
}
.headline {
height:40px;
@include border_box_sizing;
@include vertical-align-row;
.download-progress, .upload-progress {
width:200px;
line-height:30px;
.paused {
display:none;
color:#777;
font-size:11px;
}
.quiet {
display:none;
color:#777;
font-size:11px;
}
&.paused {
.paused {
display:inline-block;
}
}
&.quiet {
.quiet{
display:inline-block;
}
}
&.busy {
.busy {
display:inline-block;
width:100%;
}
}
}
.in-progress {
@include border_box_sizing;
@include vertical-align-row;
text-align:center;
.sync {
line-height:20px;
height:30px;
.type {
position:relative;
width:100%;
font-size: 10px;
}
.text {
z-index:1;
}
.avatar-tiny {
margin-top:-4px;
z-index:1;
}
.instrument-icon {
margin-top:-3px;
z-index:1;
}
}
}
}
.list-header {
width:100%;
height:16px;
font-size:12px;
.type-info {
float:left;
width:26%;
@include border_box_sizing;
@include vertical-align-column;
}
.client-state-info {
float:left;
padding: 0 4px;
width:37%;
@include border_box_sizing;
@include vertical-align-column;
}
.upload-state-info {
float:left;
width:37%;
@include border_box_sizing;
@include vertical-align-column;
}
.special {
font-size:12px;
border-width:1px 1px 0 1px;
border-style:solid;
border-color:#cccccc;
width:100%;
text-align:center;
@include border_box_sizing;
}
.special-text {
width:100%;
text-align:center;
@include border_box_sizing;
}
}
.sync {
margin:3px 0;
display:inline-block;
@include border_box_sizing;
padding:3px;
background-color:black;
width:100%;
line-height:28px;
@include border-radius(2px);
@include vertical-align-row;
}
.type {
float:left;
width:26%;
color:white;
padding:3px;
@include border_box_sizing;
@include vertical-align-row;
.instrument-icon {
width:24px;
height:24px;
}
}
.msg {
z-index:1;
}
.progress {
position:absolute;
background-color:$ColorScreenPrimary;
height:100%;
top:0px;
left:0;
}
@include mix-state-box;
.mix-state {
position:relative;
float:left;
width:74%;
@include vertical-align-row;
}
@include client-state-box;
.client-state {
position:relative;
float:left;
width:37%;
@include vertical-align-row;
}
@include upload-state-box;
.upload-state {
position:relative;
float:left;
width: 37%;
@include vertical-align-row;
}
.recording-holder {
margin-top:10px;
padding: 5px 0 0 0;
width:100%;
@include border_box_sizing;
@include vertical-align-column;
.timeago {
float:right;
}
}
.log-list {
height:392px;
overflow:auto;
border-style:solid;
border-width:0 0 1px 0;
border-color:#AAAAAA;
}
.log {
@include border_box_sizing;
@include border-radius(2px);
width:100%;
margin:3px 0;
display:inline-block;
padding:3px;
background-color:black;
height:20px;
font-size:12px;
color:white;
&.success-false {
background-color:$error;
}
.command {
float:left;
width:33%;
text-align:center;
position:relative;
img {
position:absolute;
left:0;
top:-1px;
}
}
.success {
float:left;
width:33%;
text-align:center;
}
.detail {
float:left;
width:33%;
text-align:center;
}
}
.badge {
display:none;
margin-left:5px;
height:11px;
width:12px;
color:white;
font-size:10px;
padding-top:2px;
background-color:$error;
-webkit-border-radius:6px;
-moz-border-radius:6px;
border-radius:6px;
vertical-align: top;
}
.tab-content[purpose="log"] {
display:none;
.list-header {
.list-column {
float:left;
width:33%;
text-align:center;
}
}
}
.paginator-holder {
text-align:center;
}
}

View File

@ -25,6 +25,8 @@
text-align: center;
@include border_box_sizing;
height: 36px;
display:inline-block;
white-space:nowrap;
.recording-status {
font-size:15px;
@ -34,8 +36,8 @@
font-family:Arial, Helvetica, sans-serif;
display:inline-block;
font-size:18px;
float:right;
top:3px;
position:absolute;
//top:3px;
right:4px;
}
@ -49,7 +51,10 @@
display:none;
}
}
&.no-mix {
&:not(.has-mix) {
border-width: 0; // override screen_common's .error
.play-button {
display:none;
@ -109,6 +114,7 @@
}
}
.feed-entry {
position:relative;
display:block;
@ -172,7 +178,8 @@
.recording-controls {
.recording-position {
width:70%;
margin-left:0;
margin-left:-29px;
}
.recording-playback {

View File

@ -6,7 +6,6 @@
padding:8px 5px 8px 10px;
width:98%;
position:relative;
text-align:center;
}
.landing-details .recording-controls, .landing-details .recording-controls {
@ -64,11 +63,11 @@
.recording-current {
font-family:Arial, Helvetica, sans-serif;
display:inline-block;
display:inline;
font-size:18px;
float:right;
top:3px;
right:4px;
position:absolute;
}
#btnPlayPause {

View File

@ -14,8 +14,6 @@ class ApiClaimedRecordingsController < ApiController
if !@claimed_recording.is_public && @claimed_recording.user_id != current_user.id
raise PermissionError, 'this claimed recording is not public'
end
@claimed_recording
end
def update
@ -40,10 +38,21 @@ class ApiClaimedRecordingsController < ApiController
raise PermissionError, 'this claimed recording is not public'
end
mix = @claimed_recording.recording.mixes.first!
params[:type] ||= 'ogg'
redirect_to mix.sign_url(120, params[:type])
mix = @claimed_recording.recording.mix
if mix && mix.completed
redirect_to mix.sign_url(120, params[:type])
else
quick_mix = @claimed_recording.recording.stream_mix
if quick_mix
redirect_to quick_mix.sign_url(120, params[:type])
else
render :json => {}, :status => 404
end
end
end
private

View File

@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController
before_filter :api_signed_in_user, :except => [ :add_like ]
before_filter :look_up_recording, :only => [ :show, :stop, :claim, :discard, :keep ]
before_filter :parse_filename, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :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 ]
respond_to :json
@ -35,6 +36,10 @@ class ApiRecordingsController < ApiController
def show
end
def show_recorded_track
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user)
@ -150,7 +155,7 @@ class ApiRecordingsController < ApiController
render :json => {}, :status => 200
end
def upload_next_part
def upload_next_part # track
length = params[:length]
md5 = params[:md5]
@ -174,11 +179,11 @@ class ApiRecordingsController < ApiController
end
def upload_sign
def upload_sign # track
render :json => @recorded_track.upload_sign(params[:md5]), :status => 200
end
def upload_part_complete
def upload_part_complete # track
part = params[:part]
offset = params[:offset]
@ -192,7 +197,7 @@ class ApiRecordingsController < ApiController
end
end
def upload_complete
def upload_complete # track
@recorded_track.upload_complete
@recorded_track.recording.upload_complete
@ -206,12 +211,72 @@ class ApiRecordingsController < ApiController
end
def upload_next_part_stream_mix
length = params[:length]
md5 = params[:md5]
@quick_mix.upload_next_part(length, md5)
if @quick_mix.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 => @quick_mix.errors }, :status => 422
else
result = {
:part => @quick_mix.next_part_to_upload,
:offset => @quick_mix.file_offset.to_s
}
render :json => result, :status => 200
end
end
def upload_sign_stream_mix
render :json => @quick_mix.upload_sign(params[:md5]), :status => 200
end
def upload_part_complete_stream_mix
part = params[:part]
offset = params[:offset]
@quick_mix.upload_part_complete(part, offset)
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
else
render :json => {}, :status => 200
end
end
def upload_complete_stream_mix
@quick_mix.upload_complete
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
return
else
render :json => {}, :status => 200
end
end
private
def parse_filename
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_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)
end
def look_up_recording
@recording = Recording.find(params[:id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recording.has_access?(current_user)

View File

@ -0,0 +1,41 @@
# abstracts the idea of downloading / uploading files to JamKazam.
# Recordings, JamTracks, and Video are this
class ApiUserSyncsController < ApiController
before_filter :api_signed_in_user, :except => [ ]
before_filter :auth_user
respond_to :json
@@log = Logging.logger[ApiUserSyncsController]
def show
@user_sync = UserSync.show(params[:user_sync_id], current_user.id)
if @user_sync.nil?
raise ActiveRecord::RecordNotFound
end
render "api_user_syncs/show", :layout => nil
end
# returns all downloads and uploads for a user, meaning it should return:
# all recorded_tracks (audio and video/soon)
# all mixes
# all jamtracks (soon)
def index
data = UserSync.index(
{user_id:current_user.id,
recording_id: params[:recording_id],
offset: params[:since],
limit: params[:limit]})
@user_syncs = data[:query]
@next = data[:next]
render "api_user_syncs/index", :layout => nil
end
end

View File

@ -3,7 +3,11 @@ class RecordingsController < ApplicationController
respond_to :html
def show
@claimed_recording = ClaimedRecording.find(params[:id])
@claimed_recording = ClaimedRecording.find_by_id(params[:id])
if @claimed_recording.nil?
recording = Recording.find(params[:id])
@claimed_recording = recording.candidate_claimed_recording
end
render :layout => "web"
end

View File

@ -119,5 +119,4 @@ module FeedsHelper
def recording_genre(recording)
recording.candidate_claimed_recording.genre.description
end
end

View File

@ -45,4 +45,11 @@ module RecordingHelper
def description_for_claimed_recording(claimed_recording)
truncate(claimed_recording.name, length:250)
end
def listen_mix_url(recording)
{
mp3_url: claimed_recording_download_url(recording.candidate_claimed_recording.id, 'mp3'),
ogg_url: claimed_recording_download_url(recording.candidate_claimed_recording.id, 'ogg')
}
end
end

View File

@ -12,6 +12,11 @@ node :share_url do |claimed_recording|
end
end
node :mix do |claimed_recording|
listen_mix_url(claimed_recording.recording) if claimed_recording.has_mix?
end
child(:recording => :recording) {
attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count
@ -39,14 +44,3 @@ child(:recording => :recording) {
}
}
}
if :has_mix?
node do |claimed_recording|
{
mix: {
mp3_url: claimed_recording_download_url(claimed_recording.id, 'mp3'),
ogg_url: claimed_recording_download_url(claimed_recording.id, 'ogg')
}
}
end
end

View File

@ -78,7 +78,7 @@ glue :recording do
'recording'
end
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state
node do |recording|
{
@ -96,6 +96,13 @@ glue :recording do
}
end
node :mix do |recording|
{
state: recording.mix_state,
error: recording.mix_error
}
end unless @object.mix.nil?
child(:owner => :owner) {
attributes :id, :name, :location, :photo_url
}
@ -153,18 +160,9 @@ glue :recording do
end
end
if :has_mix?
node do |claimed_recording|
{
mix: {
mp3_url: claimed_recording_download_url(claimed_recording.id, 'mp3'),
ogg_url: claimed_recording_download_url(claimed_recording.id, 'ogg')
}
}
end
node :mix do |claimed_recording|
listen_mix_url(claimed_recording.recording) if claimed_recording.has_mix?
end
}
end

View File

@ -2,6 +2,17 @@ object @recording
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count
node :mix do |recording|
if recording.mix
{
id: recording.mix.id
}
else
nil
end
end
child(:band => :band) {
attributes :id, :name, :location, :photo_url
}
@ -11,11 +22,9 @@ child(:owner => :owner) {
}
child(:recorded_tracks => :recorded_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :instrument_id
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url
}
node do |recorded_track|
partial("api_recordings/show_recorded_track", :object => recorded_track)
end
}
child(:comments => :comments) {
@ -26,6 +35,16 @@ child(:comments => :comments) {
}
}
# returns the current_user's version of the recording, i.e., their associated claimed_recording if present
node :my do |recording|
claim = recording.claim_for_user(current_user) || recording.candidate_claimed_recording
if claim
{ name: claim.name, description: claim.description, genre: claim.genre.id, genre_name: claim.genre.description }
else
nil
end
end
child(:claimed_recordings => :claimed_recordings) {
attributes :id, :name, :description, :is_public, :genre_id, :has_mix?, :user_id
@ -36,14 +55,7 @@ child(:claimed_recordings => :claimed_recordings) {
end
end
if :has_mix?
node do |claimed_recording|
{
mix: {
mp3_url: claimed_recording_download_url(claimed_recording.id, 'mp3'),
ogg_url: claimed_recording_download_url(claimed_recording.id, 'ogg')
}
}
end
node :mix do |claimed_recording|
listen_mix_url(claimed_recording.recording) if claimed_recording.has_mix?
end
}

View File

@ -0,0 +1,7 @@
object @recorded_track
attributes :id, :fully_uploaded, :client_track_id, :client_id, :instrument_id, :recording_id
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url
}

View File

@ -0,0 +1,15 @@
node :next do |page|
@next
end
node :entries do |page|
partial "api_user_syncs/show", object: @user_syncs
end
node :total_entries do |page|
@user_syncs.total_entries
end

View File

@ -0,0 +1,103 @@
object @user_sync
glue :recorded_track do
@object.current_user = current_user
node :type do |i|
'recorded_track'
end
attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :instrument_id, :fully_uploaded, :upload_failures, :part_failures, :created_at
node :user do |recorded_track|
partial("api_users/show_minimal", :object => recorded_track.user)
end
node :recording do |recorded_track|
partial("api_recordings/show", :object => recorded_track.recording)
end
node :upload do |recorded_track|
{
should_upload: true,
too_many_upload_failures: recorded_track.too_many_upload_failures?
}
end
node :download do |recorded_track|
{
should_download: recorded_track.can_download?(current_user),
too_many_downloads: recorded_track.too_many_downloads?
}
end
end
glue :mix do
@object.current_user = current_user
node :type do |i|
'mix'
end
attributes :id, :recording_id, :started_at, :completed_at, :completed, :should_retry, :download_count, :state
node :recording do |mix|
partial("api_recordings/show", :object => mix.recording)
end
node do |mix|
{
duration: mix.recording.duration,
error: mix.error
}
end
node :download do |mix|
{
should_download: mix.can_download?(current_user),
too_many_downloads: mix.too_many_downloads?
}
end
end
glue :quick_mix do
@object.current_user = current_user
node :type do |i|
'stream_mix'
end
attributes :id, :recording_id, :fully_uploaded, :upload_failures, :part_failures, :created_at
node :user do |quick_mix|
partial("api_users/show_minimal", :object => quick_mix.user)
end
node :recording do |quick_mix|
partial("api_recordings/show", :object => quick_mix.recording)
end
node :upload do |quick_mix|
{
should_upload: true,
too_many_upload_failures: quick_mix.too_many_upload_failures?
}
end
node :download do |quick_mix|
{
should_download: false,
too_many_downloads: false
}
end
end

View File

@ -0,0 +1,3 @@
object @user
attributes :id, :first_name, :last_name, :photo_url, :name

View File

@ -119,3 +119,25 @@
</div>
</script>
<script type="text/template" id="template-help-file-manager-poke">
<div class="help-file-manager-poke">
<p>After the session is over, your recording will synchronize with the server to make a final mix.</p>
<p>You can find out more by clicking <em>File Manager</em> at any time.</p>
</div>
</script>
<script type="text/template" id="template-help-sync-viewer-paused">
<div class="help-sync-viewer-paused">
JamKazam prevents file uploads and downloads while in a session.
</div>
</script>
<script type="text/template" id="template-help-sync-viewer-retry">
<div class="help-sync-viewer-retry">
Your request will be attempted as soon as possible.
</div>
</script>

View File

@ -0,0 +1,2 @@
.help-launcher
| ?

View File

@ -1,5 +1,5 @@
<!-- recording play controls -->
<div class="recording recording-controls">
<div class="recording recording-controls has-mix">
<!-- play button -->
<a class="left play-button" href="#">

View File

@ -1,4 +1,7 @@
<span id="recording-manager-viewer">
<span id="recording-manager-launcher" class="recording-manager-launcher">
File Manager
</span>
<span id="recording-manager-convert" class="recording-manager-command">
<span>converting</span><span class="percent">0</span>
</span>

View File

@ -0,0 +1,206 @@
script type="text/template" id='template-sync-viewer'
.sync-viewer
.headline
.knobs
|
.in-progress
.download-progress
.paused Downloads paused.
.quiet No download work.
.busy
.upload-progress
.paused Uploads paused.
.quiet No upload work.
.busy
.dialog-tabs
a.tab.selected purpose="recordings" Recordings
a.tab purpose="log"
| Log
span.badge !
.tab.tab-content purpose="recordings"
.list-header
.type-info
span.special-text file details
.client-state-info
span.special-text is the file on your system?
.upload-state-info
span.special-text is it uploaded?
.list
.tab.tab-content purpose="log"
.list-header
.list-column
span.special-text file details
.list-column
span.special-text did it succeed?
.list-column
span.special-text more detail
.log-list
.paginator-holder
script type="text/template" id='template-sync-viewer-recorded-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.track_id}}" data-fully-uploaded="{{data.fully_uploaded}}" data-instrument-id="{{data.instrument_id}}"
.type
span.text TRACK
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
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}}"
.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}}"
.type
span.text STREAM MIX
.client-state.bar
.progress
span.msg
.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-mix'
.mix.sync.virtual data-id="{{data.id}}" data-recording-id="{{data.recording_id}}"
.type
span.text MIX
span.duration
| {{window.JK.prettyPrintSeconds(data.duration)}}
.mix-state.bar
span.msg
script type="text/template" id='template-sync-viewer-no-syncs'
.no-syncs
| You have no recordings.
script type="text/template" id="template-sync-viewer-recording-wrapper-details"
.details
a.session-detail-page href="{{data.recording_landing_url}}" rel="external" data-session-id="{{data.recording_id}}"
span.name
| {{data.my ? data.my.name : 'Unknown Name'}}
span.export
span.timeago
| {{$.timeago(data.created_at)}}
script type="text/template" id="template-sync-viewer-hover-recorded-track"
.help-hover-recorded-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
.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-mix"
.help-hover-mix
.mix-box
.mix-state-info
span.special-text is it mixed?
.mix-state class="{{data.mixStateClass}} {{data.context}}"
span.msg
| {{data.mixStateMsg}}
.mix-state-definition.sync-definition
| {{data.mixStateDefinition}}
br clear="both"
| {% if(data.summary) { %}
.summary
.title what's next?
| {{data.summary}}
| {% } %}
script type="text/template" id="template-sync-viewer-download-progress-reset"
span.notice
| No download in progress.
script type="text/template" id="template-sync-viewer-download-progress-reset"
span.notice
| No download in progress.
script type="text/template" id="template-sync-viewer-generic-command"
.generic.sync.command
.type
span.text
| {{data.displayType}}
script type="text/template" id="template-sync-viewer-recorded-track-command"
.recorded-track.sync
.type
.progress
span.text
| {{data.action}} TRACK
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
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-log-item"
.log class="success-{{data.success}}"
.command
| {% if(data.isAsap) { %}
= image_tag('content/icon_resync.png', width:12, height: 14, title: 'This means you initiated this')
| {% } %}
| {{data.command}}
.success
| {{data.displaySuccess}}
.detail
| {{data.detail}}

View File

@ -57,6 +57,7 @@
<%= render "notify" %>
<%= render "client_update" %>
<%= render "overlay_small" %>
<%= render "sync_viewer_templates" %>
<%= render "help" %>
<%= render 'dialogs/dialogs' %>
<div id="fb-root"></div>
@ -124,7 +125,7 @@
// This is a helper class with a singleton. No need to instantiate.
JK.GenreSelectorHelper.initialize(JK.app);
var recordingManager = new JK.RecordingManager();
var recordingManager = new JK.RecordingManager(JK.app);
var acceptFriendRequestDialog = new JK.AcceptFriendRequestDialog(JK.app);
@ -250,6 +251,9 @@
var changeSearchLocationDialog = new JK.ChangeSearchLocationDialog(JK.app);
changeSearchLocationDialog.initialize();
var allSyncsDialog = new JK.AllSyncsDialog(JK.app);
allSyncsDialog.initialize();
// do a client update early check upon initialization
JK.ClientUpdateInstance.check()
@ -267,6 +271,8 @@
return;
}
JK.RecordingUtils.init();
// Let's get things rolling...
if (JK.currentUserId) {

View File

@ -0,0 +1,14 @@
.dialog.dialog-overlay-sm layout='dialog' layout-id='all-syncs-dialog' id='all-syncs-dialog'
.content-head
= image_tag "content/icon_findsession.png", {:width => 19, :height => 19, :class => 'content-icon' }
h1 Your Files
.dialog-inner
p.instructions-header Files that belong to you are shown below. You can see if they are currently on your system, and in the case of Recordings, if they have been uploaded to the server yet.
.syncs
.buttons
.right
a.button-grey.btnClose layout-action="close" CLOSE

View File

@ -6,7 +6,6 @@
= render 'dialogs/configure_tracks_dialog'
= render 'dialogs/edit_recording_dialog'
= render 'dialogs/invitationDialog'
= render 'dialogs/whatsNextDialog'
= render 'dialogs/shareDialog'
= render 'dialogs/networkTestDialog'
= render 'dialogs/recordingFinishedDialog'
@ -28,3 +27,4 @@
= render 'dialogs/gettingStartedDialog'
= render 'dialogs/joinTestSessionDialog'
= render 'dialogs/changeSearchLocationDialog'
= render 'dialogs/allSyncsDialog'

View File

@ -21,16 +21,16 @@
/ timeline and controls
.recording-controls-holder
/ recording play controls
.recording-controls{ class: feed_item.candidate_claimed_recording.has_mix? ? 'has-mix' : 'no-mix'}
.recording-controls.mix
/ play button
%a.left.play-button{:href => "#"}
= image_tag 'content/icon_playbutton.png', width:20, height:20, class:'play-icon'
- if feed_item.has_mix?
%audio{preload: 'none'}
%source{src: claimed_recording_download_url(feed_item.candidate_claimed_recording.id, 'mp3'), type:'audio/mpeg'}
%source{src: claimed_recording_download_url(feed_item.candidate_claimed_recording.id, 'ogg'), type:'audio/ogg'}
%source{src: listen_mix_url(feed_item)[:mp3_url], type:'audio/mpeg'}
%source{src: listen_mix_url(feed_item)[:ogg_url], type:'audio/ogg'}
.recording-status
%span.status-text STILL MIXING
%span.status-text
.recording-duration
= recording_duration(feed_item)
/ playback position
@ -94,5 +94,5 @@
%br/
:javascript
$(function () {
new window.JK.FeedItemRecording($('.feed-entry[data-claimed-recording-id="#{feed_item.candidate_claimed_recording.id}"]'));
new window.JK.FeedItemRecording($('.feed-entry[data-claimed-recording-id="#{feed_item.candidate_claimed_recording.id}"]'), {mix_state: #{feed_item.mix_state.to_json}});
})

View File

@ -24,7 +24,7 @@
/ timeline and controls
.right.w40
/ recording play controls
.recording-controls{ class: '{{data.mix_class}}'}
.recording-controls.mix{ class: '{{data.feed_item.mix_info.mixStateClass}} {{data.mix_class}}'}
/ play button
%a.left.play-button{:href => "#"}
= image_tag 'content/icon_playbutton.png', width:20, height:20, class:'play-icon'
@ -34,7 +34,9 @@
%source{src: '{{data.candidate_claimed_recording.mix.ogg_url}}', type:'audio/ogg'}
= "{% } %}"
.recording-status
%span.status-text STILL MIXING
%span.status-text
= '{{data.feed_item.mix_info.mixStateMsg}}'
= render 'help_launcher'
.recording-duration
%time{class: 'tick-duration duration', duration: '{{data.feed_item.duration}}'}
= '{{data.feed_item.duration}}'
@ -101,5 +103,6 @@
= '{% } %}'
= '{% }) %}'
%br{:clear => "all"}/
%br/

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"
config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver"
# 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.
@ -247,7 +247,7 @@ if defined?(Bundler)
# packet size (bytes) of test
config.ftue_network_test_packet_size = 60
# number of times that the backend retries before giving up
config.ftue_network_test_backend_retries = 10
config.ftue_network_test_backend_retries = 3
# amount of time that we want passed until we run the next network test
config.ftue_network_test_min_wait_since_last_score = 5
# the maximum amount of allowable latency

View File

@ -317,6 +317,11 @@ SampleApp::Application.routes.draw do
match '/users/:id/plays' => 'api_users#add_play', :via => :post, :as => 'api_users_add_play'
match '/users/:id/affiliate' => 'api_users#affiliate_report', :via => :get, :as => 'api_users_affiliate'
# downloads/uploads
match '/users/:id/syncs' => 'api_user_syncs#index', :via => :get
match '/users/:id/syncs/:user_sync_id' => 'api_user_syncs#show', :via => :get
# bands
match '/bands' => 'api_bands#index', :via => :get
match '/bands/validate' => 'api_bands#validate', :via => :post
@ -396,11 +401,16 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/comments' => 'api_recordings#add_comment', :via => :post, :as => 'api_recordings_add_comment'
match '/recordings/:id/likes' => 'api_recordings#add_like', :via => :post, :as => 'api_recordings_add_like'
match '/recordings/:id/discard' => 'api_recordings#discard', :via => :post, :as => 'api_recordings_discard'
match '/recordings/:id/tracks/:track_id' => 'api_recordings#show_recorded_track', :via => :get, :as => 'api_recordings_show_recorded_track'
match '/recordings/:id/tracks/:track_id/download' => 'api_recordings#download', :via => :get, :as => 'api_recordings_download'
match '/recordings/:id/tracks/:track_id/upload_next_part' => 'api_recordings#upload_next_part', :via => :get
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
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
# Claimed Recordings
match '/claimed_recordings' => 'api_claimed_recordings#index', :via => :get

View File

@ -18,15 +18,16 @@ describe ApiClaimedRecordingsController do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", "description", @genre, true)
@recording.reload
@claimed_recording = @recording.claimed_recordings.first
@claimed_recording = @recording.claimed_recordings.first!
@recording.has_mix?.should be_false
end
describe "GET 'show'" do
describe "show" do
it "should show the right thing when one recording just finished" do
pending
controller.current_user = @user
get :show, :id => @claimed_recording.id
get :show, {format:'json', id: @claimed_recording.id}
response.should be_success
json = JSON.parse(response.body)
json.should_not be_nil
@ -38,6 +39,7 @@ describe ApiClaimedRecordingsController do
json["recording"]["recorded_tracks"].first["id"].should == @recording.recorded_tracks.first.id
json["recording"]["recorded_tracks"].first["instrument_id"].should == @instrument.id
json["recording"]["recorded_tracks"].first["user"]["id"].should == @user.id
json["mix"].should be_nil
end
it "should show the right thing when one recording was just uploaded" do
@ -71,7 +73,7 @@ describe ApiClaimedRecordingsController do
end
end
describe "GET 'index'" do
describe "index" do
it "should generate a single output" do
pending
controller.current_user = @user
@ -85,6 +87,83 @@ describe ApiClaimedRecordingsController do
end
end
describe "download" do
it "no mix" do
controller.current_user = @user
get :download, id: @claimed_recording.id
response.status.should == 404
end
it "quick mix, not completed" do
quick_mix = FactoryGirl.create(:quick_mix)
controller.current_user = @user
puts quick_mix.recording.candidate_claimed_recording
get :download, id: quick_mix.recording.candidate_claimed_recording.id
response.status.should == 404
end
it "quick mix, completed" do
quick_mix = FactoryGirl.create(:quick_mix_completed)
controller.current_user = @user
get :download, id: quick_mix.recording.candidate_claimed_recording.id
response.status.should == 302
response.headers['Location'].include?('recordings/ogg').should be_true
get :download, id: quick_mix.recording.candidate_claimed_recording.id, type:'mp3'
response.status.should == 302
response.headers['Location'].include?('recordings/mp3').should be_true
end
it "final mix, not completed" do
mix = FactoryGirl.create(:mix, completed:false)
controller.current_user = @user
get :download, id: mix.recording.candidate_claimed_recording.id
response.status.should == 404
end
it "final mix, completed" do
mix = FactoryGirl.create(:mix, completed:true)
controller.current_user = @user
get :download, id: mix.recording.candidate_claimed_recording.id
response.status.should == 302
response.headers['Location'].include?('recordings/mixed/ogg').should be_true
get :download, id: mix.recording.candidate_claimed_recording.id, type:'mp3'
response.status.should == 302
response.headers['Location'].include?('recordings/mixed/mp3').should be_true
end
it "both completed final mix and stream mix" do
# when both quick mix and mix are present, this should let mix win
mix = FactoryGirl.create(:mix, completed:true)
quick_mix = FactoryGirl.create(:quick_mix_completed, recording: mix.recording, user: mix.recording.owner)
controller.current_user = @user
get :download, id: mix.recording.candidate_claimed_recording.id
response.status.should == 302
response.headers['Location'].include?('recordings/mixed/ogg').should be_true
get :download, id: mix.recording.candidate_claimed_recording.id, type:'mp3'
response.status.should == 302
response.headers['Location'].include?('recordings/mixed/mp3').should be_true
end
it "completed stream mix, incomplete final mix" do
# when both quick mix and mix are present, this should let mix win
mix = FactoryGirl.create(:mix, completed:false)
quick_mix = FactoryGirl.create(:quick_mix_completed, recording: mix.recording, user: mix.recording.owner)
controller.current_user = @user
get :download, id: mix.recording.candidate_claimed_recording.id
response.status.should == 302
response.headers['Location'].include?('recordings/ogg').should be_true
get :download, id: quick_mix.recording.candidate_claimed_recording.id, type:'mp3'
response.status.should == 302
response.headers['Location'].include?('recordings/mp3').should be_true
end
end
=begin
We can't test these because rspec doesn't like that we return 204. It causes rails to return a 406.
describe "DELETE 'destroy'" do

View File

@ -15,7 +15,7 @@ describe ApiRecordingsController do
describe "start" do
it "should work" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
post :start, { :format => 'json', :music_session_id => @music_session.id }
response.should be_success
response_body = JSON.parse(response.body)
response_body['id'].should_not be_nil

View File

@ -0,0 +1,163 @@
require 'spec_helper'
describe ApiUserSyncsController do
render_views
let(:user1) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
before(:each) {
controller.current_user = user1
}
it "requires logged in" do
controller.current_user = nil
get :index
json = JSON.parse(response.body, :symbolize_names => true)
json.should == {message: "not logged in"}
end
it "can return empty results" do
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 0
end
describe "one recording with two users" 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)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2)
recording.save!
recording.reload
recording
}
let(:sorted_tracks) {
Array.new(recording1.recorded_tracks).sort_by! {|rt| -rt.id}
}
it "no claimed_recordings" do
# every is supposed to upload immediately, but no downloads until you try claim it. The assertions below validate this
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 2
recorded_track1 = json[:entries][0]
recorded_track1[:upload][:should_upload].should be_true
recorded_track1[:upload][:too_many_upload_failures].should be_false
recorded_track1[:download][:should_download].should be_false
recorded_track1[:download][:too_many_downloads].should be_false
recorded_track2 = json[:entries][1]
recorded_track2[:upload][:should_upload].should be_true
recorded_track2[:upload][:too_many_upload_failures].should be_false
recorded_track2[:download][:should_download].should be_false
recorded_track2[:download][:too_many_downloads].should be_false
end
it "recording isn't over" do
recording1.duration = nil
recording1.save!
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 0
end
it "one user decides to keep the recording" do
FactoryGirl.create(:claimed_recording, user: user1, recording: recording1, discarded:false)
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 2
recorded_track1 = json[:entries][0]
recorded_track1[:upload][:should_upload].should be_true
recorded_track1[:upload][:too_many_upload_failures].should be_false
recorded_track1[:download][:should_download].should be_true
recorded_track1[:download][:too_many_downloads].should be_false
recorded_track2 = json[:entries][1]
recorded_track2[:upload][:should_upload].should be_true
recorded_track2[:upload][:too_many_upload_failures].should be_false
recorded_track2[:download][:should_download].should be_true
recorded_track2[:download][:too_many_downloads].should be_false
controller.current_user = user2
get :index, { :format => 'json', :id => user2.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 2
recorded_track1 = json[:entries][0]
recorded_track1[:upload][:should_upload].should be_true
recorded_track1[:upload][:too_many_upload_failures].should be_false
recorded_track1[:download][:should_download].should be_false
recorded_track1[:download][:too_many_downloads].should be_false
recorded_track2 = json[:entries][1]
recorded_track2[:upload][:should_upload].should be_true
recorded_track2[:upload][:too_many_upload_failures].should be_false
recorded_track2[:download][:should_download].should be_false
recorded_track2[:download][:too_many_downloads].should be_false
end
it "one user decides to discard the recording" do
FactoryGirl.create(:claimed_recording, user: user1, recording: recording1, discarded:true)
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 2
recorded_track1 = json[:entries][0]
recorded_track1[:upload][:should_upload].should be_true
recorded_track1[:upload][:too_many_upload_failures].should be_false
recorded_track1[:download][:should_download].should be_false
recorded_track1[:download][:too_many_downloads].should be_false
recorded_track2 = json[:entries][1]
recorded_track2[:upload][:should_upload].should be_true
recorded_track2[:upload][:too_many_upload_failures].should be_false
recorded_track2[:download][:should_download].should be_false
recorded_track2[:download][:too_many_downloads].should be_false
controller.current_user = user2
get :index, { :format => 'json', :id => user2.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 2
recorded_track1 = json[:entries][0]
recorded_track1[:upload][:should_upload].should be_true
recorded_track1[:upload][:too_many_upload_failures].should be_false
recorded_track1[:download][:should_download].should be_false
recorded_track1[:download][:too_many_downloads].should be_false
recorded_track2 = json[:entries][1]
recorded_track2[:upload][:should_upload].should be_true
recorded_track2[:upload][:too_many_upload_failures].should be_false
recorded_track2[:download][:should_download].should be_false
recorded_track2[:download][:too_many_downloads].should be_false
end
it "both users decide to discard the recording" do
recording1.all_discarded = true
recording1.save!
get :index, { :format => 'json', :id => user1.id }
json = JSON.parse(response.body, :symbolize_names => true)
json[:next].should be_nil
json[:entries].length.should == 0
end
end
end

View File

@ -309,9 +309,41 @@ FactoryGirl.define do
association :user, factory: :user
before(:create) { |claimed_recording|
claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user)
claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user) unless claimed_recording.recording
}
end
factory :quick_mix, :class => JamRuby::QuickMix do
started_at nil
completed_at nil
ogg_md5 nil
ogg_length 0
ogg_url nil
mp3_md5 nil
mp3_length 0
mp3_url nil
completed false
before(:create) {|mix, evaluator|
user = FactoryGirl.create(:user)
mix.user = user
if evaluator.recording.nil?
mix.recording = FactoryGirl.create(:recording_with_track, owner: user)
mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording)
end
}
factory :quick_mix_completed do
started_at 1.minute.ago
completed_at Time.now
ogg_md5 'a'
ogg_length 1
ogg_url 'recordings/ogg'
mp3_md5 'a'
mp3_length 1
mp3_url 'recordings/mp3'
completed true
end
end
factory :icecast_limit, :class => JamRuby::IcecastLimit do
@ -508,16 +540,20 @@ FactoryGirl.define do
completed_at Time.now
ogg_md5 'abc'
ogg_length 1
sequence(:ogg_url) { |n| "recordings/ogg/#{n}" }
mp3_md5 'abc'
mp3_length 1
sequence(:mp3_url) { |n| "recordings/mp3/#{n}" }
sequence(:mp3_url) { |n| "recordings/mixed/mp3/#{n}" }
completed true
before(:create) {|mix|
user = FactoryGirl.create(:user)
mix.recording = FactoryGirl.create(:recording_with_track, owner: user)
mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording)
mix.recording.has_final_mix = true
mix.recording.save!
}
after(:create) {|mix|
Mix.where(:id => mix.id).update_all(:ogg_url => 'recordings/mixed/ogg' ) # the uploader on the model prevents using typical FactoryGirl mechanisms for this
}
end

View File

@ -12,7 +12,7 @@ describe "Alternate Landing Pages", :js => true, :type => :feature, :capybara_fe
find('a.landing_wb', text: 'Watch a Video to See How to Get Started').trigger(:click)
find('h1', text: 'See How to Get Started')
find('div.fb-like')
find('iframe.twitter-follow-button')
find('a.twitter-follow-button')
find('.g-follow-btn iframe')
end
@ -24,7 +24,7 @@ describe "Alternate Landing Pages", :js => true, :type => :feature, :capybara_fe
find('a.landing_wb', text: 'See How to Get Started Using JamKazam').trigger(:click)
find('h1', text: 'See How to Get Started Using JamKazam')
find('div.fb-like')
find('iframe.twitter-follow-button')
find('a.twitter-follow-button')
find('.g-follow-btn iframe')
end
@ -34,7 +34,7 @@ describe "Alternate Landing Pages", :js => true, :type => :feature, :capybara_fe
find('h2', text: "It's FREE! Ready to Go?")
find('h2', text: 'Not a Good Time to Sign Up?')
find('div.fb-like')
find('iframe.twitter-follow-button')
find('a.twitter-follow-button')
find('.g-follow-btn iframe')
end
@ -44,7 +44,7 @@ describe "Alternate Landing Pages", :js => true, :type => :feature, :capybara_fe
find('h2', text: "It's FREE! Ready to Go?")
find('h2', text: 'Not a Good Time to Sign Up?')
find('div.fb-like')
find('iframe.twitter-follow-button')
find('a.twitter-follow-button')
find('.g-follow-btn iframe')
end

View File

@ -0,0 +1,2 @@
= render "clients/sync_viewer_templates"
= render "clients/paginator"

View File

@ -0,0 +1,174 @@
{
"next": null,
"entries": [
{
"id": 20,
"recording_id": "52480b2a-046d-4be3-bfc0-8fd8bbb527b8",
"client_id": "7987a826-803a-4cb8-9c74-85ffba25d6d3",
"track_id": "1ac48188-a84f-4607-b074-37d252828a2e",
"client_track_id": "37bf89de-4f11-469a-a061-67e4e5451eb4",
"md5": "Nx29Yo0ku8MclPu5rmYsPg==",
"length": 159976,
"download_count": 0,
"instrument_id": "bass guitar",
"fully_uploaded": false,
"upload_failures": 0,
"part_failures": 0,
"created_at": "2014-10-15T02:50:44Z",
"type": "recorded_track",
"user": {
"id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"first_name": "Seth",
"last_name": "Call",
"photo_url": null,
"name": "Seth Call"
},
"recording": {
"id": "52480b2a-046d-4be3-bfc0-8fd8bbb527b8",
"band": null,
"created_at": "2014-10-15T02:50:44Z",
"duration": 3,
"comment_count": 0,
"like_count": 0,
"play_count": 0,
"my": {
"name": "Cheerful4",
"description": "",
"genre": "pop",
"genre_name": "Pop"
},
"owner": {
"id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"name": "Seth Call",
"location": "Austin, TX",
"photo_url": null
},
"recorded_tracks": [
{
"id": 20,
"fully_uploaded": true,
"client_track_id": "37bf89de-4f11-469a-a061-67e4e5451eb4",
"client_id": "7987a826-803a-4cb8-9c74-85ffba25d6d3",
"instrument_id": "bass guitar",
"recording_id": "52480b2a-046d-4be3-bfc0-8fd8bbb527b8",
"user": {
"id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"first_name": "Seth",
"last_name": "Call",
"city": "Austin",
"state": "TX",
"country": "US",
"location": "Austin, TX",
"photo_url": null
}
}
],
"comments": [
],
"claimed_recordings": [
{
"id": "4fa1ac49-169a-49f2-9ae5-dd008fec121b",
"name": "Cheerful4",
"description": "",
"is_public": true,
"genre_id": "pop",
"has_mix?": false,
"user_id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"share_url": "http:\/\/localhost:3000\/s\/NJEHSKOMME",
"mix": {
"mp3_url": "http:\/\/localhost:3000\/api\/claimed_recordings\/4fa1ac49-169a-49f2-9ae5-dd008fec121b\/download\/mp3",
"ogg_url": "http:\/\/localhost:3000\/api\/claimed_recordings\/4fa1ac49-169a-49f2-9ae5-dd008fec121b\/download\/ogg"
}
}
]
},
"upload": {
"should_upload": true,
"too_many_upload_failures": false
},
"download": {
"should_download": true,
"too_many_downloads": false
}
},
{
"id": 19,
"recording_id": "3aefac5f-b960-4196-90aa-1446825b4612",
"started_at": "2014-10-15T02:47:15Z",
"completed_at": "2014-10-15T02:47:17Z",
"completed": true,
"should_retry": false,
"download_count": 1,
"state": "mixed",
"type": "mix",
"recording": {
"id": "3aefac5f-b960-4196-90aa-1446825b4612",
"band": null,
"created_at": "2014-10-15T02:46:54Z",
"duration": 3,
"comment_count": 0,
"like_count": 0,
"play_count": 0,
"my": {
"name": "Cheerful3",
"description": "",
"genre": "pop",
"genre_name": "Pop"
},
"owner": {
"id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"name": "Seth Call",
"location": "Austin, TX",
"photo_url": null
},
"recorded_tracks": [
{
"id": 18,
"fully_uploaded": true,
"client_track_id": "ac651da5-7609-494a-aa2c-17287772441b",
"client_id": "7987a826-803a-4cb8-9c74-85ffba25d6d3",
"instrument_id": "bass guitar",
"recording_id": "3aefac5f-b960-4196-90aa-1446825b4612",
"user": {
"id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"first_name": "Seth",
"last_name": "Call",
"city": "Austin",
"state": "TX",
"country": "US",
"location": "Austin, TX",
"photo_url": null
}
}
],
"comments": [
],
"claimed_recordings": [
{
"id": "870fba4c-59de-4f57-a9d2-8200bdbffba9",
"name": "Cheerful3",
"description": "",
"is_public": true,
"genre_id": "pop",
"has_mix?": true,
"user_id": "eab2713f-8631-4baa-b5d5-cc4eb8b2d86a",
"share_url": "http:\/\/localhost:3000\/s\/IBB8ACWSLOA",
"mix": {
"mp3_url": "http:\/\/localhost:3000\/api\/claimed_recordings\/870fba4c-59de-4f57-a9d2-8200bdbffba9\/download\/mp3",
"ogg_url": "http:\/\/localhost:3000\/api\/claimed_recordings\/870fba4c-59de-4f57-a9d2-8200bdbffba9\/download\/ogg"
}
}
]
},
"duration": 3,
"error": null,
"download": {
"should_download": true,
"too_many_downloads": false
}
}
],
"total_entries": 2
}

View File

@ -1,700 +0,0 @@
/*!
Jasmine-jQuery: a set of jQuery helpers for Jasmine tests.
Version 1.5.92
https://github.com/velesin/jasmine-jquery
Copyright (c) 2010-2013 Wojciech Zawistowski, Travis Jeffery
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+function (jasmine, $) { "use strict";
jasmine.spiedEventsKey = function (selector, eventName) {
return [$(selector).selector, eventName].toString()
}
jasmine.getFixtures = function () {
return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
}
jasmine.getStyleFixtures = function () {
return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures()
}
jasmine.Fixtures = function () {
this.containerId = 'jasmine-fixtures'
this.fixturesCache_ = {}
this.fixturesPath = 'spec/javascripts/fixtures'
}
jasmine.Fixtures.prototype.set = function (html) {
this.cleanUp()
return this.createContainer_(html)
}
jasmine.Fixtures.prototype.appendSet= function (html) {
this.addToContainer_(html)
}
jasmine.Fixtures.prototype.preload = function () {
this.read.apply(this, arguments)
}
jasmine.Fixtures.prototype.load = function () {
this.cleanUp()
this.createContainer_(this.read.apply(this, arguments))
}
jasmine.Fixtures.prototype.appendLoad = function () {
this.addToContainer_(this.read.apply(this, arguments))
}
jasmine.Fixtures.prototype.read = function () {
var htmlChunks = []
, fixtureUrls = arguments
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
}
return htmlChunks.join('')
}
jasmine.Fixtures.prototype.clearCache = function () {
this.fixturesCache_ = {}
}
jasmine.Fixtures.prototype.cleanUp = function () {
$('#' + this.containerId).remove()
}
jasmine.Fixtures.prototype.sandbox = function (attributes) {
var attributesToSet = attributes || {}
return $('<div id="sandbox" />').attr(attributesToSet)
}
jasmine.Fixtures.prototype.createContainer_ = function (html) {
var container = $('<div>')
.attr('id', this.containerId)
.html(html)
$(document.body).append(container)
return container
}
jasmine.Fixtures.prototype.addToContainer_ = function (html){
var container = $(document.body).find('#'+this.containerId).append(html)
if(!container.length){
this.createContainer_(html)
}
}
jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) {
if (typeof this.fixturesCache_[url] === 'undefined') {
this.loadFixtureIntoCache_(url)
}
return this.fixturesCache_[url]
}
jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
var self = this
, url = this.makeFixtureUrl_(relativeUrl)
, request = $.ajax({
async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
cache: false,
url: url,
success: function (data, status, $xhr) {
self.fixturesCache_[relativeUrl] = $xhr.responseText
},
error: function (jqXHR, status, errorThrown) {
throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
}
})
}
jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){
return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
}
jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
return this[methodName].apply(this, passedArguments)
}
jasmine.StyleFixtures = function () {
this.fixturesCache_ = {}
this.fixturesNodes_ = []
this.fixturesPath = 'spec/javascripts/fixtures'
}
jasmine.StyleFixtures.prototype.set = function (css) {
this.cleanUp()
this.createStyle_(css)
}
jasmine.StyleFixtures.prototype.appendSet = function (css) {
this.createStyle_(css)
}
jasmine.StyleFixtures.prototype.preload = function () {
this.read_.apply(this, arguments)
}
jasmine.StyleFixtures.prototype.load = function () {
this.cleanUp()
this.createStyle_(this.read_.apply(this, arguments))
}
jasmine.StyleFixtures.prototype.appendLoad = function () {
this.createStyle_(this.read_.apply(this, arguments))
}
jasmine.StyleFixtures.prototype.cleanUp = function () {
while(this.fixturesNodes_.length) {
this.fixturesNodes_.pop().remove()
}
}
jasmine.StyleFixtures.prototype.createStyle_ = function (html) {
var styleText = $('<div></div>').html(html).text()
, style = $('<style>' + styleText + '</style>')
this.fixturesNodes_.push(style)
$('head').append(style)
}
jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
jasmine.getJSONFixtures = function () {
return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
}
jasmine.JSONFixtures = function () {
this.fixturesCache_ = {}
this.fixturesPath = 'spec/javascripts/fixtures/json'
}
jasmine.JSONFixtures.prototype.load = function () {
this.read.apply(this, arguments)
return this.fixturesCache_
}
jasmine.JSONFixtures.prototype.read = function () {
var fixtureUrls = arguments
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
this.getFixtureData_(fixtureUrls[urlIndex])
}
return this.fixturesCache_
}
jasmine.JSONFixtures.prototype.clearCache = function () {
this.fixturesCache_ = {}
}
jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) {
if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url)
return this.fixturesCache_[url]
}
jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
var self = this
, url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
$.ajax({
async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
cache: false,
dataType: 'json',
url: url,
success: function (data) {
self.fixturesCache_[relativeUrl] = data
},
error: function (jqXHR, status, errorThrown) {
throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
}
})
}
jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
return this[methodName].apply(this, passedArguments)
}
jasmine.JQuery = function () {}
jasmine.JQuery.browserTagCaseIndependentHtml = function (html) {
return $('<div/>').append(html).html()
}
jasmine.JQuery.elementToString = function (element) {
return $(element).map(function () { return this.outerHTML; }).toArray().join(', ')
}
jasmine.JQuery.matchersClass = {}
!function (namespace) {
var data = {
spiedEvents: {}
, handlers: []
}
namespace.events = {
spyOn: function (selector, eventName) {
var handler = function (e) {
data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments)
}
$(selector).on(eventName, handler)
data.handlers.push(handler)
return {
selector: selector,
eventName: eventName,
handler: handler,
reset: function (){
delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
}
}
},
args: function (selector, eventName) {
var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
if (!actualArgs) {
throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent."
}
return actualArgs
},
wasTriggered: function (selector, eventName) {
return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
},
wasTriggeredWith: function (selector, eventName, expectedArgs, env) {
var actualArgs = jasmine.JQuery.events.args(selector, eventName).slice(1)
if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') {
actualArgs = actualArgs[0]
}
return env.equals_(expectedArgs, actualArgs)
},
wasPrevented: function (selector, eventName) {
var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
, e = args ? args[0] : undefined
return e && e.isDefaultPrevented()
},
wasStopped: function (selector, eventName) {
var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
, e = args ? args[0] : undefined
return e && e.isPropagationStopped()
},
cleanUp: function () {
data.spiedEvents = {}
data.handlers = []
}
}
}(jasmine.JQuery)
!function (){
var jQueryMatchers = {
toHaveClass: function (className) {
return this.actual.hasClass(className)
},
toHaveCss: function (css){
for (var prop in css){
var value = css[prop]
// see issue #147 on gh
;if (value === 'auto' && this.actual.get(0).style[prop] === 'auto') continue
if (this.actual.css(prop) !== value) return false
}
return true
},
toBeVisible: function () {
return this.actual.is(':visible')
},
toBeHidden: function () {
return this.actual.is(':hidden')
},
toBeSelected: function () {
return this.actual.is(':selected')
},
toBeChecked: function () {
return this.actual.is(':checked')
},
toBeEmpty: function () {
return this.actual.is(':empty')
},
toExist: function () {
return this.actual.length
},
toHaveLength: function (length) {
return this.actual.length === length
},
toHaveAttr: function (attributeName, expectedAttributeValue) {
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
},
toHaveProp: function (propertyName, expectedPropertyValue) {
return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
},
toHaveId: function (id) {
return this.actual.attr('id') == id
},
toHaveHtml: function (html) {
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
},
toContainHtml: function (html){
var actualHtml = this.actual.html()
, expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
return (actualHtml.indexOf(expectedHtml) >= 0)
},
toHaveText: function (text) {
var trimmedText = $.trim(this.actual.text())
if (text && $.isFunction(text.test)) {
return text.test(trimmedText)
} else {
return trimmedText == text
}
},
toContainText: function (text) {
var trimmedText = $.trim(this.actual.text())
if (text && $.isFunction(text.test)) {
return text.test(trimmedText)
} else {
return trimmedText.indexOf(text) != -1
}
},
toHaveValue: function (value) {
return this.actual.val() === value
},
toHaveData: function (key, expectedValue) {
return hasProperty(this.actual.data(key), expectedValue)
},
toBe: function (selector) {
return this.actual.is(selector)
},
toContain: function (selector) {
return this.actual.find(selector).length
},
toBeMatchedBy: function (selector) {
return this.actual.filter(selector).length
},
toBeDisabled: function (selector){
return this.actual.is(':disabled')
},
toBeFocused: function (selector) {
return this.actual[0] === this.actual[0].ownerDocument.activeElement
},
toHandle: function (event) {
var events = $._data(this.actual.get(0), "events")
if(!events || !event || typeof event !== "string") {
return false
}
var namespaces = event.split(".")
, eventType = namespaces.shift()
, sortedNamespaces = namespaces.slice(0).sort()
, namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
if(events[eventType] && namespaces.length) {
for(var i = 0; i < events[eventType].length; i++) {
var namespace = events[eventType][i].namespace
if(namespaceRegExp.test(namespace)) {
return true
}
}
} else {
return events[eventType] && events[eventType].length > 0
}
},
toHandleWith: function (eventName, eventHandler) {
var normalizedEventName = eventName.split('.')[0]
, stack = $._data(this.actual.get(0), "events")[normalizedEventName]
for (var i = 0; i < stack.length; i++) {
if (stack[i].handler == eventHandler) return true
}
return false
}
}
var hasProperty = function (actualValue, expectedValue) {
if (expectedValue === undefined) return actualValue !== undefined
return actualValue == expectedValue
}
var bindMatcher = function (methodName) {
var builtInMatcher = jasmine.Matchers.prototype[methodName]
jasmine.JQuery.matchersClass[methodName] = function () {
if (this.actual
&& (this.actual instanceof $
|| jasmine.isDomNode(this.actual))) {
this.actual = $(this.actual)
var result = jQueryMatchers[methodName].apply(this, arguments)
, element
if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
this.actual = jasmine.JQuery.elementToString(this.actual)
return result
}
if (builtInMatcher) {
return builtInMatcher.apply(this, arguments)
}
return false
}
}
for(var methodName in jQueryMatchers) {
bindMatcher(methodName)
}
}()
beforeEach(function () {
this.addMatchers(jasmine.JQuery.matchersClass)
this.addMatchers({
toHaveBeenTriggeredOn: function (selector) {
this.message = function () {
return [
"Expected event " + this.actual + " to have been triggered on " + selector,
"Expected event " + this.actual + " not to have been triggered on " + selector
]
}
return jasmine.JQuery.events.wasTriggered(selector, this.actual)
}
})
this.addMatchers({
toHaveBeenTriggered: function (){
var eventName = this.actual.eventName
, selector = this.actual.selector
this.message = function () {
return [
"Expected event " + eventName + " to have been triggered on " + selector,
"Expected event " + eventName + " not to have been triggered on " + selector
]
}
return jasmine.JQuery.events.wasTriggered(selector, eventName)
}
})
this.addMatchers({
toHaveBeenTriggeredOnAndWith: function () {
var selector = arguments[0]
, expectedArgs = arguments[1]
, wasTriggered = jasmine.JQuery.events.wasTriggered(selector, this.actual)
this.message = function () {
if (wasTriggered) {
var actualArgs = jasmine.JQuery.events.args(selector, this.actual, expectedArgs)[1]
return [
"Expected event " + this.actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs),
"Expected event " + this.actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs)
]
} else {
return [
"Expected event " + this.actual + " to have been triggered on " + selector,
"Expected event " + this.actual + " not to have been triggered on " + selector
]
}
}
return wasTriggered && jasmine.JQuery.events.wasTriggeredWith(selector, this.actual, expectedArgs, this.env)
}
})
this.addMatchers({
toHaveBeenPreventedOn: function (selector) {
this.message = function () {
return [
"Expected event " + this.actual + " to have been prevented on " + selector,
"Expected event " + this.actual + " not to have been prevented on " + selector
]
}
return jasmine.JQuery.events.wasPrevented(selector, this.actual)
}
})
this.addMatchers({
toHaveBeenPrevented: function () {
var eventName = this.actual.eventName
, selector = this.actual.selector
this.message = function () {
return [
"Expected event " + eventName + " to have been prevented on " + selector,
"Expected event " + eventName + " not to have been prevented on " + selector
]
}
return jasmine.JQuery.events.wasPrevented(selector, eventName)
}
})
this.addMatchers({
toHaveBeenStoppedOn: function (selector) {
this.message = function () {
return [
"Expected event " + this.actual + " to have been stopped on " + selector,
"Expected event " + this.actual + " not to have been stopped on " + selector
]
}
return jasmine.JQuery.events.wasStopped(selector, this.actual)
}
})
this.addMatchers({
toHaveBeenStopped: function () {
var eventName = this.actual.eventName
, selector = this.actual.selector
this.message = function () {
return [
"Expected event " + eventName + " to have been stopped on " + selector,
"Expected event " + eventName + " not to have been stopped on " + selector
]
}
return jasmine.JQuery.events.wasStopped(selector, eventName)
}
})
jasmine.getEnv().addEqualityTester(function (a, b) {
if(a instanceof jQuery && b instanceof jQuery) {
if(a.size() != b.size()) {
return jasmine.undefined
}
else if(a.is(b)) {
return true
}
}
return jasmine.undefined
})
})
afterEach(function () {
jasmine.getFixtures().cleanUp()
jasmine.getStyleFixtures().cleanUp()
jasmine.JQuery.events.cleanUp()
})
}(window.jasmine, window.jQuery)
+function (jasmine, global) { "use strict";
global.readFixtures = function () {
return jasmine.getFixtures().proxyCallTo_('read', arguments)
}
global.preloadFixtures = function () {
jasmine.getFixtures().proxyCallTo_('preload', arguments)
}
global.loadFixtures = function () {
jasmine.getFixtures().proxyCallTo_('load', arguments)
}
global.appendLoadFixtures = function () {
jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
}
global.setFixtures = function (html) {
return jasmine.getFixtures().proxyCallTo_('set', arguments)
}
global.appendSetFixtures = function () {
jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
}
global.sandbox = function (attributes) {
return jasmine.getFixtures().sandbox(attributes)
}
global.spyOnEvent = function (selector, eventName) {
return jasmine.JQuery.events.spyOn(selector, eventName)
}
global.preloadStyleFixtures = function () {
jasmine.getStyleFixtures().proxyCallTo_('preload', arguments)
}
global.loadStyleFixtures = function () {
jasmine.getStyleFixtures().proxyCallTo_('load', arguments)
}
global.appendLoadStyleFixtures = function () {
jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments)
}
global.setStyleFixtures = function (html) {
jasmine.getStyleFixtures().proxyCallTo_('set', arguments)
}
global.appendSetStyleFixtures = function (html) {
jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments)
}
global.loadJSONFixtures = function () {
return jasmine.getJSONFixtures().proxyCallTo_('load', arguments)
}
global.getJSONFixture = function (url) {
return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url]
}
}(jasmine, window);

View File

@ -1,8 +1,9 @@
// Teaspoon includes some support files, but you can use anything from your own support path too.
// require support/sinon
// require support/chai
// require support/expect
// require support/jasmine-jquery-1.7.0
// require support/jasmine-jquery-2.0.0
// require support/sinon
// require support/your-support-file
//
// PhantomJS (Teaspoons default driver) doesn't have support for Function.prototype.bind, which has caused confusion.
// Use this polyfill to avoid the confusion.
@ -29,3 +30,6 @@
// You can require your own javascript files here. By default this will include everything in application, however you
// may get better load performance if you require the specific files that are being used in the spec that tests them.
//= require application
//= require support/sinon
// require support/expect
//= require support/jasmine-jquery-1.7.0

View File

@ -1,76 +0,0 @@
# src_files
#
# Return an array of filepaths relative to src_dir to include before jasmine specs.
# Default: []
#
# EXAMPLE:
#
# src_files:
# - lib/source1.js
# - lib/source2.js
# - dist/**/*.js
#
src_files:
- assets/application.js
# stylesheets
#
# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
# Default: []
#
# EXAMPLE:
#
# stylesheets:
# - css/style.css
# - stylesheets/*.css
#
stylesheets:
- stylesheets/**/*.css
# helpers
#
# Return an array of filepaths relative to spec_dir to include before jasmine specs.
# Default: ["helpers/**/*.js"]
#
# EXAMPLE:
#
# helpers:
# - helpers/**/*.js
#
helpers:
- helpers/**/*.js
# spec_files
#
# Return an array of filepaths relative to spec_dir to include.
# Default: ["**/*[sS]pec.js"]
#
# EXAMPLE:
#
# spec_files:
# - **/*[sS]pec.js
#
spec_files:
- '**/*[sS]pec.js'
# src_dir
#
# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
# Default: project root
#
# EXAMPLE:
#
# src_dir: public
#
src_dir:
# spec_dir
#
# Spec directory path. Your spec_files must be returned relative to this path.
# Default: spec/javascripts
#
# EXAMPLE:
#
# spec_dir: spec/javascripts
#
spec_dir: spec/javascripts

View File

@ -0,0 +1,228 @@
describe "SyncViewer", ->
beforeEach ->
this.fixtures = fixture.load("syncViewer.html", "user_sync_track1.json"); # append these fixtures which were already cached
this.server = sinon.fakeServer.create();
window.jamClient = sinon.stub()
this.syncViewer = new JK.SyncViewer()
this.syncViewer.init()
$('body').append(this.syncViewer.root)
this.track1 = this.fixtures[1]["entries"][0]
callback = sinon.spy()
window.jamClient.GetLocalRecordingState = sinon.stub().returns({recordings: []})
window.jamClient.GetRecordingManagerState = sinon.stub().returns({running: false})
window.jamClient.IsNativeClient = sinon.stub().returns(true)
afterEach ->
this.server.restore();
it "display state correctly", ->
$track = this.syncViewer.createTrack(this.track1)
this.syncViewer.updateTrackState($track)
expect($track.find('.client-state .msg')).toContainText('MISSING')
expect($track.find('.upload-state .msg')).toContainText('PENDING UPLOAD')
this.track1.fully_uploaded = true
this.syncViewer.updateTrackState($track)
expect($track.find('.upload-state .msg')).toContainText('UPLOADED')
describe "no track", ->
beforeEach ->
this.syncViewer.load()
this.server.requests[0].respond(
200,
{ "Content-Type": "application/json" },
JSON.stringify({ next: null, entries: []})
);
it "loads empty", ->
expect(this.syncViewer.list.find('.no-syncs')).toExist();
describe "one track", ->
beforeEach ->
this.syncViewer.load()
this.server.requests[0].respond(
200,
{ "Content-Type": "application/json" },
JSON.stringify({ next: null, entries: [this.track1]})
);
it "loads single track", ->
expect(this.syncViewer.list.find('.no-syncs')).not.toExist();
$track = this.syncViewer.list.find('.recorded-track')
expect($track).toHaveLength(1);
expect($track.find('.client-state .msg')).toContainText('MISSING')
expect($track.find('.upload-state .msg')).toContainText('PENDING UPLOAD')
it "handles recorded_track upload progress events correctly", ->
commandMetadata = {queue: 'upload', type: 'recorded_track', action: 'upload', recording_id: this.track1.recording_id, track_id: this.track1.client_track_id}
cmd = {commandId: '1', commandType: 'upload', commandMetadata: commandMetadata }
# first we should see that recording manager is paused
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
# send in a start command
this.syncViewer.fileManagerCmdStart(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('busy')
$recordedTrackProgress = this.syncViewer.uploadProgress.find('.recorded-track.sync')
expect($recordedTrackProgress).toHaveLength(1)
expect($recordedTrackProgress.find('.progress')).toBeHidden()
cmd.percentage = 50 # half way done
this.syncViewer.fileManagerCmdProgress(null, cmd)
expect($recordedTrackProgress.find('.progress')).toBeVisible()
# command is over; progress element should be removed now
this.syncViewer.fileManagerCmdStop(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
expect($recordedTrackProgress).not.toBeInDOM() # got removed
it "handles mix download progress events correctly", ->
commandMetadata = {queue: 'download', type: 'mix', action: 'download'}
cmd = {commandId: '1', commandType: 'download', commandMetadata: commandMetadata }
# first we should see that recording manager is paused
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
# send in a start command
this.syncViewer.fileManagerCmdStart(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('busy')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
$progress = this.syncViewer.downloadProgress.find('.generic.sync')
expect($progress).toHaveLength(1)
expect($progress.find('span.text')).toHaveText('DOWNLOADING MIX')
cmd.percentage = 50 # half way done
this.syncViewer.fileManagerCmdProgress(null, cmd)
# nothing happens in percentage update
# command is over; progress element should be removed now
this.syncViewer.fileManagerCmdStop(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
expect($progress).not.toBeInDOM() # got removed
it "handles recorded track convert progress events correctly", ->
commandMetadata = {queue: 'upload', type: 'recorded_track', action: 'convert'}
cmd = {commandId: '1', commandType: 'upload', commandMetadata: commandMetadata }
# first we should see that recording manager is paused
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
# send in a start command
this.syncViewer.fileManagerCmdStart(null, cmd)
expect(this.syncViewer.uploadProgress).toHaveClass('busy')
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
$progress = this.syncViewer.uploadProgress.find('.generic.sync')
expect($progress).toHaveLength(1)
expect($progress.find('span.text')).toHaveText('COMPRESSING TRACK')
cmd.percentage = 50 # half way done
this.syncViewer.fileManagerCmdProgress(null, cmd)
# nothing happens in percentage update
# command is over; progress element should be removed now
this.syncViewer.fileManagerCmdStop(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
expect($progress).not.toBeInDOM() # got removed
it "handles stream mix upload progress events correctly", ->
commandMetadata = {queue: 'upload', type: 'stream_mix', action: 'upload'}
cmd = {commandId: '1', commandType: 'upload', commandMetadata: commandMetadata }
# first we should see that recording manager is paused
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
# send in a start command
this.syncViewer.fileManagerCmdStart(null, cmd)
expect(this.syncViewer.uploadProgress).toHaveClass('busy')
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
$progress = this.syncViewer.uploadProgress.find('.generic.sync')
expect($progress).toHaveLength(1)
expect($progress.find('span.text')).toHaveText('UPLOADING STREAM MIX')
cmd.percentage = 50 # half way done
this.syncViewer.fileManagerCmdProgress(null, cmd)
# nothing happens in percentage update
# command is over; progress element should be removed now
this.syncViewer.fileManagerCmdStop(null, cmd)
expect(this.syncViewer.downloadProgress).toHaveClass('paused')
expect(this.syncViewer.uploadProgress).toHaveClass('paused')
expect($progress).not.toBeInDOM() # got removed
it "displays hover info over recorded-track", ->
$track = this.syncViewer.list.find('.recorded-track')
# should show over client state
$clientState = $track.find('.client-state')
expect($clientState).toHaveLength(1);
$clientState.btOn()
$hoverHelp = $('.help-hover-recorded-tracks')
expect($hoverHelp).toHaveLength(1);
# also verify that the message is correct about client state and upload state
expect($hoverHelp.find('.client-state .msg')).toContainText('MISSING')
expect($hoverHelp.find('.upload-state .msg')).toContainText('PENDING UPLOAD')
$clientState.btOff()
# as well as show over upload state
$uploadState = $track.find('.upload-state')
expect($uploadState).toHaveLength(1);
$uploadState.btOn()
$hoverHelp = $('.help-hover-recorded-tracks')
expect($hoverHelp).toHaveLength(1);
# also verify that the message is correct about client state and upload state
expect($hoverHelp.find('.client-state .msg')).toContainText('MISSING')
expect($hoverHelp.find('.upload-state .msg')).toContainText('PENDING UPLOAD')
$uploadState.btOff()

File diff suppressed because it is too large Load Diff