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.

-
- - -
-
- - -
-
- - -
-
- CREATE MIX - {errorHtml} -
-
+ 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.

+
+ + +
+
+ + +
+
+ + +
+
+ CREATE MIX + {errorHtml} +
+
+ +
` + 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 = ` -
-

My Mixes {showMyMixesText}

- - {myMixes} - -

Create Custom Mix {showMixControlsText}

- - {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` + `
{header} - + {extraControls} - {closeLinkText} +
+ {helpButton} + {closeLinkText} +
` 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 = `
-
- - -
+ + chatHelp = `[?]` + + recordingJSX = + `
+
+

Recording Type

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