diff --git a/admin/bin/_guard-core b/admin/bin/_guard-core
new file mode 100755
index 000000000..915e26678
--- /dev/null
+++ b/admin/bin/_guard-core
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application '_guard-core' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('guard', '_guard-core')
diff --git a/admin/bin/autospec b/admin/bin/autospec
new file mode 100755
index 000000000..64dcb9cb0
--- /dev/null
+++ b/admin/bin/autospec
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'autospec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rspec-core', 'autospec')
diff --git a/admin/bin/aws-rb b/admin/bin/aws-rb
new file mode 100755
index 000000000..921a7d734
--- /dev/null
+++ b/admin/bin/aws-rb
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'aws-rb' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('aws-sdk-v1', 'aws-rb')
diff --git a/admin/bin/bourbon b/admin/bin/bourbon
new file mode 100755
index 000000000..5d5fa4b0e
--- /dev/null
+++ b/admin/bin/bourbon
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'bourbon' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('bourbon', 'bourbon')
diff --git a/admin/bin/bundler b/admin/bin/bundler
new file mode 100755
index 000000000..72c62ec0b
--- /dev/null
+++ b/admin/bin/bundler
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'bundler' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('bundler', 'bundler')
diff --git a/admin/bin/coderay b/admin/bin/coderay
new file mode 100755
index 000000000..5be1c0095
--- /dev/null
+++ b/admin/bin/coderay
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'coderay' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('coderay', 'coderay')
diff --git a/admin/bin/erubis b/admin/bin/erubis
new file mode 100755
index 000000000..2c7348b8b
--- /dev/null
+++ b/admin/bin/erubis
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'erubis' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('erubis', 'erubis')
diff --git a/admin/bin/fission b/admin/bin/fission
new file mode 100755
index 000000000..d08aeffd0
--- /dev/null
+++ b/admin/bin/fission
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'fission' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('fission', 'fission')
diff --git a/admin/bin/fog b/admin/bin/fog
new file mode 100755
index 000000000..ea27efbeb
--- /dev/null
+++ b/admin/bin/fog
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'fog' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('fog', 'fog')
diff --git a/admin/bin/fpm b/admin/bin/fpm
new file mode 100755
index 000000000..606005397
--- /dev/null
+++ b/admin/bin/fpm
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'fpm' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('fpm', 'fpm')
diff --git a/admin/bin/guard b/admin/bin/guard
new file mode 100755
index 000000000..0c1a532bd
--- /dev/null
+++ b/admin/bin/guard
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'guard' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('guard', 'guard')
diff --git a/admin/bin/haml b/admin/bin/haml
new file mode 100755
index 000000000..3c6d074f6
--- /dev/null
+++ b/admin/bin/haml
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'haml' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('haml', 'haml')
diff --git a/admin/bin/htmldiff b/admin/bin/htmldiff
new file mode 100755
index 000000000..c70e238dc
--- /dev/null
+++ b/admin/bin/htmldiff
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'htmldiff' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('diff-lcs', 'htmldiff')
diff --git a/admin/bin/jam_db b/admin/bin/jam_db
new file mode 100755
index 000000000..7b76ec134
--- /dev/null
+++ b/admin/bin/jam_db
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'jam_db' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('jam_db', 'jam_db')
diff --git a/admin/bin/jasmine b/admin/bin/jasmine
new file mode 100755
index 000000000..74f64e6b9
--- /dev/null
+++ b/admin/bin/jasmine
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'jasmine' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('jasmine', 'jasmine')
diff --git a/admin/bin/launchy b/admin/bin/launchy
new file mode 100755
index 000000000..92b254ad9
--- /dev/null
+++ b/admin/bin/launchy
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'launchy' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('launchy', 'launchy')
diff --git a/admin/bin/ldiff b/admin/bin/ldiff
new file mode 100755
index 000000000..8e3524a92
--- /dev/null
+++ b/admin/bin/ldiff
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'ldiff' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('diff-lcs', 'ldiff')
diff --git a/admin/bin/listen b/admin/bin/listen
new file mode 100755
index 000000000..093a5fc30
--- /dev/null
+++ b/admin/bin/listen
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'listen' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('listen', 'listen')
diff --git a/admin/bin/mix_cron.rb b/admin/bin/mix_cron.rb
new file mode 100755
index 000000000..ccf8c838a
--- /dev/null
+++ b/admin/bin/mix_cron.rb
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'mix_cron.rb' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('jam_ruby', 'mix_cron.rb')
diff --git a/admin/bin/nokogiri b/admin/bin/nokogiri
new file mode 100755
index 000000000..d55f84b05
--- /dev/null
+++ b/admin/bin/nokogiri
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'nokogiri' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('nokogiri', 'nokogiri')
diff --git a/admin/bin/pg_migrate b/admin/bin/pg_migrate
new file mode 100755
index 000000000..207cfe44c
--- /dev/null
+++ b/admin/bin/pg_migrate
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'pg_migrate' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('pg_migrate', 'pg_migrate')
diff --git a/admin/bin/pry b/admin/bin/pry
new file mode 100755
index 000000000..54678a32c
--- /dev/null
+++ b/admin/bin/pry
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'pry' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('pry', 'pry')
diff --git a/admin/bin/pry-remote b/admin/bin/pry-remote
new file mode 100755
index 000000000..f264d2fcd
--- /dev/null
+++ b/admin/bin/pry-remote
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'pry-remote' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('pry-remote', 'pry-remote')
diff --git a/admin/bin/puma b/admin/bin/puma
new file mode 100755
index 000000000..d24478bed
--- /dev/null
+++ b/admin/bin/puma
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'puma' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('puma', 'puma')
diff --git a/admin/bin/pumactl b/admin/bin/pumactl
new file mode 100755
index 000000000..f3f7b2bd1
--- /dev/null
+++ b/admin/bin/pumactl
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'pumactl' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('puma', 'pumactl')
diff --git a/admin/bin/rackup b/admin/bin/rackup
new file mode 100755
index 000000000..8cc9953e5
--- /dev/null
+++ b/admin/bin/rackup
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rackup' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rack', 'rackup')
diff --git a/admin/bin/rails b/admin/bin/rails
new file mode 100755
index 000000000..657440d20
--- /dev/null
+++ b/admin/bin/rails
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rails' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('railties', 'rails')
diff --git a/admin/bin/rake b/admin/bin/rake
new file mode 100755
index 000000000..26c7a2d5b
--- /dev/null
+++ b/admin/bin/rake
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rake' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rake', 'rake')
diff --git a/admin/bin/rdoc b/admin/bin/rdoc
new file mode 100755
index 000000000..f57260f36
--- /dev/null
+++ b/admin/bin/rdoc
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rdoc' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rdoc', 'rdoc')
diff --git a/admin/bin/recurly b/admin/bin/recurly
new file mode 100755
index 000000000..7f6362898
--- /dev/null
+++ b/admin/bin/recurly
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'recurly' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('recurly', 'recurly')
diff --git a/admin/bin/resque b/admin/bin/resque
new file mode 100755
index 000000000..2e40831b1
--- /dev/null
+++ b/admin/bin/resque
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'resque' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('resque', 'resque')
diff --git a/admin/bin/resque-scheduler b/admin/bin/resque-scheduler
new file mode 100755
index 000000000..57e4d9dd5
--- /dev/null
+++ b/admin/bin/resque-scheduler
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'resque-scheduler' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('resque-scheduler', 'resque-scheduler')
diff --git a/admin/bin/resque-web b/admin/bin/resque-web
new file mode 100755
index 000000000..b49543cd2
--- /dev/null
+++ b/admin/bin/resque-web
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'resque-web' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('resque', 'resque-web')
diff --git a/admin/bin/restclient b/admin/bin/restclient
new file mode 100755
index 000000000..4d7bdcf92
--- /dev/null
+++ b/admin/bin/restclient
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'restclient' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rest-client', 'restclient')
diff --git a/admin/bin/ri b/admin/bin/ri
new file mode 100755
index 000000000..90f2517da
--- /dev/null
+++ b/admin/bin/ri
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'ri' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rdoc', 'ri')
diff --git a/admin/bin/rspec b/admin/bin/rspec
new file mode 100755
index 000000000..0c86b5c6f
--- /dev/null
+++ b/admin/bin/rspec
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rspec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rspec-core', 'rspec')
diff --git a/admin/bin/ruby-protoc b/admin/bin/ruby-protoc
new file mode 100755
index 000000000..b8e8f49f6
--- /dev/null
+++ b/admin/bin/ruby-protoc
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'ruby-protoc' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('ruby-protocol-buffers', 'ruby-protoc')
diff --git a/admin/bin/rubygems-cabin-test b/admin/bin/rubygems-cabin-test
new file mode 100755
index 000000000..3b1515059
--- /dev/null
+++ b/admin/bin/rubygems-cabin-test
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rubygems-cabin-test' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('cabin', 'rubygems-cabin-test')
diff --git a/admin/bin/sass b/admin/bin/sass
new file mode 100755
index 000000000..d65bb10a3
--- /dev/null
+++ b/admin/bin/sass
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'sass' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('sass', 'sass')
diff --git a/admin/bin/sass-convert b/admin/bin/sass-convert
new file mode 100755
index 000000000..ddde743f3
--- /dev/null
+++ b/admin/bin/sass-convert
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'sass-convert' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('sass', 'sass-convert')
diff --git a/admin/bin/scss b/admin/bin/scss
new file mode 100755
index 000000000..9f5e435d6
--- /dev/null
+++ b/admin/bin/scss
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'scss' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('sass', 'scss')
diff --git a/admin/bin/slimrb b/admin/bin/slimrb
new file mode 100755
index 000000000..d9152e290
--- /dev/null
+++ b/admin/bin/slimrb
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'slimrb' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('slim', 'slimrb')
diff --git a/admin/bin/sprockets b/admin/bin/sprockets
new file mode 100755
index 000000000..09a1ad185
--- /dev/null
+++ b/admin/bin/sprockets
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'sprockets' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('sprockets', 'sprockets')
diff --git a/admin/bin/thor b/admin/bin/thor
new file mode 100755
index 000000000..8421e001e
--- /dev/null
+++ b/admin/bin/thor
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'thor' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('thor', 'thor')
diff --git a/admin/bin/tilt b/admin/bin/tilt
new file mode 100755
index 000000000..09fe73eb3
--- /dev/null
+++ b/admin/bin/tilt
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'tilt' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('tilt', 'tilt')
diff --git a/admin/bin/tt b/admin/bin/tt
new file mode 100755
index 000000000..6e3920b8c
--- /dev/null
+++ b/admin/bin/tt
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'tt' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('treetop', 'tt')
diff --git a/admin/bin/unicorn b/admin/bin/unicorn
new file mode 100755
index 000000000..5e0e14fa9
--- /dev/null
+++ b/admin/bin/unicorn
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'unicorn' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('unicorn', 'unicorn')
diff --git a/admin/bin/unicorn_rails b/admin/bin/unicorn_rails
new file mode 100755
index 000000000..93e1c8bf3
--- /dev/null
+++ b/admin/bin/unicorn_rails
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'unicorn_rails' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('unicorn', 'unicorn_rails')
diff --git a/db/manifest b/db/manifest
index be8d928cd..42496dba9 100755
--- a/db/manifest
+++ b/db/manifest
@@ -304,4 +304,6 @@ jam_track_searchability.sql
harry_fox_agency.sql
jam_track_slug.sql
mixdown.sql
+aac_master.sql
+video_recording.sql
jam_track_lang_idx.sql
diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql
new file mode 100644
index 000000000..28f67f81d
--- /dev/null
+++ b/db/up/aac_master.sql
@@ -0,0 +1,3 @@
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint;
\ No newline at end of file
diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql
new file mode 100644
index 000000000..96a5374b1
--- /dev/null
+++ b/db/up/crash_dumps_2.sql
@@ -0,0 +1,6 @@
+ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255);
+ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000);
+ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100);
+ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100);
+ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey;
+
diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql
index 5adb15684..24295cdae 100644
--- a/db/up/mixdown.sql
+++ b/db/up/mixdown.sql
@@ -49,4 +49,13 @@ ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) RE
ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE;
-ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
\ No newline at end of file
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE;
+ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
+
+CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued);
+CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at);
+CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at);
+
+CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued);
+CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at);
+CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at);
diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql
new file mode 100644
index 000000000..44a976d30
--- /dev/null
+++ b/db/up/video_recording.sql
@@ -0,0 +1 @@
+ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb
index c6ef0b135..43f77bdb7 100644
--- a/ruby/lib/jam_ruby/jam_track_importer.rb
+++ b/ruby/lib/jam_ruby/jam_track_importer.rb
@@ -34,6 +34,70 @@ module JamRuby
end
+ def synchronize_preview_dev(jam_track)
+ jam_track.jam_track_tracks.each do |track|
+ next if track.track_type != 'Master'
+
+
+ most_recent_aac = nil
+ most_recent_ogg = nil
+ most_recent_mp3 = nil
+ public_jamkazam_s3_manager.list_files(track.preview_directory).each do |s3_preview_item|
+
+ s3_object = public_jamkazam_s3_manager.object(s3_preview_item)
+
+ if s3_preview_item.end_with?('.aac')
+ if most_recent_aac
+ if s3_object.last_modified > most_recent_aac.last_modified
+ most_recent_aac = s3_object
+ end
+ else
+ most_recent_aac = s3_object
+ end
+ end
+
+ if s3_preview_item.end_with?('.mp3')
+ if most_recent_mp3
+ if s3_object.last_modified > most_recent_mp3.last_modified
+ most_recent_mp3 = s3_object
+ end
+ else
+ most_recent_mp3 = s3_object
+ end
+ end
+
+ if s3_preview_item.end_with?('.ogg')
+ if most_recent_ogg
+ if s3_object.last_modified > most_recent_ogg.last_modified
+ most_recent_ogg = s3_object
+ end
+ else
+ most_recent_ogg = s3_object
+ end
+ end
+ end
+
+ if most_recent_aac
+ track['preview_aac_md5'] = 'md5'
+ track['preview_aac_url'] = most_recent_aac.key
+ track['preview_aac_length'] = most_recent_aac.content_length
+ end
+
+ if most_recent_mp3
+ track['preview_mp3_md5'] = 'md5'
+ track['preview_mp3_url'] = most_recent_mp3.key
+ track['preview_mp3_length'] = most_recent_mp3.content_length
+ end
+
+ if most_recent_ogg
+ track['preview_md5'] = 'md5'
+ track['preview_url'] = most_recent_ogg.key
+ track['preview_length'] = most_recent_ogg.content_length
+ end
+
+ track.save
+ end
+ end
# this method was created due to Tency-sourced data having no master track
# it goes through all audio tracks, and creates a master mix from it. (mix + normalize)
def create_master(metadata, metalocation)
@@ -116,7 +180,6 @@ module JamRuby
end
-
temp_file = File.join(tmp_dir, "temp.wav")
output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav")
output_file = File.join(tmp_dir, output_filename)
@@ -149,7 +212,7 @@ module JamRuby
# now we need to upload the output back up
s3_target = audio_path + '/' + output_filename
@@log.debug("uploading #{output_file} to #{s3_target}")
- JamTrackImporter.song_storage_manager.upload(s3_target, output_file )
+ JamTrackImporter.song_storage_manager.upload(s3_target, output_file)
finish('success', nil)
end
@@ -195,7 +258,7 @@ module JamRuby
meta[:licensor] = vendor
- File.open(meta_yml, 'w') {|f| f.write meta.to_yaml }
+ File.open(meta_yml, 'w') { |f| f.write meta.to_yaml }
jamkazam_s3_manager.upload(metalocation, meta_yml)
end
@@ -338,7 +401,7 @@ module JamRuby
genres << Genre.find('asian')
else
found = Genre.find_by_id(genre)
- genres << found if found
+ genres << found if found
end
end
@@ -1166,7 +1229,7 @@ module JamRuby
total_time = `#{total_time_command}`.to_f
result_code = -20
- stripped_time = total_time # default to the case where we just start the preview at the beginning
+ stripped_time = total_time # default to the case where we just start the preview at the beginning
burp_gaps.each do |gap|
command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%"
@@ -1218,6 +1281,53 @@ module JamRuby
end
+ def synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_digest)
+ begin
+ aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac')
+ convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\""
+ @@log.debug("converting to aac using: " + convert_aac_cmd)
+
+ convert_output = `#{convert_aac_cmd}`
+
+ aac_digest = ::Digest::MD5.file(aac_44100)
+
+ track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest
+
+ # upload 44100 aac to public location
+ @@log.debug("uploading aac preview to #{track.preview_filename('aac')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
+
+
+ track.skip_uploader = true
+
+ original_aac_preview_url = track["preview_aac_url"]
+
+ # and finally update the JamTrackTrack with the new info
+ track["preview_aac_url"] = track.preview_filename(aac_md5, 'aac')
+ track["preview_aac_length"] = File.new(aac_44100).size
+ track["preview_start_time"] = 0
+
+ if !track.save
+ finish("save_master_preview", track.errors.to_s)
+ return false
+ end
+
+ # if all that worked, now delete old previews, if present
+ begin
+ public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
+ rescue
+ puts "UNABLE TO CLEANUP OLD PREVIEW URL"
+ end
+ rescue Exception => e
+ finish("sync_master_preview_exception", e.to_s)
+ return false
+ end
+
+
+ return true
+
+ end
+
def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest)
begin
@@ -1229,20 +1339,33 @@ module JamRuby
mp3_digest = ::Digest::MD5.file(mp3_44100)
+ aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac')
+ convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\""
+ @@log.debug("converting to aac using: " + convert_aac_cmd)
+
+ convert_output = `#{convert_aac_cmd}`
+
+ aac_digest = ::Digest::MD5.file(aac_44100)
+
+
track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
+ track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest
- # upload 44100 ogg and mp3 to public location as well
+ # upload 44100 ogg, mp3, aac to public location as well
@@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}")
public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}")
public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
+ @@log.debug("uploading aac preview to #{track.preview_filename('aac')}")
+ public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest)
track.skip_uploader = true
original_ogg_preview_url = track["preview_url"]
original_mp3_preview_url = track["preview_mp3_url"]
+ original_aac_preview_url = track["preview_aac_url"]
# and finally update the JamTrackTrack with the new info
track["preview_url"] = track.preview_filename(ogg_md5, 'ogg')
@@ -1250,6 +1373,8 @@ module JamRuby
# and finally update the JamTrackTrack with the new info
track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3')
track["preview_mp3_length"] = File.new(mp3_44100).size
+ track["preview_aac_url"] = track.preview_filename(aac_md5, 'mp3')
+ track["preview_aac_length"] = File.new(aac_44100).size
track["preview_start_time"] = 0
if !track.save
@@ -1261,6 +1386,7 @@ module JamRuby
begin
public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"]
public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
+ public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
end
@@ -1499,13 +1625,13 @@ module JamRuby
CSV.open("only_in_s3.csv", "wb") do |csv|
only_in_s3.each do |song_id|
- csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ]
+ csv << [song_id, in_s3[song_id][:artist], in_s3[song_id][:song]]
end
end
CSV.open("only_in_2k_selection.csv", "wb") do |csv|
only_in_mapping.each do |song_id|
- csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ]
+ csv << [song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song]]
end
end
@@ -1518,6 +1644,7 @@ module JamRuby
break
end
end
+
def create_masters
iterate_song_storage do |metadata, metalocation|
next if metadata.nil?
@@ -1584,6 +1711,38 @@ module JamRuby
importer
end
+ # hunts for the most recent .aac, .mp3, or .ogg file
+ def synchronize_preview_dev(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ importer.synchronize_preview_dev(jam_track)
+
+ importer.finish('success', nil)
+ importer
+ end
+
+ def synchronize_jamtrack_aac_preview(jam_track)
+ importer = JamTrackImporter.new
+ importer.name = jam_track.name
+
+ track = jam_track.master_track
+
+ if track
+ Dir.mktmpdir do |tmp_dir|
+ ogg_44100 = File.join(tmp_dir, 'input.ogg')
+ private_s3_manager.download(track.url_by_sample_rate(44), ogg_44100)
+ ogg_44100_digest = ::Digest::MD5.file(ogg_44100)
+ if importer.synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_44100_digest)
+ importer.finish("success", nil)
+ end
+ end
+ else
+ importer.finish('no_master_track', nil)
+ end
+ importer
+ end
+
def synchronize_jamtrack_master_preview(jam_track)
importer = JamTrackImporter.new
importer.name = jam_track.name
@@ -1606,6 +1765,30 @@ module JamRuby
importer
end
+ def synchronize_previews_dev
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_preview_dev(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "no_preview_start_time"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+ end
+ end
def synchronize_previews
importers = []
@@ -1632,6 +1815,33 @@ module JamRuby
end
end
+ def synchronize_jamtrack_aac_previews
+
+ importers = []
+
+ JamTrack.all.each do |jam_track|
+ importers << synchronize_jamtrack_aac_preview(jam_track)
+ end
+
+ @@log.info("SUMMARY")
+ @@log.info("-------")
+ importers.each do |importer|
+ if importer
+ if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing"
+ @@log.info("#{importer.name} #{importer.reason}")
+ else
+ @@log.error("#{importer.name} failed to import.")
+ @@log.error("#{importer.name} reason=#{importer.reason}")
+ @@log.error("#{importer.name} detail=#{importer.detail}")
+ end
+ else
+ @@log.error("NULL IMPORTER")
+ end
+
+
+ end
+ end
+
def synchronize_jamtrack_master_previews
importers = []
@@ -1883,11 +2093,11 @@ module JamRuby
genre4 = value[:genre4]
genre5 = value[:genre5]
- genres << genre1.downcase.strip if genre1
- genres << genre2.downcase.strip if genre2
- genres << genre3.downcase.strip if genre3
- genres << genre4.downcase.strip if genre4
- genres << genre5.downcase.strip if genre5
+ genres << genre1.downcase.strip if genre1
+ genres << genre2.downcase.strip if genre2
+ genres << genre3.downcase.strip if genre3
+ genres << genre4.downcase.strip if genre4
+ genres << genre5.downcase.strip if genre5
value[:genres] = genres
end
diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb
index cf86fdc9b..3398a6a3c 100644
--- a/ruby/lib/jam_ruby/lib/s3_manager.rb
+++ b/ruby/lib/jam_ruby/lib/s3_manager.rb
@@ -121,6 +121,10 @@ module JamRuby
s3_bucket.objects[filename].exists?
end
+ def object(filename)
+ s3_bucket.objects[filename]
+ end
+
def length(filename)
s3_bucket.objects[filename].content_length
end
diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb
index 6c4e54d84..13087fed0 100644
--- a/ruby/lib/jam_ruby/models/crash_dump.rb
+++ b/ruby/lib/jam_ruby/models/crash_dump.rb
@@ -15,7 +15,7 @@ module JamRuby
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
- self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
+ self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}"
end
def user_email
diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb
index a20a8f0cc..417e0c92f 100644
--- a/ruby/lib/jam_ruby/models/jam_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track.rb
@@ -451,7 +451,7 @@ module JamRuby
end
def mixdowns_for_user(user)
- JamTrackMixdown.where(user_id: user.id, jam_track_id: self.id).order('created_at DESC')
+ JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id)
end
def short_plan_code
@@ -466,7 +466,6 @@ module JamRuby
def generate_slug
self.slug = sluggarize(original_artist) + '-' + sluggarize(name)
-
end
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb
index 19d6be638..71a2ca0a1 100644
--- a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb
@@ -16,7 +16,7 @@ module JamRuby
validates :jam_track, presence: true
validates :settings, presence: true
- validates_uniqueness_of :name, scope: :jam_track_id
+ validates_uniqueness_of :name, scope: :user_id
validate :verify_settings
validate :verify_max_mixdowns
@@ -49,10 +49,13 @@ module JamRuby
# the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do
- tweaked = false
- all_quiet = true
-
parsed = JSON.parse(self.settings)
+ specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0
+
+ tweaked = false
+ all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track'
+
+
if parsed["speed"]
tweaked = true
end
@@ -83,7 +86,7 @@ module JamRuby
if all_quiet
errors.add(:settings, 'are all muted')
end
- if !tweaked
+ if !tweaked && !parsed['full']
errors.add(:settings, 'have nothing specified')
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb
index 4233fcfe9..687727f4c 100644
--- a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb
@@ -37,19 +37,15 @@ module JamRuby
MAX_JAM_TRACK_DOWNLOADS = 1000
- def estimated_queue_time
+ def self.estimated_queue_time
jam_track_signing_count = JamTrackRight.where(queued: true).count
- mixdowns = JamTrackMixdownPackage.select('count(queued) as queue_count, count(speed_pitched) as speed_pitch_count').where(queued: true).first
- total_mixdowns = mixdowns['queue_count']
- slow_mixdowns = mixdowns['speed_pitch_count']
+ mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).first
+ total_mixdowns = mixdowns['queue_count'].to_i
+ slow_mixdowns = mixdowns['speed_pitch_count'].to_i
fast_mixdowns = total_mixdowns - slow_mixdowns
guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns
- # knock off about a minute based on number of nodes
- guess = guess - ((APP_CONFIG.num_signing_nodes - 1) * 60)
- guess = 0 if guess < 0
-
Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns})
guess
end
@@ -66,6 +62,7 @@ module JamRuby
def self.create(mixdown, file_type, sample_rate, encrypt_type)
package = JamTrackMixdownPackage.new
+ package.speed_pitched = mixdown.will_pitch_shift?
package.jam_track_mixdown = mixdown
package.file_type = file_type
package.sample_rate = sample_rate
@@ -148,11 +145,14 @@ module JamRuby
self.signing_queued_at = Time.now
self.signing_started_at = nil
self.last_signed_at = nil
+ self.queued = true
self.save
+ queue_time = JamTrackMixdownPackage.estimated_queue_time
+
# is_pitch_speed_shifted?
Resque.enqueue(JamTrackMixdownPackager, self.id)
- true
+ return queue_time
rescue Exception => e
puts "e: #{e}"
# implies redis is down. we don't update started_at by bailing out here
@@ -166,8 +166,7 @@ module JamRuby
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
else
- enqueue
- true
+ return enqueue
end
end
@@ -233,10 +232,10 @@ module JamRuby
def self.stats
stats = {}
- result = JamTrackMixdownPackage.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count').first
+ result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count')
- stats['count'] = result['total'].to_i
- stats['signing_count'] = result['signing_count'].to_i
+ stats['count'] = result[0]['total'].to_i
+ stats['signing_count'] = result[0]['signing_count'].to_i
stats
end
diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb
index 12c8a2d7e..f48ceb498 100644
--- a/ruby/lib/jam_ruby/models/jam_track_right.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_right.rb
@@ -66,6 +66,7 @@ module JamRuby
def finish_errored(error_reason, error_detail, sample_rate)
self.last_signed_at = Time.now
+ self.queued = false
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
@@ -85,6 +86,7 @@ module JamRuby
def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now
+ self.queued = false
if bitrate==48
self.length_48 = length
self.md5_48 = md5
@@ -120,7 +122,7 @@ module JamRuby
def enqueue(sample_rate=48)
begin
- JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
+ JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil, :queued => true)
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e
diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb
index c0548e6fd..16f00cd21 100644
--- a/ruby/lib/jam_ruby/models/jam_track_track.rb
+++ b/ruby/lib/jam_ruby/models/jam_track_track.rb
@@ -49,7 +49,11 @@ module JamRuby
# md5-'ed because we cache forever
def preview_filename(md5, ext='ogg')
original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}"
- "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}"
+ "#{preview_directory}/#{original_name}"
+ end
+
+ def preview_directory
+ "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}"
end
def has_preview?
@@ -58,7 +62,16 @@ module JamRuby
# generates a URL that points to a public version of the preview
def preview_public_url(media_type='ogg')
- url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url]
+ case media_type
+ when 'ogg'
+ url = self[:preview_url]
+ when 'mp3'
+ url = self[:preview_mp3_url]
+ when 'aac'
+ url = self[:preview_aac_url]
+ else
+ raise "unknown media_type #{media_type}"
+ end
if url
s3_public_manager.public_url(url,{ :secure => true})
else
@@ -154,6 +167,7 @@ module JamRuby
# input is the original ogg file for the track. tmp_dir is where this code can safely generate output stuff and have it cleaned up later
def process_preview(input, tmp_dir)
+ raise "Does not include AAC generation. Must be updated before used."
uuid = SecureRandom.uuid
output = File.join(tmp_dir, "#{uuid}.ogg")
output_mp3 = File.join(tmp_dir, "#{uuid}.mp3")
@@ -176,7 +190,6 @@ module JamRuby
# now create mp3 off of ogg preview
convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\""
-
@@log.debug("converting to mp3 using: " + convert_mp3_cmd)
convert_output = `#{convert_mp3_cmd}`
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index 99f21c09e..1ec43b9aa 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -205,7 +205,7 @@ module JamRuby
end
# Start recording a session.
- def self.start(music_session, owner)
+ def self.start(music_session, owner, record_video: false)
recording = nil
# Use a transaction and lock to avoid races.
music_session.with_lock do
@@ -213,6 +213,7 @@ module JamRuby
recording.music_session = music_session
recording.owner = owner
recording.band = music_session.band
+ recording.video = record_video
if recording.save
GoogleAnalyticsEvent.report_band_recording(recording.band)
diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb
index 20b13dc2b..1d54bf67d 100644
--- a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb
+++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb
@@ -73,6 +73,7 @@ module JamRuby
@mixdown_package.signing = true
@mixdown_package.should_retry = false
@mixdown_package.last_step_at = last_step_at
+ @mixdown_package.queued = false
@mixdown_package.save
SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
index 359bdc514..381bb8b83 100644
--- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
+++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb
@@ -42,7 +42,7 @@ module JamRuby
signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44
signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44
last_step_at = Time.now
- JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true)
+ JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true, queued: false)
# because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
@jam_track_right.current_packaging_step = 0
@jam_track_right.packaging_steps = total_steps
@@ -50,6 +50,7 @@ module JamRuby
@jam_track_right[signing_state_symbol] = true
@jam_track_right.should_retry = false
@jam_track_right.last_step_at = Time.now
+ @jam_track_right.queued = false
SubscriptionMessage.jam_track_signing_job_change(@jam_track_right)
JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate)
diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
index 5039fc862..bf0942cd6 100644
--- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
+++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb
@@ -24,6 +24,11 @@ module JamRuby
def perform
# this needs more testing
+
+ # let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough
+ JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
+ JamTrackMixdownPackage.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false)
+
return
#JamTrackRight.ready_to_clean.each do |jam_track_right|
# log.debug("deleting files for jam_track_right #{jam_track_right.id}")
diff --git a/ruby/spec/jam_ruby/models/#jam_track_mixdown_spec.rb# b/ruby/spec/jam_ruby/models/#jam_track_mixdown_spec.rb#
new file mode 100644
index 000000000..848dc0b29
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/#jam_track_mixdown_spec.rb#
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe JamTrackMixdown do
+
+ let(:user) {FactoryGirl.create(:user)}
+ let(:jam_track) {FactoryGirl.create(:jam_track)}
+ let(:settings) { {speed:5} }
+
+ it "can be created (factory girl)" do
+ mixdown = FactoryGirl.create(:jam_track_mixdown)
+
+ mixdown = JamTrackMixdown.find(mixdown.id)
+ mixdown.settings.should eq('{"speed":5}')
+ end
+
+ it "can be created" do
+ mixdown = JamTrackMixdown.create('abc', 'description', user, jam_track, settings)
+ mixdown.errors.any?.should == false
+ end
+
+ it "index" do
+ query, start, count = JamTrackMixdown.index({id: jam_track}, user)
+
+ query.length.should eq(0)
+ start.should be_nil
+ count.should eq(0)
+
+ mixdown = FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track)
+
+ query, start, count = JamTrackMixdown.index({id: jam_track}, user)
+ query[0].should eq(mixdown)
+ start.should be_nil
+ count.should eq(1)
+ end
+
+ describe "settings" do
+ it "validates empty settings" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["have nothing specified"])
+ end
+
+ it "validates speed numeric" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed": "5"}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer speed"])
+ end
+
+ it "validates pitch numeric" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch": "5"}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer pitch"])
+ end
+
+ it "validates speed not-float" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed": 5.5}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer speed"])
+ end
+
+ it "validates pitch not-float" do
+ invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch": 10.5}.to_json)
+ invalid.save
+ invalid.errors.any?.should be_true
+ invalid.errors["settings"].should eq(["has non-integer pitch"])
+ end
+ end
+
+
+ mixdown.settings.should eq('{}')
+ end
+
+ it "can be created" do
+ mixdown = JamTrackMixdown.create('abc', user, jam_track, {})
+ mixdown.errors.any?.should == false
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb
index 53d14b5e2..3a90ffcc3 100644
--- a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb
+++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb
@@ -12,7 +12,6 @@ describe JamTrackMixdownPackage do
package = JamTrackMixdownPackage.create(mixdown, JamTrackMixdownPackage::FILE_TYPE_OGG, 48, 'jkz')
- puts package.errors.inspect
package.errors.any?.should == false
end
@@ -34,12 +33,12 @@ describe JamTrackMixdownPackage do
end
it "signing" do
- package = FactoryGirl.create(:jam_track_mixdown_package, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing:true, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
package.signing_state.should eq('SIGNING')
end
it "signing timeout" do
- package = FactoryGirl.create(:jam_track_mixdown_package, signing_started_at: Time.now - 100, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing: true, signing_started_at: Time.now - (APP_CONFIG.signing_job_signing_max_time + 1), packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
package.signing_state.should eq('SIGNING_TIMEOUT')
end
@@ -49,7 +48,7 @@ describe JamTrackMixdownPackage do
end
it "signing timeout" do
- package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.signing_job_queue_max_time + 1))
+ package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.mixdown_job_queue_max_time + 1))
package.signing_state.should eq('QUEUED_TIMEOUT')
end
end
@@ -74,6 +73,30 @@ describe JamTrackMixdownPackage do
end
describe "estimated_queue_time" do
+ it "succeeds with no data" do
+ JamTrackMixdownPackage.estimated_queue_time.should eq(0)
+ end
+
+ it "mixdown packages of different sorts" do
+ package = FactoryGirl.create(:jam_track_mixdown_package, speed_pitched: true)
+ JamTrackMixdownPackage.estimated_queue_time.should eq(0)
+
+ package.queued = true
+ package.save!
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_slow_mixdown_time * 1)
+
+ package.speed_pitched = false
+ package.save!
+
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
+
+ right = FactoryGirl.create(:jam_track_right)
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
+
+ right.queued = true
+ right.save!
+ JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1 + APP_CONFIG.estimated_jam_track_time * 1)
+ end
end
end
diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb
index ad6f0b2c1..d909b57d2 100644
--- a/ruby/spec/support/utilities.rb
+++ b/ruby/spec/support/utilities.rb
@@ -179,7 +179,7 @@ def app_config
end
def signing_job_queue_max_time
- 20 # 20 seconds
+ 600 # 20 seconds
end
def one_free_jamtrack_per_user
@@ -226,6 +226,18 @@ def app_config
2
end
+ def signing_job_signing_max_time
+ 300
+ end
+
+ def mixdown_job_queue_max_time
+ 600
+ end
+
+ def mixdown_step_max_time
+ 300
+ end
+
private
def audiomixer_workspace_path
diff --git a/web/Gemfile b/web/Gemfile
index d34a1d2a8..49217de8a 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -99,7 +99,7 @@ gem 'react-rails-img'
source 'https://rails-assets.org' do
gem 'rails-assets-reflux'
gem 'rails-assets-classnames'
- gem 'rails-assets-react-select'
+ gem 'rails-assets-react-select', '0.6.7'
end
#group :development, :production do
diff --git a/web/app/assets/images/content/icon-delete.png b/web/app/assets/images/content/icon-delete.png
new file mode 100644
index 000000000..cae694fba
Binary files /dev/null and b/web/app/assets/images/content/icon-delete.png differ
diff --git a/web/app/assets/images/content/icon-delete@2X.png b/web/app/assets/images/content/icon-delete@2X.png
new file mode 100644
index 000000000..b120befa5
Binary files /dev/null and b/web/app/assets/images/content/icon-delete@2X.png differ
diff --git a/web/app/assets/images/content/icon-edit.png b/web/app/assets/images/content/icon-edit.png
new file mode 100644
index 000000000..26ed5892f
Binary files /dev/null and b/web/app/assets/images/content/icon-edit.png differ
diff --git a/web/app/assets/images/content/icon-edit@2X.png b/web/app/assets/images/content/icon-edit@2X.png
new file mode 100644
index 000000000..7cd9052d5
Binary files /dev/null and b/web/app/assets/images/content/icon-edit@2X.png differ
diff --git a/web/app/assets/images/content/icon-mix-fail@2X.png b/web/app/assets/images/content/icon-mix-fail@2X.png
new file mode 100644
index 000000000..265022204
Binary files /dev/null and b/web/app/assets/images/content/icon-mix-fail@2X.png differ
diff --git a/web/app/assets/images/content/icon-play.png b/web/app/assets/images/content/icon-play.png
new file mode 100644
index 000000000..ecb26e78c
Binary files /dev/null and b/web/app/assets/images/content/icon-play.png differ
diff --git a/web/app/assets/images/content/icon-retry@2X.png b/web/app/assets/images/content/icon-retry@2X.png
new file mode 100644
index 000000000..584a0dca1
Binary files /dev/null and b/web/app/assets/images/content/icon-retry@2X.png differ
diff --git a/web/app/assets/images/content/icon-save@2X.png b/web/app/assets/images/content/icon-save@2X.png
new file mode 100644
index 000000000..ac80ef9c4
Binary files /dev/null and b/web/app/assets/images/content/icon-save@2X.png differ
diff --git a/web/app/assets/images/content/icon_open@2X.png b/web/app/assets/images/content/icon_open@2X.png
new file mode 100644
index 000000000..1d188a2ea
Binary files /dev/null and b/web/app/assets/images/content/icon_open@2X.png differ
diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js
index ee570a513..fb16ffa71 100644
--- a/web/app/assets/javascripts/JamServer.js
+++ b/web/app/assets/javascripts/JamServer.js
@@ -23,6 +23,7 @@
var mode = null;
// heartbeat
+ var startHeartbeatTimeout = null;
var heartbeatInterval = null;
var heartbeatMS = null;
var connection_expire_time = null;
@@ -79,6 +80,7 @@
function initiateReconnect(activeElementVotes, in_error) {
var initialConnect = !!activeElementVotes;
+ console.log("activeElementVotes", activeElementVotes)
freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true));
if (in_error) {
@@ -104,6 +106,12 @@
heartbeatInterval = null;
}
+ // stop the heartbeat start delay from happening
+ if (startHeartbeatTimeout != null) {
+ clearTimeout(startHeartbeatTimeout);
+ startHeartbeatTimeout = null;
+ }
+
// stop checking for heartbeat acks
if (heartbeatAckCheckInterval != null) {
clearTimeout(heartbeatAckCheckInterval);
@@ -236,9 +244,41 @@
heartbeatMS = payload.heartbeat_interval * 1000;
connection_expire_time = payload.connection_expire_time * 1000;
logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's');
- heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
- heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
- lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
+
+ // add some randomness to help move heartbeats apart from each other
+
+ // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time
+ var randomStartTime = connection_expire_time * (Math.random() / 2)
+
+ if (startHeartbeatTimeout) {
+ logger.warn("start heartbeat timeout is active; should be null")
+ clearTimeout(startHeartbeatTimeout)
+ }
+
+ if (heartbeatInterval != null) {
+ logger.warn("heartbeatInterval is active; should be null")
+ clearInterval(heartbeatInterval);
+ heartbeatInterval = null;
+ }
+
+ if (heartbeatAckCheckInterval != null) {
+ logger.warn("heartbeatAckCheckInterval is active; should be null")
+ clearInterval(heartbeatAckCheckInterval);
+ heartbeatAckCheckInterval = null;
+ }
+
+ startHeartbeatTimeout = setTimeout(function() {
+ if(server.connected) {
+ heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
+ heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
+ lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
+ }
+ }, randomStartTime)
+
+ logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's')
+
+
+
connectDeferred.resolve();
$self.triggerHandler(EVENTS.CONNECTION_UP)
@@ -295,6 +335,11 @@
logger.debug(payload.error_code + ": no longer reconnecting")
server.noReconnect = true; // stop trying to log in!!
}
+ else if (payload.error_code == 'no_reconnect') {
+ logger.debug(payload.error_code + ": no longer reconnecting")
+ server.noReconnect = true; // stop trying to log in!!
+ context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.")
+ }
}
///////////////////
@@ -384,7 +429,7 @@
}
function formatDelaySecs(secs) {
- return $('' + secs + ' ' + (secs == 1 ? ' second.s ' : 'seconds.') + ' ');
+ return $('' + secs + ' ' + (secs == 1 ? ' second.s ' : 'seconds.') + ' ');
}
function setCountdown($parent) {
diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
index 08785bc50..5793d7941 100644
--- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee
+++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee
@@ -19,7 +19,7 @@ context.JK.AccountJamTracks = class AccountJamTracks
@screen = $('#account-jamtracks')
beforeShow:() =>
- rest.getPurchasedJamTracks({})
+ rest.getPurchasedJamTracks({limit: 40})
.done(@populateJamTracks)
.fail(@app.ajaxError);
diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
index cceaaedc2..c9440dd80 100644
--- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
+++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js
@@ -13,6 +13,13 @@
// remove all display errors
$('#recording-finished-dialog form .error-text').remove()
$('#recording-finished-dialog form .error').removeClass("error")
+ console.log("save video?", recording)
+ if(recording.video) {
+ $dialog.find('.save-video').show()
+ }
+ else {
+ $dialog.find('.save-video').hide()
+ }
removeGoogleLoginErrors()
}
@@ -103,6 +110,14 @@
}
function afterHide() {
+ if(recording && recording.video) {
+ var name = $('#recording-finished-dialog form input[name=name]').val();
+ name = name.replace(/[^A-Za-z0-9\-\ ]/g, '');
+ var keep = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
+ logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keep=" + keep)
+ context.jamClient.VideoDecision(recording.id, name, keep)
+ }
+
recording = null;
playbackControls.stopMonitor();
context.jamClient.ClosePreviewRecording();
diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js
index 882c847c5..ec36d0a03 100644
--- a/web/app/assets/javascripts/playbackControls.js
+++ b/web/app/assets/javascripts/playbackControls.js
@@ -39,6 +39,7 @@
var $self = $(this);
+ var disabled = false;
var playbackPlaying = false;
var playbackDurationMs = 0;
var playbackPositionMs = 0;
@@ -173,6 +174,11 @@
}
$playButton.on('click', function (e) {
+
+ if (disabled) {
+ logger.debug("PlaybackControls are disabled; ignoring start button")
+ return;
+ }
startPlay();
return false;
});
@@ -484,6 +490,10 @@
playbackPlaying = false;
}
+ function setDisabled(_disabled) {
+ disabled = _disabled;
+ }
+
this.update = update;
this.setPlaybackMode = setPlaybackMode;
this.startMonitor = startMonitor;
@@ -492,6 +502,7 @@
this.onPlayStopEvent = onPlayStopEvent;
this.onPlayStartEvent = onPlayStartEvent;
this.onPlayPauseEvent = onPlayPauseEvent;
+ this.setDisabled = setDisabled;
return this;
}
diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee
index fed32bea6..ed241cf9e 100644
--- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee
@@ -19,9 +19,11 @@ MixerActions = reactContext.MixerActions
MediaPlaybackStore = reactContext.MediaPlaybackStore
SessionActions = reactContext.SessionActions
MediaPlaybackActions = reactContext.MediaPlaybackActions
+JamTrackStore = reactContext.JamTrackStore
mixins.push(Reflux.listenTo(MixerStore,"onInputsChanged"))
mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
+mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged'))
@MediaControls = React.createClass({
@@ -29,6 +31,10 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
mixins: mixins
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 ]
+ onJamTrackStateChanged: (jamTrackState) ->
+ @monitorControls(@state.controls, @state.mediaSummary, jamTrackState)
+ @setState({jamTrackState: jamTrackState})
+
onMediaStateChanged: (changes) ->
if changes.playbackStateChanged
if @state.controls?
@@ -53,7 +59,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
mediaSummary = mixers.mediaSummary
metro = mixers.metro
- @monitorControls(@state.controls, mediaSummary)
+ @monitorControls(@state.controls, mediaSummary, @state.jamTrackState)
@setState({mediaSummary: mediaSummary, metro: metro})
@updateMetronomeDetails(metro, @state.initializedMetronomeControls)
@@ -69,10 +75,10 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
logger.debug("settingcricket", mode)
$root.find('#metronome-playback-select').metronomeSetPlaybackMode(mode)
- monitorControls: (controls, mediaSummary) ->
+ monitorControls: (controls, mediaSummary, jamTrackState) ->
- if mediaSummary.mediaOpen || mediaSummary.jamTrack?
- if mediaSummary.jamTrack?
+ if mediaSummary.mediaOpen || mediaSummary.jamTrack? || jamTrackState?.jamTrack?
+ if mediaSummary.jamTrackOpen? || mediaSummary.jamTrack? || jamTrackState?.jamTrack?
controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK)
else if mediaSummary.backingTrackOpen
controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE)
@@ -200,13 +206,19 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
$root = jQuery(this.getDOMNode())
controls = context.JK.PlaybackControls($root, {mediaActions: MediaPlaybackActions})
+ controls.setDisabled(@props.disabled)
mediaSummary = MixerStore.mixers.mediaSummary
metro = MixerStore.mixers.metro
+ jamTrackState = JamTrackStore.getState()
- @monitorControls(controls, mediaSummary)
+ @monitorControls(controls, mediaSummary, jamTrackState)
@tryPrepareMetronome(metro)
- @setState({mediaSummary: mediaSummary, controls: controls, metro: metro})
+ @setState({mediaSummary: mediaSummary, controls: controls, metro: metro, jamTrackState: jamTrackState})
+
+ componentWillUpdate: (nextProps) ->
+
+ @state.controls.setDisabled(nextProps.disabled) if @state.controls?
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee
index 97bf2f218..9aa0ccdf0 100644
--- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee
@@ -16,8 +16,10 @@ if window.opener?
if accessOpener
+ AppActions = window.opener.AppActions
SessionActions = window.opener.SessionActions
MixerActions = window.opener.MixerActions
+ MixerStore = window.opener.MixerStore
JamTrackActions = window.opener.JamTrackActions
JamTrackMixdownActions = window.opener.JamTrackMixdownActions
#JamTrackMixdownStore = window.opener.JamTrackMixdownStore
@@ -33,10 +35,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
mixins: mixins
- onMixersChanged: (sessionMixers) ->
+ updateFromMixerHelper: (mixers, session) ->
- session = sessionMixers.session
- mixers = sessionMixers.mixers
# the backend delete/adds the metronome rapidly when the user hits play. this is custom code to deal with that
@@ -49,16 +49,20 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
metronome: mixers.metronome
recordingName: mixers.recordingName()
jamTrackName: mixers.jamTrackName()
+ jamTrackMixdown: session.jamTrackMixdown()
- @setState(media: state, downloadingJamTrack: session.downloadingJamTrack)
+ return {media: state, downloadingJamTrack: session.downloadingJamTrack}
+
+ onMixersChanged: (sessionMixers) ->
+ session = sessionMixers.session
+ mixers = sessionMixers.mixers
+
+ @setState(@updateFromMixerHelper(mixers, session))
onMediaStateChanged: (changes) ->
if changes.currentTimeChanged && @root?
@setState({time: changes.time})
- onJamTrackMixdownChanged: (changes) ->
- @setState({mixdown: changes})
-
onJamTrackChanged: (changes) ->
logger.debug("PopupMediaControls: jamtrack changed", changes)
@setState({jamTrackState: changes})
@@ -69,19 +73,32 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
SessionActions.showNativeMetronomeGui()
getInitialState: () ->
- {
+
+ if accessOpener
+
+ state = @updateFromMixerHelper(MixerStore.mixers, MixerStore.session)
+ state.jamTrackState = JamTrackStore.getState()
+ return state
+ else
+
+ return {
media: @props.media,
mixdown: @props.mixdown,
jamTrackState: @props.jamTrackState,
creatingMixdown: false,
createMixdownErrors: null,
editingMixdownId: null,
- downloadingJamTrack: @props.downloadingJamTrack
+ downloadingJamTrack: @props.downloadingJamTrack,
+ jamTrackMixdown: {}
}
close: () ->
window.close()
+ help: (e) ->
+ e.preventDefault()
+
+ AppActions.openExternalUrl($(e.target).attr('href'))
render: () ->
@@ -95,235 +112,268 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
mediaName = @state.media.recordedTracks[0].recordingName
closeLinkText = 'close recording'
header = `
{mediaType}: {mediaName} `
- else if @state.jamTrackState.jamTrack?
- jamTrack = @state.jamTrackState.jamTrack
- mediaType = "JamTrack"
- mediaName = jamTrack.name
- closeLinkText = 'CLOSE JAMTRACK'
+ else if @state.media.mediaSummary.jamTrackOpen || @state.jamTrackState.jamTrack?
+ if @state.media.mediaSummary.isOpener || @state.jamTrackState.jamTrack?
+ # if you opened the JamTrack, then you get all the good info
+ jamTrack = @state.jamTrackState.jamTrack
+ mediaType = "JamTrack"
+ mediaName = jamTrack.name
+ closeLinkText = 'CLOSE JAMTRACK'
+ helpLink = 'https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch'
+
+ selectedMixdown = jamTrack.activeMixdown
- selectedMixdown = jamTrack.activeMixdown
+ if selectedMixdown?
+ jamTrackTypeHeader = 'Custom Mix'
-
- if selectedMixdown?
- jamTrackTypeHeader = 'Custom Mix'
-
- disabled = true
- if selectedMixdown.client_state?
- switch selectedMixdown.client_state
- when 'cant_open'
+ disabled = true
+ if selectedMixdown.client_state?
+ switch selectedMixdown.client_state
+ when 'cant_open'
+ customMixName = `{selectedMixdown.name} `
+ when 'keying_timeout'
+ customMixName = `{selectedMixdown.name} `
+ when 'download_fail'
+ customMixName = `{selectedMixdown.name} `
+ when 'keying'
+ customMixName = `Loading selected mix... `
+ when 'downloading'
+ customMixName = `Loading selected mix... `
+ when 'ready'
+ customMixName = `{selectedMixdown.name} `
+ disabled = false
+ else
+ if selectedMixdown.myPackage
+ customMixName = `Creating mixdown... `
+ else
customMixName = `{selectedMixdown.name} `
- when 'keying_timeout'
- customMixName = `{selectedMixdown.name} `
- when 'download_fail'
- customMixName = `{selectedMixdown.name} `
- when 'keying'
- customMixName = `Loading selected mix... `
- when 'downloading'
- customMixName = `Loading selected mix... `
- when 'ready'
- customMixName = `{selectedMixdown.name} `
- disabled = false
+
else
- customMixName = `Creating mixdown... `
+ if SessionStore.downloadingJamTrack
+ downloader = ` `
- else
- if SessionStore.downloadingJamTrack
- downloader = ` `
+ jamTrackTypeHeader = `Full JamTrack {downloader} `
- jamTrackTypeHeader = `Full JamTrack {downloader} `
+ header = `
+
+
{mediaType}: {mediaName}
+ {jamTrackTypeHeader}
+ {customMixName}
+ `
- header = `
-
-
{mediaType}: {mediaName}
- {jamTrackTypeHeader}
- {customMixName}
- `
+ myMixes = null
+ if @state.showMyMixes
+ myMixdowns = []
- myMixes = null
- if @state.showMyMixes
- myMixdowns = []
+ boundPlayClick = this.jamTrackPlay.bind(this, jamTrack);
- boundPlayClick = this.jamTrackPlay.bind(this, jamTrack);
-
- active = jamTrack.last_mixdown_id == null
-
- myMixdowns.push `
-
-
- Full JamTrack
-
-
-
-
-
`
-
- for mixdown in jamTrack.mixdowns
- boundPlayClick = this.mixdownPlay.bind(this, mixdown);
- boundEditClick = this.mixdownEdit.bind(this, mixdown);
- boundSaveClick = this.mixdownSave.bind(this, mixdown);
- boundDeleteClick = this.mixdownDelete.bind(this, mixdown);
- boundErrorClick = this.mixdownError.bind(this, mixdown);
- boundEditKeydown = this.onEditKeydown.bind(this, mixdown);
-
- mixdown_package = mixdown.myPackage
-
- active = mixdown.id == jamTrack.last_mixdown_id
-
- editing = mixdown.id == @state.editingMixdownId
-
- # if there is a package, check it's state; otherwise let the user enqueue it
- if mixdown_package
- switch mixdown_package.signing_state
- when 'QUIET_TIMEOUT'
- action = ` `
- when 'QUIET'
- action = ` `
- when 'QUEUED'
- action = ` `
- when 'QUEUED_TIMEOUT'
- action = ` `
- when 'SIGNING'
- action = ` `
- when 'SIGNING_TIMEOUT'
- action = ` `
- when 'SIGNED'
- action = ` `
- when 'ERROR'
- action = ` `
- else
- action = ` `
-
- if editing
- mixdownName = ` `
- editIcon = ` `
- else
- mixdownName = mixdown.name
- editIcon = ` `
+ active = jamTrack.last_mixdown_id == null
myMixdowns.push `
-
-
- {mixdownName}
-
-
- {action}
+
+
+ Full JamTrack
+
+
+
+
+
`
- {editIcon}
+ for mixdown in jamTrack.mixdowns
+ boundPlayClick = this.mixdownPlay.bind(this, mixdown);
+ boundEditClick = this.mixdownEdit.bind(this, mixdown);
+ boundSaveClick = this.mixdownSave.bind(this, mixdown);
+ boundDeleteClick = this.mixdownDelete.bind(this, mixdown);
+ boundErrorClick = this.mixdownError.bind(this, mixdown);
+ boundEditKeydown = this.onEditKeydown.bind(this, mixdown);
-
-
-
`
+ mixdown_package = mixdown.myPackage
- myMixes = `{myMixdowns}
`
+ active = mixdown.id == jamTrack.last_mixdown_id
- mixControls = null
- if @state.showCustomMixes
+ editing = mixdown.id == @state.editingMixdownId
- nameClassData = {field: true}
- if @state.createMixdownErrors?
+ # if there is a package, check it's state; otherwise let the user enqueue it
+ if mixdown_package
+ switch mixdown_package.signing_state
+ when 'QUIET_TIMEOUT'
+ action = ` `
+ when 'QUIET'
+ action = ` `
+ when 'QUEUED'
+ action = ` `
+ when 'QUEUED_TIMEOUT'
+ action = ` `
+ when 'SIGNING'
+ action = ` `
+ when 'SIGNING_TIMEOUT'
+ action = ` `
+ when 'SIGNED'
+ action = ` `
+ when 'ERROR'
+ action = ` `
+ else
+ action = ` `
- errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
+ if editing
+ mixdownName = ` `
+ editIcon = ` `
+ else
+ mixdownName = mixdown.name
+ editIcon = ` `
- createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown})
- mixControls = `
-
-
Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button. Please note that changing the tempo or pitch of the JamTrack may take a long time, and won't be ready right away.
-
- Change Tempo:
-
- No change
- ------------
- Slower by 5%
- Slower by 10%
- Slower by 15%
- Slower by 20%
- Slower by 25%
- Slower by 30%
- Slower by 35%
- Slower by 40%
- Slower by 45%
- Slower by 50%
- Slower by 60%
- Slower by 70%
- Slower by 80%
- ------------
- Faster by 5%
- Faster by 10%
- Faster by 15%
- Faster by 20%
- Faster by 30%
- Faster by 40%
- Faster by 50%
-
-
-
- Change Pitch:
-
- No change
- ---------------
- Down 1 Semitone
- Down 2 Semitones
- Down 3 Semitones
- Down 4 Semitones
- Down 5 Semitones
- Down 6 Semitones
- Down 7 Semitones
- Down 8 Semitones
- Down 9 Semitones
- Down 10 Semitones
- Down 11 Semitones
- Down 12 Semitones
- ---------------
- Up 1 Semitone
- Up 2 Semitones
- Up 3 Semitones
- Up 4 Semitones
- Up 5 Semitones
- Up 6 Semitones
- Up 7 Semitones
- Up 8 Semitones
- Up 9 Semitones
- Up 10 Semitones
- Up 11 Semitones
- Up 12 Semitones
-
-
-
- Mix Name:
-
-
-
-
+ myMixdowns.push `
+
+
+ {mixdownName}
+
+
+ {action}
+ {editIcon}
+
+
+
+
`
+
+ myMixes = `
{myMixdowns}
`
+
+ mixControls = null
+ if @state.showCustomMixes
+
+ nameClassData = {field: true}
+ if @state.createMixdownErrors?
+
+ errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
+
+ createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown})
+
+ if !selectedMixdown?
+ mixControls = `
+
+
Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button. Please note that changing the tempo or pitch of the JamTrack may take a long time, and won't be ready right away.
+
+ Change Tempo:
+
+ No change
+ ------------
+ Slower by 5%
+ Slower by 10%
+ Slower by 15%
+ Slower by 20%
+ Slower by 25%
+ Slower by 30%
+ Slower by 35%
+ Slower by 40%
+ Slower by 45%
+ Slower by 50%
+ Slower by 60%
+ Slower by 70%
+ Slower by 80%
+ ------------
+ Faster by 5%
+ Faster by 10%
+ Faster by 15%
+ Faster by 20%
+ Faster by 30%
+ Faster by 40%
+ Faster by 50%
+
+
+
+ Change Pitch:
+
+ No change
+ ---------------
+ Down 1 Semitone
+ Down 2 Semitones
+ Down 3 Semitones
+ Down 4 Semitones
+ Down 5 Semitones
+ Down 6 Semitones
+ Down 7 Semitones
+ Down 8 Semitones
+ Down 9 Semitones
+ Down 10 Semitones
+ Down 11 Semitones
+ Down 12 Semitones
+ ---------------
+ Up 1 Semitone
+ Up 2 Semitones
+ Up 3 Semitones
+ Up 4 Semitones
+ Up 5 Semitones
+ Up 6 Semitones
+ Up 7 Semitones
+ Up 8 Semitones
+ Up 9 Semitones
+ Up 10 Semitones
+ Up 11 Semitones
+ Up 12 Semitones
+
+
+
+ Mix Name:
+
+
+
+
+
+
`
+ else
+
+ mixControls =
+ `
+
To create a custom mix, you must open the Full JamTrack in the My Mixes section above.
+
`
+
+ if @state.showMyMixes
+ showMyMixesText = `
hide my mixes
`
+ else
+ showMyMixesText = `
show my mixes
`
+
+ if @state.showCustomMixes
+ showMixControlsText = `
hide mix controls
`
+ else
+ showMixControlsText = `
show mix controls
`
+
+
+ extraControls = `
+
+
My Mixes {showMyMixesText}
+
+ {myMixes}
+
+ Create Custom Mix {showMixControlsText}
+
+ {mixControls}
+
+ `
+ else
+
+ mediaType = "JamTrack"
+ mediaName = @state.media.jamTrackName
+ closeLinkText = 'CLOSE JAMTRACK'
+
+ # implies we have a mixdown
+ if @state.media.jamTrackMixdown.id?
+ jamTrackTypeHeader = 'Custom Mix'
+ else
+ jamTrackTypeHeader = 'Full JamTrack'
+
+ header = `
+
+
{mediaType}: {mediaName}
+ {jamTrackTypeHeader}
+ {customMixName}
`
- if @state.showMyMixes
- showMyMixesText = 'hide my mixes'
- else
- showMyMixesText = 'show my mixes'
-
- if @state.showCustomMixes
- showMixControlsText = 'hide mix controls'
- else
- showMixControlsText = 'show mix controls'
-
-
- extraControls = `
-
-
-
- {myMixes}
-
-
-
- {mixControls}
-
-
`
+>>>>>>> develop
else if @state.media.mediaSummary.backingTrackOpen
mediaType = "Audio File"
@@ -348,11 +398,17 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
else
mediaType = ""
+ if helpLink?
+ helpButton = `
HELP `
+
`
`
windowUnloaded: () ->
@@ -436,7 +492,23 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
return unless action?
if confirm(action)
- JamTrackMixdownActions.enqueueMixdown(mixdown)
+ JamTrackMixdownActions.enqueueMixdown(mixdown, @enqueueDone)
+
+ enqueueDone: (enqueued) ->
+ @promptEstimate(enqueued)
+
+ promptEstimate: (enqueued) ->
+ time = enqueued.queue_time
+
+ if time == 0
+ alert("Your custom mix will take about 1 minute to be created.")
+ else
+ guess = Math.ceil(time / 60.0)
+ if guess == 1
+ msg = '1 minute'
+ else
+ msg = "#{guess} minutes"
+ alert("Your custom mix will take about #{msg} to be created.")
createMix: (e) ->
e.preventDefault()
@@ -478,6 +550,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
# automatically close the create custom mix area
@setState({creatingMixdown: false, showCustomMixes: false, showMyMixes: true})
+ @promptEstimate(created)
+
createMixdownFail: (jqXHR) ->
logger.debug("create mixdown fail (within PopupMediaControls)", jqXHR.status)
@setState({creatingMixdown: false})
@@ -533,13 +607,12 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
window.resizeTo(width, height + offset)
- componentWillUpdate: (nextProps, nextState) ->
-
+ computeDisableLoading: (state) ->
@disableLoading = false
return unless nextState?
- selectedMixdown = nextState?.jamTrackState?.jamTrack?.activeMixdown
+ selectedMixdown = state?.jamTrackState?.jamTrack?.activeMixdown
mixdownDownloading = false
if selectedMixdown?
@@ -552,5 +625,10 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
@disableLoading = SessionStore.downloadingJamTrack || mixdownDownloading
+ componentWillMount: () ->
+ @computeDisableLoading(@state)
+
+ componentWillUpdate: (nextProps, nextState) ->
+ @computeDisableLoading(nextState)
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee
index d13f69dc6..8c9092b1e 100644
--- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee
@@ -1,24 +1,40 @@
context = window
+logger = context.JK.logger
+
+NoVideoRecordActive = 0
+WebCamRecordActive = 1
+ScreenRecordActive = 2
mixins = []
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
+# this check ensures we attempt to listen if this component is created in a popup
+reactContext = if window.opener? then window.opener else window
+
accessOpener = false
if window.opener?
try
m = window.opener.MixerStore
accessOpener = true
catch e
+ reactContext = window
+MixerStore = reactContext.MixerStore
+RecordingStore = reactContext.RecordingStore
+VideoStore = reactContext.VideoStore
if accessOpener
- mixins.push(Reflux.listenTo(window.opener.RecordingStore,"onRecordingStateChanged"))
+ mixins.push(Reflux.listenTo(RecordingStore,"onRecordingStateChanged"))
+ # mixins.push(Reflux.listenTo(MixerStore,"onMixersChanged"))
@PopupRecordingStartStop = React.createClass({
mixins: mixins
+ #onMixersChanged: (mixers) ->
+ # this.setState(chatMixer: mixers.chatMixer)
+
onRecordingStateChanged: (recordingState) ->
this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording)
@@ -26,13 +42,39 @@ if accessOpener
if this.state.isRecording
window.opener.RecordingActions.stopRecording()
else
- window.opener.RecordingActions.startRecording()
+ recordChat = false
+ recordVideo = NoVideoRecordActive
+
+ $root = $(this.getDOMNode())
+
+ if @inputType != 'audio-only'
+
+ if $root.find('#recording-selection').val() == 'video-window'
+ recordVideo = ScreenRecordActive
+ else
+ recordVideo = WebCamRecordActive
+
+
+ recordChat = $root.find('#include-chat').is(':checked')
+
+
+ # if the video window isn't open, but a video option was selected...
+ if recordVideo != NoVideoRecordActive && !VideoStore.videoShared
+ logger.debug("prevent video from opening", VideoStore)
+ context.JK.prodBubble($root.find('.control'), 'video-window-not-open', {}, {positions:['bottom']})
+ return
+ logger.debug("@inputType, @udiotye", recordChat, recordVideo)
+ window.opener.RecordingActions.startRecording(recordVideo, recordChat)
onNoteShowHide: () ->
+
+ $root = $(this.getDOMNode())
+ audioVideoValue = $root.find('input[name="recording-input-type"]').val()
+ console.log("audio video value", audioVideoValue)
this.setState(showNote: !this.state.showNote)
getInitialState: () ->
- {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false}
+ {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false, chatMixer: MixerStore.mixers?.chatMixer}
render: () ->
@@ -53,16 +95,33 @@ if accessOpener
`
- recordingJSX = `
-
-
-
Record both video and audio
-
+
+ chatHelp = `
[?] `
+
+ recordingJSX =
+ `
+
+
Recording Type
+
+
+
+
+ Record session video window
+ Record my webcam only
+
+
-
-
-
Record audio only
-
+
+
+ Include voice chat in recorded audio {chatHelp}
`
@@ -108,6 +167,20 @@ if accessOpener
windowUnloaded: () ->
window.opener.RecordingActions.recordingControlsClosed()
+ onChatHelp: (e) ->
+ e.preventDefault()
+
+ context.JK.prodBubble($(e.target), 'vid-record-chat-input', {}, {positions:['left']})
+ trackInputType: (e) ->
+ $checkedType = $(e.target);
+ @inputType = $checkedType.val()
+ logger.debug("updated @inputType",e.target, @inputType)
+
+ trackAudioType: (e) ->
+ $checkedType = $(e.target);
+ @audioType = $checkedType.val()
+ logger.debug("updated @audioType", @inputType)
+
componentDidMount: () ->
$(window).unload(@windowUnloaded)
@@ -116,6 +189,21 @@ if accessOpener
$recordingType = $root.find('input[type="radio"]')
context.JK.checkbox($recordingType)
+ @inputType = 'audio-only'
+ @audioType = 'audio-only'
+
+ $root.find('input[name="recording-input-type"]').on('ifChanged', @trackInputType)
+ $root.find('input[name="recording-input-chat-option"]').on('ifChanged', @trackAudioType)
+
+ $recordingRegion = $root.find('#recording-selection')
+ #console.log("$recordingou", $recordingRegion)
+ #context.JK.dropdown($recordingRegion)
+
+ $includeChat = $root.find('#include-chat')
+ context.JK.checkbox($includeChat)
+
+
+
@resizeWindow()
# this is necessary due to whatever the client's rendering behavior is.
@@ -124,6 +212,9 @@ if accessOpener
componentDidUpdate: () ->
@resizeWindow()
+ $root = jQuery(this.getDOMNode())
+ $includeChat = $root.find('#include-chat')
+
resizeWindow: () =>
$container = $('#minimal-container')
width = $container.width()
diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee
index afb9a13f2..ca7012871 100644
--- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee
@@ -15,6 +15,8 @@ ChannelGroupIds = context.JK.ChannelGroupIds
Reflux.listenTo(@JamTrackStore, "onJamTrackStateChanged")]
onJamTrackStateChanged: (jamTrackState) ->
+ @setState({jamTrackState: jamTrackState})
+
if jamTrackState.fullTrackActivated || jamTrackState.opened && jamTrackState.jamTrack.activeMixdown == null
@loadJamTrack(jamTrackState.jamTrack)
else if jamTrackState.closed
@@ -29,6 +31,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds
SessionActions.closeMedia(true)
+
#inputsChangedProcessed: (state) ->
@@ -225,12 +228,12 @@ ChannelGroupIds = context.JK.ChannelGroupIds
contents = closeOptions
- else if this.state.mediaSummary.mediaOpen
+ else if this.state.mediaSummary.mediaOpen || @state.jamTrackState?.jamTrack?
# give the users options to close it
if this.state.mediaSummary.recordingOpen
mediaType = "Recording"
- else if this.state.mediaSummary.jamTrackOpen
+ else if this.state.mediaSummary.jamTrackOpen || @state.jamTrackState?.jamTrack?
mediaType = "JamTrack"
else if this.state.mediaSummary.backingTrackOpen
mediaType = "Audio File"
@@ -308,7 +311,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds
getInitialState:() ->
- {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null}
+ {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null, jamTrackState: {}}
onAppInit: (app) ->
@app = app
@@ -343,7 +346,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds
@handlePopup()
handlePopup: () ->
- if @state.mediaSummary.userNeedsMediaControls
+ if @state.mediaSummary.userNeedsMediaControls || @state.jamTrackState?.jamTrack?
unless @childWindow?
logger.debug("opening media control window")
@childWindow = window.open("/popups/media-controls", 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350')
diff --git a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee
index 46288400d..c3e0e0381 100644
--- a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee
@@ -68,6 +68,7 @@ SessionActions = @SessionActions
beforeDisconnect: () ->
@logger.debug("session beforeDisconnect")
+ return { freezeInteraction: true };
onAllowLeaveSession: () ->
@allowLeave = true
diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
index 6c054e3ec..dbe55b7e4 100644
--- a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
@@ -2,4 +2,5 @@ context = window
@AppActions = Reflux.createActions({
appInit: {}
+ openExternalUrl: {}
})
diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
index c1fe26514..5e6fe8618 100644
--- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
+++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
@@ -69,6 +69,7 @@ MIX_MODES = context.JK.MIX_MODES;
backingTracks = @session.backingTracks()
recordedJamTracks = @session.recordedJamTracks()
jamTracks = @session.jamTracks()
+ jamTrackMixdown = @session.jamTrackMixdown()
###
with mixer info, we use these to decide what kind of tracks are open in the backend
@@ -92,6 +93,7 @@ MIX_MODES = context.JK.MIX_MODES;
@metronomeTrackMixers = []
@adhocTrackMixers = []
+
groupByType = (mixers, isLocalMixer) =>
for mixer in mixers
mediaType = mixer.media_type
@@ -106,7 +108,10 @@ MIX_MODES = context.JK.MIX_MODES;
isJamTrack = false;
- if jamTracks
+ if mixer.id == jamTrackMixdown.id
+ isJamTrack = true;
+
+ if !isJamTrack && jamTracks
# check if the ID matches that of an open jam track
for jamTrack in jamTracks
if mixer.id == jamTrack.id
@@ -186,6 +191,8 @@ MIX_MODES = context.JK.MIX_MODES;
backingTrackOpen: @backingTracks.length > 0
metronomeOpen: @session.isMetronomeOpen()
+
+
# figure out if any media is open
mediaOpenSummary = false
for mediaType, mediaOpen of @mediaSummary
@@ -352,6 +359,10 @@ MIX_MODES = context.JK.MIX_MODES;
else
logger.debug("MixerHelper: full jamtrack is active")
+ if jamTrackMixers.length == 1
+ logger.warn("ignoring wrong amount of mixers for JamTrack in Full Track mode")
+ return _jamTracks
+
for jamTrack in jamTracks
mixer = null
preMasteredClass = ""
diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
index bb5a11d98..887a38326 100644
--- a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
@@ -8,5 +8,9 @@ logger = context.JK.logger
onAppInit: (app) ->
@trigger(app)
+
+ onOpenExternalUrl: (href) ->
+
+ context.JK.popExternalLink(href)
}
)
diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
index 1daf1beec..9bea103ce 100644
--- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
@@ -177,7 +177,9 @@ JamTrackActions = @JamTrackActions
, null, true)
when 'unknown'
- if @jamTrack.activeMixdown.client_state != 'downloading'
+ # we need to check if @keyCheckTimeout exists; because if it does, we don't want to download while keying.
+ # 'unknown' is tricky here because the file probably is actually on disk, but the bridge API can say unknown until you've tried to key at least once
+ if @jamTrack.activeMixdown.client_state != 'downloading' && !@keyCheckTimeout?
@jamTrack.activeMixdown.client_state = 'downloading'
logger.debug("JamTrackStore: initiating download of mixdown")
context.jamClient.JamTrackDownload(@jamTrack.id, @jamTrack.activeMixdown.id, context.JK.currentUserId,
@@ -501,6 +503,8 @@ JamTrackActions = @JamTrackActions
downloadFailureCallback: (errorMsg) ->
+ logger.debug("mixdown download failed", errorMsg);
+
if @jamTrack?.activeMixdown?
@jamTrack.activeMixdown.client_state = 'download_fail'
@reportError(@jamTrack.activeMixdown)
diff --git a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
index f68e4d091..684013ec9 100644
--- a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
@@ -23,8 +23,15 @@ logger = context.JK.logger
@recordingModel = recordingModel
this.trigger({isRecording: @recordingModel.isRecording()})
- onStartRecording: () ->
- @recordingModel.startRecording()
+ onStartRecording: (recordVideo, recordChat) ->
+
+ frameRate = context.jamClient.GetCurrentVideoFrameRate() || 30;
+
+ NoVideoRecordActive = 0
+ WebCamRecordActive = 1
+ ScreenRecordActive = 2
+ logger.debug("onStartRecording: recordVideo: #{recordVideo}, recordChat: #{recordChat} frameRate: #{frameRate}")
+ @recordingModel.startRecording(recordVideo, recordChat, frameRate)
onStopRecording: () ->
@recordingModel.stopRecording()
@@ -67,7 +74,7 @@ logger = context.JK.logger
popupRecordingControls: () ->
logger.debug("poupRecordingControls")
- @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=350')
+ @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=340')
@recordingWindow.ParentRecordingStore = context.RecordingStore
@recordingWindow.ParentIsRecording = @recordingModel.isRecording()
diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js
index aff392fba..b74fb24d9 100644
--- a/web/app/assets/javascripts/recordingModel.js
+++ b/web/app/assets/javascripts/recordingModel.js
@@ -79,7 +79,7 @@
return context.JK.dkeys(groupedTracks);
}
- function startRecording() {
+ function startRecording(recordVideo, recordChat, recordFramerate) {
$self.triggerHandler('startingRecording', {});
@@ -88,14 +88,15 @@
context.RecordingActions.startingRecording({isRecording: false})
- currentRecording = rest.startRecording({"music_session_id": sessionId})
+ // 0 indicates the NoVideoRecordActive mode; so anything but that means video got recorded
+ currentRecording = rest.startRecording({"music_session_id": sessionId, record_video: recordVideo != 0})
.done(function(recording) {
currentRecordingId = recording.id;
currentOrLastRecordingId = recording.id;
// ask the backend to start the session.
var groupedTracks = groupTracksToClient(recording);
- jamClient.StartRecording(recording["id"], groupedTracks, 0, false, 0);
+ jamClient.StartRecording(recording["id"], groupedTracks, recordVideo, recordChat, recordFramerate);
})
.fail(function(jqXHR) {
var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false }
diff --git a/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss b/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss
index 22c66dd7b..4a41f3d9f 100644
--- a/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss
+++ b/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss
@@ -53,5 +53,9 @@
.signed_in_to_google {
color: yellow;
}
+
+ .save-video {
+ margin-top:10px;
+ }
}
diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss
index ee4290fd6..addf80c3d 100644
--- a/web/app/assets/stylesheets/minimal/media_controls.css.scss
+++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss
@@ -33,10 +33,21 @@ body.media-controls-popup.popup {
margin-bottom:5px;
}
- .close-link {
+ .actions {
+ position:relative;
margin-top:20px;
font-size:11px;
margin-bottom:10px;
+
+ }
+
+ .help-link {
+ position:absolute;
+ left:-6px;
+ top:0;
+ }
+ .close-link {
+
}
.display-metronome {
@@ -183,6 +194,10 @@ body.media-controls-popup.popup {
border-width:1px 0;
padding: 7px 0 20px;
+ &.not-active {
+ padding:7px 0 7px;
+ }
+
p {
line-height:125%;
color:$ColorTextTypical;
@@ -222,4 +237,27 @@ body.media-controls-popup.popup {
}
}
}
+
+ .arrow-down {
+ float:none;
+ margin-left:5px;
+ margin-top:0;
+ margin-right:0;
+ border-top: 4px solid #fc0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ display:inline-block;
+ padding-top:1px;
+ }
+ .arrow-up {
+ float:none;
+ margin-right:0;
+ margin-left:5px;
+ margin-bottom:2px;
+ border-bottom: 4px solid #fc0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ display:inline-block;
+ padding-top:1px;
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/minimal.css.scss b/web/app/assets/stylesheets/minimal/minimal.css.scss
index 5ac30bc35..0e3be9153 100644
--- a/web/app/assets/stylesheets/minimal/minimal.css.scss
+++ b/web/app/assets/stylesheets/minimal/minimal.css.scss
@@ -7,6 +7,8 @@
*= require client/ftue
*= require client/help
*= require icheck/minimal/minimal
+*= require easydropdown
+*= require easydropdown_jk
*= require_directory .
*= require client/metronomePlaybackModeSelect
*= require_directory ../client/react-components
diff --git a/web/app/assets/stylesheets/minimal/recording_controls.css.scss b/web/app/assets/stylesheets/minimal/recording_controls.css.scss
index b2817b8ee..1cbfaec28 100644
--- a/web/app/assets/stylesheets/minimal/recording_controls.css.scss
+++ b/web/app/assets/stylesheets/minimal/recording_controls.css.scss
@@ -10,12 +10,12 @@ body.recording-start-stop {
}
.recording-start-stop {
- padding-left:44px;
+ padding-left:30px;
}
.control-holder {
width:100%;
- margin: 1em 0;
+ margin: 10px 0 20px;
}
.helper {
@@ -24,7 +24,20 @@ body.recording-start-stop {
vertical-align: middle;
}
+ .audio-settings {
+ margin-top:40px;
+
+ label {
+ display:inline;
+ margin-left:6px;
+ }
+
+ .icheckbox_minimal {
+ vertical-align:middle;
+ }
+ }
.control {
+ margin-left:20px;
width:231px;
height:34px;
@include border_box_sizing;
@@ -38,7 +51,10 @@ body.recording-start-stop {
color:#ccc;
}
-
+ .chat-help {
+ text-decoration:none;
+ outline:0;
+ }
.control img {
vertical-align:middle;
margin-right:5px;
@@ -59,12 +75,16 @@ body.recording-start-stop {
.field {
height:18px;
- &:nth-child(1) {
+ &:nth-of-type(1) {
}
- &:nth-child(2) {
+ &:nth-of-type(2) {
margin-top:9px;
}
+ &:nth-of-type(3) {
+ margin-top: 10px;
+ padding-left: 22px;
+ }
}
.note-show-hide {
@@ -77,12 +97,22 @@ body.recording-start-stop {
}
.important-note {
- margin-top:30px;
+ margin-top:15px;
line-height:150%;
font-size:12px;
width:260px;
}
+ h3 {
+ font-size:14px;
+ font-weight:bold;
+ margin-bottom:6px;
+ }
+
+ .video-settings {
+ margin-bottom:20px;
+ }
+
a.note-show-hide {
margin-top:5px;
text-decoration:underline;
diff --git a/web/app/controllers/api_jam_track_mixdowns_controller.rb b/web/app/controllers/api_jam_track_mixdowns_controller.rb
index 2da9c9ff3..a38c2f734 100644
--- a/web/app/controllers/api_jam_track_mixdowns_controller.rb
+++ b/web/app/controllers/api_jam_track_mixdowns_controller.rb
@@ -114,6 +114,7 @@ class ApiJamTrackMixdownsController < ApiController
enqueued = @package.enqueue_if_needed
log.debug("jamtrack mixdown #{enqueued ? "ENQUEUED" : "NOT ENQUEUED"}: mixdown_package=#{@package.id} ")
+ @queue_time = enqueued ? enqueued : 0
return
else
render :json => { :message => "download limit surpassed", :errors=>@package.errors }, :status => 403
diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb
index 0767d9053..77358b13c 100644
--- a/web/app/controllers/api_recordings_controller.rb
+++ b/web/app/controllers/api_recordings_controller.rb
@@ -83,7 +83,7 @@ class ApiRecordingsController < ApiController
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless music_session.users.exists?(current_user)
- @recording = Recording.start(music_session, current_user)
+ @recording = Recording.start(music_session, current_user, record_video: params[:record_video])
if @recording.errors.any?
response.status = :unprocessable_entity
diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb
index 49547bd95..53adc7e58 100644
--- a/web/app/controllers/api_search_controller.rb
+++ b/web/app/controllers/api_search_controller.rb
@@ -7,7 +7,7 @@ class ApiSearchController < ApiController
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
- query = parasobj.clone
+ query = params.clone
query[:remote_ip] = request.remote_ip
if 1 == query[Search::PARAM_MUSICIAN].to_i
@search = Search.musician_filter(query, current_user)
diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb
index 18c2db8f8..17d96fd5d 100644
--- a/web/app/controllers/api_users_controller.rb
+++ b/web/app/controllers/api_users_controller.rb
@@ -1,5 +1,6 @@
require 'sanitize'
-class ApiUsersController < ApiController
+class
+ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
@@ -575,14 +576,14 @@ class ApiUsersController < ApiController
# This should largely be moved into a library somewhere in jam-ruby.
def crash_dump
# example of using curl to access this API:
- # curl -L -T some_file -X PUT http://localhost:3000/api/dumps?client_type=[MACOSX/Win32/JamBox]&client_version=[VERSION]&client_id=[CLIENT_ID]&session_id=[SESSION_ID]×tamp=[TIMESTAMP]
+ # curl -L -T some_file -X PUT http://localhost:3000/api/dumps?client_type=[MacOSX/Win32/JamBox]&client_version=[VERSION]&client_id=[CLIENT_ID]&session_id=[SESSION_ID]×tamp=[TIMESTAMP]
# user_id is deduced if possible from the user's cookie.
@dump = CrashDump.new
@dump.client_type = params[:client_type]
@dump.client_version = params[:client_version]
@dump.client_id = params[:client_id]
- @dump.user_id = current_user.try(:id)
+ @dump.user_id = params[:user_id]
@dump.session_id = params[:session_id]
@dump.timestamp = params[:timestamp]
@@ -603,7 +604,7 @@ class ApiUsersController < ApiController
read_url = bucket.objects[uri].url_for(:read,
:expires => expire,
:'response_content_type' => 'application/octet-stream').to_s
- @dump.update_attribute(:uri, read_url)
+ #@dump.update_attribute(:uri, read_url)
write_url = bucket.objects[uri].url_for(:write,
:expires => Rails.application.config.crash_dump_data_signed_url_timeout,
@@ -611,7 +612,7 @@ class ApiUsersController < ApiController
logger.debug("crash_dump can read from url #{read_url}")
- redirect_to write_url
+ redirect_to write_url, status: 307
else
# we should store it here to aid in development, but we don't have to until someone wants the feature
# so... just return 200
diff --git a/web/app/views/api_jam_track_mixdowns/enqueue.rabl b/web/app/views/api_jam_track_mixdowns/enqueue.rabl
index bcf0d7191..5fde4660c 100644
--- a/web/app/views/api_jam_track_mixdowns/enqueue.rabl
+++ b/web/app/views/api_jam_track_mixdowns/enqueue.rabl
@@ -1,3 +1,7 @@
object @package
+node :queue_time do
+ @queue_time
+end
+
extends "api_jam_track_mixdowns/show_package"
\ No newline at end of file
diff --git a/web/app/views/api_jam_track_mixdowns/show.rabl b/web/app/views/api_jam_track_mixdowns/show.rabl
index 828c29e73..a599f8a64 100644
--- a/web/app/views/api_jam_track_mixdowns/show.rabl
+++ b/web/app/views/api_jam_track_mixdowns/show.rabl
@@ -1,6 +1,6 @@
object @jam_track_mixdown
-attributes :id, :name, :description, :jam_track_id, :created_at, :updated_at
+attributes :id, :name, :description, :jam_track_id
node :settings do |item|
JSON.parse(item.settings)
diff --git a/web/app/views/api_jam_track_mixdowns/show_package.rabl b/web/app/views/api_jam_track_mixdowns/show_package.rabl
index a52ab67d0..b4899b037 100644
--- a/web/app/views/api_jam_track_mixdowns/show_package.rabl
+++ b/web/app/views/api_jam_track_mixdowns/show_package.rabl
@@ -1,3 +1,3 @@
object @package
-attributes :id, :jam_track_mixdown_id, :file_type, :sample_rate, :encrypt_type, :error_count, :error_reason, :error_detail, :signing_state, :packaging_steps, :current_packaging_step, :version, :created_at, :updated_at
+attributes :id, :jam_track_mixdown_id, :file_type, :sample_rate, :encrypt_type, :error_count, :error_reason, :error_detail, :signing_state, :packaging_steps, :current_packaging_step, :version
diff --git a/web/app/views/api_jam_tracks/keys.rabl b/web/app/views/api_jam_tracks/keys.rabl
index 2b482e44a..3f99828f0 100644
--- a/web/app/views/api_jam_tracks/keys.rabl
+++ b/web/app/views/api_jam_tracks/keys.rabl
@@ -21,7 +21,7 @@ node do |jam_track|
# now include mixdown info
mixdowns_44 = []
- mixdown_info = @jamtrack_mixdowns[id]
+ mixdown_info = @jamtrack_mixdowns[id + '-44']
if mixdown_info
mixdown_info.each do |mixdown_id|
mixdowns_44 << {
diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl
index e05ca0cbf..c54341870 100644
--- a/web/app/views/api_jam_tracks/show.rabl
+++ b/web/app/views/api_jam_tracks/show.rabl
@@ -20,7 +20,8 @@ child(:jam_track_tracks => :tracks) {
node do |track|
{
preview_mp3_url: track.preview_public_url('mp3'),
- preview_ogg_url: track.preview_public_url('ogg')
+ preview_ogg_url: track.preview_public_url('ogg'),
+ preview_aac_url: track.preview_public_url('aac')
}
end
}
diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl
index c2e5874a2..6bde111a7 100644
--- a/web/app/views/api_jam_tracks/show_for_client.rabl
+++ b/web/app/views/api_jam_tracks/show_for_client.rabl
@@ -15,7 +15,15 @@ node :jam_track_right_id do |jam_track|
end
child(:jam_track_tracks => :tracks) {
- attributes :id, :part, :instrument, :track_type
+ attributes :id, :part, :instrument, :track_type, :position
+
+ node do |track|
+ {
+ preview_mp3_url: track.preview_public_url('mp3'),
+ preview_ogg_url: track.preview_public_url('ogg'),
+ preview_aac_url: track.preview_public_url('aac')
+ }
+ end
}
node :last_mixdown_id do |jam_track|
diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl
index d59732cb3..285d7bbb6 100644
--- a/web/app/views/api_recordings/show.rabl
+++ b/web/app/views/api_recordings/show.rabl
@@ -1,6 +1,6 @@
object @recording
-attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session
+attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session, :video
node :fan_access do |recording|
recording.non_active_music_session.fan_access
diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim
index 6823d0b45..b379f0947 100644
--- a/web/app/views/clients/_help.html.slim
+++ b/web/app/views/clients/_help.html.slim
@@ -352,4 +352,11 @@ script type="text/template" id="template-help-ftue-video-disable"
script type="text/template" id="template-help-no-change-while-loading"
span Certain actions are disabled while a track is being loaded.
+script type="text/template" id="template-help-video-window-not-open"
+ .video-window-not-open
+ p You've selected to record video, but the video window is not open.
+ p Click the VIDEO button in the main window and try again.
+script type="text/template" id="template-help-vid-record-chat-input"
+ .vid-record-chat-input
+ p Any chat inputs in the session will also be included in the video if checked.
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb
index 950622bda..4eb4f3797 100644
--- a/web/app/views/clients/index.html.erb
+++ b/web/app/views/clients/index.html.erb
@@ -366,7 +366,7 @@
}
JK.app = JK.JamKazam();
- var jamServer = new JK.JamServer(JK.app, function(event_type) {JK.app.activeElementEvent(event_type)});
+ var jamServer = new JK.JamServer(JK.app, function(event_type) {return JK.app.activeElementEvent(event_type)});
jamServer.initialize();
var clientInit = new JK.ClientInit();
diff --git a/web/app/views/dialogs/_recordingFinishedDialog.html.haml b/web/app/views/dialogs/_recordingFinishedDialog.html.haml
index bbbdfa701..4447f4d8f 100644
--- a/web/app/views/dialogs/_recordingFinishedDialog.html.haml
+++ b/web/app/views/dialogs/_recordingFinishedDialog.html.haml
@@ -25,12 +25,12 @@
%br/
%textarea#claim-recording-description.w100{:name => "description"}
-if (Rails.application.config.video_available=="full") || (current_user && current_user.admin)
- .field.left{:purpose => "save_video"}
+ .save-video.field.left{:purpose => "save_video"}
%input{:name => "save_video", :type => "checkbox"}/
%label{:for => "save_video"} Save Video to Computer
- .field.left{:purpose => "upload_to_youtube"}
+ .hidden.field.left{:purpose => "upload_to_youtube"}
%span
- %input{:name => "upload_to_youtube", :type => "checkbox"}/
+ %input{:name => "upload_to_youtube", :type => "checkbox", :checked => "checked"}/
%label{:for => "upload_to_youtube"} Upload Video to YouTube
%span
= render(:partial => "shared/google_login")
diff --git a/web/config/application.rb b/web/config/application.rb
index 6a4873d79..390ce2266 100644
--- a/web/config/application.rb
+++ b/web/config/application.rb
@@ -109,10 +109,10 @@ if defined?(Bundler)
# Websocket-gateway embedded configs
config.websocket_gateway_enable = false
- config.websocket_gateway_connect_time_stale_client = 40 # 40 matches production
- config.websocket_gateway_connect_time_expire_client = 60 # 60 matches production
- config.websocket_gateway_connect_time_stale_browser = 40 # 40 matches production
- config.websocket_gateway_connect_time_expire_browser = 60 # 60 matches production
+ config.websocket_gateway_connect_time_stale_client = 80
+ config.websocket_gateway_connect_time_expire_client = 120
+ config.websocket_gateway_connect_time_stale_browser = 80
+ config.websocket_gateway_connect_time_expire_browser = 120
config.websocket_gateway_cidr = ['0.0.0.0/0']
config.websocket_gateway_internal_debug = false
config.websocket_gateway_port = 6767 + ENV['JAM_INSTANCE'].to_i
@@ -350,7 +350,7 @@ if defined?(Bundler)
config.recurly_tax_estimate_jam_track_plan = 'jamtrack-acdc-backinblack'
config.minimal_curtain = false
- config.video_available = "none"
+ config.video_available = "full"
config.alerts_api_enabled = true
config.gear_check_ignore_high_latency = false
@@ -371,6 +371,5 @@ if defined?(Bundler)
config.time_shift_style = :sbsms # or sox
config.middleware.use Rack::Deflater
-
end
end
diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb
index 032b3a33e..ad49ae6b7 100644
--- a/web/config/environments/development.rb
+++ b/web/config/environments/development.rb
@@ -69,10 +69,10 @@ SampleApp::Application.configure do
# it's nice to have even admin accounts (which all the default ones are) generate GA data for testing
config.ga_suppress_admin = false
- config.websocket_gateway_connect_time_stale_client = 40 # 40 matches production
- config.websocket_gateway_connect_time_expire_client = 60 # 60 matches production
- config.websocket_gateway_connect_time_stale_browser = 40 # 40 matches production
- config.websocket_gateway_connect_time_expire_browser = 60 # 60 matches production
+ config.websocket_gateway_connect_time_stale_client = 80
+ config.websocket_gateway_connect_time_expire_client = 120
+ config.websocket_gateway_connect_time_stale_browser = 80
+ config.websocket_gateway_connect_time_expire_browser = 120
config.audiomixer_path = ENV['AUDIOMIXER_PATH'] || audiomixer_workspace_path || "/var/lib/audiomixer/audiomixer/audiomixerapp"
@@ -92,7 +92,7 @@ SampleApp::Application.configure do
config.jam_tracks_available = true
config.purchases_enabled = true
config.minimal_curtain = true
- config.video_available= ENV['VIDEO_AVAILABILITY'] || "none"
+ config.video_available= ENV['VIDEO_AVAILABILITY'] || "full"
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com'
config.guard_against_fraud = true
diff --git a/web/config/routes.rb b/web/config/routes.rb
index a7adf0ee1..1aa31786a 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -579,7 +579,7 @@ SampleApp::Application.routes.draw do
match '/artifacts/clients' => 'artifacts#client_downloads'
# crash logs
- match '/dumps' => 'api_users#crash_dump', :via => :put
+ match '/crashes' => 'api_users#crash_dump', :via => :put
# feedback from corporate site api
match '/feedback' => 'api_corporate#feedback', :via => :post
diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml
index ea694ba60..339dfd4f6 100644
--- a/web/config/scheduler.yml
+++ b/web/config/scheduler.yml
@@ -16,9 +16,9 @@ IcecastSourceCheck:
description: "Finds icecast mounts that need their 'sourced' state to change, but haven't in some time"
JamTracksCleaner:
- cron: "0 5 * * *"
- class: "JamRuby::UnusedMusicNotationCleaner"
- description: "Remove unused music notations"
+ cron: "0,30 * * * *"
+ class: "JamRuby::JamTracksCleaner"
+ description: "Clean up JamTrack related stuff; every 30 minutes"
CleanupFacebookSignup:
cron: "30 2 * * *"
diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake
index f74a6f775..b1e2161fd 100644
--- a/web/lib/tasks/jam_tracks.rake
+++ b/web/lib/tasks/jam_tracks.rake
@@ -103,6 +103,15 @@ namespace :jam_tracks do
importer = JamTrackImporter.synchronize_jamtrack_master_previews
end
+ task sync_master_aac: :environment do |task, args|
+ JamTrackImporter.synchronize_jamtrack_aac_previews
+ end
+
+ # popuplate preview info without uploading/processing audio files (use what's in S3)
+ task sync_previews_dev: :environment do |task, args|
+ JamTrackImporter.synchronize_previews_dev
+ end
+
# syncs just one master track for a give JamTrack
task sync_master_preview: :environment do |task, args|
plan_code = ENV['PLAN_CODE']
diff --git a/web/spec/controllers/#api_jam_track_mixdowns_controller_spec.rb# b/web/spec/controllers/#api_jam_track_mixdowns_controller_spec.rb#
new file mode 100644
index 000000000..7f36ea02e
--- /dev/null
+++ b/web/spec/controllers/#api_jam_track_mixdowns_controller_spec.rb#
@@ -0,0 +1,148 @@
+require 'spec_helper'
+
+describe ApiJamTrackMixdownsController, type: :controller do
+ render_views
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:jam_track) { FactoryGirl.create(:jam_track) }
+ let(:mixdown) { FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track) }
+ let(:jam_track_right) { FactoryGirl.create(:jam_track_right, jam_track: jam_track, user:user)}
+ let(:package) {FactoryGirl.create(:jam_track_mixdown_package, jam_track_mixdown: mixdown)}
+
+ before(:each) do
+ controller.current_user = user
+ JamTrackMixdown.destroy_all
+ end
+
+ describe "index" do
+
+ it "one result" do
+
+ # make a mixdown with no packages
+ get :index, {id: mixdown.jam_track.id}
+ response.status.should eq(200)
+ json = JSON.parse(response.body)
+ json["next"].should be_nil
+ json["count"].should eq(1)
+ json["mixdowns"][0]["settings"].should eq({"speed" => 5})
+
+ # and then add a package
+ package = FactoryGirl.create(:jam_track_mixdown_package, jam_track_mixdown: mixdown)
+
+ get :index, {id: mixdown.jam_track.id}
+ response.status.should eq(200)
+ json = JSON.parse(response.body)
+ json["next"].should be_nil
+ json["count"].should eq(1)
+ json["mixdowns"][0]["packages"][0]["signing_state"].should eq('QUIET')
+ end
+ end
+
+ describe "create" do
+
+ it "success" do
+ post :create, {:format => 'json', jamTrackID: jam_track.id, name: 'some name', description: 'some description', settings: {speed:5}}
+
+ response.status.should eq(200)
+
+ json = JSON.parse(response.body)
+ json["name"].should eq('some name')
+ json["jam_track_id"].should eq(jam_track.id)
+ json["description"].should eq('some description')
+ json["settings"].should eq({"speed" => 5})
+ json["packages"].should eq([])
+ end
+
+ it "validates name" do
+ post :create, {:format => 'json', jamTrackID: jam_track.id, description: 'some description', settings: {speed:5}}
+
+ response.status.should eq(422)
+
+ json = JSON.parse(response.body)
+ json["errors"]["name"].should eq(["can't be blank"])
+ end
+ end
+
+ describe "enqueue" do
+ it "success" do
+
+ jam_track_right.touch
+ post :enqueue, {:format => 'json', id: mixdown.id, file_type: JamTrackMixdownPackage::FILE_TYPE_AAC, encrypt_type: nil, sample_rate: 48}
+
+ response.status.should eq(200)
+
+ json = JSON.parse(response.body)
+ puts json
+ json["id"].should_not be_nil
+
+ package = JamTrackMixdownPackage.find(json["id"])
+ package.file_type.should eq(JamTrackMixdownPackage::FILE_TYPE_AAC)
+ package.encrypt_type.should eq(nil)
+ package.sample_rate.should eq(48)
+ end
+
+ it "validates file_type" do
+ jam_track_right.touch
+ post :enqueue, {:format => 'json', id: mixdown.id, file_type: 'wrong', encrypt_type: nil, sample_rate: 48}
+
+ response.status.should eq(422)
+
+ json = JSON.parse(response.body)
+ json["errors"]["file_type"].should eq(["is not included in the list"])
+ end
+
+ it "finds existing package to enqueue" do
+ jam_track_right.touch
+ package.touch
+ JamTrackMixdownPackage.count.should eq(1)
+
+ package.jam_track_mixdown.should eq(mixdown)
+ post :enqueue, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
+
+ response.status.should eq(200)
+
+ json = JSON.parse(response.body)
+ puts json
+ json["id"].should eq(package.id)
+ JamTrackMixdownPackage.count.should eq(1)
+ end
+ end
+
+ describe "download" do
+
+ it "enqueues if not available" do
+
+ jam_track_right.touch
+ package.touch
+
+ post :download, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
+
+ response.status.should eq(202)
+
+ json = JSON.parse(response.body)
+ json["message"].should eq("not available, digitally signing Jam Track Mixdown offline.")
+
+ package.reload
+ package.signing_state.should eq('QUEUED')
+ end
+
+ it "success" do
+
+ jam_track_right.touch
+ package.touch
+ package.enqueue_if_needed
+ package.signed = true
+ package.url = 'some/bogus/place'
+ package.save!
+
+ post :download, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
+
+ response.status.should eq(302)
+
+ response['Location'].should include('/some/bogus/place')
+
+ end
+
+ end
+end
+
diff --git a/web/spec/controllers/.#api_jam_track_mixdowns_controller_spec.rb b/web/spec/controllers/.#api_jam_track_mixdowns_controller_spec.rb
new file mode 120000
index 000000000..4a5e6e661
--- /dev/null
+++ b/web/spec/controllers/.#api_jam_track_mixdowns_controller_spec.rb
@@ -0,0 +1 @@
+jam@ubuntu.2319:1444052631
\ No newline at end of file
diff --git a/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb b/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb
index 5dcb8874e..688394971 100644
--- a/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb
+++ b/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb
@@ -24,7 +24,11 @@ describe ApiJamTrackMixdownsController, type: :controller do
json = JSON.parse(response.body)
json["next"].should be_nil
json["count"].should eq(1)
+<<<<<<< HEAD
json["mixdowns"][0]["settings"].should eq({})
+=======
+ json["mixdowns"][0]["settings"].should eq({"speed" => 5})
+>>>>>>> develop
# and then add a package
package = FactoryGirl.create(:jam_track_mixdown_package, jam_track_mixdown: mixdown)
@@ -41,7 +45,11 @@ describe ApiJamTrackMixdownsController, type: :controller do
describe "create" do
it "success" do
+<<<<<<< HEAD
post :create, {:format => 'json', jamTrackID: jam_track.id, name: 'some name', description: 'some description', settings: {}}
+=======
+ post :create, {:format => 'json', jamTrackID: jam_track.id, name: 'some name', description: 'some description', settings: {speed:5}}
+>>>>>>> develop
response.status.should eq(200)
@@ -49,12 +57,20 @@ describe ApiJamTrackMixdownsController, type: :controller do
json["name"].should eq('some name')
json["jam_track_id"].should eq(jam_track.id)
json["description"].should eq('some description')
+<<<<<<< HEAD
json["settings"].should eq({})
+=======
+ json["settings"].should eq({"speed" => 5})
+>>>>>>> develop
json["packages"].should eq([])
end
it "validates name" do
+<<<<<<< HEAD
post :create, {:format => 'json', jamTrackID: jam_track.id, description: 'some description', settings: {}}
+=======
+ post :create, {:format => 'json', jamTrackID: jam_track.id, description: 'some description', settings: {speed:5}}
+>>>>>>> develop
response.status.should eq(422)
@@ -72,7 +88,11 @@ describe ApiJamTrackMixdownsController, type: :controller do
response.status.should eq(200)
json = JSON.parse(response.body)
+<<<<<<< HEAD
json["message"].should eq("enqueued")
+=======
+ puts json
+>>>>>>> develop
json["id"].should_not be_nil
package = JamTrackMixdownPackage.find(json["id"])
@@ -102,7 +122,11 @@ describe ApiJamTrackMixdownsController, type: :controller do
response.status.should eq(200)
json = JSON.parse(response.body)
+<<<<<<< HEAD
json["message"].should eq("enqueued")
+=======
+ puts json
+>>>>>>> develop
json["id"].should eq(package.id)
JamTrackMixdownPackage.count.should eq(1)
end
diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb
index 25f971ec9..46a7d9c2d 100644
--- a/web/spec/controllers/api_jam_tracks_controller_spec.rb
+++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb
@@ -115,7 +115,7 @@ describe ApiJamTracksController do
it "handle api call 500" do
post :played, { id: 999, user: @user }
- expect(response.status).to eq(500)
+ expect(response.status).to eq(422)
json = JSON.parse(response.body)
expect(/Unexpected error occurred/).to match(json['message'])
end
@@ -155,8 +155,8 @@ describe ApiJamTracksController do
get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running'
response.status.should == 202
right.download_count.should eq(0)
- right.private_key_44.should be_nil
- right.private_key_48.should be_nil
+ right.private_key_44.should_not be_nil
+ right.private_key_48.should_not be_nil
qname = "#{ResqueSpec.queue_name(JamRuby::JamTracksBuilder)}"
#puts "ResqueSpec.peek(qname)#{ResqueSpec.peek(qname)}"
@@ -167,7 +167,7 @@ describe ApiJamTracksController do
JamTracksBuilder.should_not have_queued(right.id,nil).in(:jam_tracks_builder)
right.reload
- right.private_key_44.should be_nil
+ right.private_key_44.should_not be_nil
right.private_key_48.should_not be_nil
right.download_count.should eq(0)
@@ -186,8 +186,8 @@ describe ApiJamTracksController do
get :download, :id=>@jam_track.id, :sample_rate=>44, all_fp: 'all', running_fp: 'running'
response.status.should == 202
right.download_count.should eq(0)
- right.private_key_44.should be_nil
- right.private_key_48.should be_nil
+ right.private_key_44.should_not be_nil
+ right.private_key_48.should_not be_nil
qname = "#{ResqueSpec.queue_name(JamRuby::JamTracksBuilder)}"
#puts "ResqueSpec.peek(qname)#{ResqueSpec.peek(qname)}"
@@ -199,7 +199,7 @@ describe ApiJamTracksController do
JamTracksBuilder.should_not have_queued(right.id, 44).in(:jam_tracks_builder)
right.reload
right.private_key_44.should_not be_nil
- right.private_key_48.should be_nil
+ right.private_key_48.should_not be_nil
right.download_count.should eq(0)
get :download, :id=>@jam_track.id, :sample_rate=>44, all_fp: 'all', running_fp: 'running'
@@ -239,11 +239,11 @@ describe ApiJamTracksController do
json = JSON.parse(response.body)
json.length.should == 1
json[0]['44'].should_not be_nil
- json[0]['44']['private'].should be_nil
- json[0]['44']['error'].should == 'no_key'
+ json[0]['44']['private'].should_not be_nil
+ json[0]['44']['error'].should be_nil
json[0]['48'].should_not be_nil
- json[0]['48']['private'].should be_nil
- json[0]['48']['error'].should == 'no_key'
+ json[0]['48']['private'].should_not be_nil
+ json[0]['48']['error'].should be_nil
end
it "track with key" do
@@ -254,11 +254,11 @@ describe ApiJamTracksController do
json.length.should == 1
json[0]['id'].should == @jam_track.id.to_s
json[0]['44'].should_not be_nil
- json[0]['44']['private'].should eq('abc')
+ json[0]['44']['private'].should eq(right.private_key_44)
json[0]['44']['error'].should be_nil
json[0]['48'].should_not be_nil
- json[0]['48']['private'].should be_nil
- json[0]['48']['error'].should == 'no_key'
+ json[0]['48']['private'].should eq(right.private_key_48)
+ json[0]['48']['error'].should be_nil
end
it "non-owning user asking for a real track" do
diff --git a/web/spec/features/individual_jamtrack_spec.rb b/web/spec/features/individual_jamtrack_spec.rb
index 103e0eae8..536c53b49 100644
--- a/web/spec/features/individual_jamtrack_spec.rb
+++ b/web/spec/features/individual_jamtrack_spec.rb
@@ -53,7 +53,7 @@ describe "Individual JamTrack", :js => true, :type => :feature, :capybara_featur
end
end
find('.browse-band a')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack/search")
- find('.browse-all a')['href'].should eq("/client#/jamtrack/search")
+ find('.browse-all a')['href'].should eq("/client?search=#/jamtrack/search")
find('a.cta-free-jamtrack')['href'].should eq("/client#/jamtrack/search")
find('a.cta-free-jamtrack').trigger(:click)
find('h1', text: 'check out')
diff --git a/web/spec/requests/active_music_sessions_api_spec.rb b/web/spec/requests/active_music_sessions_api_spec.rb
index 61836348e..af4f9a5b6 100755
--- a/web/spec/requests/active_music_sessions_api_spec.rb
+++ b/web/spec/requests/active_music_sessions_api_spec.rb
@@ -262,10 +262,13 @@ describe "Active Music Session API ", :type => :api do
login(user2)
get location_header + ".json", "CONTENT_TYPE" => 'application/json'
- participant = JSON.parse(last_response.body)
+ music_session = JSON.parse(last_response.body)
- # and the creator should be in the session
+
+ # and the second person should be in the session
# and should have tracks
+ music_session["participants"].length.should == 2
+ participant = music_session["participants"][1]
participant["tracks"].length.should == 1
participant["tracks"][0]["instrument_id"].should == 'bass guitar'
participant["tracks"][0]["sound"].should == 'mono'
@@ -451,18 +454,18 @@ describe "Active Music Session API ", :type => :api do
# users are friends, but no invitation... so we shouldn't be able to join as user 2
login(user2)
- post "/api/sessions/#{session["music_session_id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
join_response = JSON.parse(last_response.body)
join_response["errors"]["musician_access"].should == [ValidationMessages::INVITE_REQUIRED]
# but let's make sure if we then invite, that we can then join'
login(user)
- post '/api/invitations.json', { :music_session => session["music_session_id"], :receiver => user2.id }.to_json, "CONTENT_TYPE" => 'application/json'
+ post '/api/invitations.json', { :music_session => music_session["id"], :receiver => user2.id }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
login(user2)
- post "/api/sessions/#{session["music_session_id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end
diff --git a/websocket-gateway/bin/websocket_gateway b/websocket-gateway/bin/websocket_gateway
index 456592701..0045cd96a 100755
--- a/websocket-gateway/bin/websocket_gateway
+++ b/websocket-gateway/bin/websocket_gateway
@@ -22,6 +22,8 @@ if jam_instance == 0
exit 1
end
+#Resque.redis = "localhost:6380"
+
# now bring in the Jam code
require 'jam_websockets'
diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb
index 4d79c3647..fd5202450 100644
--- a/websocket-gateway/lib/jam_websockets/router.rb
+++ b/websocket-gateway/lib/jam_websockets/router.rb
@@ -27,9 +27,17 @@ module JamWebsockets
:heartbeat_interval_browser,
:connect_time_expire_browser,
:connect_time_stale_browser,
+ :maximum_minutely_heartbeat_rate_browser,
+ :maximum_minutely_heartbeat_rate_client,
:max_connections_per_user,
:gateway_name,
- :client_lookup
+ :client_lookup,
+ :time_it_sums,
+ :profile_it_sums,
+ :highest_drift,
+ :heartbeat_tracker
+ :temp_ban
+
def initialize()
@log = Logging.logger[self]
@@ -51,14 +59,25 @@ module JamWebsockets
@heartbeat_interval_browser= nil
@connect_time_expire_browser= nil
@connect_time_stale_browser= nil
+ @maximum_minutely_heartbeat_rate_browser = nil
+ @maximum_minutely_heartbeat_rate_client = nil
@gateway_name = nil
@ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base]
@message_stats = {}
+ @time_it_sums = {}
+ @profile_it_sums = {}
+ @heartbeat_tracker = {}
+ @temp_ban = {}
@login_success_count = 0
@login_fail_count = 0
@connected_count = 0
@disconnected_count = 0
+ @user_message_counts = {}
+ @largest_message = nil
+ @largest_message_user = nil
+ @highest_drift = 0
+
end
def start(connect_time_stale_client, connect_time_expire_client, connect_time_stale_browser, connect_time_expire_browser, options={:host => "localhost", :port => 5672, :max_connections_per_user => 10, :gateway => 'default', :allow_dynamic_registration => true}, &block)
@@ -75,6 +94,11 @@ module JamWebsockets
@gateway_name = options[:gateway]
@allow_dynamic_registration = options[:allow_dynamic_registration]
+ # determine the maximum amount of heartbeats we should get per user
+ @maximum_minutely_heartbeat_rate_client = ((@heartbeat_interval_client / 60.0) * 2).ceil + 3
+ @maximum_minutely_heartbeat_rate_browser = ((@heartbeat_interval_browser / 60.0) * 2).ceil + 3
+
+ @log.info("maxmium minutely timer #{maximum_minutely_heartbeat_rate_client}")
begin
@amqp_connection_manager = AmqpConnectionManager.new(true, 4, :host => options[:host], :port => options[:port])
@amqp_connection_manager.connect do |channel|
@@ -147,6 +171,7 @@ module JamWebsockets
# subscribe for any messages to users
@user_topic.subscribe(:ack => false) do |headers, msg|
+ time_it('user_topic') {
begin
routing_key = headers.routing_key
user_id = routing_key["user.".length..-1]
@@ -175,6 +200,7 @@ module JamWebsockets
@log.error "unhandled error in messaging to client"
@log.error e
end
+ }
end
MQRouter.user_exchange = @users_exchange
@@ -189,6 +215,7 @@ module JamWebsockets
# subscribe for any p2p messages to a client
@client_topic.subscribe(:ack => false) do |headers, msg|
+ time_it('p2p_topic') {
begin
routing_key = headers.routing_key
client_id = routing_key["client.".length..-1]
@@ -240,6 +267,7 @@ module JamWebsockets
@log.error "unhandled error in messaging to client"
@log.error e
end
+ }
end
MQRouter.client_exchange = @clients_exchange
@@ -252,6 +280,7 @@ module JamWebsockets
# subscribe for any p2p messages to a client
@subscription_topic.subscribe(:ack => false) do |headers, msg|
+ time_it('subscribe_topic') {
begin
routing_key = headers.routing_key
type_and_id = routing_key["subscription.".length..-1]
@@ -276,6 +305,7 @@ module JamWebsockets
@log.error "unhandled error in messaging to client for mount"
@log.error e
end
+ }
end
MQRouter.subscription_exchange = @subscriptions_exchange
@@ -376,29 +406,33 @@ module JamWebsockets
client.onopen { |handshake|
- stats_connected
+ time_it('onopen') {
+ stats_connected
- # a unique ID for this TCP connection, to aid in debugging
- client.channel_id = handshake.query["channel_id"]
+ # a unique ID for this TCP connection, to aid in debugging
+ client.channel_id = handshake.query["channel_id"]
- @log.debug "client connected #{client} with channel_id: #{client.channel_id}"
+ @log.debug "client connected #{client} with channel_id: #{client.channel_id}"
- # check for '?pb' or '?pb=true' in url query parameters
- query_pb = handshake.query["pb"]
+ # check for '?pb' or '?pb=true' in url query parameters
+ query_pb = handshake.query["pb"]
- if !query_pb.nil? && (query_pb == "" || query_pb == "true")
- client.encode_json = false
- end
+ if !query_pb.nil? && (query_pb == "" || query_pb == "true")
+ client.encode_json = false
+ end
- websocket_comm(client, nil) do
- handle_login(client, handshake.query, handshake.headers["X-Forwarded-For"])
- end
+ websocket_comm(client, nil) do
+ handle_login(client, handshake.query, handshake.headers["X-Forwarded-For"])
+ end
+ }
}
client.onclose {
- @log.debug "connection closed. marking stale: #{client.context}"
- cleanup_client(client)
+ time_it('onclose') {
+ @log.debug "connection closed. marking stale: #{client.context}"
+ cleanup_client(client)
+ }
}
client.onerror { |error|
@@ -416,6 +450,12 @@ module JamWebsockets
msg = nil
+ if @largest_message.nil? || data.length > @largest_message.length
+ @largest_message = data
+ @largest_message_user = client.user_id
+ end
+
+
# extract the message safely
websocket_comm(client, nil) do
if client.encode_json
@@ -503,15 +543,15 @@ module JamWebsockets
elsif @message_factory.client_directed? client_msg
to_client_id = client_msg.route_to[MessageFactory::CLIENT_TARGET_PREFIX.length..-1]
- handle_client_directed(to_client_id, client_msg, client)
+ time_it('client_directed') { handle_client_directed(to_client_id, client_msg, client) }
elsif @message_factory.session_directed? client_msg
session_id = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1]
- handle_session_directed(session_id, client_msg, client)
+ time_it('session_directed') { handle_session_directed(session_id, client_msg, client) }
elsif @message_factory.user_directed? client_msg
user_id = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1]
- handle_user_directed(user_id, client_msg, client)
+ time_it('user_directed') { handle_user_directed(user_id, client_msg, client) }
else
raise SessionError, "client_msg.route_to is unknown type: #{client_msg.route_to}"
@@ -524,16 +564,16 @@ module JamWebsockets
if client_msg.type == ClientMessage::Type::LOGIN
- handle_login(client_msg.login, client)
+ time_it('login') { handle_login(client_msg.login, client) }
elsif client_msg.type == ClientMessage::Type::HEARTBEAT
- sane_logging { handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) }
+ time_it('heartbeat') { sane_logging { handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) } }
elsif client_msg.type == ClientMessage::Type::SUBSCRIBE_BULK
- sane_logging { handle_bulk_subscribe(client_msg.subscribe_bulk, client) }
+ time_it('subscribe_bulk') { sane_logging { handle_bulk_subscribe(client_msg.subscribe_bulk, client) } }
elsif client_msg.type == ClientMessage::Type::SUBSCRIBE
- sane_logging { handle_subscribe(client_msg.subscribe, client) }
+ time_it('subscribe') { sane_logging { handle_subscribe(client_msg.subscribe, client) } }
elsif client_msg.type == ClientMessage::Type::UNSUBSCRIBE
- sane_logging { handle_unsubscribe(client_msg.unsubscribe, client) }
+ time_it('unsubscribe') { sane_logging { handle_unsubscribe(client_msg.unsubscribe, client) } }
else
raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.route_to}-directed message"
end
@@ -827,41 +867,92 @@ module JamWebsockets
end
end
+ def add_to_ban(user, reason)
+ user_ban = @temp_ban[user.id]
+
+ if user_ban.nil?
+ user_ban = {}
+ @temp_ban[user.id] = user_ban
+ end
+
+ # allow user back in, after 10 minutes
+ user_ban[:allow] = Time.now + 600
+
+ @log.info("user #{user} banned for 10 minutes. reason #{reason}")
+ end
+
+ def runaway_heartbeat(heartbeat, context)
+ heartbeat_count = @heartbeat_tracker[context.user.id] || 0
+ heartbeat_count += 1
+ @heartbeat_tracker[context.user.id] = heartbeat_count
+
+ if heartbeat_count > (context.client_type == 'browser' ? @maximum_minutely_heartbeat_rate_browser : @maximum_minutely_heartbeat_rate_client)
+ @log.warn("user #{context.user} sending too many heartbeats: #{heartbeat_count}") if heartbeat_count % 100 == 0
+
+ add_to_ban(context.user, 'too many heartbeats')
+ raise SessionError.new('too many heartbeats', 'empty_login')
+ else
+ false
+ end
+
+ end
+
def handle_heartbeat(heartbeat, heartbeat_message_id, client)
unless context = @clients[client]
- @log.warn "*** WARNING: unable to find context when handling heartbeat. client_id=#{client.client_id}; killing session"
- #Diagnostic.missing_client_state(client.user_id, client.context)
- raise SessionError, 'context state is gone. please reconnect.'
+ profile_it('heartbeat_context_gone') {
+ @log.warn "*** WARNING: unable to find context when handling heartbeat. client_id=#{client.client_id}; killing session"
+ #Diagnostic.missing_client_state(client.user_id, client.context)
+ raise SessionError, 'context state is gone. please reconnect.'
+ }
else
- connection = Connection.find_by_client_id(context.client.client_id)
+ if runaway_heartbeat(heartbeat, context)
+ return
+ end
+
+ connection = nil
+ profile_it('heartbeat_find_conn') {
+ connection = Connection.find_by_client_id(context.client.client_id)
+ }
track_changes_counter = nil
if connection.nil?
- @log.warn "*** WARNING: unable to find connection when handling heartbeat. context= #{context}; killing session"
- Diagnostic.missing_connection(client.user_id, client.context)
- raise SessionError, 'connection state is gone. please reconnect.'
+ profile_it('heartbeat_diag_missing') {
+ @log.warn "*** WARNING: unable to find connection when handling heartbeat. context= #{context}; killing session"
+ Diagnostic.missing_connection(client.user_id, client.context)
+ raise SessionError, 'connection state is gone. please reconnect.'
+ }
else
- Connection.transaction do
- # send back track_changes_counter if in a session
- if connection.music_session_id
- music_session = ActiveMusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id)
- track_changes_counter = music_session.track_changes_counter if music_session
+ #profile_it('heartbeat_transaction') {
+ #Connection.transaction do
+ # send back track_changes_counter if in a session
+ profile_it('heartbeat_session') {
+ if connection.music_session_id
+ music_session = ActiveMusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id)
+ track_changes_counter = music_session.track_changes_counter if music_session
+ end
+ }
+
+ profile_it('heartbeat_touch') {
+ # update connection updated_at
+ connection.touch
+ }
+
+ profile_it('heartbeat_notification') {
+ # update user's notification_seen_at field if the heartbeat indicates it saw one
+ # first we try to use the notification id, which should usually exist.
+ # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification
+ update_notification_seen_at(connection, context, heartbeat) if client.context.client_type != Connection::TYPE_LATENCY_TESTER
+ }
+ #end
+ #}
+
+ profile_it('heartbeat_stale') {
+ if connection.stale?
+ ConnectionManager.active_record_transaction do |connection_manager|
+ heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type)
+ connection_manager.reconnect(connection, client.channel_id, connection.music_session_id, nil, connection_stale_time, connection_expire_time, nil, @gateway_name)
+ end
end
-
- # update connection updated_at
- connection.touch
-
- # update user's notification_seen_at field if the heartbeat indicates it saw one
- # first we try to use the notification id, which should usually exist.
- # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification
- update_notification_seen_at(connection, context, heartbeat) if client.context.client_type != Connection::TYPE_LATENCY_TESTER
- end
-
- if connection.stale?
- ConnectionManager.active_record_transaction do |connection_manager|
- heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type)
- connection_manager.reconnect(connection, client.channel_id, connection.music_session_id, nil, connection_stale_time, connection_expire_time, nil, @gateway_name)
- end
- end
+ }
end
heartbeat_ack = @message_factory.heartbeat_ack(track_changes_counter)
@@ -884,7 +975,7 @@ module JamWebsockets
def update_notification_seen_at(connection, context, heartbeat)
notification_id_field = heartbeat.notification_seen if heartbeat.value_for_tag(1)
- if notification_id_field
+ if notification_id_field && notification_id_field != ''
notification = Notification.find_by_id(notification_id_field)
if notification
connection.user.notification_seen_at = notification.created_at
@@ -921,6 +1012,13 @@ module JamWebsockets
@log.debug "no user found with token #{token}"
return nil
else
+
+ # check against temp ban list
+ if @temp_ban[user.id]
+ @log.debug("user #{user} is still banned; rejecting login")
+ raise SessionError.new('login rejected temporarily', 'empty_login')
+ end
+
@log.debug "#{user} login via token"
return user
end
@@ -931,6 +1029,12 @@ module JamWebsockets
# attempt login with username and password
user = User.find_by_email(username)
+ # check against temp ban list
+ if !user.nil? && @temp_ban[user.id]
+ @log.debug("user #{user} is still banned; rejecting login")
+ raise SessionError.new('login rejected temporarily', 'empty_login')
+ end
+
if !user.nil? && user.valid_password?(password)
@log.debug "#{user} login via password"
return user
@@ -1117,12 +1221,46 @@ module JamWebsockets
end
def periodical_stats_dump
+
# assume 60 seconds per status dump
stats = @message_stats.sort_by{|k,v| -v}
stats.map { |i| i[1] = (i[1] / 60.0).round(2) }
- @log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', '));
+ @log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', '))
+ @log.info("largest msg from #{@largest_message_user}: #{@largest_message ? @largest_message.length : 0}b")
+ if @highest_drift > 1
+ @log.info("highest drift: #{@highest_drift - 2}")
+ end
+
+ total_time = 0
+ time_sums = @time_it_sums.sort_by{|k,v| -v}
+
+ log_num = 3
+ count = 0
+ time_sums.each do | cat, cat_time |
+ count += 1
+ if count <= log_num
+ @log.info("timed #{cat} used time: #{cat_time}")
+ end
+
+ total_time += cat_time
+ end
+
+ @log.info("total used time: #{total_time}")
+
+ profile_sums = @profile_it_sums.sort_by{|k,v| -v}
+ profile_sums.each do | cat, cat_time |
+ @log.info("profiled #{cat} used time: #{cat_time}")
+ end
+
+
+ @temp_ban.each do |user_id, data|
+ if Time.now > data[:allow]
+ @log.info("user #{user_id} allowed back in")
+ @temp_ban.delete(user_id)
+ end
+ end
# stuff in extra stats into the @message_stats and send it all off
@message_stats['gateway_name'] = @gateway_name
@@ -1130,6 +1268,11 @@ module JamWebsockets
@message_stats['login_fail'] = @login_fail_count
@message_stats['connected'] = @connected_count
@message_stats['disconnected'] = @disconnected_count
+ @message_stats['largest_msg'] = @largest_message ? @largest_message.length : 0
+ @message_stats['highest_drift'] = @highest_drift - 2 # 2 comes from the server's 2 second timer for the drift check
+ @message_stats['total_time'] = total_time
+ @message_stats['banned_users'] = @temp_ban.length
+
Stats.write('gateway.stats', @message_stats)
@@ -1139,6 +1282,12 @@ module JamWebsockets
@login_fail_count = 0
@connected_count = 0
@disconnected_count = 0
+ @user_message_counts = {}
+ @largest_message = nil
+ @largest_message_user = nil
+ @time_it_sums = {}
+ @highest_drift = 0
+ @heartbeat_tracker = {}
end
def cleanup_clients_with_ids(expired_connections)
@@ -1245,6 +1394,30 @@ module JamWebsockets
private
+ def time_it(cat, &blk)
+ start = Time.now
+
+ blk.call
+
+ time = Time.now - start
+
+ @time_it_sums[cat] = (@time_it_sums[cat] || 0 )+ time
+
+ @log.warn("LONG TIME: #{cat}: #{time}") if time > 1
+ end
+
+ def profile_it(cat, &blk)
+ start = Time.now
+
+ blk.call
+
+ time = Time.now - start
+
+ @profile_it_sums[cat] = (@profile_it_sums[cat] || 0 )+ time
+
+ @log.warn("LONG TIME: #{cat}: #{time}") if time > 1
+ end
+
def sane_logging(&blk)
# used around repeated transactions that cause too much ActiveRecord::Base logging
begin
diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb
index fd45f7c42..25322dc98 100644
--- a/websocket-gateway/lib/jam_websockets/server.rb
+++ b/websocket-gateway/lib/jam_websockets/server.rb
@@ -10,6 +10,8 @@ module JamWebsockets
@count=0
@router = Router.new
@ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base]
+
+ @last_conn_check = nil
end
def run(options={})
@@ -26,6 +28,7 @@ module JamWebsockets
rabbitmq_port = options[:rabbitmq_port].to_i
allow_dynamic_registration = options[:allow_dynamic_registration].nil? ? true : options[:allow_dynamic_registration]
+
Stats::init(options)
calling_thread = options[:calling_thread]
@@ -61,6 +64,17 @@ module JamWebsockets
EventMachine::stop_event_loop
end
+
+ def check_for_em_drift(timer)
+ # if our timer check is a full second off, say what's up
+ drift = Time.now - @last_conn_check
+ @router.highest_drift = drift if drift > @router.highest_drift
+
+ @last_conn_check = Time.now
+ end
+
+
+
def start_websocket_listener(listen_ip, port, trust_port, trust_check, emwebsocket_debug)
EventMachine::WebSocket.run(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws|
#@log.info "new client #{ws}"
@@ -84,8 +98,11 @@ module JamWebsockets
# one cleanup on startup
@router.periodical_check_connections
- EventMachine::PeriodicTimer.new(2) do
- safety_net { sane_logging { @router.periodical_check_connections } }
+ @last_conn_check = Time.now
+ timer = 2
+ EventMachine::PeriodicTimer.new(timer) do
+ check_for_em_drift(timer)
+ time_it('conn_expire') { safety_net { sane_logging { @router.periodical_check_connections } } }
end
end
@@ -94,7 +111,7 @@ module JamWebsockets
@router.periodical_check_clients
EventMachine::PeriodicTimer.new(30) do
- safety_net { sane_logging { @router.periodical_check_clients } }
+ time_it('client_expire') { safety_net { sane_logging { @router.periodical_check_clients } } }
end
end
@@ -103,13 +120,13 @@ module JamWebsockets
@router.periodical_flag_connections
EventMachine::PeriodicTimer.new(2) do
- safety_net { sane_logging { @router.periodical_flag_connections } }
+ time_it('conn_flagger') { safety_net { sane_logging { @router.periodical_flag_connections } } }
end
end
def start_stats_dump
EventMachine::PeriodicTimer.new(60) do
- safety_net { @router.periodical_stats_dump }
+ time_it('stats_dump') { safety_net { @router.periodical_stats_dump } }
end
end
@@ -124,8 +141,26 @@ module JamWebsockets
rescue => e
Bugsnag.notify(e)
@log.error("unhandled exception in EM Timer #{e}")
+ puts "Error during processing: #{$!}"
+ puts "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
+
end
end
+
+ def time_it(cat, &blk)
+ start = Time.now
+
+ blk.call
+
+ time = Time.now - start
+
+
+ @router.time_it_sums[cat] = (@router.time_it_sums[cat] || 0) + time
+
+ @log.warn("LONG TIME #{cat}: #{time}") if time > 1
+ end
+
+
def sane_logging(&blk)
# used around repeated transactions that cause too much ActiveRecord::Base logging
# example is handling heartbeats