merging feature/musician_profile_enhancements

This commit is contained in:
Jonathan Kolyer 2015-02-24 06:08:33 +00:00
commit 2ea1259060
117 changed files with 5067 additions and 937 deletions

View File

@ -74,6 +74,7 @@ gem 'sanitize'
gem 'slim'
gem 'influxdb', '0.1.8'
gem 'influxdb-rails', '0.1.10'
gem 'recurly'
group :libv8 do
gem 'libv8', "~> 3.11.8"

View File

@ -0,0 +1,87 @@
require 'jam_ruby/recurly_client'
ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
menu :label => 'Purchased JamTracks', :parent => 'JamTracks'
config.sort_order = 'updated_at DESC'
config.batch_actions = false
#form :partial => 'form'
index do
default_actions
column "Order" do |right|
link_to("Place", order_admin_jam_track_right_path(right)) + " | " +
link_to("Refund", refund_admin_jam_track_right_path(right))
end
column "Last Name" do |right|
right.user.last_name
end
column "First Name" do |right|
right.user.first_name
end
column "Jam Track" do |right|
link_to(right.jam_track.name, admin_jam_track_right_path(right.jam_track))
# right.jam_track
end
column "Plan Code" do |right|
right.jam_track.plan_code
end
end
form do |f|
f.inputs 'New Jam Track Right' do
f.input :jam_track, :required=>true, collection: JamTrack.all, include_blank: false
f.input :user, :required=>true, collection: User.all, include_blank: false
end
f.actions
end
member_action :order, :method => :get do
right = JamTrackRight.where("id=?",params[:id]).first
user = right.user
jam_track = right.jam_track
client = RecurlyClient.new
billing_info = {
first_name: user.first_name,
last_name: user.last_name,
address1: 'Test Address 1',
address2: 'Test Address 2',
city: user.city,
state: user.state,
country: user.country,
zip: '12345',
number: '4111-1111-1111-1111',
month: '08',
year: '2017',
verification_value: '111'
}
begin
client.find_or_create_account(user, billing_info)
client.place_order(user, jam_track)
rescue RecurlyClientError=>x
redirect_to admin_jam_track_rights_path, notice: "Could not order #{jam_track} for #{user.to_s}: #{x.errors.inspect}"
else
redirect_to admin_jam_track_rights_path, notice: "Placed order of #{jam_track} for #{user.to_s}."
end
end
member_action :refund, :method => :get do
right = JamTrackRight.where("id=?",params[:id]).first
client = RecurlyClient.new
begin
client.refund_user_subscription(right.user, right.jam_track)
rescue RecurlyClientError=>x
redirect_to admin_jam_track_rights_path, notice: "Could not issue refund on #{right.jam_track} for #{right.user.to_s}: #{x.errors.inspect}"
else
redirect_to admin_jam_track_rights_path, notice: "Issued full refund on #{right.jam_track} for #{right.user.to_s}"
end
end
end

View File

@ -223,7 +223,6 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
bpm 100.1
time_signature '4/4'
status 'Production'
recording_type 'Cover'

View File

@ -247,6 +247,12 @@ user_model_about_changes.sql
performance_samples.sql
user_presences.sql
discard_scores_optimized.sql
backing_tracks.sql
metronome.sql
recorded_backing_tracks.sql
recorded_backing_tracks_add_filename.sql
user_syncs_include_backing_tracks.sql
remove_bpm_from_jamtracks.sql
alter_type_columns.sql
user_presences_rename.sql
add_genre_type.sql

2
db/up/backing_tracks.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN backing_track_path VARCHAR(1024);
ALTER TABLE active_music_sessions ADD COLUMN backing_track_initiator_id VARCHAR(64);

2
db/up/metronome.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN metronome_active BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE active_music_sessions ADD COLUMN metronome_initiator_id VARCHAR(64);

View File

@ -0,0 +1,38 @@
CREATE UNLOGGED TABLE backing_tracks (
id VARCHAR(64) NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
filename VARCHAR(1024) NOT NULL,
connection_id VARCHAR(64) NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
client_track_id VARCHAR(64) NOT NULL,
client_resource_id VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE recorded_backing_tracks (
id BIGINT PRIMARY KEY,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
backing_track_id VARCHAR(64),
recording_id VARCHAR(64) NOT NULL,
client_track_id VARCHAR(64) NOT NULL,
is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE,
next_part_to_upload INTEGER NOT NULL DEFAULT 0,
upload_id CHARACTER VARYING(1024),
part_failures INTEGER NOT NULL DEFAULT 0,
discard BOOLEAN,
download_count INTEGER NOT NULL DEFAULT 0,
md5 CHARACTER VARYING(100),
length BIGINT,
client_id VARCHAR(64) NOT NULL,
file_offset BIGINT,
url VARCHAR(1024) NOT NULL,
fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE,
upload_failures INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE recorded_backing_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');

View File

@ -0,0 +1,2 @@
ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL;
ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE;

View File

@ -0,0 +1 @@
ALTER TABLE jam_tracks DROP COLUMN bpm;

View File

@ -0,0 +1,47 @@
DROP VIEW user_syncs;
CREATE VIEW user_syncs AS
SELECT DISTINCT b.id AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
b.id AS unified_id,
a.user_id AS user_id,
b.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id
UNION ALL
SELECT CAST(NULL AS BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
a.id AS recorded_backing_track_id,
a.id AS unified_id,
a.user_id AS user_id,
a.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
mixes.id AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
mixes.id AS unified_id,
claimed_recordings.user_id AS user_id,
NULL as fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
quick_mixes.id AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
quick_mixes.id AS unified_id,
quick_mixes.user_id,
quick_mixes.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE;

View File

@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[])
users.each do |user|
in_client(user) do
users.reject {|u| u==user}.each do |other|
find('div.track-label', text: other.name)
find('div.track-label > span', text: other.name)
#puts user.name + " is able to see " + other.name + "\'s track"
end
end

View File

@ -49,6 +49,7 @@ gem 'iso-639'
gem 'rubyzip'
gem 'sanitize'
gem 'influxdb', '0.1.8'
gem 'recurly'
group :test do
gem 'simplecov', '~> 0.7.1'

View File

@ -62,6 +62,7 @@ require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/mq_router"
require "jam_ruby/recurly_client"
require "jam_ruby/base_manager"
require "jam_ruby/connection_manager"
require "jam_ruby/version"
@ -87,6 +88,7 @@ require "jam_ruby/lib/stats.rb"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"
require "jam_ruby/models/backing_track"
require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer"
#require "jam_ruby/models/max_mind_geo"
@ -132,8 +134,11 @@ require "jam_ruby/models/search"
require "jam_ruby/models/recording"
require "jam_ruby/models/recording_comment"
require "jam_ruby/models/recording_liker"
require "jam_ruby/models/recorded_backing_track"
require "jam_ruby/models/recorded_backing_track_observer"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/recorded_track_observer"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/quick_mix"
require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token"
@ -197,7 +202,6 @@ 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"
require "jam_ruby/models/text_message"
require "jam_ruby/jam_tracks_manager"
require "jam_ruby/models/performance_sample"

View File

@ -11,7 +11,7 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded.
def extension_white_list
%w(ogg)
%w(ogg wav)
end
def store_dir

View File

@ -311,14 +311,19 @@ SQL
end
end
else
# there are still people in the session
conn.exec("UPDATE active_music_sessions set backing_track_initiator_id = NULL, backing_track_path = NULL where backing_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set metronome_initiator_id = NULL, metronome_active = FALSE where metronome_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
#ensure that there is no active claimed recording if the owner of that recording left the session
conn.exec("UPDATE active_music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
[user_id, previous_music_session_id])
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
[user_id, previous_music_session_id])
end
end

View File

@ -82,6 +82,8 @@ module ValidationMessages
MUST_BE_KNOWN_TIMEZONE = "not valid"
JAM_TRACK_ALREADY_OPEN = 'another jam track already open'
RECORDING_ALREADY_IN_PROGRESS = "recording being made"
METRONOME_ALREADY_OPEN = 'another metronome already open'
BACKING_TRACK_ALREADY_OPEN = 'another audio file already open'
# notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'

View File

@ -1,3 +1,18 @@
# initialize actionmailer
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__)
ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__)
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
case JamRuby::Environment
when 'production'
Recurly.api_key = "7d623daabfc2434fa2a893bb008eb3e6"
Recurly.subdomain = 'jamkazam'
when 'development'
Recurly.api_key = "7d623daabfc2434fa2a893bb008eb3e6"
Recurly.subdomain = 'jamkazam-development'
else
Recurly.api_key = "4631527f203b41848523125b3ae51341"
Recurly.subdomain = 'jamkazam-test'
end
Recurly.default_currency = 'USD'

View File

@ -25,6 +25,9 @@ module JamRuby
Dir.mktmpdir do |tmp_dir|
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ
# use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata
nm = jam_track_track.id + File.extname(jam_track_track.filename)
track_filename = File.join(tmp_dir, nm)

View File

@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions'
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
@ -15,6 +15,9 @@ module JamRuby
belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions
belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id"
belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id"
belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id"
has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id'
has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id'
belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id
@ -27,6 +30,8 @@ module JamRuby
validate :creator_is_musician
validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track
validate :validate_opening_backing_track, :if => :opening_backing_track
validate :validate_opening_metronome, :if => :opening_metronome
after_create :started_session
@ -73,22 +78,52 @@ module JamRuby
if is_jam_track_open?
errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
if is_backing_track_open?
errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
if is_metronome_open?
errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN)
end
end
def validate_opening_jam_track
validate_other_audio(:jam_track)
end
def validate_opening_backing_track
validate_other_audio(:backing_track)
end
def validate_opening_metronome
validate_other_audio(:metronome)
end
def validate_other_audio(error_key)
# validate that there is no metronome already open in this session
if metronome_active_was
errors.add(error_key, ValidationMessages::METRONOME_ALREADY_OPEN)
end
# validate that there is no backing track already open in this session
if backing_track_path_was.present?
errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
# validate that there is no jam track already open in this session
unless jam_track_id_was.nil?
errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
if jam_track_id_was.present?
errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
# validate that there is no recording being made
if is_recording?
errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
end
# validate that there is no recording being played back to the session
if is_playing_recording?
errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
end
end
@ -593,6 +628,14 @@ module JamRuby
!self.jam_track.nil?
end
def is_backing_track_open?
self.backing_track_path.present?
end
def is_metronome_open?
self.metronome_active.present?
end
# is this music session currently recording?
def is_recording?
recordings.where(:duration => nil).count > 0
@ -742,6 +785,35 @@ module JamRuby
self.save
end
# @param backing_track_path is a relative path:
def open_backing_track(user, backing_track_path)
self.backing_track_path = backing_track_path
self.backing_track_initiator = user
self.opening_backing_track = true
self.save
self.opening_backing_track = false
end
def close_backing_track
self.backing_track_path = nil
self.backing_track_initiator = nil
self.save
end
def open_metronome(user)
self.metronome_active = true
self.metronome_initiator = user
self.opening_metronome = true
self.save
self.opening_metronome = false
end
def close_metronome
self.metronome_active = false
self.metronome_initiator = nil
self.save
end
def self.sync(session_history)
music_session = MusicSession.find_by_id(session_history.id)

View File

@ -0,0 +1,19 @@
module JamRuby
class BackingTrack < ActiveRecord::Base
self.table_name = "backing_tracks"
self.primary_key = 'id'
default_scope order('created_at ASC')
belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id'
validates :connection, presence: true
validates :client_track_id, presence: true
validates :filename, presence: true
def user
self.connection.user
end
end
end

View File

@ -18,6 +18,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id
has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
validates :as_musician, :inclusion => {:in => [true, false, nil]}

View File

@ -20,7 +20,6 @@ module JamRuby
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :description, length: {maximum: 1000}
validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/
validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES}
validates :status, inclusion: {in: [nil] + STATUS}
validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE}

View File

@ -138,11 +138,17 @@ module JamRuby
manifest = { "files" => [], "timeline" => [] }
mix_params = []
recording.recorded_tracks.each do |recorded_track|
manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
recording.recorded_backing_tracks.each do |recorded_backing_track|
manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
manifest["output"] = { "codec" => "vorbis" }
manifest["recording_id"] = self.recording.id

View File

@ -0,0 +1,196 @@
module JamRuby
# BackingTrack analog to JamRuby::RecordedTrack
class RecordedBackingTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin
attr_accessor :marking_complete
attr_writer :current_user
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks
validates :filename, :presence => true
validates :client_id, :presence => true # not a connection relation on purpose
validates :backing_track_id, :presence => true # not a track relation on purpose
validates :client_track_id, :presence => true
validates :md5, :presence => true, :if => :upload_starting?
validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely...
validates :user, presence: true
validates :download_count, presence: true
before_destroy :delete_s3_files
validate :validate_fully_uploaded
validate :validate_part_complete
validate :validate_too_many_upload_failures
validate :verify_download_count
def self.create_from_backing_track(backing_track, recording)
recorded_backing_track = self.new
recorded_backing_track.recording = recording
recorded_backing_track.client_id = backing_track.connection.client_id
recorded_backing_track.backing_track_id = backing_track.id
recorded_backing_track.client_track_id = "R" + backing_track.client_track_id # Matches behavior in RecordingManager.cpp#getWavComment
recorded_backing_track.user = backing_track.connection.user
recorded_backing_track.filename = backing_track.filename
recorded_backing_track.next_part_to_upload = 0
recorded_backing_track.file_offset = 0
recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id)
recorded_backing_track.save
recorded_backing_track
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def can_download?(some_user)
claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user }
if claimed_recording
!claimed_recording.discarded
else
false
end
end
def too_many_upload_failures?
upload_failures >= APP_CONFIG.max_track_upload_failures
end
def too_many_downloads?
(self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
end
def upload_starting?
next_part_to_upload_was == 0 && next_part_to_upload == 1
end
def validate_too_many_upload_failures
if upload_failures >= APP_CONFIG.max_track_upload_failures
errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED)
end
end
def validate_fully_uploaded
if marking_complete && fully_uploaded && fully_uploaded_was
errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED)
end
end
def validate_part_complete
# if we see a transition from is_part_uploading from true to false, we validate
if is_part_uploading_was && !is_part_uploading
if next_part_to_upload_was + 1 != next_part_to_upload
errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED)
end
if file_offset > length
errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH)
end
elsif next_part_to_upload_was + 1 == next_part_to_upload
# this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start
if next_part_to_upload_was != 0
# we see that the part number was ticked--but was is_part_upload set to true before this transition?
if !is_part_uploading_was && !is_part_uploading
errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED)
end
end
end
end
def verify_download_count
if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
errors.add(:download_count, "must be less than or equal to 100")
end
end
def upload_start(length, md5)
#self.upload_id set by the observer
self.next_part_to_upload = 1
self.length = length
self.md5 = md5
save
end
# if for some reason the server thinks the client can't carry on with the upload,
# this resets everything to the initial state
def reset_upload
self.upload_failures = self.upload_failures + 1
self.part_failures = 0
self.file_offset = 0
self.next_part_to_upload = 0
self.upload_id = nil
self.md5 = nil
self.length = 0
self.fully_uploaded = false
self.is_part_uploading = false
save :validate => false # skip validation because we need this to always work
end
def upload_next_part(length, md5)
self.marking_complete = true
if next_part_to_upload == 0
upload_start(length, md5)
end
self.is_part_uploading = true
save
end
def upload_sign(content_md5)
s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id)
end
def upload_part_complete(part, offset)
# validated by :validate_part_complete
self.marking_complete = true
self.is_part_uploading = false
self.next_part_to_upload = self.next_part_to_upload + 1
self.file_offset = offset.to_i
self.part_failures = 0
save
end
def upload_complete
# validate from happening twice by :validate_fully_uploaded
self.fully_uploaded = true
self.marking_complete = true
save
end
def increment_part_failures(part_failure_before_error)
self.part_failures = part_failure_before_error + 1
RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'")
end
def stored_filename
# construct a path from s3
RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id)
end
def update_download_count(count=1)
self.download_count = self.download_count + count
self.last_downloaded_at = Time.now
end
def delete_s3_files
s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url])
end
def mark_silent
destroy
# check if we have all the files we need, now that the recorded_backing_track is out of the way
recording.preconditions_for_mix?
end
private
def self.construct_filename(created_at, recording_id, client_track_id)
raise "unknown ID" unless client_track_id
"recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg"
end
end
end

View File

@ -0,0 +1,91 @@
module JamRuby
class RecordedBackingTrackObserver < ActiveRecord::Observer
# if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here
observe JamRuby::RecordedBackingTrack
def before_validation(recorded_backing_tracks)
# if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded
if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading
begin
aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1)
# calling size on a part that does not exist will throw an exception... that's what we want
aws_part.size
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later on 500.
rescue Exception => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue RuntimeError => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
end
end
# if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together
if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded
multipart_success = false
begin
recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id)
multipart_success = true
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later.
rescue Exception => e
#recorded_track.reload
recorded_backing_tracks.reset_upload
recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD)
end
# unlike RecordedTracks, only the person who uploaded can download it, so no need to notify
# tell all users that a download is available, except for the user who just uploaded
# recorded_backing_tracks.recording.users.each do |user|
#Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user
# end
end
end
def after_commit(recorded_backing_track)
end
# here we tick upload failure counts, or revert the state of the model, as needed
def after_rollback(recorded_backing_track)
# if fully uploaded, don't increment failures
if recorded_backing_track.fully_uploaded
return
end
# increment part failures if there is a part currently being uploaded
if recorded_backing_track.is_part_uploading_was
#recorded_track.reload # we don't want anything else that the user set to get applied
recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was)
if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures
# save upload id before we abort this bad boy
upload_id = recorded_backing_track.upload_id
begin
recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id)
rescue => e
puts e.inspect
end
recorded_backing_track.reset_upload
if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures
# do anything?
end
end
end
end
def before_save(recorded_backing_track)
# if we are on the 1st part, then we need to make sure we can save the upload_id
if recorded_backing_track.next_part_to_upload == 1
recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url])
end
end
end
end

View File

@ -11,6 +11,7 @@ module JamRuby
has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy
has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
@ -179,6 +180,14 @@ module JamRuby
recorded_tracks.where(:user_id => user.id)
end
def recorded_backing_tracks_for_user(user)
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
end
recorded_backing_tracks.where(:user_id => user.id)
end
def has_access?(user)
users.exists?(user)
end
@ -209,6 +218,10 @@ module JamRuby
connection.video_sources.each do |video|
recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording)
end
connection.backing_tracks.each do |backing_track|
recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording)
end
end
end
end
@ -321,8 +334,7 @@ module JamRuby
}
)
end
latest_recorded_track = downloads[-1][:next] if downloads.length > 0
latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0
Mix.joins(:recording).joins(:recording => :claimed_recordings)
.order('mixes.id')
@ -345,16 +357,31 @@ module JamRuby
}
)
end
latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0
latest_mix = downloads[-1][:next] if downloads.length > 0
if !latest_mix.nil? && !latest_recorded_track.nil?
next_date = [latest_mix, latest_recorded_track].max
elsif latest_mix.nil?
next_date = latest_recorded_track
else
next_date = latest_mix
RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings)
.order('recorded_backing_tracks.id')
.where('recorded_backing_tracks.fully_uploaded = TRUE')
.where('recorded_backing_tracks.id > ?', since)
.where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back
.where('all_discarded = false')
.where('deleted = false')
.where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track|
downloads.push(
{
:type => "recorded_backing_track",
:id => recorded_backing_track.client_track_id,
:recording_id => recorded_backing_track.recording_id,
:length => recorded_backing_track.length,
:md5 => recorded_backing_track.md5,
:url => recorded_backing_track[:url],
:next => recorded_backing_track.id
}
)
end
latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0
next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max
if next_date.nil?
next_date = since # echo back to the client the same value they passed in, if there are no results
@ -417,6 +444,20 @@ module JamRuby
Arel::Nodes::As.new('stream_mix', Arel.sql('item_type'))
]).reorder("")
# Select fields for quick mix. Note that it must include
# the same number of fields as the track or video in order for
# the union to work:
backing_track_arel = RecordedBackingTrack.select([
:id,
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
Arel::Nodes::As.new('backing_track', Arel.sql('item_type'))
]).reorder("")
# Glue them together:
union = track_arel.union(vid_arel)
@ -439,7 +480,25 @@ module JamRuby
])
# And repeat:
union_all = arel.union(quick_mix_arel)
union_quick = arel.union(quick_mix_arel)
utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick)
arel = arel.from(utable_quick)
arel = arel.except(:select)
arel = arel.select([
"recorded_items_quick.id",
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
# And repeat for backing track:
union_all = arel.union(backing_track_arel)
utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all)
arel = arel.from(utable_all)
@ -455,7 +514,6 @@ module JamRuby
:item_type
])
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
@ -492,6 +550,13 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
elsif recorded_item.item_type == 'backing_track'
uploads << ({
:type => "recorded_backing_track",
:recording_id => recorded_item.recording_id,
:client_track_id => recorded_item.client_track_id,
:next => recorded_item.id
})
else
end
@ -513,6 +578,11 @@ module JamRuby
recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
recorded_backing_tracks.each do |recorded_backing_track|
return false unless recorded_backing_track.fully_uploaded
end
true
end

View File

@ -55,11 +55,64 @@ module JamRuby
return query
end
def self.diff_track(track_class, existing_tracks, new_tracks, &blk)
result = []
if new_tracks.length == 0
existing_tracks.delete_all
else
# we will prune from this as we find matching tracks
to_delete = Set.new(existing_tracks)
to_add = Array.new(new_tracks)
existing_tracks.each do |existing_track|
new_tracks.each do |new_track|
if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id
to_delete.delete(existing_track)
to_add.delete(new_track)
blk.call(existing_track, new_track)
result.push(existing_track)
if existing_track.save
next
else
result = existing_track
raise ActiveRecord::Rollback
end
end
end
end
to_add.each do |new_track|
existing_track = track_class.new
blk.call(existing_track, new_track)
if existing_track.save
result.push(existing_track)
else
result = existing_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
end
result
end
# this is a bit different from a normal track synchronization in that the client just sends up all tracks,
# ... some may already exist
def self.sync(clientId, tracks)
result = []
def self.sync(clientId, tracks, backing_tracks = [])
result = {}
backing_tracks = [] unless backing_tracks
Track.transaction do
connection = Connection.find_by_client_id!(clientId)
@ -68,67 +121,28 @@ module JamRuby
msh = MusicSessionUserHistory.find_by_client_id!(clientId)
instruments = []
if tracks.length == 0
connection.tracks.delete_all
else
connection_tracks = connection.tracks
tracks.each do |track|
instruments << track[:instrument_id]
end
# we will prune from this as we find matching tracks
to_delete = Set.new(connection_tracks)
to_add = Array.new(tracks)
result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.instrument_id = track_info[:instrument_id]
track_record.sound = track_info[:sound]
end
tracks.each do |track|
instruments << track[:instrument_id]
end
result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.filename = track_info[:filename]
end
connection_tracks.each do |connection_track|
tracks.each do |track|
if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id
to_delete.delete(connection_track)
to_add.delete(track)
# don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
result.push(connection_track)
if connection_track.save
next
else
result = connection_track
raise ActiveRecord::Rollback
end
end
end
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
to_add.each do |track|
connection_track = Track.new
connection_track.connection = connection
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
if connection_track.save
result.push(connection_track)
else
result = connection_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
end

View File

@ -122,6 +122,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 :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user
has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user
# invited users
@ -349,7 +350,7 @@ module JamRuby
def age
now = Time.now.utc.to_date
self.birth_date.nil? ? "unspecified" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0)
self.birth_date.nil? ? "" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0)
end
def session_count

View File

@ -4,6 +4,7 @@ module JamRuby
belongs_to :recorded_track
belongs_to :mix
belongs_to :quick_mix
belongs_to :recorded_backing_track
def self.show(id, user_id)
self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first
@ -22,7 +23,7 @@ module JamRuby
raise 'no user id specified' if user_id.blank?
query = UserSync
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[])
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[])
.joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id")
.where(user_id: user_id)
.where(%Q{

View File

@ -8,9 +8,11 @@ module JamRuby
options = account_hash(current_user, billing_info)
account = nil
begin
#puts "Recurly.api_key: #{Recurly.api_key}"
account = Recurly::Account.create(options)
raise RecurlyClientError.new(account.errors) if account.errors.any?
rescue Recurly::Error, NoMethodError => x
puts "Error: #{x} : #{Kernel.caller}"
raise RecurlyClientError, x.to_s
else
if account
@ -68,6 +70,46 @@ module JamRuby
account
end
def refund_user_subscription(current_user, jam_track)
jam_track_right=JamRuby::JamTrackRight.where("user_id=? AND jam_track_id=?", current_user.id, jam_track.id).first
if jam_track_right
refund_subscription(jam_track_right)
else
raise RecurlyClientError, "The user #{current_user} does not have a subscription to #{jam_track}"
end
end
def refund_subscription(jam_track_right)
account = get_account(jam_track_right.user)
if (account.present?)
terminated = false
begin
jam_track = jam_track_right.jam_track
account.subscriptions.find_each do |subscription|
puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}"
if(subscription.plan.plan_code == jam_track.plan_code)
subscription.terminate(:full)
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
terminated = true
end
end
if terminated
jam_track_right.destroy()
else
raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund."
end
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
else
raise RecurlyClientError, "Could not find account to refund order."
end
account
end
def place_order(current_user, jam_track)
account = get_account(current_user)
if (account.present?)

View File

@ -232,6 +232,11 @@ FactoryGirl.define do
sequence(:client_resource_id) { |n| "resource_id#{n}"}
end
factory :backing_track, :class => JamRuby::BackingTrack do
sequence(:client_track_id) { |n| "client_track_id#{n}"}
filename 'foo.mp3'
end
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
@ -250,6 +255,20 @@ FactoryGirl.define do
association :recording, factory: :recording
end
factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do
sequence(:client_id) { |n| "client_id-#{n}"}
sequence(:backing_track_id) { |n| "track_id-#{n}"}
sequence(:client_track_id) { |n| "client_track_id-#{n}"}
sequence(:filename) { |n| "filename-{#n}"}
sequence(:url) { |n| "/recordings/blah/#{n}"}
md5 'abc'
length 1
fully_uploaded true
association :user, factory: :user
association :recording, factory: :recording
end
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}
fully_uploaded true
@ -700,7 +719,6 @@ FactoryGirl.define do
factory :jam_track, :class => JamRuby::JamTrack do
sequence(:name) { |n| "jam-track-#{n}" }
sequence(:description) { |n| "description-#{n}" }
bpm 100.1
time_signature '4/4'
status 'Production'
recording_type 'Cover'
@ -716,6 +734,7 @@ FactoryGirl.define do
licensor_royalty_amount 0.999
pro_royalty_amount 0.999
available true
plan_code 'jamtrack-acdc-backinblack'
genre JamRuby::Genre.first
association :licensor, factory: :jam_track_licensor

View File

@ -745,6 +745,29 @@ describe ActiveMusicSession do
@music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when backing track is open" do
# open the backing track
@backing_track = "foo.mp3"
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when metronome is open" do
# open the metronome
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
end
end
@ -830,5 +853,143 @@ describe ActiveMusicSession do
music_sessions[0].connections[0].tracks.should have(1).items
end
end
describe "open_backing_track" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
@backing_track = "foo/bar.mp3"
end
it "allow a backing track to be associated" do
# simple success case; just open the backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should == @backing_track
@music_session.backing_track_initiator.should == @user1
end
it "allow a backing track to be closed" do
# simple success case; close an opened backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.close_backing_track
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should be_nil
@music_session.backing_track_initiator.should be_nil
end
it "disallow a backing track to be opened when another is already opened" do
# if a backing track is open, don't allow another to be opened
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a backing track to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a backing track to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
describe "open_metronome" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
end
it "allow a metronome to be activated" do
# simple success case; just open the metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should == true
@music_session.metronome_initiator.should == @user1
end
it "allow a metronome to be closed" do
# simple success case; close an opened metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.close_metronome
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should be_false
@music_session.metronome_initiator.should be_nil
end
it "disallow a metronome to be opened when another is already opened" do
# if a metronome is open, don't allow another to be opened
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
it "disallow a metronome to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a metronome to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
end

View File

@ -55,25 +55,6 @@ describe JamTrack do
end
describe "validations" do
describe "bpm" do
it "1" do
FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true
end
it "100" do
FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true
end
it "100.1" do
FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true
end
it "100.12" do
jam_track = FactoryGirl.build(:jam_track, bpm: 100.12)
jam_track.valid?.should be_false
jam_track.errors[:bpm].should == ['is invalid']
end
end
describe "price" do

View File

@ -0,0 +1,228 @@
require 'spec_helper'
require 'rest-client'
describe RecordedBackingTrack do
include UsesTempFiles
before do
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
end
it "should copy from a regular track properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.user.id.should == @backing_track.connection.user.id
@recorded_backing_track.filename.should == @backing_track.filename
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.fully_uploaded.should == false
@recorded_backing_track.client_id = @connection.client_id
@recorded_backing_track.backing_track_id = @backing_track.id
end
it "should update the next part to upload properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)"
@recorded_backing_track.errors[:md5][0].should == "can't be blank"
end
it "properly finds a recorded track given its upload filename" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.save.should be_true
RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track
end
it "gets a url for the track" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg"
end
it "signs url" do
stub_const("APP_CONFIG", app_config)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.sign_url.should_not be_nil
end
it "can not be downloaded if no claimed recording" do
user2 = FactoryGirl.create(:user)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.can_download?(user2).should be_false
@recorded_backing_track.can_download?(@user).should be_false
end
it "can be downloaded if there is a claimed recording" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false
@recorded_backing_track.can_download?(@user).should be_true
end
describe "aws-based operations", :aws => true do
def put_file_to_aws(signed_data, contents)
begin
RestClient.put( signed_data[:url],
contents,
{
:'Content-Type' => 'audio/ogg',
:Date => signed_data[:datetime],
:'Content-MD5' => signed_data[:md5],
:Authorization => signed_data[:authorization]
})
rescue => e
puts e.response
raise e
end
end
# create a test file
upload_file='some_file.ogg'
in_directory_with_file(upload_file)
upload_file_contents="ogg binary stuff in here"
md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp
test_config = app_config
s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
before do
stub_const("APP_CONFIG", app_config)
# this block of code will fully upload a sample file to s3
content_for_file(upload_file_contents)
s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging
end
it "cant mark a part complete without having started it" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED
end
it "no parts" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_next_part(1000, "abc")
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS
end
it "enough part failures reset the upload" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = !part_failure_rollover
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == 1
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
end
it "enough upload failures fails the upload forever" do
APP_CONFIG.stub(:max_track_upload_failures).and_return(1)
APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
APP_CONFIG.max_track_upload_failures.times do |j|
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = part_failure_rollover ? false : true
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.upload_failures.should == j + 1
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
# try to poke it and get the right kind of error back
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED]
end
describe "correctly uploaded a file" do
before do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
signed_data = @recorded_backing_track.upload_sign(md5)
@response = put_file_to_aws(signed_data, upload_file_contents)
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.marking_complete = false
end
it "can download an updated file" do
@response = RestClient.get @recorded_backing_track.sign_url
@response.body.should == upload_file_contents
end
it "can't mark completely uploaded twice" do
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for a next part if fully uploaded" do
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for mark part complete if fully uploaded" do
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
end
end
end

View File

@ -211,6 +211,20 @@ describe Recording do
user1_recorded_tracks[0].discard = true
user1_recorded_tracks[0].save!
end
it "should allow finding of backing tracks" do
user2 = FactoryGirl.create(:user)
connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session)
track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument)
backing_track = FactoryGirl.create(:backing_track, :connection => connection2)
@recording = Recording.start(@music_session, @user)
@recording.recorded_backing_tracks_for_user(@user).length.should eq(0)
user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2)
user2_recorded_tracks.length.should == 1
user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0]
end
it "should set up the recording properly when recording is started with 1 user in the session" do
@music_session.is_recording?.should be_false
@ -547,6 +561,8 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
# We should have 2 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(3).items

View File

@ -7,8 +7,10 @@ describe Track do
let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) }
let (:track) { FactoryGirl.create(:track, :connection => connection)}
let (:track2) { FactoryGirl.create(:track, :connection => connection)}
let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)}
let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) }
let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} }
before(:each) do
msuh.touch
@ -16,7 +18,8 @@ describe Track do
describe "sync" do
it "create one track" do
tracks = Track.sync(connection.client_id, [track_hash])
result = Track.sync(connection.client_id, [track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -25,7 +28,8 @@ describe Track do
end
it "create two tracks" do
tracks = Track.sync(connection.client_id, [track_hash, track_hash])
result = Track.sync(connection.client_id, [track_hash, track_hash])
tracks = result[:tracks]
tracks.length.should == 2
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -40,7 +44,8 @@ describe Track do
it "delete only track" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [])
result = Track.sync(connection.client_id, [])
tracks = result[:tracks]
tracks.length.should == 0
end
@ -49,7 +54,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -62,7 +68,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -75,7 +82,8 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -87,7 +95,8 @@ describe Track do
it "updates a single track using .client_track_id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -99,11 +108,69 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
end
describe "backing tracks" do
it "create one track and one backing track" do
result = Track.sync(connection.client_id, [track_hash], [backing_track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
track = backing_tracks[0]
track.client_track_id.should == backing_track_hash[:client_track_id]
end
it "delete only backing_track" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
connection.backing_tracks.length.should == 1
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 0
end
it "does not touch updated_at when nothing changes" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
set_updated_at(backing_track, 1.days.ago)
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
found = backing_tracks[0]
expect(found.id).to eq backing_track.id
expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i
end
end
end
end

View File

@ -676,7 +676,7 @@ describe User do
user.age.should == 9
user.birth_date = nil
user.age.should == "unspecified"
user.age.should == ""
end
end

View File

@ -20,6 +20,49 @@ describe UserSync do
data[:next].should be_nil
end
describe "backing_tracks" do
let!(:recording1) {
recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false)
recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.save!
recording.reload
recording
}
let(:sorted_tracks) {
Array.new(recording1.recorded_tracks).sort! {|a, b|
if a.created_at == b.created_at
a.id <=> b.id
else
a.created_at <=> b.created_at
end
}
}
# backing tracks should only list download, or upload, for the person who opened it, for legal reasons
it "lists backing track for opener" do
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(3)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0]
end
it "does not list backing track for non-opener" do
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(2)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
end
it "one mix and quick mix" do
mix = FactoryGirl.create(:mix)
mix.recording.duration = 1

View File

@ -1,5 +1,5 @@
require 'spec_helper'
require "recurly_client"
require "jam_ruby/recurly_client"
describe RecurlyClient do
let(:jamtrack) { FactoryGirl.create(:jam_track) }
#let(:client) { RecurlyClient.new }
@ -98,6 +98,24 @@ describe RecurlyClient do
@user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id)
end
it "can refund subscription" do
@client.find_or_create_account(@user, @billing_info)
# Place order:
expect{@client.place_order(@user, @jamtrack)}.not_to raise_error()
active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'}
@jamtrack.reload
@jamtrack.jam_track_rights.should have(1).items
# Refund:
expect{@client.refund_user_subscription(@user, @jamtrack)}.not_to raise_error()
active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'}
active_subs.should have(0).items
@jamtrack.reload
@jamtrack.jam_track_rights.should have(0).items
end
it "detects error on double order" do
@client.find_or_create_account(@user, @billing_info)
expect{@client.place_order(@user, @jamtrack)}.not_to raise_error()

View File

@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance
ActiveRecord::Base.add_observer UserObserver.instance
ActiveRecord::Base.add_observer FeedbackObserver.instance
ActiveRecord::Base.add_observer RecordedTrackObserver.instance
ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance
ActiveRecord::Base.add_observer QuickMixObserver.instance
#RecordedTrack.observers.disable :all # only a few tests want this observer active

View File

@ -75,7 +75,7 @@ gem 'netaddr'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
gem 'rest_client'
gem 'rest-client'
gem 'iso-639'
gem 'language_list'
gem 'rubyzip'

View File

@ -1,6 +1,3 @@
TODO:
====
Jasmine Javascript Unit Tests
=============================
@ -11,5 +8,3 @@ $ bundle
$ rake jasmine
Open browser to localhost:8888

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -7,6 +7,7 @@
var rest = context.JK.Rest();
var showing = false;
var perPage = 10;
var openingRecording = false;
function tbody() {
return $('#local-recordings-dialog table.local-recordings tbody');
@ -22,6 +23,7 @@
function beforeShow() {
openingRecording = false;
emptyList();
resetPagination();
showing = true;
@ -89,6 +91,12 @@
function registerStaticEvents() {
$('#local-recordings-dialog table.local-recordings tbody').on('click', 'tr', function(e) {
if(openingRecording) {
// prevent double-click spam
logger.debug("localRecordingDialog: ignoring duplicate open attempt")
return false;
}
var localState = $(this).attr('data-local-state');
if(localState == 'MISSING') {
@ -109,9 +117,15 @@
{
var claimedRecording = $(this).data('server-model');
openingRecording = true;
// tell the server we are about to start a recording
rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id})
.done(function(response) {
// update session info
context.JK.CurrentSessionModel.updateSession(response);
var recordingId = $(this).attr('data-recording-id');
var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording);
@ -142,6 +156,9 @@
app.notifyServerError(jqXHR, "Unable to Open Recording For Playback");
})
.always(function() {
openingRecording = false;
})
}

View File

@ -0,0 +1,147 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.OpenBackingTrackDialog = function(app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var showing = false;
var perPage = 10;
var $dialog = null;
var $tbody = null;
var $paginatorHolder = null;
var $templateOpenBackingTrackRow = null;
var $downloadedTrackHelp = null;
var $whatAreBackingTracks = null;
var $displayAudioFileFolder = null;
function emptyList() {
$tbody.empty();
}
function resetPagination() {
$dialog.find('.paginator').remove();
}
function beforeShow() {
emptyList();
resetPagination();
showing = true;
getBackingTracks();
$dialog.data('result', null);
// .done(function(data, textStatus, jqXHR) {
// // initialize pagination
// var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected)
// $paginatorHolder.append($paginator);
// });
}
function afterHide() {
showing = false;
}
function onPageSelected(targetPage) {
return getBackingTracks(targetPage);
}
function getBackingTracks(page) {
var result = context.jamClient.getBackingTrackList();
console.log("result", result)
var backingTracks = result.backing_tracks;
if (!backingTracks || backingTracks.length == 0) {
$tbody.append("<tr><td colspan='100%'>No Tracks found</td></tr>");
} else {
$.each(backingTracks, function(index, backingTrack) {
var extension = backingTrack.name
var options = {
backingTrackState: null,
name: backingTrack.name,
type: getExtension(backingTrack.name),
length: displaySize(backingTrack.size)
}
var $tr = $(context._.template($templateOpenBackingTrackRow.html(), options, { variable: 'data' }));
$tr.data('server-model', backingTrack);
$tbody.append($tr);
});
}//end
}
// from http://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript
function getExtension(filename) {
return filename.substr((~-filename.lastIndexOf(".") >>> 0) + 2)
}
// from seth:
function displaySize(length) {
var size = (length==null || typeof(length)=='undefined') ? 0 : Number(length)
return (Math.round(size * 10 / (1024 * 1024) ) / 10).toString() + "M"
}
function registerStaticEvents() {
$tbody.on('click', 'tr', function(e) {
var backingTrack = $(this).data('server-model');
// tell the server we are about to open a backing track:
rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name})
.done(function(response) {
var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false);
console.log("BackingTrackPlay response: %o", result);
// TODO: Possibly actually check the result. Investigate
// what real client returns:
// // if(result) {
// let callers see which backing track was chosen
$dialog.data('result', backingTrack);
app.layout.closeDialog('open-backing-track-dialog');
// }
// else {
// logger.error("unable to open backing track")
// }
context.JK.CurrentSessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback");
})
return false;
})
context.JK.helpBubble($whatAreBackingTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog})
$whatAreBackingTracks.on('click', false) // no help yet
$displayAudioFileFolder.on('click', function(e) {
e.stopPropagation();
context.jamClient.OpenBackingTracksDirectory();
})
}
function initialize(){
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('open-backing-track-dialog', dialogBindings);
$dialog = $('#open-backing-track-dialog');
$tbody = $dialog.find('table.open-backing-tracks tbody');
$paginatorHolder = $dialog.find('.paginator-holder');
$templateOpenBackingTrackRow = $('#template-backing-track-row')
$whatAreBackingTracks = $dialog.find('.what-are-backingtracks')
$displayAudioFileFolder = $dialog.find('.display-backingtracks-folder')
registerStaticEvents();
};
this.initialize = initialize;
this.isShowing = function isShowing() { return showing; }
}
return this;
})(window,jQuery);

View File

@ -78,7 +78,7 @@
rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id})
.done(function(response) {
context.jamClient.JamTrackStopPlay();
var result = context.jamClient.JamTrackPlay('t');
var result = context.jamClient.JamTrackPlay(jamTrack.id);
logger.debug("JamTrackPlay response: %o", result);

View File

@ -67,7 +67,7 @@
else {
// load recording
var openRecordingResult = context.jamClient.OpenRecording(recording);
var openRecordingResult = context.jamClient.PreviewRecording(recording);
logger.debug("OpenRecording response: %o", openRecordingResult);
@ -78,6 +78,23 @@
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
// hunt for missing backing tracks; if so, mark them as silent
context._.each(openRecordingResult.backing_tracks, function(backingTrack) {
if(backingTrack.local_state == "MISSING") {
// mark this as deleted
logger.debug("marking recorded track as deleted")
rest.markRecordedBackingTrackSilent({recording_id: openRecordingResult.recording_id, backing_track_id: backingTrack.client_track_id})
.fail(function() {
app.notify({
"title": "Unable to Mark Backing Track",
"text": "A backing track was never played, but we could not tell the server to remove it from the recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}
})
}
playbackControls.startMonitor();
}
@ -88,7 +105,7 @@
function afterHide() {
recording = null;
playbackControls.stopMonitor();
context.jamClient.CloseRecording();
context.jamClient.ClosePreviewRecording();
}
function discardRecording(e) {

View File

@ -7,6 +7,7 @@
//= require backend_alerts
//= require stun
//= require influxdb-latest
//= require jam_track_utils
(function (context, $) {
@ -17,6 +18,7 @@
var ALERT_NAMES = context.JK.ALERT_NAMES;
var logger = context.JK.logger;
var stun = null;
var rest = context.JK.Rest();
$(document).on('JAMKAZAM_CONSTRUCTED', function(e, data) {
@ -51,6 +53,8 @@
operationalEvents(app);
handleGettingStarted(app);
initShoppingCart(app);
});
function watchPreferencesEvent(app) {
@ -207,4 +211,14 @@
}
}
function initShoppingCart(app) {
var user = app.user()
if(user) {
user.done(function(userProfile) {
context.JK.JamTrackUtils.checkShoppingCart();
})
}
}
})(window, jQuery);

View File

@ -28,6 +28,15 @@
return false;
}
if($fader.data('showHelpAboutMediaMixers')) {
if(window.JK.CurrentSessionModel) {
if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) {
window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')})
window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp()
}
}
}
draggingOrientation = $fader.attr('orientation');
var offset = $fader.offset();
var position = { top: e.pageY - offset.top, left: e.pageX - offset.left}
@ -137,6 +146,16 @@
}
function onFaderDragStop(e, ui) {
if($draggingFader.data('showHelpAboutMediaMixers')) {
if(window.JK.CurrentSessionModel) {
if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) {
window.JK.prodBubble($draggingFader, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $draggingFader.closest('.screen')})
window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp()
}
}
}
var faderPct = faderValue($draggingFader, e, ui.position);
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
@ -179,7 +198,10 @@
selector.html(g._.template(templateSource, options));
selector.find('div[control="fader"]').data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener'))
selector.find('div[control="fader"]')
.data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
selector.find('div[control="fader-handle"]').draggable({
drag: onFaderDrag,
@ -187,7 +209,9 @@
stop: onFaderDragStop,
containment: "parent",
axis: options.faderType === 'horizontal' ? 'x' : 'y'
}).data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener'))
}).data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
// Embed any custom styles, applied to the .fader below selector
if ("style" in options) {

View File

@ -21,6 +21,12 @@
var frameSize = 2.5;
var fakeJamClientRecordings = null;
var p2pCallbacks = null;
var metronomeActive=false;
var metronomeBPM=false;
var metronomeSound=false;
var metronomeMeter=0;
var backingTrackPath="";
var backingTrackLoop=false;
function dbg(msg) { logger.debug('FakeJamClient: ' + msg); }
@ -398,21 +404,42 @@
}
function SessionGetControlState(mixerIds, isMasterOrPersonal) {
dbg("SessionGetControlState");
var groups = [0, 1, 2, 3, 7, 9];
var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12];
var names = [
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"",
""
"",
"",
"",
"",
""
];
var media_types = [
"Master",
"Monitor",
"AudioInputMusic",
"AudioInputChat",
"StreamOutMusic",
"UserMusicInput",
"PeerAudioInputMusic",
"PeerMediaTrack",
"JamTrack",
"MetronomeTrack"
]
var clientIds = [
"",
"",
"",
"",
"3933ebec-913b-43ab-a4d3-f21dc5f8955b",
"",
"",
"",
"",
""
];
var response = [];
@ -422,6 +449,7 @@
group_id: groups[i],
id: mixerIds[i] + (isMasterOrPersonal ? 'm' : 'p'),
master: isMasterOrPersonal,
media_type: media_types[i],
monitor: !isMasterOrPersonal,
mute: false,
name: names[i],
@ -686,6 +714,55 @@
function GetScoreWorkTimingInterval() { return {interval: 1000, backoff:60000} }
function SetScoreWorkTimingInterval(knobs) {return true;}
function SessionOpenBackingTrackFile(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionSetBackingTrackFileLoop(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionCloseBackingTrackFile(path) {
backingTrackPath=""
}
function SessionOpenMetronome(bpm, click, meter, mode){
console.log("Setting metronome BPM: ", bpm)
metronomeActive =true
metronomeBPM = bpm
metronomeSound = click
metronomeMeter = meter
}
//change setting - click. Mode 0: = mono, 1, = left ear, 2= right ear
function SessionSetMetronome(bpm,click,meter, mode){
SessionOpenMetronome(bpm, click, meter, mode)
}
//close everywhere
function SessionCloseMetronome(){
metronomeActive=false
}
function setMetronomeOpenCallback(callback) {
}
function getMyNetworkState() {
return {
ntp_stable: Math.random() > 0.5
}
}
function getPeerState(clientId) {
return {
ntp_stable: Math.random() > 0.5
}
}
// stun
function NetworkTestResult() { return {remote_udp_blocked: false} }
@ -717,6 +794,14 @@
fire();
}
function getBackingTrackList() {
return {backing_tracks: [
{name:"This is a really long name for a song dude.mp3", size:4283},
{name:"foo.mp3",size:325783838}
]};
}
function ClientUpdateStartUpdate(path, successCallback, failureCallback) {}
// -------------------------------
@ -796,7 +881,11 @@
function OpenRecording(claimedRecording) {
return {success: true}
}
function PreviewRecording(claimedRecording) {
return OpenRecording(claimedRecording);
}
function CloseRecording() {}
function ClosePreviewRecording() {CloseRecording();}
function OnDownloadAvailable() {}
function SaveToClipboard(text) {}
function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ return false; }
@ -977,6 +1066,20 @@
this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval;
this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval;
// Backing tracks:
this.getBackingTrackList = getBackingTrackList;
this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile;
this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile;
this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop;
// Metronome:
this.SessionCloseMetronome = SessionCloseMetronome;
this.SessionOpenMetronome = SessionOpenMetronome;
this.SessionSetMetronome = SessionSetMetronome;
this.setMetronomeOpenCallback = setMetronomeOpenCallback;
this.getMyNetworkState = getMyNetworkState;
this.getPeerState = getPeerState;
// Client Update
this.IsAppInWritableVolume = IsAppInWritableVolume;
this.ClientUpdateVersion = ClientUpdateVersion;
@ -1000,6 +1103,8 @@
this.GetLocalRecordingState = GetLocalRecordingState;
this.OpenRecording = OpenRecording;
this.CloseRecording = CloseRecording;
this.PreviewRecording = PreviewRecording;
this.ClosePreviewRecording = ClosePreviewRecording;
this.OnDownloadAvailable = OnDownloadAvailable;
// Clipboard

View File

@ -1057,6 +1057,18 @@
})
}
function markRecordedBackingTrackSilent(options) {
var recordingId = options["recording_id"];
var trackId = options["backing_track_id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
data: {},
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + '/silent'
});
}
function getRecordedTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
@ -1069,6 +1081,18 @@
});
}
function getRecordedBackingTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId
});
}
function getRecording(options) {
var recordingId = options["id"];
@ -1171,6 +1195,32 @@
})
}
function openBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/open",
data: JSON.stringify(options)
})
}
function closeBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/close",
data: JSON.stringify(options)
})
}
function openJamTrack(options) {
var musicSessionId = options["id"];
var jamTrackId = options["jam_track_id"];
@ -1199,6 +1249,32 @@
})
}
function openMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/open",
data: JSON.stringify(options)
})
}
function closeMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/close",
data: JSON.stringify(options)
})
}
function discardRecording(options) {
var recordingId = options["id"];
@ -1381,6 +1457,15 @@
});
}
function getBackingTracks(options) {
return $.ajax({
type: "GET",
url: '/api/backing_tracks?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function addJamtrackToShoppingCart(options) {
return $.ajax({
type: "POST",
@ -1515,6 +1600,7 @@
this.cancelSession = cancelSession;
this.updateScheduledSession = updateScheduledSession;
this.getUserDetail = getUserDetail;
this.getUserProfile = getUserProfile;
this.getCities = getCities;
this.getRegions = getRegions;
this.getCountries = getCountries;
@ -1577,6 +1663,7 @@
this.stopRecording = stopRecording;
this.getRecording = getRecording;
this.getRecordedTrack = getRecordedTrack;
this.getRecordedBackingTrack = getRecordedBackingTrack;
this.getClaimedRecordings = getClaimedRecordings;
this.getClaimedRecording = getClaimedRecording;
this.updateClaimedRecording = updateClaimedRecording;
@ -1585,8 +1672,13 @@
this.claimRecording = claimRecording;
this.startPlayClaimedRecording = startPlayClaimedRecording;
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
this.openJamTrack = openJamTrack;
this.openJamTrack = openJamTrack
this.openBackingTrack = openBackingTrack
this.closeBackingTrack = closeBackingTrack
this.closeMetronome = closeMetronome;
this.closeJamTrack = closeJamTrack;
this.openMetronome = openMetronome;
this.closeMetronome = closeMetronome;
this.discardRecording = discardRecording;
this.putTrackSyncChange = putTrackSyncChange;
this.createBand = createBand;
@ -1617,6 +1709,7 @@
this.updateAudioLatency = updateAudioLatency;
this.getJamtracks = getJamtracks;
this.getPurchasedJamTracks = getPurchasedJamTracks;
this.getBackingTracks = getBackingTracks;
this.addJamtrackToShoppingCart = addJamtrackToShoppingCart;
this.getShoppingCarts = getShoppingCarts;
this.removeShoppingCart = removeShoppingCart;
@ -1631,6 +1724,7 @@
this.getMount = getMount;
this.createSourceChange = createSourceChange;
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
return this;
};

View File

@ -0,0 +1,29 @@
$ = jQuery
context = window
context.JK ||= {};
class JamTrackUtils
constructor: () ->
@logger = context.JK.logger
@rest = new context.JK.Rest();
init: () =>
# check if the shopping cart should be shown
checkShoppingCart: () =>
@rest.getShoppingCarts().done(this.displayCartIcon)
displayCartIcon: (carts) =>
cartLink = $("a[href='" + "/client#/shoppingCart" + "']")
if carts.length > 0
cartLink.removeClass("hidden")
else
cartLink.addClass("hidden")
# global instance
context.JK.JamTrackUtils = new JamTrackUtils()

View File

@ -11,9 +11,94 @@
var rest = context.JK.Rest();
var decrementedFriendCountOnce = false;
var sentFriendRequest = false;
var profileScreen = null;
var textMessageDialog = null;
var feed = null;
var profileUtils = context.JK.ProfileUtils;
var NOT_SPECIFIED_TEXT = 'Not specified';
var $screen = $('#user-profile');
// musical experience
var $instruments = $screen.find('#instruments');
var $musicianStatus = $screen.find('#musician-status');
var $genres = $screen.find('#genres');
var $concertCount = $screen.find('#concert-count');
var $studioCount = $screen.find('#studio-count');
// performance samples
var $noSamples = $screen.find('no-samples');
// online presence
var $noOnlinePresence = $screen.find('no-online-presence');
// current interests
var $noInterests = $screen.find('#no-interests');
var $paidGigSection = $screen.find('#paid-gigs');
var $paidGigDetails = $screen.find('#paid-gig-details');
var $freeGigSection = $screen.find('#free-gigs');
var $freeGigDetails = $screen.find('#free-gig-details');
var $cowritingSection = $screen.find('#cowriting');
var $cowritingDetails = $screen.find('#cowriting-details');
var $traditionalBandSection = $screen.find("#traditional-band");
var $traditionalBandDetails = $screen.find('#traditional-band-details');
var $virtualBandSection = $screen.find("#virtual-band");
var $virtualBandDetails = $screen.find('#virtual-band-details');
// tabs
var $aboutLink = $screen.find('#about-link');
var $aboutContent = $screen.find('#about-content');
var $historyLink = $screen.find('#history-link');
var $historyContent = $screen.find('#history-content');
var $bandsLink = $screen.find('#bands-link');
var $bandsContent = $screen.find('#bands-content');
var $socialLink = $screen.find('#social-link');
var $socialContent = $screen.find('#social-content');
var $favoritesLink = $screen.find('#favorites-link');
var $favoritesContent = $screen.find('#favorites-content');
// stats
var $friendStats = $screen.find('#friend-stats');
var $followerStats = $screen.find('#follower-stats');
var $sessionStats = $screen.find('#session-stats');
var $recordingStats = $screen.find('#recording-stats');
var $followingStats = $screen.find('#following-stats');
var $favoriteStats = $screen.find('#favorite-stats');
// miscellaneous
var $userName = $screen.find('#username');
var $avatar = $screen.find('#avatar');
var $typeLabel = $screen.find('#type-label');
var $location = $screen.find('#location');
var $age = $screen.find('#age');
// buttons
var $btnEdit = $screen.find('#btn-edit');
var $btnAddFriend = $screen.find('#btn-add-friend');
var $btnFollowUser = $screen.find('#btn-follow-user');
var $btnMessageUser = $screen.find('#btn-message-user');
// social
var $socialLeft = $screen.find('.profile-social-left');
var $socialFriends = $screen.find('#social-friends');
var $socialFollowings = $screen.find('#social-followings');
var $socialFollowers = $screen.find('#social-followers');
var $bioTextArea = $screen.find('.user-biography');
var $showBio = $screen.find('.have-bio');
var $noBio = $screen.find('.no-bio');
var $biographyEditor = $screen.find('.update-biography');
var $submitBiographyButton = $screen.find('#btn-update-user-biography');
var $cancelBiographyButton = $screen.find('#btn-cancel-user-biography');
var $biographyText = $screen.find('#biography');
var instrument_logo_map = context.JK.getInstrumentIconMap24();
@ -44,23 +129,23 @@
}
function resetForm() {
$('#profile-instruments').empty();
$instruments.empty();
$('#profile-about').show();
$('#profile-history').hide();
$('#profile-bands').hide();
$('#profile-social').hide();
$('#profile-favorites').hide();
$aboutContent.show();
$historyContent.hide();
$bandsContent.hide();
$socialContent.hide();
$favoritesContent.hide();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-about-link').addClass('active');
$aboutLink.addClass('active');
}
function initUser() {
user = null;
decrementedFriendCountOnce = false;
sentFriendRequest = false;
userDefer = rest.getUserDetail({id: userId})
userDefer = rest.getUserProfile({id: userId})
.done(function (response) {
user = response;
configureUserType();
@ -89,43 +174,43 @@
function configureUserType() {
if (isMusician()) {
$('#profile-history-link').show();
$('#profile-bands-link').show();
$('#profile-instruments').show();
$('#profile-session-stats').show();
$('#profile-recording-stats').show();
// $('#profile-following-stats').hide();
// $('#profile-favorites-stats').hide();
$('.profile-social-left').show();
$('#profile-type-label').text('musician');
$('#profile-location-label').text('Location');
$historyLink.show();
$bandsLink.show();
$instruments.show();
$sessionStats.show();
$recordingStats.show();
// $followingStats.hide();
// $favoriteStats.hide();
$socialLeft.show();
$typeLabel.text('musician');
$location.text('Location');
}
else {
$('#profile-history-link').hide();
$('#profile-bands-link').hide();
$('#profile-instruments').hide();
$('#profile-session-stats').hide();
$('#profile-recording-stats').hide();
// $('#profile-following-stats').show();
// $('#profile-favorites-stats').show();
$('.profile-social-left').hide();
$('#profile-type-label').text('fan');
$('#profile-location-label').text('Presence');
$historyLink.hide();
$bandsLink.hide();
$instruments.hide();
$sessionStats.hide();
$recordingStats.hide();
// $followingStats.show();
// $favoriteStats.show();
$socialLeft.hide();
$typeLabel.text('fan');
$location.text('Presence');
}
if (isCurrentUser()) {
$('#btn-profile-edit').show();
$('#btn-add-friend').hide();
$('#btn-follow-user').hide();
$('#btn-message-user').hide();
$btnEdit.show();
$btnAddFriend.hide();
$btnFollowUser.hide();
$btnMessageUser.hide();
}
else {
configureFriendFollowersControls();
$('#btn-profile-edit').hide();
$('#btn-add-friend').show();
$('#btn-follow-user').show();
$('#btn-message-user').show();
$btnEdit.hide();
$btnAddFriend.show();
$btnFollowUser.show();
$btnMessageUser.show();
}
}
@ -141,26 +226,30 @@
// events for main screen
function events() {
// wire up panel clicks -- these need to check deferred because they can't be hidden when in an invalid state
$('#profile-about-link').click(function () {
$aboutLink.click(function () {
renderTabDeferred(renderAbout)
});
$('#profile-history-link').click(function () {
$historyLink.click(function () {
renderTabDeferred(renderHistory)
});
$('#profile-bands-link').click(function () {
$bandsLink.click(function () {
renderTabDeferred(renderBands)
});
$('#profile-social-link').click(function () {
$socialLink.click(function () {
renderTabDeferred(renderSocial)
});
$('#profile-favorites-link').click(function () {
$favoritesLink.click(function () {
renderTabDeferred(renderFavorites)
});
// this doesn't need deferred because it's only shown when valid
$('#btn-add-friend').click(handleFriendChange);
$('#btn-follow-user').click(handleFollowingChange);
$('#btn-message-user').click(handleMessageMusician);
$btnAddFriend.click(handleFriendChange);
$btnFollowUser.click(handleFollowingChange);
$btnMessageUser.click(handleMessageMusician);
}
function handleFriendChange(evt) {
@ -221,10 +310,10 @@
function configureFriendButton() {
if (isFriend()) {
$('#btn-add-friend').text('DISCONNECT');
$btnAddFriend.text('DISCONNECT');
}
else {
$('#btn-add-friend').text('CONNECT');
$btnAddFriend.text('CONNECT');
}
}
@ -267,32 +356,32 @@
function configureFollowingButton() {
if (isFollowing()) {
$('#btn-follow-user').text('UNFOLLOW');
$btnFollowUser.text('UNFOLLOW');
}
else {
$('#btn-follow-user').text('FOLLOW');
$btnFollowUser.text('FOLLOW');
}
}
function configureEditProfileButton() {
$('#btn-follow-user').click(addFollowing);
$btnFollowUser.click(addFollowing);
}
// refreshes the currently active tab
function renderActive() {
if ($('#profile-about-link').hasClass('active')) {
if ($aboutLink.hasClass('active')) {
renderAbout();
}
else if ($('#profile-history-link').hasClass('active')) {
else if ($historyLink.hasClass('active')) {
renderHistory();
}
else if ($('#profile-bands-link').hasClass('active')) {
else if ($bandsLink.hasClass('active')) {
renderBands();
}
else if ($('#profile-social-link').hasClass('active')) {
else if ($socialLink.hasClass('active')) {
renderSocial();
}
else if ($('#profile-favorites-link').hasClass('active')) {
else if ($favoritesLink.hasClass('active')) {
renderFavorites();
}
}
@ -310,29 +399,29 @@
/****************** ABOUT TAB *****************/
function renderAbout() {
$('#profile-instruments').empty();
$instruments.empty();
$('#profile-about').show();
$('#profile-history').hide();
$('#profile-bands').hide();
$('#profile-social').hide();
$('#profile-favorites').hide();
$aboutContent.show();
$historyContent.hide();
$bandsContent.hide();
$socialContent.hide();
$favoritesContent.hide();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-about-link').addClass('active');
$aboutLink.addClass('active');
bindAbout();
}
function bindAbout() {
$('#profile-instruments').empty();
$instruments.empty();
// name
$('#profile-username').html(user.name);
$userName.html(user.name);
// avatar
$('#profile-avatar').attr('src', context.JK.resolveAvatarUrl(user.photo_url));
$avatar.attr('src', context.JK.resolveAvatarUrl(user.photo_url));
// instruments
if (user.instruments) {
@ -351,35 +440,152 @@
proficiency_level_css: proficiencyCssMap[proficiency]
});
$('#profile-instruments').append(instrumentHtml);
$instruments.append(instrumentHtml);
}
}
$('#profile-genres').empty();
for (var i=0; i< user.genres.length; i++) {
$('#profile-genres').append(user.genres[i].description + '<br />');
}
// status
var status = user.skill_level;
$musicianStatus.html(status ? profileUtils.skillLevelMap[status] + ' musician' : NOT_SPECIFIED_TEXT)
// genres
$genres.empty();
var profileGenres = profileUtils.profileGenreList(user.genres);
$genres.append(profileGenres);
// concert gigs
var concertGigCount = user.concert_count;
$concertCount.html(concertGigCount > 0 ? 'Has played ' + profileUtils.gigMap[concertGigCount] + ' live concert gigs' : NOT_SPECIFIED_TEXT);
// studio gigs
var studioGigCount = user.studio_session_count;
$studioCount.html(studioGigCount > 0 ? 'Has played ' + profileUtils.gigMap[studioGigCount] + ' studio session gigs' : NOT_SPECIFIED_TEXT);
// location
$('#profile-location').html(user.location);
$location.html(user.location);
$age.html(user.age ? user.age + " years old" : "");
// stats
var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend";
$('#profile-friend-stats').html('<span class="friend-count">' + user.friend_count + '</span>' + text);
$friendStats.html('<span class="friend-count">' + user.friend_count + '</span>' + text);
text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower";
$('#profile-follower-stats').html('<span class="follower-count">' + user.follower_count + '</span>' + text);
$followerStats.html('<span class="follower-count">' + user.follower_count + '</span>' + text);
// text = user.following_count > 1 || user.following_count === 0 ? " Followings" : " Following";
// $('#profile-following-stats').html('<span class="following-count">' + user.following_count + '</span>' + text);
// performance samples
var performanceSamples = user.performance_samples;
if (!performanceSamples || performanceSamples.length === 0) {
$noSamples.show();
}
else {
$noSamples.hide();
}
// online presences
var onlinePresences = user.online_presences;
if ((!onlinePresences || onlinePresences.length === 0) && !user.website) {
$noOnlinePresence.show();
}
else {
$noOnlinePresence.hide();
}
// current interests
var noInterests = !user.paid_sessions && !user.free_sessions && !user.cowriting && !user.virtual_band && !user.traditional_band;
if (noInterests) {
$noInterests.show();
$paidGigSection.hide();
$freeGigSection.hide();
$cowritingSection.hide();
$traditionalBandSection.hide();
$virtualBandSection.hide();
}
else {
// paid sessions
if (user.paid_sessions) {
$paidGigSection.show();
$paidGigDetails.find("ul li:nth-child(1)").append(profileUtils.paidSessionGenreList(user.genres));
var hourlyRate = user.paid_sessions_hourly_rate;
$paidGigDetails.find("ul li:nth-child(2)").append(hourlyRate ? hourlyRate : NOT_SPECIFIED_TEXT);
var dailyRate = user.paid_sessions_daily_rate;
$paidGigDetails.find("ul li:nth-child(3)").append(dailyRate ? dailyRate : NOT_SPECIFIED_TEXT);
}
else {
$paidGigSection.hide();
}
// free sessions
if (user.free_sessions) {
$freeGigSection.show();
$freeGigDetails.find("ul li:nth-child(1)").append(profileUtils.freeSessionGenreList(user.genres));
}
else {
$freeGigSection.hide();
}
// cowriting
if (user.cowriting) {
$cowritingSection.show();
$cowritingDetails.find("ul li:nth-child(1)").append(profileUtils.cowritingGenreList(user.genres));
var purpose = user.cowriting_purpose;
$cowritingDetails.find("ul li:nth-child(2)").append(purpose ? profileUtils.cowritingPurposeMap[purpose] : NOT_SPECIFIED_TEXT);
}
else {
$cowritingSection.hide();
}
// traditional bands
if (user.traditional_band) {
$traditionalBandSection.show();
$traditionalBandDetails.find("ul li:nth-child(1)").append(profileUtils.traditionalBandGenreList(user.genres));
var commitment = user.traditional_band_commitment;
$traditionalBandDetails.find("ul li:nth-child(2)").append(commitment ? profileUtils.bandCommitmentMap[commitment] : NOT_SPECIFIED_TEXT);
var canTour = user.traditional_band_touring;
var canTourResponse = canTour ? "Yes" : (canTour === false ? "No" : NOT_SPECIFIED_TEXT);
$traditionalBandDetails.find("ul li:nth-child(3)").append(canTourResponse);
}
else {
$traditionalBandSection.hide();
}
// virtual band
if (user.virtual_band) {
$virtualBandSection.show();
$virtualBandDetails.find("ul li:nth-child(1)").append(profileUtils.virtualBandGenreList(user.genres));
var commitment = user.virtual_band_commitment;
$virtualBandDetails.find("ul li:nth-child(2)").append(commitment ? profileUtils.bandCommitmentMap[commitment] : NOT_SPECIFIED_TEXT);
}
else {
$virtualBandSection.hide();
}
}
if (isMusician()) {
text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session";
$('#profile-session-stats').html(user.session_count + text);
$sessionStats.html(user.session_count + text);
text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording";
$('#profile-recording-stats').html(user.recording_count + text);
$recordingStats.html(user.recording_count + text);
} else {
text = " Following";
$('#profile-following-stats').html(user.following_count + text);
$followingStats.html(user.following_count + text);
text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite";
$('#profile-favorite-stats').html(user.favorite_count + text);
$favoriteStats.html(user.favorite_count + text);
}
renderBio();
@ -400,12 +606,6 @@
if(user.biography) {
$showBio.show();
if(isCurrentUser()) {
$editBiographyButton.show();
}
else {
$editBiographyButton.hide();
}
$biographyText.text(user.biography).show();
}
else {
@ -415,30 +615,12 @@
}
}
var $bioTextArea = $('.user-biography', profileScreen);
var $showBio = $('.have-bio', profileScreen);
var $noBio = $('.no-bio', profileScreen);
var $biographyEditor = $('.update-biography', profileScreen);
var $addBiographyButton = $('a.enter-bio', profileScreen);
var $editBiographyButton = $('#profile-edit-biography', profileScreen);
var $submitBiographyButton = $('#btn-update-user-biography', profileScreen);
var $cancelBiographyButton = $('#btn-cancel-user-biography', profileScreen);
var $biographyText = $('#profile-biography', profileScreen);
initializeBioVisibility();
$addBiographyButton.unbind('click').click(function() {
$biographyEditor.val(user.biography).show();
return false;
});
$editBiographyButton.unbind('click').click(function() {
$editBiographyButton.hide();
$biographyText.hide();
$bioTextArea.val(user.biography);
$biographyEditor.show();
return false;
})
// $addBiographyButton.unbind('click').click(function() {
// $biographyEditor.val(user.biography).show();
// return false;
// });
$submitBiographyButton.unbind('click').click(function() {
var bio = $bioTextArea.val();
@ -477,18 +659,18 @@
/****************** SOCIAL TAB *****************/
function renderSocial() {
$('#profile-social-friends').empty();
$('#profile-social-followings').empty();
$('#profile-social-followers').empty();
$socialFriends.empty();
$socialFollowings.empty();
$socialFollowers.empty();
$('#profile-about').hide();
$('#profile-history').hide();
$('#profile-bands').hide();
$('#profile-social').show();
$('#profile-favorites').hide();
$aboutContent.hide();
$historyContent.hide();
$bandsContent.hide();
$socialContent.show();
$favoritesContent.hide();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-social-link').addClass('active');
$socialLink.addClass('active');
bindSocial();
}
@ -511,11 +693,11 @@
type: "Friends"
});
$('#profile-social-friends').append(friendHtml);
$socialFriends.append(friendHtml);
});
}
else {
$('#profile-social-friends').html('&nbsp;');
$socialFriends.html('&nbsp;');
}
context.JK.bindHoverEvents();
})
@ -536,11 +718,11 @@
location: val.location
});
$('#profile-social-followings').append(followingHtml);
$socialFollowings.append(followingHtml);
});
}
else {
$('#profile-social-followings').html('&nbsp;');
$socialFollowings.html('&nbsp;');
}
context.JK.bindHoverEvents();
})
@ -559,7 +741,7 @@
location: val.location
});
$('#profile-social-followers').append(followerHtml);
$socialFollowers.append(followerHtml);
});
context.JK.bindHoverEvents();
})
@ -568,14 +750,14 @@
/****************** HISTORY TAB *****************/
function renderHistory() {
$('#profile-about').hide();
$('#profile-history').show();
$('#profile-bands').hide();
$('#profile-social').hide();
$('#profile-favorites').hide();
$aboutContent.hide();
$historyContent.show();
$bandsContent.hide();
$socialContent.hide();
$favoritesContent.hide();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-history-link').addClass('active');
$historyLink.addClass('active');
bindHistory();
}
@ -586,16 +768,16 @@
/****************** BANDS TAB *****************/
function renderBands() {
$('#profile-bands').empty();
$bandsContent.empty();
$('#profile-about').hide();
$('#profile-history').hide();
$('#profile-bands').show();
$('#profile-social').hide();
$('#profile-favorites').hide();
$aboutContent.hide();
$historyContent.hide();
$bandsContent.show();
$socialContent.hide();
$favoritesContent.hide();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-bands-link').addClass('active');
$bandsLink.addClass('active');
bindBands();
}
@ -606,7 +788,7 @@
.done(function (response) {
if ((!response || response.length === 0) && isCurrentUser()) {
var noBandHtml = $('#template-no-bands').html();
$("#profile-bands").html(noBandHtml);
$bandsContent.html(noBandHtml);
}
else {
addMoreBandsLink();
@ -664,7 +846,7 @@
musicians: musicianHtml
});
$('#profile-bands').append(bandHtml);
$bandsContent.append(bandHtml);
$('.profile-band-link-member-true').each(function(idx) {
isBandMember ? $(this).show() : $(this).hide();
@ -689,7 +871,7 @@
function addMoreBandsLink() {
if (isCurrentUser()) {
var moreBandsHtml = $('#template-more-bands').html();
$("#profile-bands").append(moreBandsHtml);
$bandsContent.append(moreBandsHtml);
}
}
@ -716,13 +898,13 @@
}
function updateFollowingCount(value) {
var followingCount = $('#profile-follower-stats span.follower-count');
followingCount.text(value + parseInt(followingCount.text()));
var $followingCount = $('#follower-stats span.follower-count');
$followingCount.text(value + parseInt($followingCount.text()));
}
function updateBandFollowingCount(bandId, value) {
var bandFollowing = $('div[band-id="' + bandId + '"].profile-bands span.follower-count');
bandFollowing.text(value + parseInt(bandFollowing.text()));
var $bandFollowing = $('div[band-id="' + bandId + '"].profile-bands span.follower-count');
$bandFollowing.text(value + parseInt($bandFollowing.text()));
}
function addBandFollowing(evt) {
@ -734,7 +916,6 @@
rest.addFollowing(newFollowing)
.done(function (response) {
logger.debug("following band " + bandId);
updateBandFollowingCount(bandId, 1); // increase counter
configureBandFollowingButton(true, bandId);
context.JK.GA.trackJKSocial(context.JK.GA.Categories.jkFollow, context.JK.GA.JKSocialTargets.band);
@ -743,7 +924,7 @@
}
function configureBandFollowingButton(following, bandId) {
var $btnFollowBand = $('div[band-id=' + bandId + ']', '#profile-bands').find('#btn-follow-band-2');
var $btnFollowBand = $('div[band-id=' + bandId + ']', '#bands-content').find('#btn-follow-band');
$btnFollowBand.unbind("click");
if (following) {
@ -762,14 +943,14 @@
/****************** FAVORITES TAB *****************/
function renderFavorites() {
$('#profile-about').hide();
$('#profile-history').hide();
$('#profile-bands').hide();
$('#profile-social').hide();
$('#profile-favorites').show();
$aboutContent.hide();
$historyContent.hide();
$bandsContent.hide();
$socialContent.hide();
$favoritesContent.show();
$('.profile-nav a.active').removeClass('active');
$('.profile-nav a#profile-favorites-link').addClass('active');
$favoritesLink.addClass('active');
bindFavorites();
}
@ -780,16 +961,16 @@
function initializeFeed() {
var $scroller = profileScreen.find('.content-body-scroller#user-profile-feed-scroller');
var $content = profileScreen.find('.feed-content#user-profile-feed-entry-list');
var $noMoreFeeds = $('#user-profile-end-of-feeds-list');
var $refresh = profileScreen.find('.btn-refresh-entries');
var $sortFeedBy = profileScreen.find('#feed_order_by');
var $includeDate = profileScreen.find('#feed_date');
var $includeType = profileScreen.find('#feed_show');
var $scroller = $screen.find('.content-body-scroller#user-profile-feed-scroller');
var $content = $screen.find('.feed-content#user-profile-feed-entry-list');
var $noMoreFeeds = $screen.find('#user-profile-end-of-feeds-list');
var $refresh = $screen.find('.btn-refresh-entries');
var $sortFeedBy = $screen.find('#feed_order_by');
var $includeDate = $screen.find('#feed_date');
var $includeType = $screen.find('#feed_show');
feed = new context.JK.Feed(app);
feed.initialize(profileScreen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType, {time_range: 'all'});
feed.initialize($screen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType, {time_range: 'all'});
}
function initialize(textMessageDialogInstance) {
@ -800,7 +981,6 @@
'beforeHide' : beforeHide
};
app.bindScreen('profile', screenBindings);
profileScreen = $('#user-profile');
events();
initializeFeed();
}

View File

@ -0,0 +1,173 @@
/**
* Common utility functions.
*/
(function (context, $) {
"use strict";
context.JK = context.JK || {};
var profileUtils = {};
context.JK.ProfileUtils = profileUtils;
// genre types
var PROFILE_GENRE_TYPE = 'profile';
var VIRTUAL_BAND_GENRE_TYPE = 'virtual_band';
var TRADITIONAL_BAND_GENRE_TYPE = 'traditional_band';
var PAID_SESSION_GENRE_TYPE = 'paid_session';
var FREE_SESSION_GENRE_TYPE = 'free_session';
var COWRITING_GENRE_TYPE = 'cowriting';
// performance samples
var JAMKAZAM = 'jamkazam';
var SOUNDCLOUD = 'soundcloud';
var YOUTUBE = 'youtube';
var USER_TYPE = 'JamRuby::User';
profileUtils.skillLevelMap = {
"1": "Amateur",
"2": "Professional"
};
profileUtils.gigMap = {
"0": "zero",
"1": "under 10",
"2": "10 to 50",
"3": "50 to 100",
"4": "over 100"
};
profileUtils.cowritingPurposeMap = {
"1": "just for fun",
"2": "sell music"
};
profileUtils.bandCommitmentMap = {
"1": "infrequent",
"2": "once a week",
"3": "2-3 times a week",
"4": "4+ times a week"
}
function buildGenreList(genres) {
var list = '';
for (var i=0; i < genres.length; i++) {
list = list.concat(genres[i].genre_id);
if (i !== genres.length - 1) {
list = list.concat(', ');
}
}
return list.length > 0 ? list : 'None specified';
}
// profile genres
profileUtils.profileGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === PROFILE_GENRE_TYPE;
});
return matches;
}
profileUtils.profileGenreList = function(genres) {
var matches = profileUtils.profileGenres(genres);
return buildGenreList(matches);
}
// virtual band genres
profileUtils.virtualBandGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === VIRTUAL_BAND_GENRE_TYPE;
});
return matches;
}
profileUtils.virtualBandGenreList = function(genres) {
var matches = profileUtils.virtualBandGenres(genres);
return buildGenreList(matches);
}
// traditional band genres
profileUtils.traditionalBandGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === TRADITIONAL_BAND_GENRE_TYPE;
});
return matches;
}
profileUtils.traditionalBandGenreList = function(genres) {
var matches = profileUtils.traditionalBandGenres(genres);
return buildGenreList(matches);
}
// paid session genres
profileUtils.paidSessionGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === PAID_SESSION_GENRE_TYPE;
});
return matches;
}
profileUtils.paidSessionGenreList = function(genres) {
var matches = profileUtils.paidSessionGenres(genres);
return buildGenreList(matches);
}
// free session genres
profileUtils.freeSessionGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === FREE_SESSION_GENRE_TYPE;
});
return matches;
}
profileUtils.freeSessionGenreList = function(genres) {
var matches = profileUtils.freeSessionGenres(genres);
return buildGenreList(matches);
}
// cowriting genres
profileUtils.cowritingGenres = function(genres) {
var matches = $.grep(genres, function(g) {
return g.player_type === USER_TYPE && g.genre_type === COWRITING_GENRE_TYPE;
});
return matches;
}
profileUtils.cowritingGenreList = function(genres) {
var matches = profileUtils.cowritingGenres(genres);
return buildGenreList(matches);
}
profileUtils.jamkazamSamples = function(samples) {
var matches = $.grep(samples, function(s) {
return s.service_type === JAMKAZAM;
});
return matches;
}
profileUtils.soundCloudSamples = function(samples) {
var matches = $.grep(samples, function(s) {
return s.service_type === SOUNDCLOUD;
});
return matches;
}
profileUtils.youTubeSamples = function(samples) {
var matches = $.grep(samples, function(s) {
return s.service_type === YOUTUBE;
});
return matches;
}
})(window, jQuery);

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,9 @@
var sessionPageEnterTimeout = null;
var startTime = null;
var joinDeferred = null;
var previousBackingTracks = [];
var openBackingTrack = null;
var shownAudioMediaMixerHelp = false;
var mixerMode = MIX_MODES.PERSONAL;
@ -67,7 +70,7 @@
function isPlayingRecording() {
// this is the server's state; there is no guarantee that the local tracks
// requested from the backend will have corresponding track information
return currentSession && currentSession.claimed_recording;
return !!(currentSession && currentSession.claimed_recording);
}
function recordedTracks() {
@ -79,6 +82,28 @@
}
}
function recordedBackingTracks() {
if(currentSession && currentSession.claimed_recording) {
return currentSession.claimed_recording.recording.recorded_backing_tracks
}
else {
return null;
}
}
function backingTracks() {
var backingTracks = []
// this may be wrong if we loosen the idea that only one person can have a backing track open.
// but for now, the 1st person we find with a backing track open is all there is to find...
context._.each(participants(), function(participant) {
if(participant.backing_tracks.length > 0) {
backingTracks = participant.backing_tracks;
return false; // break
}
})
return backingTracks;
}
function jamTracks() {
if(currentSession && currentSession.jam_track) {
return currentSession.jam_track.tracks
@ -88,6 +113,27 @@
}
}
function backingTrack() {
if(currentSession) {
// TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
return {
path: currentSession.backing_track_path
}
}
else {
return null;
}
}
function metronomeActive() {
if(currentSession) {
return currentSession.metronome_active
}
else {
return null;
}
}
function creatorId() {
if(!currentSession) {
throw "creator is not known"
@ -303,6 +349,9 @@
}
currentSessionId = null;
currentParticipants = {}
previousBackingTracks = []
openBackingTrack = null
shownAudioMediaMixerHelp = false
}
// you should only update currentSession with this function
@ -321,6 +370,25 @@
}
}
function updateSession(response) {
updateSessionInfo(response, null, true);
}
function updateSessionInfo(response, callback, force) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
}
/**
* Reload the session data from the REST server, calling
* the provided callback when complete.
@ -344,18 +412,7 @@
type: "GET",
url: url,
success: function(response) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
updateSessionInfo(response, callback, force);
},
error: function(jqXHR) {
if(jqXHR.status != 404) {
@ -532,9 +589,49 @@
return mixerMode == MIX_MODES.PERSONAL;
}
function getMixMode() {
return mixerMode;
}
function getMixMode() {
return mixerMode;
}
function syncTracks(backingTracks) {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return null;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// backingTracks can be passed in as an optimization, so that we don't hit the backend excessively
if(backingTracks === undefined ) {
backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
}
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.backing_tracks = backingTracks;
syncTrackRequest.id = id();
return rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
}
function onWebsocketDisconnected(in_error) {
// kill the streaming of the session immediately
@ -678,45 +775,25 @@
// wait until we are fully in session before trying to sync tracks to server
if(joinDeferred) {
joinDeferred.done(function() {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
syncTracks();
})
}
}, 100);
}
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
refreshCurrentSession(true);
var backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
// the way we know if backing tracks changes, or recordings are opened, is via this event.
// but we want to report to the user when backing tracks change; so we need to detect change on our own
if(previousBackingTracks != backingTracks) {
logger.debug("backing tracks changed")
syncTracks(backingTracks);
}
else {
refreshCurrentSession(true);
}
}
else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
setMixerMode(MIX_MODES.MASTER);
@ -729,6 +806,10 @@
// Public interface
this.id = id;
this.start = start;
this.backingTrack = backingTrack;
this.backingTracks = backingTracks;
this.recordedBackingTracks = recordedBackingTracks;
this.metronomeActive = metronomeActive;
this.setUserTracks = setUserTracks;
this.recordedTracks = recordedTracks;
this.jamTracks = jamTracks;
@ -736,6 +817,7 @@
this.joinSession = joinSession;
this.leaveCurrentSession = leaveCurrentSession;
this.refreshCurrentSession = refreshCurrentSession;
this.updateSession = updateSession;
this.subscribe = subscribe;
this.participantForClientId = participantForClientId;
this.isPlayingRecording = isPlayingRecording;
@ -767,6 +849,19 @@
this.getParticipant = function(clientId) {
return participantsEverSeen[clientId]
};
this.setBackingTrack = function(backingTrack) {
openBackingTrack = backingTrack;
};
this.getBackingTrack = function() {
return openBackingTrack;
};
this.hasShownAudioMediaMixerHelp = function() {
return shownAudioMediaMixerHelp;
}
this.markShownAudioMediaMixerHelp = function() {
shownAudioMediaMixerHelp = true;
}
// call to report if the current user was able to establish audio with the specified clientID
this.setAudioEstablished = function(clientId, audioEstablished) {

View File

@ -5,6 +5,7 @@
context.JK.ShoppingCartScreen = function(app) {
var logger = context.JK.logger;
var jamTrackUtils = context.JK.JamTrackUtils;
var $screen = null;
var $content = null;
@ -16,6 +17,10 @@
function afterShow(data) {
}
function afterHide() {
jamTrackUtils.checkShoppingCart();
}
function events() {
$screen.find("a.remove-cart").on('click', removeCart);
$screen.find("a.proceed-checkout").on('click', proceedCheckout);
@ -94,7 +99,8 @@
function initialize() {
var screenBindings = {
'beforeShow': beforeShow,
'afterShow': afterShow
'afterShow': afterShow,
'afterHide' : afterHide
};
app.bindScreen('shoppingCart', screenBindings);

View File

@ -11,74 +11,157 @@ context.JK.SiteValidator = class SiteValidator
@input_div = $(".site_validator#"+site_type+"_validator")
@data_input = @input_div.find('input')
@logger = context.JK.logger
@site_status = null
@spinner = @input_div.find('span.spinner-small')
@checkmark = @input_div.find('.validate-checkmark')
this.show_format_status()
this.setSiteStatus(null)
this.showFormatStatus()
@is_rec_src = false
@deferred_status_check = null
@is_validating = false
init: () =>
this.renderErrors({})
@spinner.hide()
validator = this
@data_input.on 'blur', ->
validator.did_blur()
validator.didBlur()
@data_input.on 'focus', ->
validator.show_format_status()
validator.showFormatStatus()
@data_input.on 'change', ->
@site_status = null
data_to_validate: () =>
dataToValidate: () =>
url = @data_input.val()
if 0 < url.length
url.substring(0,2000)
else
null
show_format_status: () =>
data = this.data_to_validate()
showFormatStatus: () =>
data = this.dataToValidate()
yn = true
if data && 'url' == @site_type
if data && ('url' == @site_type || @is_rec_src)
regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
yn = regexp.test(this.data_to_validate())
if yn
@checkmark.show()
else
yn = regexp.test(this.dataToValidate())
unless yn
@checkmark.hide()
yn
did_blur: () =>
if this.show_format_status()
this.validate_site()
didBlur: () =>
if this.showFormatStatus()
this.validateSite()
validate_site: () =>
@site_status = null
validateSite: () =>
unless data = this.dataToValidate()
return null
this.setSiteStatus(null)
@spinner.show()
@checkmark.hide()
@rest.validateUrlSite(this.data_to_validate(), @site_type)
.done(this.processSiteCheck)
@rest.validateUrlSite(data, @site_type)
.done(this.processSiteCheckSucceed)
.fail(this.processSiteCheckFail)
processSiteCheck: (response) =>
processSiteCheckSucceed: (response) =>
@spinner.hide()
if 'Valid Site' == response.message
@site_status = 'valid'
this.setSiteStatus(true)
this.renderErrors({})
if @deferred_status_check
@deferred_status_check.resolve()
else
@site_status = 'invalid'
this.setSiteStatus(false)
this.renderErrors(response)
if @deferred_status_check
@deferred_status_check.reject()
@deferred_status_check = null
@logger.debug("site_status = "+@site_status)
processSiteCheckFail: (response) =>
@spinner.hide()
@checkmark.hide()
@logger.error("site check error")
@site_status = 'invalid'
this.setSiteStatus(false)
if @deferred_status_check
@deferred_status_check.reject()
@deferred_status_check = null
setSiteStatus: (status) =>
@site_status = status
@spinner.hide()
if true == status
@checkmark.show()
else
@checkmark.hide()
siteIsValid: () =>
this.setSiteStatus(true)
siteIsInvalid: () =>
this.setSiteStatus(false)
renderErrors: (errors) =>
errdiv = @input_div.find('.error')
if errmsg = context.JK.format_errors("site", errors)
@checkmark.hide()
errdiv.show()
errdiv.html(errmsg)
else
@checkmark.show()
errdiv.hide()
errdiv.html('')
state: () =>
dfr = $.Deferred()
if null == @site_status
@deferred_status_check = dfr
this.validateSite()
else
if true == @site_status
dfr.resolve()
else
dfr.reject()
return dfr.promise()
context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator
constructor: (site_type) ->
super(site_type)
@recording_sources = []
@is_rec_src = true
@add_btn = @input_div.find('a.add-recording-source')
init: (sources) =>
super()
if sources
@recording_sources = sources
@add_btn.on 'click', =>
this.attemptAdd()
processSiteCheckSucceed: (response) =>
super(response)
@add_btn.removeClass('disabled')
@recording_sources.push({ url: response.data, recording_id: response.recording_id })
processSiteCheckFail: (response) =>
super(response)
@add_btn.removeClass('disabled')
didBlur: () =>
# do nothing, validate on add only
validateSite: () =>
@add_btn.addClass('disabled')
super()
attemptAdd: () =>
if data = this.dataToValidate()
unless this.containsRecordingUrl(data)
this.validateSite()
removeRecordingId: (recording_id) =>
start_len = @recording_sources.length
@recording_sources = $.grep @recording_sources, (src_data) ->
src_data['recording_id'] != recording_id
start_len != @recording_sources.length
containsRecordingUrl: (url) =>
vals = $.grep @recording_sources, (src_data) ->
src_data['url'] == url
0 < vals.length

View File

@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer
@list = @root.find('.list')
@logList = @root.find('.log-list')
@templateRecordedTrack = $('#template-sync-viewer-recorded-track')
@templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track')
@templateStreamMix = $('#template-sync-viewer-stream-mix')
@templateMix = $('#template-sync-viewer-mix')
@templateNoSyncs = $('#template-sync-viewer-no-syncs')
@templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details')
@templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track')
@templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track')
@templateHoverMix = $('#template-sync-viewer-hover-mix')
@templateDownloadReset = $('#template-sync-viewer-download-progress-reset')
@templateUploadReset = $('#template-sync-viewer-upload-progress-reset')
@templateGenericCommand = $('#template-sync-viewer-generic-command')
@templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command')
@templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command')
@templateLogItem = $('#template-sync-viewer-log-item')
@tabSelectors = @root.find('.dialog-tabs .tab')
@tabs = @root.find('.tab-content')
@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer
them_upload_soon: 'them-upload-soon'
missing: 'missing',
me_uploaded: 'me-uploaded',
them_uploaded: 'them-uploaded'
them_uploaded: 'them-uploaded',
not_mine: 'not-mine'
}
@clientStates = {
unknown: 'unknown',
@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer
hq: 'hq',
sq: 'sq',
missing: 'missing',
discarded: 'discarded'
discarded: 'discarded',
not_mine: 'not-mine'
}
throw "no sync-viewer" if not @root.exists()
@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer
$clientRetry.hide()
$uploadRetry.hide()
updateBackingTrackState: ($track) =>
clientInfo = $track.data('client-info')
serverInfo = $track.data('server-info')
myTrack = serverInfo.user.id == context.JK.currentUserId
# determine client state
clientStateMsg = 'UNKNOWN'
clientStateClass = 'unknown'
clientState = @clientStates.unknown
if serverInfo.mine
if serverInfo.download.should_download
if serverInfo.download.too_many_downloads
clientStateMsg = 'EXCESS DOWNLOADS'
clientStateClass = 'error'
clientState = @clientStates.too_many_uploads
else
if clientInfo?
if clientInfo.local_state == 'HQ'
clientStateMsg = 'HIGHEST QUALITY'
clientStateClass = 'hq'
clientState = @clientStates.hq
else if clientInfo.local_state == 'MISSING'
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'DISCARDED'
clientStateClass = 'discarded'
clientState = @clientStates.discarded
else
clientStateMsg = 'NOT MINE'
clientStateClass = 'not_mine'
clientState = @clientStates.not_mine
# determine upload state
uploadStateMsg = 'UNKNOWN'
uploadStateClass = 'unknown'
uploadState = @uploadStates.unknown
if serverInfo.mine
if !serverInfo.fully_uploaded
if serverInfo.upload.too_many_upload_failures
uploadStateMsg = 'UPLOAD FAILURE'
uploadStateClass = 'error'
uploadState = @uploadStates.too_many_upload_failures
else
if myTrack
if clientInfo?
if clientInfo.local_state == 'HQ'
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.me_upload_soon
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.them_upload_soon
else
uploadStateMsg = 'UPLOADED'
uploadStateClass = 'uploaded'
if myTrack
uploadState = @uploadStates.me_uploaded
else
uploadState = @uploadStates.them_uploaded
else
uploadStateMsg = 'NOT MINE'
uploadStateClass = 'not_mine'
uploadState = @uploadStates.not_mine
$clientState = $track.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
$clientStateProgress = $clientState.find('.progress')
$uploadState = $track.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
$uploadStateProgress = $uploadState.find('.progress')
$clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
$clientStateMsg.text(clientStateMsg)
$clientStateProgress.css('width', '0')
$uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
$uploadStateMsg.text(uploadStateMsg)
$uploadStateProgress.css('width', '0')
# this allows us to make styling decisions based on the combination of both client and upload state.
$track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}")
$clientRetry = $clientState.find('.retry')
$uploadRetry = $uploadState.find('.retry')
if gon.isNativeClient
# handle client state
# only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already
if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
$clientRetry.show()
else
$clientRetry.hide()
# only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it
if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon)
$uploadRetry.show()
else
$uploadRetry.hide()
else
$clientRetry.hide()
$uploadRetry.hide()
associateClientInfo: (recording) =>
for clientInfo in recording.local_tracks
$track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
for clientInfo in recording.backing_tracks
$track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
$track = @list.find(".mix[data-recording-id='#{recording.recording_id}']")
$track.data('client-info', recording.mix)
$track.data('total-size', recording.size)
@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer
uploadStateClass: uploadStateClass}
{variable: 'data'})
onHoverOfStateIndicator: () ->
displayBackingTrackHover: ($recordedTrack) =>
$clientState = $recordedTrack.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
clientStateClass = $clientState.data('custom-class')
clientState = $clientState.attr('data-state')
clientInfo = $recordedTrack.data('client-info')
$uploadState = $recordedTrack.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
uploadStateClass = $uploadState.data('custom-class')
uploadState = $uploadState.attr('data-state')
serverInfo = $recordedTrack.data('server-info')
# decide on special case strings first
summary = ''
if clientState == @clientStates.not_mine && @uploadStates.them_uploaded
# this is not our backing track
summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you."
else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon
# this is not our backing track
summary = "#{serverInfo.user.name} has not yet uploaded their backing track."
else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded
# we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing."
else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.discarded
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded )
summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made."
clientStateDefinition = switch clientState
when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed."
when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track."
when @clientStates.missing then "MISSING means you do not have this backing track anymore."
when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track."
else 'There is no help for this state'
uploadStateDefinition = switch uploadState
when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted."
when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon."
when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this backing track soon."
when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track."
when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track."
when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either."
when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track."
context._.template(@templateHoverRecordedBackingTrack.html(),
{summary: summary,
clientStateDefinition: clientStateDefinition,
uploadStateDefinition: uploadStateDefinition,
clientStateMsg: $clientStateMsg.text(),
uploadStateMsg: $uploadStateMsg.text(),
clientStateClass: clientStateClass,
uploadStateClass: uploadStateClass}
{variable: 'data'})
onTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayTrackHover($recordedTrack)
onBackingTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-backing-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayBackingTrackHover($recordedTrack)
onStreamMixHover: () ->
$streamMix = $(this).closest('.stream-mix.sync')
self = $streamMix.data('sync-viewer')
@ -512,6 +709,39 @@ context.JK.SyncViewer = class SyncViewer
return false
retryDownloadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_track',
action: 'download'
queue: 'download',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
retryUploadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_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'
@ -548,8 +778,26 @@ context.JK.SyncViewer = class SyncViewer
$uploadStateRetry.click(this.retryUploadRecordedTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
createBackingTrack: (userSync) =>
$track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'}))
$track.data('server-info', userSync)
$track.data('sync-viewer', this)
$clientState = $track.find('.client-state')
$uploadState = $track.find('.upload-state')
$clientStateRetry = $clientState.find('.retry')
$clientStateRetry.click(this.retryDownloadRecordedBackingTrack)
$uploadStateRetry = $uploadState.find('.retry')
$uploadStateRetry.click(this.retryUploadRecordedBackingTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
@ -687,6 +935,8 @@ context.JK.SyncViewer = class SyncViewer
for userSync in response.entries
if userSync.type == 'recorded_track'
@list.append(this.createTrack(userSync))
if userSync.type == 'recorded_backing_track'
@list.append(this.createBackingTrack(userSync))
else if userSync.type == 'mix'
@list.append(this.createMix(userSync))
else if userSync.type == 'stream_mix'
@ -707,6 +957,8 @@ context.JK.SyncViewer = class SyncViewer
for track in @list.find('.recorded-track.sync')
this.updateTrackState($(track))
for track in @list.find('.recorded-backing-track.sync')
this.updateBackingTrackState($(track))
for streamMix in @list.find('.stream-mix.sync')
this.updateStreamMixState($(streamMix))
@ -726,6 +978,18 @@ context.JK.SyncViewer = class SyncViewer
deferred.resolve(matchingTrack.data('server-info'))
return deferred
resolveBackingTrack: (commandMetadata) =>
recordingId = commandMetadata['recording_id']
clientTrackId = commandMetadata['track_id']
matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if matchingTrack.length == 0
return @rest.getRecordedBackingTrack({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')
@ -738,6 +1002,18 @@ context.JK.SyncViewer = class SyncViewer
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
renderFullUploadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
$busy.empty().append($track)
@uploadProgress.find('.progress').css('width', '0%')
renderFullDownloadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.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
@ -756,6 +1032,23 @@ context.JK.SyncViewer = class SyncViewer
deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'download', commandMetadata)
deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderUploadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'upload', commandMetadata)
deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderGeneric: (commandId, category, commandMetadata) =>
commandMetadata.displayType = this.displayName(commandMetadata)
@ -794,6 +1087,8 @@ context.JK.SyncViewer = class SyncViewer
@downloadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'download', commandMetadata)
else if commandMetadata.queue == 'upload'
@ -803,6 +1098,8 @@ context.JK.SyncViewer = class SyncViewer
@uploadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'upload', commandMetadata)
else if commandMetadata.queue == 'cleanup'
@ -820,6 +1117,12 @@ context.JK.SyncViewer = class SyncViewer
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
else if userSync.type == 'recorded_backing_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.updateBackingTrackState($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}']")
@ -839,20 +1142,6 @@ context.JK.SyncViewer = class SyncViewer
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")
@ -892,10 +1181,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @downloadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @downloadMetadata.type == 'recorded_track'
if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track'
clientTrackId = @downloadMetadata['track_id']
recordingId = @downloadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
$matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'download', percentage)
@ -903,10 +1192,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @uploadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload'
if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_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}']")
$matchingTrack = @list.find(".track-item.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'
@ -977,15 +1266,15 @@ context.JK.SyncViewer = class SyncViewer
this.logResult(data.commandMetadata, false, data.commandReason, true)
displayName: (metadata) =>
if metadata.type == 'recorded_track' && metadata.action == 'download'
if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download'
return 'DOWNLOADING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'upload'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_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'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert'
return 'COMPRESSING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'delete'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete'
return 'CLEANUP TRACK'
else if metadata.type == 'stream_mix' && metadata.action == 'upload'
return 'UPLOADING STREAM MIX'

View File

@ -30,6 +30,28 @@
return tracks;
},
getBackingTracks: function(jamClient) {
var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4);
console.log("mediaTracks", mediaTracks)
var backingTracks = []
context._.each(mediaTracks, function(mediaTrack) {
// the check for 'not managed' means this is not a track opened by a recording, basically
// we do not try and sync these sorts of backing tracks to the server, because they
// are already encompassed by
if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) {
var track = {};
track.client_track_id = mediaTrack.persisted_track_id;
track.client_resource_id = mediaTrack.rid;
track.filename = mediaTrack.filename;
backingTracks.push(track);
}
})
return backingTracks;
},
/**
* This function resolves which tracks to configure for a user
* when creating or joining a session. By default, tracks are pulled

View File

@ -987,6 +987,15 @@
return hasFlash;
}
context.JK.getNameOfFile = function(filename) {
var index = filename.lastIndexOf('/');
if(index == -1) {
index = filename.lastIndexOf('\\');
}
return index == -1 ? filename : filename.substring(index + 1, filename.length)
}
context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result);

View File

@ -208,6 +208,10 @@ $fair: #cc9900;
background-color: $error;
}
&.not_mine {
background-color: $good;
}
&.discarded {
background-color: $unknown;
}
@ -252,6 +256,10 @@ $fair: #cc9900;
background-color: $good;
}
&.not_mine {
background-color: $good;
}
.retry {
display:none;
position:absolute;

View File

@ -45,7 +45,7 @@ body.jam, body.web, .dialog{
}
}
.help-hover-recorded-tracks, .help-hover-stream-mix {
.help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks {
font-size:12px;
padding:5px;

View File

@ -19,22 +19,26 @@
}
.profile-header {
padding:10px 20px;
// height:120px;
}
.profile-header h2 {
font-weight:200;
font-size: 28px;
float:left;
margin: 0px 150px 0px 0px;
margin: 0px 0px 0px 0px;
}
.profile-status {
.profile-about-right .section-header {
font-weight:600;
font-size:18px;
float:left;
margin: 0px 0px 10px 0px;
}
.profile-details {
font-size:12px;
float:left;
display:inline-block;
vertical-align:middle;
line-height:30px;
width: 80px;
}
.profile-photo {
@ -159,7 +163,7 @@
font-weight:600;
}
#profile-bands .when-empty {
#bands-content .when-empty {
margin: 0px;
padding:0px;
display:block;
@ -170,7 +174,7 @@
line-height: 150%;
}
#profile-bands .when-empty a {
#bands-content .when-empty a {
text-decoration: underline;
color: inherit;
}
@ -330,7 +334,7 @@
display:none;
}
#profile-history {
#history-content {
padding:0 10px 0 20px;
width:100%;
position:relative;

View File

@ -18,7 +18,7 @@
.track {
width:70px;
height:290px;
height:300px;
display:inline-block;
margin-right:8px;
position:relative;
@ -50,6 +50,9 @@
vertical-align:top;
}
.session-recordedtracks-container {
//display: block;
}
.recording-controls {
display:none;
@ -74,17 +77,23 @@
left:5px;
}
.open-media-file-header {
.open-media-file-header, .use-metronome-header {
font-size:16px;
line-height:100%;
margin:0;
float:left;
img {
position:relative;
top:3px;
}
}
.open-media-file-header {
float: left;
}
.use-metronome-header {
clear: both;
}
.open-media-file-options {
font-size:16px;
@ -110,8 +119,21 @@
.session-recording-name-wrapper{
position:relative;
white-space:nowrap;
display:none;
white-space:normal;
display:none;
.session-recording-name {
position:relative;
margin-top:9px;
margin-bottom:8px;
font-size:16px;
height: 22px;
min-height: 22px;
max-height: 22px;
display: inline-block;
width:60%;
text-overflow:ellipsis;
}
.session-add {
margin-top:9px;
@ -126,13 +148,6 @@
}
}
.session-recording-name {
width:60%;
overflow:hidden;
margin-top:9px;
margin-bottom:8px;
font-size:16px;
}
}
@ -201,6 +216,9 @@ table.vu td {
position: absolute;
text-align:center;
width: 55px;
height: 15px;
min-height: 11px;
max-height: 33px;
max-width: 55px;
white-space:normal;
top: 3px;
@ -208,6 +226,7 @@ table.vu td {
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
font-weight: bold;
text-overflow:ellipsis;
}
.track-close {
@ -319,8 +338,6 @@ table.vu td {
color: inherit;
}
.session-add {
margin-top:9px;
margin-bottom:8px;
@ -345,7 +362,7 @@ table.vu td {
overflow-x:auto;
overflow-y:hidden;
width:100%;
height:340px;
height:370px;
float:left;
white-space:nowrap;
}
@ -482,12 +499,9 @@ table.vu td {
.track-gain {
position:absolute;
width:28px;
height:83px;
height:63px;
top:138px;
left:23px;
background-image:url('/assets/content/bkg_gain_slider.png');
background-repeat:repeat-y;
background-position:bottom;
}
.track-gain-wrapper {
@ -514,6 +528,45 @@ table.vu td {
height: 18px;
background-image:url('/assets/content/icon_mute.png');
background-repeat:no-repeat;
text-align: center;
}
.track-icon-loop {
cursor: pointer;
position:absolute;
top:250px;
left:11px;
width: 20px;
height: 18px;
text-align: center;
font-size: 8pt;
font-weight: bold;
.icheckbox_minimal {
top:5px;
margin-right:5px;
}
}
.metronome-selects {
position: absolute;
width: 52px;
top:252px;
left: 10px;
height: 18px;
text-align: center;
//display: block;
//padding: 4px;
select.metronome-select {
position: relative;
padding: 4px 0px 4px 0px;
margin: 0;
width: 100% !important;
font-size: 10px;
font-weight: normal;
}
}
.track-icon-mute.muted {
@ -524,12 +577,12 @@ table.vu td {
}
.session-livetracks .track-icon-mute, .session-recordings .track-icon-mute {
top:245px;
top:225px;
}
.track-icon-settings {
position:absolute;
top:255px;
top:235px;
left:28px;
}

View File

@ -1,7 +1,7 @@
@import "client/common";
table.findsession-table, table.local-recordings, table.open-jam-tracks, #account-session-detail {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, #account-session-detail {
.latency-unacceptable {
width: 50px;
@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, #account
text-align:center;
}
}
table.findsession-table, table.local-recordings, table.open-jam-tracks {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks {
width:98%;
height:10%;
font-size:11px;

View File

@ -1,9 +1,17 @@
@import "client/common";
.site_validator {
.validator-input {
float: left;
}
.validator-add-rec {
float: left;
}
input {
width: 100%;
padding: 5px;
padding: 5px 5px 5px 30px;
float: left;
}
.validate-checkmark {
@ -15,10 +23,8 @@
background-size: 50% 50%;
display:inline-block;
vertical-align: middle;
position: absolute;
margin-top: 0px;
margin-left: 520px;
position: absolute;
position: relative;
margin-top: -40px;
left: 0px;
}
.error {
@ -26,8 +32,7 @@
span.spinner-small {
display:inline-block;
vertical-align: middle;
position: absolute;
margin-top: 0px;
margin-left: 520px;
position: relative;
margin-top: -40px;
}
}

View File

@ -0,0 +1,40 @@
@import "client/common";
#open-backing-track-dialog {
table.open-backing-tracks {
tbody {
tr:hover {
background-color: #777;
cursor:pointer;
}
tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] {
background-color:#777;
color:#aaa;
}
}
}
.right {
margin-right:10px;
}
.help-links {
text-align: left;
position: absolute;
margin: 0 auto;
width: 70%;
//left: 15%;
font-size: 12px;
padding-top:5px;
a {
margin:0 10px;
}
}
.paginator-holder {
padding-top:3px;
}
}

View File

@ -0,0 +1,32 @@
class ApiBackingTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_silent ]
respond_to :json
def index
tracks = [
{:name=>'foo',:path=>"foobar.mp3", :length=>4283},
{:name=>'bar',:path=>"foo.mp3",:length=>3257}
]
@backing_tracks, @next = tracks, nil
render "api_backing_tracks/index", :layout => nil
end
def backing_track_silent
@recorded_backing_track.mark_silent
render :json => {}, :status => 200
end
private
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
end # class ApiBackingTracksController

View File

@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [ :add_like, :show, :show_history, :add_session_info_comment ]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close, :backing_track_open, :backing_track_close, :metronome_open, :metronome_close]
skip_before_filter :api_signed_in_user, only: [:perf_upload]
respond_to :json
@ -357,7 +357,7 @@ class ApiMusicSessionsController < ApiController
end
def track_sync
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks])
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_tracks])
unless @tracks.kind_of? Array
# we have to do this because api_session_detail_url will fail with a bad @tracks
@ -597,8 +597,44 @@ class ApiMusicSessionsController < ApiController
respond_with_model(@music_session)
end
def backing_track_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
private
@backing_track_path = params[:backing_track_path]
@music_session.open_backing_track(current_user, @backing_track_path)
respond_with_model(@music_session)
end
def backing_track_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_backing_track()
respond_with_model(@music_session)
end
def metronome_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.open_metronome(current_user)
respond_with_model(@music_session)
end
def metronome_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_metronome()
respond_with_model(@music_session)
end
private
def lookup_session
@music_session = ActiveMusicSession.find(params[:id])

View File

@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ]
before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ]
before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ]
before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ]
@ -43,7 +44,11 @@ class ApiRecordingsController < ApiController
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download
def show_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download # track
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user)
@recorded_track.current_user = current_user
@ -58,6 +63,21 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_download
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user)
@recorded_backing_track.current_user = current_user
@recorded_backing_track.update_download_count
@recorded_backing_track.valid?
if !@recorded_backing_track.errors.any?
@recorded_backing_track.save!
redirect_to @recorded_backing_track.sign_url
else
render :json => { :message => "download limit surpassed" }, :status => 404
end
end
def start
music_session = ActiveMusicSession.find(params[:music_session_id])
@ -227,6 +247,61 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_upload_next_part
length = params[:length]
md5 = params[:md5]
@recorded_backing_track.upload_next_part(length, md5)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
# this is not typical, but please don't change this line unless you are sure it won't break anything
# this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's
# state doesn't cause errors to shoot out like normal.
render :json => { :errors => @recorded_backing_track.errors }, :status => 422
else
result = {
:part => @recorded_backing_track.next_part_to_upload,
:offset => @recorded_backing_track.file_offset.to_s
}
render :json => result, :status => 200
end
end
def backing_track_upload_sign
render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200
end
def backing_track_upload_part_complete
part = params[:part]
offset = params[:offset]
@recorded_backing_track.upload_part_complete(part, offset)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
else
render :json => {}, :status => 200
end
end
def backing_track_upload_complete
@recorded_backing_track.upload_complete
@recorded_backing_track.recording.upload_complete
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
return
else
render :json => {}, :status => 200
end
end
# POST /api/recordings/:id/videos/:video_id/upload_sign
def video_upload_sign
length = params[:length]
@ -314,6 +389,11 @@ class ApiRecordingsController < ApiController
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user)
end
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
def lookup_stream_mix
@quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user)

View File

@ -1,4 +1,4 @@
require 'recurly_client'
require 'jam_ruby/recurly_client'
class ApiRecurlyController < ApiController
before_filter :api_signed_in_user
before_filter :create_client

View File

@ -759,14 +759,23 @@ class ApiUsersController < ApiController
site = params[:sitetype]
if site.blank? || 'url'==site
url = data
elsif Utils.recording_source?(site)
rec_id = Utils.extract_recording_id(site, data)
if rec_id
render json: { message: 'Valid Site', recording_id: rec_id, data: data }, status: 200
return
else
render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200
return
end
else
url = Utils.username_url(data, site)
end
unless url.blank?
if errmsg = Utils.site_validator(url, site)
render json: { message: 'Invalid Site', errors: { site: [errmsg] } }, status: 200
render json: { message: 'Invalid Site', data: data, errors: { site: [errmsg] } }, status: 200
else
render json: { message: 'Valid Site' }, status: 200
render json: { message: 'Valid Site', data: data }, status: 200
end
else
render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity

View File

@ -42,4 +42,8 @@ class SpikesController < ApplicationController
render :layout => 'web'
end
def recording_source
render :layout => 'web'
end
end

View File

@ -91,4 +91,10 @@ module SessionsHelper
current_user.musician? ? 'Musician' : 'Fan'
end
end
def metronome_tempos
[
40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208
]
end
end

View File

@ -0,0 +1,7 @@
node :next do |page|
@next
end
node :backing_tracks do |page|
@backing_tracks
end

View File

@ -2,6 +2,8 @@
# I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata
# depictable.
# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState
object @claimed_recording
attributes :id, :name, :description, :is_public, :genre_id, :discarded
@ -36,6 +38,18 @@ child(:recording => :recording) {
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url
}
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -13,7 +13,7 @@ if !current_user
}
else
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active
node :can_join do |session|
session.can_join?(current_user, true)
@ -54,6 +54,10 @@ else
child(:tracks => :tracks) {
attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at
}
child(:backing_tracks => :backing_tracks) {
attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at
}
}
child({:invitations => :invitations}) {
@ -114,6 +118,14 @@ else
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
}
}

View File

@ -27,6 +27,12 @@ child(:recorded_tracks => :recorded_tracks) {
end
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
node do |recorded_backing_track|
partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track)
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -0,0 +1,11 @@
object @recorded_backing_track
attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url
}

View File

@ -18,7 +18,6 @@ glue :recorded_track do
partial("api_recordings/show", :object => recorded_track.recording)
end
node :upload do |recorded_track|
{
should_upload: true,
@ -35,6 +34,45 @@ glue :recorded_track do
end
glue :recorded_backing_track do
@object.current_user = current_user
node :type do |i|
'recorded_backing_track'
end
attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename
node :user do |recorded_backing_track|
partial("api_users/show_minimal", :object => recorded_backing_track.user)
end
node :recording do |recorded_backing_track|
partial("api_recordings/show", :object => recorded_backing_track.recording)
end
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
node :upload do |recorded_backing_track|
{
should_upload: true,
too_many_upload_failures: recorded_backing_track.too_many_upload_failures?
}
end
node :download do |recorded_backing_track|
{
should_download: recorded_backing_track.can_download?(current_user),
too_many_downloads: recorded_backing_track.too_many_downloads?
}
end
end
glue :mix do
@object.current_user = current_user

View File

@ -17,4 +17,21 @@ end
child :genre_players => :genres do
attributes :genre_id, :player_type, :genre_type
end
end
child :band_musicians => :bands do
attributes :id, :name, :admin, :photo_url, :logo_url
child :genres => :genres do
attributes :id, :description
#partial('api_genres/index', :object => @user.bands.genres)
end
end
child :musician_instruments => :instruments do
attributes :description, :proficiency_level, :priority, :instrument_id
end
# child :genres do
# attributes :description, :id
# end

View File

@ -27,7 +27,7 @@
<form id="account-edit-profile-form">
<h2>profile:</h2>
<h2>edit profile: basics</h2>
<div class="location w30 left">
<a href="#" class="avatar_large"><img src="{photoUrl}" id="profile-avatar" /></a>

View File

@ -199,6 +199,10 @@ script type="text/template" id="template-help-media-controls-disabled"
| Only the person who opened the recording can control the volume levels.
| {% } %}
script type="text/template" id="template-help-volume-media-mixers"
| Audio files only expose both master and personal mix controls, so any change here will also affect everyone in the session.
script type="text/template" id="template-help-downloaded-jamtrack"
.downloaded-jamtrack
p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client.

View File

@ -69,7 +69,7 @@
.jamtrack-price
{{"$ " + data.jamtrack.price}}
= "{% if (data.jamtrack.added_cart) { %}"
%a.jamtrack-add-cart-disabled.button-grey.button-disabled{href: "javascript:void(0)"} Added to Cart
%a.jamtrack-add-cart-disabled.button-grey.button-disabled{href: "javascript:void(0)"} Purchased
= "{% } else { %}"
%a.jamtrack-add-cart.button-orange{href: "#", "data-jamtrack-id" => "{{data.jamtrack.id}}"} Add to Cart
= "{% }; %}"

View File

@ -1,159 +1,271 @@
<!-- Profile -->
<div layout="screen" layout-id="profile" layout-arg="id" class="screen secondary" id="user-profile">
<div class="content-head">
<div class="content-icon">
<%= image_tag "content/icon_profile.png", :size => "19x19" %>
<div class="content-head">
<div class="content-icon">
<%= image_tag "content/icon_profile.png", :size => "19x19" %>
</div>
<h1><span id="type-label">musician</span> profile</h1>
<%= render "screen_navigation" %>
</div>
<div class="content-body">
<form id="profile-form" class="inner-content">
<div class="profile-header profile-head">
<div class="left">
<h2 id="username"></h2>
<%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-edit", :class => "button-orange") %>
</div>
<h1><span id="profile-type-label">musician</span> profile</h1>
<!-- action buttons -->
<div class="right">
<a id="btn-add-friend" class="button-orange">ADD FRIEND</a>
<a id="btn-follow-user" class="button-orange">FOLLOW</a>
<a id="btn-message-user" class="button-orange">MESSAGE</a>
</div>
<br clear="all" /><br />
<%= render "screen_navigation" %>
</div>
<div class="content-body">
<form id="profile-form" class="inner-content">
<div class="profile-header profile-head">
<!-- avatar -->
<div class="profile-photo">
<div class="avatar-profile">
<img id="avatar" width="200" height="200" />
</div>
</div>
<!-- profile name -->
<h2 id="profile-username"></h2>
<!-- profile navigation -->
<div class="profile-nav">
<a id="about-link" class="active">about</a>
<a id="history-link">history</a>
<a id="bands-link">bands</a>
<a id="social-link">social</a>
<a id="favorites-link" class="last">favorites</a>
</div>
<div class="clearall"></div>
</div>
<!-- profile status -->
<div class="profile-status">
</div>
<div class="profile-body">
<div id="about-content" class="profile-body-content">
<!-- stats & location -->
<div class="profile-wrapper">
<div class="profile-about-left">
<h3>Location:</h3><br />
<span id="location"></span><br />
<span id="age"></span><br /><br />
<h3>Stats:</h3><br />
<span id="friend-stats"></span><br />
<span id="follower-stats"></span><br />
<span id="session-stats"></span><br />
<span id="recording-stats"></span><br />
<span id="following-stats"></span><br />
<span id="favorite-stats"></span><br />
</div>
<div class="profile-about-right">
<div class="section-header">Bio</div>
<br clear="all" />
<div class="no-bio">
Not specified.
</div>
<div><a href="/client#/account/profile" class="enter-bio">Edit Bio</a></div>
<br clear="all" />
<!-- action buttons -->
<div class="right">
<a id="btn-add-friend" class="button-orange">ADD FRIEND</a>
<a id="btn-follow-user" class="button-orange">FOLLOW</a>
<a id="btn-message-user" class="button-orange">MESSAGE</a>
<%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-profile-edit", :class => "button-orange") %>
</div>
<br clear="all" /><br />
<!-- avatar -->
<div class="profile-photo">
<div class="avatar-profile">
<img id="profile-avatar" width="200" height="200" />
</div>
</div>
<!-- profile navigation -->
<div class="profile-nav">
<a id="profile-about-link" class="active">about</a>
<a id="profile-history-link">history</a>
<a id="profile-bands-link">bands</a>
<a id="profile-social-link">social</a>
<a id="profile-favorites-link" class="last">favorites</a>
</div>
<div class="clearall"></div>
<div class="have-bio">
<p id="biography"></p>
</div>
<!-- <div class="update-biography">
<div class="field">
<textarea name="biography" class="user-biography"></textarea>
</div>
<br clear="left" /><br />
<div class="right">
<a id="btn-update-user-biography" layout-action="close" class="button-orange">OK</a>
</div>
<div class="right">
<a id="btn-cancel-user-biography" layout-action="close" class="button-grey">CANCEL</a>
</div>
<br clear="all" />
</div> -->
<br clear="all" />
<div class="profile-body">
<div id="profile-about" class="profile-body-content">
<!-- stats & location -->
<div class="profile-wrapper">
<div class="profile-about-left">
<h3>Location:</h3><br />
<span id="profile-location"></span><br /><br /><br />
<h3>Stats:</h3><br />
<span id="profile-friend-stats"></span><br />
<span id="profile-follower-stats"></span><br />
<span id="profile-following-stats"></span><br />
<span id="profile-favorite-stats"></span><br />
<span id="profile-session-stats"></span><br />
<span id="profile-recording-stats"></span><br />
</div>
<div class="profile-about-right">
<div class="no-bio">
<span>You have no bio to describe yourself as a musician. <a href="#" class="enter-bio">Enter one now!</a></span>
</div>
<div class="have-bio">
<p id="profile-biography"></p><a id="profile-edit-biography" class="button-orange right" href="#">EDIT BIO</a>
</div>
<div class="update-biography">
<div class="field">
<textarea name="biography" class="user-biography"></textarea>
</div>
<br clear="left" /><br />
<div class="right">
<a id="btn-update-user-biography" layout-action="close" class="button-orange">OK</a>
</div>
<div class="right">
<a id="btn-cancel-user-biography" layout-action="close" class="button-grey">CANCEL</a>
</div>
<br clear="all" />
</div>
<br />
<div id="profile-instruments"></div>
<br />
<div id="profile-genres"></div>
</div>
<br clear="all" />
</div>
</div>
<div id="profile-history" class="profile-wrapper">
<%= form_tag('', {:id => 'user-feed-form', :class => 'inner-content'}) do %>
<%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED, :id => 'user-feed-controls'}) %>
<div class="filter-body">
<div class="content-body-scroller" id="user-profile-feed-scroller">
<div class="profile-wrapper">
<div class="feed-content" id="user-profile-feed-entry-list"></div>
<div id="user-profile-end-of-feeds-list" class="end-of-list">No more feed entries</div>
<div id="user-profile-loading-feeds" class="infinite-scroll-loader" style="padding:5px">Loading ...</div>
</div>
</div>
<div class="section-header">Musical Experience</div>
<br clear="all" />
<div id="instruments"></div>
<br clear="all" />
<br clear="all" />
<div>
<div class="left profile-details">Status:</div>
<div id="musician-status"></div>
</div>
<div>
<div class="left profile-details">Genres:</div>
<div id="genres"></div>
</div>
<div>
<div class="left profile-details">Concert Gigs:</div>
<div id="concert-count"></div>
</div>
<div>
<div class="left profile-details">Studio Gigs:</div>
<div id="studio-count"></div>
</div>
<br clear="all" />
<br clear="all" />
<div class="section-header">Performance Samples</div>
<br clear="all" />
<div id="no-samples" class="left">None specified</div>
<br clear="all" />
<br clear="all" />
<div class="section-header">Online Presence</div>
<br clear="all" />
<div id="no-online-presence" class="left">None specified</div>
<br clear="all" />
<br clear="all" />
<div class="section-header">Current Interests</div>
<br clear="all" />
<div id="no-interests" class="left">None specified</div>
<div id="paid-gigs">
<div class="left profile-details">I'm interested in playing paid gigs</div>
<br clear="all" />
<div id="paid-gig-details">
<ul>
<li>Genre(s): </li>
<li>Hourly rate = </li>
<li>Day rate = </li>
</ul>
</div>
</div>
<br clear="all" />
<div id="free-gigs">
<div class="left profile-details">I'm interested in playing free gigs</div>
<br clear="all" />
<div id="free-gig-details">
<ul>
<li>Genre(s): </li>
</ul>
</div>
</div>
<br clear="all" />
<div id="cowriting">
<div class="left profile-details">Concert Gigs:</div>
<br clear="all" />
<div id="cowriting-details">
<ul>
<li>Genre(s): </li>
<li>Purpose: </li>
</ul>
</div>
</div>
<br clear="all" />
<div id="traditional-band">
<div class="left">I'm interested in forming traditional band(s)</div>
<br clear="all" />
<div id="traditional-band-details">
<ul>
<li>Genre(s): </li>
<li>Commitment: </li>
<li>Touring: </li>
</ul>
</div>
</div>
<br clear="all" />
<div id="virtual-band">
<div class="left profile-details">I'm interested in forming virtual band(s)</div>
<br clear="all" />
<div id="virtual-band-details">
<ul>
<li>Genre(s): </li>
<li>Commitment: </li>
</ul>
</div>
</div>
<br clear="all" />
<br clear="all" />
</div>
</div>
<div id="history-content" class="profile-wrapper">
<%= form_tag('', {:id => 'user-feed-form', :class => 'inner-content'}) do %>
<%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED, :id => 'user-feed-controls'}) %>
<div class="filter-body">
<div class="content-body-scroller" id="user-profile-feed-scroller">
<div class="profile-wrapper">
<div class="feed-content" id="user-profile-feed-entry-list"></div>
<div id="user-profile-end-of-feeds-list" class="end-of-list">No more feed entries</div>
<div id="user-profile-loading-feeds" class="infinite-scroll-loader" style="padding:5px">Loading ...</div>
</div>
</div>
</div>
<% end %>
</div>
<div id="bands-content" class="profile-wrapper profile-body-content">
<br clear="all" />
</div>
<div id="social-content" class="profile-body-content outer">
<div class="profile-social-head">
<div class="profile-social-left">
<h2>Friends</h2>
</div>
<div class="profile-social-mid">
<h2>Following</h2>
</div>
<div class="profile-social-right">
<h2>Followers</h2>
</div>
<div class="clearall"></div>
</div>
<div class="profile-social-body">
<div class="profile-social-body-wrapper">
<div class="content-body-scroller">
<!-- @FIXME: seems like too many divs -->
<div class="profile-social-content">
<div class="profile-social-left">
<div id="social-friends">
</div>
<% end %>
</div>
<div id="profile-bands" class="profile-wrapper profile-body-content">
<br clear="all" />
</div>
<div id="profile-social" class="profile-body-content outer">
<div class="profile-social-head">
<div class="profile-social-left">
<h2>Friends</h2>
</div>
<div class="profile-social-mid">
<h2>Following</h2>
</div>
<div class="profile-social-right">
<h2>Followers</h2>
</div>
<div class="clearall"></div>
</div>
<div class="profile-social-body">
<div class="profile-social-body-wrapper">
<div class="content-body-scroller">
<!-- @FIXME: seems like too many divs -->
<div class="profile-social-content">
<div class="profile-social-left">
<div id="profile-social-friends">
</div>
</div>
<div class="profile-social-mid">
<div id="profile-social-followings">
</div>
</div>
<div class="profile-social-right">
<div id="profile-social-followers">
</div>
</div>
<div class="clearall"></div>
</div>
<!-- @FIXME: end -->
</div>
</div>
</div>
</div>
<div id="profile-favorites" class="profile-body-content">
<div class="profile-wrapper">
<div class="content-body-scroller">
</div>
</div>
</div>
<!-- </div> -->
</div>
<div class="profile-social-mid">
<div id="social-followings">
</div>
</div>
<div class="profile-social-right">
<div id="social-followers">
</div>
</div>
<div class="clearall"></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div id="favorites-content" class="profile-body-content">
<div class="profile-wrapper">
<div class="content-body-scroller">
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<script type="text/template" id="template-no-bands">
@ -208,7 +320,7 @@
</div>
<div class="result-list-button-wrapper">
<a class="button-orange smallbutton" href="{profile_url}">PROFILE</a>
<span class="profile-band-link-member-false"><a id="btn-follow-band-2" class="button-orange smallbutton">FOLLOW</a></span>
<span class="profile-band-link-member-false"><a id="btn-follow-band" class="button-orange smallbutton">FOLLOW</a></span>
<span class="profile-band-link-member-true"><a href="{band_edit_url}" class="button-orange smallbutton">EDIT BAND</a></span>
<span class="profile-band-link-member-true"><a href="{band_member_url}" class="button-orange smallbutton">INVITE</a></span>
</div>

View File

@ -1,199 +0,0 @@
<!-- Actual Session Screen -->
<div layout="screen" layout-id="session" layout-arg="id" class="screen secondary" id="session-screen">
<div class="content-head">
<div class="content-icon">
<%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %>
</div>
<h1>session</h1>
</div>
<div class="content-body">
<!-- session controls -->
<div id="session-controls">
<a class="button-grey resync left" id="session-resync">
<%= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} %>
RESYNC
</a>
<a class="button-grey left" layout-link="session-settings" id="session-settings-button">
<%= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} %>
SETTINGS
</a>
<a layout-link="share-dialog" class="button-grey left">
<%= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} %>
SHARE
</a>
<!-- Volume Slider -->
<div class="block">
<div class="label">VOLUME:</div>
<div id="volume" class="fader lohi" mixer-id=""></div>
</div>
<!-- Mix: Me versus Others -->
<div class="block monitor-mode-holder">
<div class="label">MIX:</div>
<select class="monitor-mode easydropdown">
<option value="personal" class="label">Personal</option>
<option value="master">Master</option>
</select>
</div>
<!--
<div class="block">
<div class="label">MONITOR:</div>
<div class="label"><small>others</small></div>
<div id="l2m" class="fader flat" mixer-id="__L2M__"></div>
<div class="label"><small>me</small></div>
</div>
-->
<!-- Leave Button -->
<a class="button-grey right leave" href="/client#/home" id="session-leave">X&nbsp;&nbsp;LEAVE</a>
</div>
<!-- end session controls -->
<!-- content scrolling area -->
<div id="tracks">
<div class="content-scroller">
<!-- content wrapper -->
<div class="content-wrapper">
<!-- my tracks -->
<div class="session-mytracks">
<h2>my tracks</h2>
<div id="track-settings" class="session-add" style="display:block;" layout-link="configure-tracks">
<%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %>
<span>Settings</span>
</div>
<div class="session-tracks-scroller">
<div id="session-mytracks-container"></div>
<div id="voice-chat" class="voicechat" style="display:none;" mixer-id="">
<div class="voicechat-label">CHAT</div>
<div class="voicechat-gain"></div>
<div class="voicechat-mute enabled" control="mute" mixer-id=""></div>
</div>
</div>
</div>
<!-- live tracks -->
<div class="session-livetracks">
<h2>live tracks</h2>
<div class="session-add" layout-link="select-invites">
<a href="#" id="session-invite-musicians">
<%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} %>&nbsp;&nbsp;Invite Musicians
</a>
</div>
<div class="session-tracks-scroller">
<div id="session-livetracks-container">
<div class="when-empty livetracks">
No other musicians <br/>
are in your session
</div>
</div>
<br clear="all" />
<div class="recording" id="recording-start-stop">
<a>
<%= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} %>&nbsp;&nbsp;<span id="recording-status">Make a Recording</span>
</a>
</div>
</div>
</div>
<!-- recordings -->
<div class="session-recordings">
<h2>other audio</h2>
<div class="session-recording-name-wrapper">
<div class="session-recording-name left">(No recording loaded)</div>
<div class="session-add right">
<a id='close-playback-recording' href="#"><%= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} %>&nbsp;&nbsp;Close</a>
</div>
</div>
<div class="session-tracks-scroller">
<div id="session-recordedtracks-container">
<div class="when-empty recordings">
<span class="open-media-file-header"><%= image_tag "content/icon_folder.png", {width:22, height:20} %> Open:</span>
<ul class="open-media-file-options">
<li><a href="#" id="open-a-recording">Recording</a></li>
<% if Rails.application.config.jam_tracks_available %>
<li><a href="#" id="open-a-jamtrack">JamTrack</a></li>
<% end %>
<!--<li>Audio File</li>-->
</ul>
</div>
</div>
<br clear="all" />
<%= render "play_controls" %>
</div>
<!-- recording name and close button -->
<!--
<div class="session-recording-name-wrapper">
<div class="session-recording-name left">(No recording loaded)</div>
<div class="session-add right">
<a>
<%= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} %>&nbsp;&nbsp;Close
</a>
</div>
</div>
-->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- dialogs needed for Session screen (ORDER MATTERS) -->
<%= render "configureTrack" %>
<%= render "addTrack" %>
<%= render "addNewGear" %>
<%= render "error" %>
<%= render "sessionSettings" %>
<!-- Track Template -->
<script type="text/template" id="template-session-track">
<div track-id="{trackId}" class="session-track track" client-id="{clientId}">
<div class="track-vu-left" mixer-id="{vuMixerId}_vul"></div>
<div class="track-vu-right" mixer-id="{vuMixerId}_vur"></div>
<div class="track-label">{name}</div>
<div id="div-track-close" track-id="{trackId}" class="track-close op30">
<%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
</div>
<div class="{avatarClass}">
<img src="{avatar}"/>
</div>
<div class="track-instrument {preMasteredClass}">
<img src="{instrumentIcon}" width="45" height="45"/>
</div>
<div class="track-gain" mixer-id="{mixerId}"></div>
<!--
<div class="track-gain-wrapper"
control="fader" orientation="vertical">
<div class="track-gain-slider" style="bottom:{gainPercent}%;" control="fader-handle">
<%= image_tag "content/slider_gain_vertical.png", {:width => 28, :height => 11} %>
</div>
</div>
</div>
-->
<div class="track-icon-mute {muteClass}" control="mute" mixer-id="{muteMixerId}">
</div>
<!-- TODO - connection class from curly param -->
<div mixer-id="{mixerId}_connection" class="track-connection grey">CONNECTION</div>
<div class="disabled-track-overlay"></div>
</div>
</script>
<script type="text/template" id="template-option">
<option value="{value}" title="{label}" {selected}>{label}</option>
</script>
<!-- Genre option template -->
<script type="text/template" id="template-genre-option">
<option value="{value}">{label}</option>
</script>

View File

@ -0,0 +1,145 @@
#session-screen.screen.secondary[layout="screen" layout-id="session" layout-arg="id"]
.content-head
.content-icon
= image_tag "shared/icon_session.png", {:height => 19, :width => 19}
h1
| session
.content-body
#session-controls
a#session-resync.button-grey.resync.left
= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12}
| RESYNC
a#session-settings-button.button-grey.left[layout-link="session-settings"]
= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12}
| SETTINGS
a.button-grey.left[layout-link="share-dialog"]
= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12}
| SHARE
.block
.label
| VOLUME:
#volume.fader.lohi[mixer-id=""]
.block.monitor-mode-holder
.label
| MIX:
select.monitor-mode.easydropdown
option.label[value="personal"]
| Personal
option[value="master"]
| Master
a#session-leave.button-grey.right.leave[href="/client#/home"]
| X  LEAVE
#tracks
.content-scroller
.content-wrapper
.session-mytracks
h2
| my tracks
#track-settings.session-add[style="display:block;" layout-link="configure-tracks"]
= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18}
span
| Settings
.session-tracks-scroller
#session-mytracks-container
#voice-chat.voicechat[style="display:none;" mixer-id=""]
.voicechat-label
| CHAT
.voicechat-gain
.voicechat-mute.enabled[control="mute" mixer-id=""]
.session-livetracks
h2
| live tracks
.session-add[layout-link="select-invites"]
a#session-invite-musicians[href="#"]
= image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"}
|   Invite Musicians
.session-tracks-scroller
#session-livetracks-container
.when-empty.livetracks
| No other musicians
br
| are in your session
br[clear="all"]
#recording-start-stop.recording
a
= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"}
|   
span#recording-status
| Make a Recording
.session-recordings
h2
| other audio
.session-recording-name-wrapper
.session-recording-name.left
| (No recording loaded)
.session-add.right
a#close-playback-recording[href="#"]
= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"}
|   Close
.session-tracks-scroller
#session-recordedtracks-container
.when-empty.recordings
span.open-media-file-header
= image_tag "content/icon_folder.png", {width:22, height:20}
| Open:
ul.open-media-file-options
li
a#open-a-recording[href="#"]
| Recording
- if Rails.application.config.jam_tracks_available
li
a#open-a-jamtrack[href="#"]
| JamTrack
- if Rails.application.config.backing_tracks_available
li
a#open-a-backingtrack[href="#"]
| Audio File
.when-empty.use-metronome-header
- if Rails.application.config.metronome_available
= image_tag "content/icon_metronome.png", {width:22, height:20}
a#open-a-metronome[href="#"]
| Use Metronome
br[clear="all"]
= render "play_controls"
= render "configureTrack"
= render "addTrack"
= render "addNewGear"
= render "error"
= render "sessionSettings"
script#template-session-track[type="text/template"]
.session-track.track client-id="{clientId}" track-id="{trackId}"
.track-vu-left.mixer-id="{vuMixerId}_vul"
.track-vu-right.mixer-id="{vuMixerId}_vur"
.track-label[title="{name}"]
span.name-text="{name}"
#div-track-close.track-close.op30 track-id="{trackId}"
=image_tag("content/icon_closetrack.png", {width: 12, height: 12})
div class="{avatarClass}"
img src="{avatar}"
.track-instrument class="{preMasteredClass}"
img height="45" src="{instrumentIcon}" width="45"
.track-gain mixer-id="{mixerId}"
.track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}"
.track-icon-loop.hidden control="loop"
input#loop-button type="checkbox" value="loop" Loop
.track-connection.grey mixer-id="{mixerId}_connection"
CONNECTION
.disabled-track-overlay
.metronome-selects.hidden
select.metronome-select.metro-sound title="Metronome Sound"
option.label value="Beep" Bleep
option.label value="Click" Click
option.label value="Snare" Drum
br
select.metronome-select.metro-tempo title="Metronome Tempo"
- metronome_tempos.each do |t|
option.label value=t
=t
script#template-option type="text/template"
option value="{value}" title="{label}" selected="{selected}"
="{label}"
script#template-genre-option type="text/template"
option value="{value}"
="{label}"

View File

@ -1,6 +1,11 @@
div class="site_validator" id="#{site_type}_validator"
span class="validate-checkmark"
span class="spinner-small upload-spinner"
input type='text' id="validate_input_#{site_type}" maxlength="2000"
br
div class="error"
div class="validator-input"
input type='text' id="validate_input_#{site_type}" maxlength="2000"
span class="validate-checkmark"
span class="spinner-small upload-spinner"
div class="error"
- if site_type =~ /^#{Utils::RECORDING_SRC_PREFIX}/
div class="validator-add-rec"
a id="add_btn_#{site_type}" class="button-grey add-recording-source right" ADD
br clear="all"

View File

@ -59,6 +59,23 @@ script type="text/template" id='template-sync-viewer-recorded-track'
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
script type="text/template" id='template-sync-viewer-recorded-backing-track'
.recorded-backing-track.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}" data-client-id="{{data.client_id}}" data-client-track-id="{{data.client_track_id}}" data-track-id="{{data.backing_track_id}}" data-fully-uploaded="{{data.fully_uploaded}}"
.type
span.text BACKING
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
.client-state.bar
.progress
span.msg
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
.upload-state.bar
.progress
span.msg
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
script type="text/template" id='template-sync-viewer-stream-mix'
.stream-mix.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}"
@ -128,6 +145,33 @@ script type="text/template" id="template-sync-viewer-hover-recorded-track"
| {{data.summary}}
| {% } %}
script type="text/template" id="template-sync-viewer-hover-recorded-backing-track"
.help-hover-recorded-backing-tracks
.client-box
.client-state-info
span.special-text is the file on your system?
.client-state class="{{data.clientStateClass}}"
span.msg
| {{data.clientStateMsg}}
.client-state-definition.sync-definition
| {{data.clientStateDefinition}}
.upload-box
.upload-state-info
span.special-text is it uploaded?
.upload-state class="{{data.uploadStateClass}}"
span.msg
| {{data.uploadStateMsg}}
.upload-state-definition.sync-definition
| {{data.uploadStateDefinition}}
br clear="both"
| {% if(data.summary) { %}
.summary
.title what's next?
| {{data.summary}}
| {% } %}
script type="text/template" id="template-sync-viewer-hover-stream-mix"
.help-hover-stream-mix
@ -189,7 +233,7 @@ script type="text/template" id="template-sync-viewer-generic-command"
| {{data.displayType}}
script type="text/template" id="template-sync-viewer-recorded-track-command"
.recorded-track.sync
.recorded-track.track-item.sync
.type
.progress
span.text
@ -198,6 +242,15 @@ script type="text/template" id="template-sync-viewer-recorded-track-command"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
img.instrument-icon data-instrument-id="{{data.instrument_id}}" hoveraction="instrument" src="{{JK.getInstrumentIconMap24()[data.instrument_id].asset}}"
script type="text/template" id="template-sync-viewer-recorded-backing-track-command"
.recorded-backing-track.track-item.sync
.type
.progress
span.text
| {{data.action}} BACKING TRACK
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
script type="text/template" id="template-sync-viewer-log-item"
.log class="success-{{data.success}}"
.command

View File

@ -155,6 +155,9 @@
var openJamTrackDialog = new JK.OpenJamTrackDialog(JK.app);
openJamTrackDialog.initialize();
var openBackingTrackDialog = new JK.OpenBackingTrackDialog(JK.app);
openBackingTrackDialog.initialize();
var configureTracksDialog = new JK.ConfigureTracksDialog(JK.app);
configureTracksDialog.initialize();

View File

@ -60,6 +60,9 @@ script type='text/template' id='template-mixer-mode-change'
li
span.definition Personal Mix
div The personal mix controls the audio mix that you individually hear while playing in the session, and you can customize this mix to hear more or less of the music stream from each other musician playing in the session. This does not affect the master mix used for recordings or broadcasts. With personal mix selected, when you adjust the faders on the session screen up or down, it changes the personal mix only for you locally.
li
span.definition Note on Audio Files
div The volume control on any audio file is always both the master and personal volume control, regardless of the current mode.
br
div
| For more detailed information on this topic, read our knowledge base article on&nbsp;

View File

@ -33,3 +33,4 @@
= render 'dialogs/allSyncsDialog'
= render 'dialogs/adjustGearSpeedDialog'
= render 'dialogs/openJamTrackDialog'
= render 'dialogs/openBackingTrackDialog'

View File

@ -0,0 +1,44 @@
.dialog.openBackingTrackDialog-overlay.ftue-overlay.tall#open-backing-track-dialog layout="dialog" layout-id="open-backing-track-dialog"
.content-head
= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' }
h1
| open an audio file
.dialog-inner
.recording-wrapper
table.open-backing-tracks cellspacing="0" cellpadding="0" border="0"
thead
tr
th align="left"
| NAME
th align="left"
| SIZE
th align="left"
| TYPE
tbody
br
/ .left.paginator-holder
.help-links
a.display-backingtracks-folder href='#'
| Display audio file folder
a.what-are-backingtracks href='#'
| What are Backing Tracks?
.right
a href="#" class="button-grey" layout-action="close"
| CANCEL
br clear="all"
script#template-backing-track-row type="text/template"
tr data-recording-id="{{data.backingTrackId}}" data-local-state="{{data.backingTrackState}}"
td
| {{data.name}}
td
| {{data.length}}
td
| {{data.type}}

View File

@ -0,0 +1,19 @@
= javascript_include_tag "site_validator"
div style="width:50%"
= render "clients/site_validator", site_type: params[:site_type]
= stylesheet_link_tag "client/site_validator"
<br />
= select_tag "site_type", options_for_select(Utils::RECORDING_SOURCES, params[:site_type])
javascript:
var initialized = false;
$(document).on('JAMKAZAM_READY', function(e, data) {
setTimeout(function() {
window.site_validator = new JK.SiteValidator('#{params[:site_type] || 'rec_youtube'}');
site_validator.init();
$('#validate_input_'+'#{params[:site_type] || 'url'}').val('jonathankolyer');
}, 1)
});
$('#site_type').change(function(){
location.href = 'recording_source?site_type='+$(this).val();
});

View File

@ -39,7 +39,7 @@ if defined?(Bundler)
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver"
config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver", "JamRuby::RecordedBackingTrackObserver"
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
@ -312,5 +312,7 @@ if defined?(Bundler)
config.show_jamblaster_notice = true
config.show_jamblaster_kickstarter_link = true
config.metronome_available = true
config.backing_tracks_available = true
end
end

View File

@ -97,7 +97,7 @@ SampleApp::Application.routes.draw do
match '/websocket', to: 'spikes#websocket'
match '/test_subscription', to: 'spikes#subscription'
match '/site_validate', to: 'spikes#site_validate'
match '/username_validate', to: 'spikes#username_validate'
match '/recording_source', to: 'spikes#recording_source'
# junk pages
match '/help', to: 'static_pages#help'
@ -184,6 +184,10 @@ SampleApp::Application.routes.draw do
match '/sessions/:id/details/comments' => 'api_music_sessions#add_session_info_comment', :via => :post
match '/sessions/:id/jam_tracks/:jam_track_id/open' => 'api_music_sessions#jam_track_open', :via => :post
match '/sessions/:id/jam_tracks/close' => 'api_music_sessions#jam_track_close', :via => :post
match '/sessions/:id/backing_tracks/open' => 'api_music_sessions#backing_track_open', :via => :post
match '/sessions/:id/backing_tracks/close' => 'api_music_sessions#backing_track_close', :via => :post
match '/sessions/:id/metronome/open' => 'api_music_sessions#metronome_open', :via => :post
match '/sessions/:id/metronome/close' => 'api_music_sessions#metronome_close', :via => :post
# music session tracks
match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post
@ -197,6 +201,9 @@ SampleApp::Application.routes.draw do
match '/music_notations' => 'api_music_notations#create', :via => :post
match '/music_notations/:id' => 'api_music_notations#download', :via => :get, :as => :download_music_notation
# Backing track_show
match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list'
# Jamtracks
match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list'
match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased'
@ -457,13 +464,22 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/tracks/:track_id/upload_sign' => 'api_recordings#upload_sign', :via => :get
match '/recordings/:id/tracks/:track_id/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :post
match '/recordings/:id/tracks/:track_id/upload_complete' => 'api_recordings#upload_complete', :via => :post
match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get
# Recordings - stream_mix
match '/recordings/:id/stream_mix/upload_sign' => 'api_recordings#upload_sign_stream_mix', :via => :get
match '/recordings/:id/stream_mix/upload_part_complete' => 'api_recordings#upload_part_complete_stream_mix', :via => :post
match '/recordings/:id/stream_mix/upload_complete' => 'api_recordings#upload_complete_stream_mix', :via => :post
match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get
# Recordings - backing tracks
match '/recordings/:id/backing_tracks/:track_id' => 'api_recordings#show_recorded_backing_track', :via => :get, :as => 'api_recordings_show_recorded_backing_track'
match '/recordings/:id/backing_tracks/:track_id/download' => 'api_recordings#backing_track_download', :via => :get, :as => 'api_recordings_download'
match '/recordings/:id/backing_tracks/:track_id/upload_next_part' => 'api_recordings#backing_track_upload_next_part', :via => :get
match '/recordings/:id/backing_tracks/:track_id/upload_sign' => 'api_recordings#backing_track_upload_sign', :via => :get
match '/recordings/:id/backing_tracks/:track_id/upload_part_complete' => 'api_recordings#backing_track_upload_part_complete', :via => :post
match '/recordings/:id/backing_tracks/:track_id/upload_complete' => 'api_recordings#backing_track_upload_complete', :via => :post
match '/recordings/:id/backing_tracks/:track_id/silent' => 'api_backing_tracks#backing_track_silent', :via => :post
# Recordings - recorded_videos
match '/recordings/:id/tracks/:video_id/upload_sign' => 'api_recordings#video_upload_sign', :via => :get
match '/recordings/:id/videos/:video_id/upload_start' => 'api_recordings#video_upload_start', :via => :post

View File

@ -140,10 +140,10 @@ class MusicSessionManager < BaseManager
Notification.send_session_depart(active_music_session, connection.client_id, user, recordingId)
end
def sync_tracks(active_music_session, client_id, new_tracks)
def sync_tracks(active_music_session, client_id, new_tracks, backing_tracks)
tracks = nil
active_music_session.with_lock do # VRFS-1297
tracks = Track.sync(client_id, new_tracks)
tracks = Track.sync(client_id, new_tracks, backing_tracks)
active_music_session.tick_track_changes
end
Notification.send_tracks_changed(active_music_session)

View File

@ -1,7 +1,40 @@
class Utils
RECORDING_SRC_PREFIX = 'rec_'
RECORDING_SOURCES = ["#{RECORDING_SRC_PREFIX}youtube", "#{RECORDING_SRC_PREFIX}soundcloud"]
USERNAME_SITES = %W{youtube facebook soundcloud bandcamp fandalism twitter reverbnation}
SITE_TYPES = ['url'].concat(USERNAME_SITES)
SITE_TYPES = ['url'].concat(USERNAME_SITES).concat(RECORDING_SOURCES)
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36"
def self.recording_source?(site)
RECORDING_SOURCES.include?(site)
end
def self.extract_recording_id(site, recording_url)
recording_url.strip!
case site
when 'rec_youtube'
# regex derived from: https://gist.github.com/afeld/1254889
if recording_url =~ /(youtu.be\/|youtube.com\/(watch\?(.*&)?v=|(embed|v)\/))([^\?&\"\'>]+)/
return $5
end
when 'rec_soundcloud'
if recording_url =~ /^https?:\/\/.*soundcloud.com\/.+/
tmpfile = Tempfile.new(site)
tmpfile.close
curl_args = "-A '#{USER_AGENT}' -L --output #{tmpfile.path} --fail --show-error "
`curl #{curl_args} '#{recording_url}' 2>&1`
result = File.read(tmpfile.path)
File.delete(tmpfile.path)
if result =~ /"soundcloud:\/\/sounds:(\d+)"/
return $1
end
end
end
nil
end
def self.username_url(username, site)
case site
@ -27,8 +60,7 @@ class Utils
end
def self.site_validator(url, site=nil)
uagent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36"
curl_args = "-A '#{uagent}' --silent --head --fail --show-error "
curl_args = "-A '#{USER_AGENT}' --silent --head --fail --show-error "
case site
when 'bandcamp'
cmd = "curl #{curl_args} '#{url}' 2>&1"

Some files were not shown because too many files have changed in this diff Show More