diff --git a/admin/Gemfile b/admin/Gemfile index 6f5c0b21e..e71c00a03 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -41,7 +41,7 @@ gem 'uuidtools', '2.1.2' gem 'jquery-rails' # , '2.3.0' # pinned because jquery-ui-rails was split from jquery-rails, but activeadmin doesn't support this gem yet gem 'jquery-ui-rails', '4.2.1' gem 'rails3-jquery-autocomplete' -gem 'activeadmin', '0.6.2' +gem 'activeadmin', github: 'activeadmin', branch: '0-6-stable' gem 'mime-types', '1.25' gem 'meta_search' gem 'fog', "~> 1.18.0" diff --git a/admin/app/admin/cohorts.rb b/admin/app/admin/cohorts.rb new file mode 100644 index 000000000..fac1fa5a3 --- /dev/null +++ b/admin/app/admin/cohorts.rb @@ -0,0 +1,63 @@ +ActiveAdmin.register Cohort, :as => 'Cohorts' do + + menu :label => 'Cohorts', :parent => 'Analytics' + + config.sort_order = 'group_start_desc' + config.batch_actions = false + config.clear_action_items! + config.filters = false + config.per_page = 50 + + controller do + + def scoped_collection + objs = super + Cohort.alltime_cohorts! if 0 == Cohort.where(all_time: true).count + objs = objs.where(all_time: true).order('group_start DESC') + logger.debug("*** scoped_collection: #{objs.to_sql}") + objs + end + + end + + index :title => "All-Time Cohorts" do + column 'Cohort' do |cc| cc.group_start_str end + column Cohort::TOTAL_LABELS[:registered_users] do |cc| cc.data_val(:registered_users) end + + column Cohort::TOTAL_LABELS[:first_downloaded_client_at] do |cc| + cc.data_val(:first_downloaded_client_at) + end + column '%' do |cc| cc.data_val(:first_downloaded_client_at, true) end + + column Cohort::TOTAL_LABELS[:first_certified_gear_at] do |cc| + cc.data_val(:first_certified_gear_at) + end + column '%' do |cc| cc.data_val(:first_certified_gear_at, true) end + + column Cohort::TOTAL_LABELS[:music_sessions_user_history] do |cc| + cc.data_val(:music_sessions_user_history) + end + column '%' do |cc| cc.data_val(:music_sessions_user_history, true) end + + column Cohort::TOTAL_LABELS[:jam_track_rights] do |cc| + cc.data_val(:jam_track_rights) + end + column '%' do |cc| cc.data_val(:jam_track_rights, true) end + + column Cohort::TOTAL_LABELS[:recorded_tracks] do |cc| + cc.data_val(:recorded_tracks) + end + column '%' do |cc| cc.data_val(:recorded_tracks, true) end + + column Cohort::TOTAL_LABELS[:friendships] do |cc| + cc.data_val(:friendships) + end + column '%' do |cc| cc.data_val(:friendships, true) end + + column Cohort::TOTAL_LABELS[:invited_users] do |cc| + cc.data_val(:invited_users) + end + column '%' do |cc| cc.data_val(:invited_users, true) end + end + +end diff --git a/admin/app/admin/cohorts_monthly.rb b/admin/app/admin/cohorts_monthly.rb new file mode 100644 index 000000000..4ef74f4d8 --- /dev/null +++ b/admin/app/admin/cohorts_monthly.rb @@ -0,0 +1,78 @@ +ActiveAdmin.register Cohort, :as => 'Cohorts Monthly' do + + menu :label => 'Cohorts Monthly', :parent => 'Analytics' + + config.sort_order = 'group_start_desc' + config.batch_actions = false + config.clear_action_items! + config.per_page = 50 + + filter(:monthly_start, as: :select, collection: Cohort.monthly_starts) + filter(:monthly_end, as: :select, collection: Cohort.monthly_ends) + + controller do + def scoped_collection + objs = super + args = params[:q] || {} + if !(args[:monthly_start_eq].nil? || args[:monthly_end_eq].nil?) + mstart, mend = Time.parse(args[:monthly_start_eq]), Time.parse(args[:monthly_end_eq]) + if mstart < mend + Cohort.monthly_cohorts(mstart, mend) # populate monthlys + objs = objs.where(monthly_start: mstart, monthly_end: mend) + end + end + objs.where(all_time: false).order('group_start DESC') + end + end + + index :title => "Monthly Cohorts" do + column 'Cohort' do |cc| cc.group_start_str end + column Cohort::MONTHLY_LABELS[:registered_users] do |cc| cc.data_val(:registered_users) end + + column Cohort::MONTHLY_LABELS[:first_downloaded_client_at] do |cc| + cc.data_val(:first_downloaded_client_at) + end + column '%' do |cc| cc.data_val(:first_downloaded_client_at, true) end + + column Cohort::MONTHLY_LABELS[:first_certified_gear_at] do |cc| + cc.data_val(:first_certified_gear_at) + end + column '%' do |cc| cc.data_val(:first_certified_gear_at, true) end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_1] do |cc| + cc.data_val(:music_sessions_user_history_1) + end + column '%' do |cc| cc.data_val(:music_sessions_user_history_1, true) end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_2_5] do |cc| + cc.data_val(:music_sessions_user_history_2_5) + end + column '%' do |cc| cc.data_val(:music_sessions_user_history_2_5, true) end + + column Cohort::MONTHLY_LABELS[:music_sessions_user_history_6_] do |cc| + cc.data_val(:music_sessions_user_history_6_) + end + column '%' do |cc| cc.data_val(:music_sessions_user_history_6_, true) end + + column Cohort::MONTHLY_LABELS[:jam_track_rights] do |cc| + cc.data_val(:jam_track_rights) + end + column '%' do |cc| cc.data_val(:jam_track_rights, true) end + + column Cohort::MONTHLY_LABELS[:recorded_tracks] do |cc| + cc.data_val(:recorded_tracks) + end + column '%' do |cc| cc.data_val(:recorded_tracks, true) end + + column Cohort::MONTHLY_LABELS[:friendships] do |cc| + cc.data_val(:friendships) + end + column '%' do |cc| cc.data_val(:friendships, true) end + + column Cohort::MONTHLY_LABELS[:invited_users] do |cc| + cc.data_val(:invited_users) + end + column '%' do |cc| cc.data_val(:invited_users, true) end + end + +end diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index ac1897c4e..96f341829 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -25,7 +25,6 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do column :original_artist column :name column :status - column :preview do |jam_track| jam_track.has_preview? ? (link_to "Download", jam_track.sign_url(3600)) : 'None' end column :master_track do |jam_track| jam_track.master_track.nil? ? 'None' : (link_to "Download", jam_track.master_track.url_by_sample_rate(44)) end column :licensor column :genre diff --git a/admin/app/models/cohort.rb b/admin/app/models/cohort.rb new file mode 100644 index 000000000..62b565f68 --- /dev/null +++ b/admin/app/models/cohort.rb @@ -0,0 +1,290 @@ +require 'date' + +class Cohort < ActiveRecord::Base + + EARLIEST_DATE = Time.parse('2014-03-01') + + KEY_SET_TOTAL = [:registered_users, :first_downloaded_client_at, :first_certified_gear_at, :music_sessions_user_history, 'play_jamtr', :jam_track_rights, :recorded_tracks, :friendships, :invited_users] + + TOTAL_LABELS = { + registered_users: 'Registered Users', + first_downloaded_client_at: 'Downloaded app', + first_certified_gear_at: 'Certified Gear', + music_sessions_user_history: 'Played Online', + play_jamtr: 'Played JamTrack', + jam_track_rights: 'Purchased JamTrack', + recorded_tracks: 'Made Recording', + friendships: 'Connected w/Friend', + invited_users: 'Invite Others', + } + + # KEY_SET_TOTAL = [:registered_users, :first_downloaded_client_at, :first_certified_gear_at, :music_session_histories, 'play_jamtr', :jam_track_rights, :recordings, :friendships, :invited_users] + KEY_SET_MONTH = [:registered_users, :first_downloaded_client_at, :first_certified_gear_at, :visit_1, :visit_2_5, :visit_6_, :play_online_1, :play_online_2_5, :play_online_6_, :play_jamtr_1, :play_jamtr_2_5, :play_jamtr_6_, :jam_track_rights_redeemed, :jam_track_rights, :recorded_tracks, :friendships, :invited_users] + + MONTHLY_LABELS = { + registered_users: 'Registered Users', + first_downloaded_client_at: 'Downloaded app', + first_certified_gear_at: 'Certified Gear', + music_sessions_user_history_1: 'Played Online 1', + music_sessions_user_history_2_5: 'Played Online 2-5', + music_sessions_user_history_6_: 'Played Online 6+', + play_jamtr: 'Played JamTrack', + jam_track_rights: 'Purchased JamTrack', + recorded_tracks: 'Made Recording', + friendships: 'Connected w/Friend', + invited_users: 'Invite Others', + } + + serialize :data_set, JSON + + before_create do + self.data_set ||= {} + end + + def self.date_tuples(from,to) + prec = from.size + start = Date.new(*from) + finish = Date.new(*to) + + filter_on = [:day,:mon].first(3-prec) + filter = ->(d) { filter_on.all? {|attr| d.send(attr) == 1 } } + + (start..finish) + .select(&filter) + .map { |d| [d.year,d.mon,d.day].first(prec) } + end + + def self.earliest_cohort + starting = User.where(admin: false).order(:created_at).first.created_at + starting = EARLIEST_DATE if starting < EARLIEST_DATE + end + + def self.cohort_group_ranges(starting=nil, ending=nil) + starting ||= self.earliest_cohort + ending ||= Time.now + dates = self.date_tuples([starting.year, starting.month], [ending.year, ending.month]) + ranges = [] + dates.each_with_index do |d1, idx| + d2 = dates[idx+1] || [Time.now.next_month.year,Time.now.next_month.month] + rr = Time.parse("#{d1[0]}-#{d1[1]}-1")..(Time.parse("#{d2[0]}-#{d2[1]}-1") - 1.second) + ranges << rr + end + ranges + end + + def self.generate_monthly_cohorts(monthly_start, monthly_end) + Cohort.delete_all(['all_time = ?',false]) + self.cohort_group_ranges.collect do |range| + next if range.first > monthly_end + cc = Cohort.new + cc.group_start = range.first + cc.group_end = range.last + cc.monthly_start = monthly_start + cc.monthly_end = monthly_end + cc.all_time = false + cc.save! + cc + end + end + + def self.generate_all_time_cohorts + self.cohort_group_ranges.collect do |range| + unless cc = Cohort.where(group_start: range.first).where(all_time: true).limit(1).first + cc = Cohort.new + cc.group_start = range.first + cc.group_end = range.last + cc.all_time = true + cc.save! + end + cc + end + end + + def _put_data_set(key, count, num_user) + self.data_set[key.to_s] = count + xx = (count.to_f / num_user.to_f) + self.data_set["#{key}%"] = 100.0 * xx.round(2) + end + + def self.cohort_users(cohort) + User.where(created_at: cohort.group_start..cohort.group_end) + end + + def _played_online_subquery(constraint) + where = if constraint.is_a?(Range) + "played.cnt >= #{constraint.first} AND played.cnt <= #{constraint.last}" + else + "played.cnt #{constraint}" + end + start_date = all_time ? self.group_start : self.monthly_start + end_date = all_time ? self.group_end : self.monthly_end + sql =<= '#{start_date}' AND msuh1.created_at <= '#{end_date}' AND + EXTRACT(EPOCH FROM (msuh1.session_removed_at - msuh1.created_at)) >= 900 AND + (SELECT COUNT(*) FROM music_sessions_user_history msuh2 + WHERE msuh1.music_session_id = msuh2.music_session_id + ) > 1 + GROUP BY user_id + ) played +WHERE #{where} +SQL + end + + def _subquery(assoc_key, num_user) + assoc = User.reflections[assoc_key] + start_date = all_time ? self.group_start : self.monthly_start + end_date = all_time ? self.group_end : self.monthly_end + sql =<= '#{start_date}' AND + tt.created_at <= '#{end_date}' +SQL + yield(sql) if block_given? + self.class.cohort_users(self).where("users.id IN (#{sql})").count + end + + def _monthly! + unless 0 < num_user = self.class.cohort_users(self).count + self.update_attribute(:data_set, {}) + return + end + + self.data_set['registered_users'] = num_user + num_user = num_user.to_f + + qq = self.class.cohort_users(self) + .where(first_downloaded_client_at: self.monthly_start..self.monthly_end) + _put_data_set(:first_downloaded_client_at, qq.count, num_user) + + qq = self.class.cohort_users(self) + .where(first_certified_gear_at: self.monthly_start..self.monthly_end) + _put_data_set(:first_certified_gear_at, qq.count, num_user) + + count = _subquery(assoc_key = :invited_users, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :recorded_tracks, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql| + subsql += " AND tt.redeemed = 't' " + end + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :friendships, num_user) + _put_data_set(assoc_key, count, num_user) + + sql = _played_online_subquery(' = 1 ') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_1, count, num_user) + + sql = _played_online_subquery(2..5) + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_2_5, count, num_user) + + sql = _played_online_subquery(' >= 6') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history_6_, count, num_user) + + self.save! + end + + def _join_user_all_time(assoc_ref) + assoc_ref.active_record + .joins("INNER JOIN users AS uu ON uu.id = #{assoc_ref.foreign_key}") + .where(created_at: self.group_start..self.group_end) + .where(['uu.created_at >= ? AND uu.created_at <= ?', self.group_start, self.group_end]) + end + + def _all_time! + unless 0 < num_user = self.class.cohort_users(self).count + self.update_attribute(:data_set, {}) + return + end + + self.data_set['registered_users'] = num_user + num_user = num_user.to_f + + count = self.class.cohort_users(self) + .where(['first_downloaded_client_at IS NOT NULL']) + .count + _put_data_set('first_downloaded_client_at', count, num_user) + + count = self.class.cohort_users(self) + .where(['first_certified_gear_at IS NOT NULL']) + .count + _put_data_set('first_certified_gear_at', count, num_user) + + count = _subquery(assoc_key = :invited_users, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :recorded_tracks, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :friendships, num_user) + _put_data_set(assoc_key, count, num_user) + + count = _subquery(assoc_key = :jam_track_rights, num_user) + _put_data_set(assoc_key, count, num_user) + + sql = _played_online_subquery(' >= 1') + count = self.class.cohort_users(self).where("users.id IN (#{sql})").count + _put_data_set(:music_sessions_user_history, count, num_user) + + self.save! + end + + def populate! + self.all_time ? _all_time! : _monthly! + end + + def self.alltime_cohorts! + Cohort.generate_all_time_cohorts.each do |cc| + cc._all_time! + end + end + + def self.monthly_cohorts(monthly_start, monthly_end) + self.generate_monthly_cohorts(monthly_start, monthly_end).compact.each do |cc| + cc._monthly! + end + end + + def group_start_str + self.group_start.strftime('%Y-%m-%d') + end + + def group_end_str + self.group_end.strftime('%Y-%m-%d') + end + + def data_val(col, percent=false) + if percent + val = self.data_set["#{col}%"] + val ? '%0.f' % val : '' + else + self.data_set[col.to_s] + end + end + + def self.monthly_starts + self.cohort_group_ranges.collect do |rr| + rr.first.to_s + end + end + + def self.monthly_ends + self.cohort_group_ranges.collect do |rr| + rr.last.to_s + end + end + +end + diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index b74dba9c1..fc0d9502a 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -4,7 +4,6 @@ = f.input :name, :input_html => { :rows=>1, :maxlength=>200 } = f.input :description, :input_html => { :rows=>5, :maxlength=>1000 } = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly' - = f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the media in the JamTrack. Changing JMEP does not count as a version change; changing anything about a track (audio, instrument, part) does.' //= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' = f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: true = f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false, hint: 'Only set to Production when end users should be able to purchase this JamTrack' @@ -23,15 +22,11 @@ = f.input :public_performance_royalty, :label => 'Public Performance Royalty' = f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'} = f.input :licensor_royalty_amount, :required=>true, :input_html=>{type:'numeric'} - = f.input :preview_start_time_raw, :label=>'Preview Start Time', :hint => 'MM:SS:MLS', :as => :string - - unless f.object.nil? || f.object[:preview_url].nil? - .current_file_holder style='margin-bottom:10px' - a href=f.object.sign_url(3600) style='padding:0 0 0 20px' - | Download //= f.input :url, :as => :file, :label => 'Audio File' = f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 }, :hint => 'Tap-Ins & Lead Silence. Examples: https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=39289025#JamKazamMeta-EventProcessor(JMEP)-CommonExamples' = f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly => true }, :hint => 'Readonly field. This is shown here just so you can see what your JMEP got converted to readily' + = f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the media in the JamTrack. Changing JMEP does not count as a version change; changing anything about a track (audio, instrument, part) does.' = f.semantic_fields_for :jam_track_tracks do |track| = render 'jam_track_track_fields', f: track diff --git a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim index 593d8ef7d..c08812dcb 100644 --- a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim +++ b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.slim @@ -4,8 +4,12 @@ = f.input :track_type, :as => :select, collection: ['Track', 'Master'], include_blank: false = f.input :instrument, collection: Instrument.all, include_blank: false = f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' } - = f.input :position + = f.input :preview_start_time_raw, :label => 'Preview Start Time', :hint => 'MM:SS:MLS', :as => :string + - unless f.object.nil? || f.object[:preview_url].nil? + .current_file_holder style='margin-bottom:10px' + a href=f.object.preview_sign_url(3600) style='padding:0 0 0 20px' + | Download Preview - if f.object.new_record? p style='margin-left:10px' @@ -16,12 +20,12 @@ - unless f.object.nil? || f.object[:url_48].nil? .current_file_holder style='margin-bottom:10px' a href=f.object.sign_url(3600) style='padding:0 0 0 20px' - | Download + | #{File.basename(f.object[:url_48])} = f.input :url_44, :as => :file, :label => 'Track file (44kHz)' - unless f.object.nil? || f.object[:url_44].nil? .current_file_holder style='margin-bottom:10px' a href=f.object.sign_url(3600, 44) style='padding:0 0 0 20px' - | Download + | #{File.basename(f.object[:url_44])} = link_to_remove_association "Delete Track", f, class: 'button', style: 'margin-left:10px' \ No newline at end of file diff --git a/admin/config/initializers/jam_track_tracks.rb b/admin/config/initializers/jam_track_tracks.rb new file mode 100644 index 000000000..dde477bad --- /dev/null +++ b/admin/config/initializers/jam_track_tracks.rb @@ -0,0 +1,101 @@ +class JamRuby::JamTrackTrack + + # add a custom validation + + attr_accessor :preview_generate_error + + validate :preview + + def preview + if preview_generate_error + errors.add(:preview_start_time, preview_generate_error) + end + end + + + + # this is used by active admin/jam-admin + def preview_start_time_raw + if self.preview_start_time.nil? || self.preview_start_time.nil? + '' + else + seconds = self.preview_start_time.to_f/1000 + time = Time.at(seconds) + time.strftime("%M:%S:#{(self.preview_start_time % 1000).to_s.rjust(3, '0')}") + end + end + + # this is used by active admin/jam-admin + def preview_start_time_raw=(new_value) + + value = nil + if new_value == nil || new_value == '' + value = nil + else + if new_value && new_value.kind_of?(String) && new_value.include?(':') + bits = new_value.split(':') + if bits.length != 3 + raise "format of preview start time must be MM:SS:MLS" + end + + value = (bits[0].to_i * 60000) + (bits[1].to_i * 1000) + (bits[2].to_i) + + else + raise "format of preview start time must be MM:SS:MLS" + end + end + + if !value.nil? && value != self.preview_start_time + self.preview_start_time = value + generate_preview + else + self.preview_start_time = value + end + end + + def generate_preview + + begin + Dir.mktmpdir do |tmp_dir| + + input = File.join(tmp_dir, 'in.ogg') + output = File.join(tmp_dir, 'out.ogg') + + start = self.preview_start_time.to_f / 1000 + stop = start + 20 + + raise 'no track' unless self["url_44"] + + s3_manager.download(self.url_by_sample_rate(44), input) + + command = "sox \"#{input}\" \"#{output}\" trim #{start} #{stop}" + + @@log.debug("trimming using: " + command) + + sox_output = `#{command}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute cut command #{sox_output}" + else + @@log.debug("uploading preview to #{self.preview_filename}") + + s3_manager.upload(self.preview_filename, output) + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename + self["preview_md5"] = ::Digest::MD5.file(output).hexdigest + self["preview_length"] = File.new(output).size + self.save! + end + end + rescue Exception => e + @@log.error("error in sox command #{e.to_s}") + @preview_generate_error = e.to_s + end + + end + +end diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb index c1c0063bf..cd02eccee 100644 --- a/admin/config/initializers/jam_tracks.rb +++ b/admin/config/initializers/jam_tracks.rb @@ -6,7 +6,6 @@ class JamRuby::JamTrack before_save :jmep_json_generate validate :jmep_text_validate - validate :preview def jmep_text_validate begin @@ -28,97 +27,4 @@ class JamRuby::JamTrack #errors.add(:jmep_text, err.to_s) end end - - def preview - if preview_generate_error - errors.add(:preview_url, preview_generate_error) - end - end - - - - # this is used by active admin/jam-admin - def preview_start_time_raw - if self.preview_start_time.nil? || self.preview_start_time.nil? - '' - else - seconds = self.preview_start_time.to_f/1000 - time = Time.at(seconds) - time.strftime("%M:%S:#{(self.preview_start_time % 1000).to_s.rjust(3, '0')}") - end - end - - # this is used by active admin/jam-admin - def preview_start_time_raw=(new_value) - - value = nil - if new_value == nil || new_value == '' - value = nil - else - if new_value && new_value.kind_of?(String) && new_value.include?(':') - bits = new_value.split(':') - if bits.length != 3 - raise "format of preview start time must be MM:SS:MLS" - end - - value = (bits[0].to_i * 60000) + (bits[1].to_i * 1000) + (bits[2].to_i) - - else - raise "format of preview start time must be MM:SS:MLS" - end - end - - if !value.nil? && value != self.preview_start_time - self.preview_start_time = value - generate_preview - else - self.preview_start_time = value - end - end - - def generate_preview - - begin - Dir.mktmpdir do |tmp_dir| - - input = File.join(tmp_dir, 'in.ogg') - output = File.join(tmp_dir, 'out.ogg') - - start = self.preview_start_time.to_f / 1000 - stop = start + 20 - - master_track = self.master_track - - raise 'no master track' unless master_track - - s3_manager.download(master_track.url_by_sample_rate(44), input) - - command = "sox \"#{input}\" \"#{output}\" trim #{start} #{stop}" - - @@log.debug("trimming using: " + command) - - sox_output = `#{command}` - - result_code = $?.to_i - - if result_code != 0 - @preview_generate_error = "unable to execute cut command #{sox_output}" - else - @@log.debug("uploading preview to #{self.preview_filename}") - - s3_manager.upload(self.preview_filename, output) - - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename - self["preview_md5"] = ::Digest::MD5.file(output).hexdigest - self["preview_length"] = File.new(output).size - self.save! - end - end - rescue Exception => e - @preview_generate_error = e.to_s - end - - end - end diff --git a/db/manifest b/db/manifest index bf08f73a2..761758fe2 100755 --- a/db/manifest +++ b/db/manifest @@ -266,3 +266,5 @@ jam_track_importer.sql jam_track_pro_licensing_update.sql jam_track_redeemed.sql connection_metronome.sql +preview_jam_track_tracks.sql +cohorts.sql diff --git a/db/up/cohorts.sql b/db/up/cohorts.sql new file mode 100644 index 000000000..3a7cbbb5b --- /dev/null +++ b/db/up/cohorts.sql @@ -0,0 +1,19 @@ +CREATE TABLE cohorts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + + data_set JSON NOT NULL DEFAULT '{}', + + group_start TIMESTAMP NOT NULL, + group_end TIMESTAMP NOT NULL, + + all_time BOOLEAN NOT NULL DEFAULT FALSE, + monthly_start TIMESTAMP, + monthly_end TIMESTAMP, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX index_group_date ON cohorts USING btree (group_start); + +CREATE INDEX msuh_music_session_idx ON music_sessions_user_history USING btree(music_session_id); diff --git a/db/up/preview_jam_track_tracks.sql b/db/up/preview_jam_track_tracks.sql new file mode 100644 index 000000000..a8c9ae224 --- /dev/null +++ b/db/up/preview_jam_track_tracks.sql @@ -0,0 +1,9 @@ +ALTER TABLE jam_tracks DROP COLUMN preview_url; +ALTER TABLE jam_tracks DROP COLUMN preview_md5; +ALTER TABLE jam_tracks DROP COLUMN preview_length; +ALTER TABLE jam_tracks DROP COLUMN preview_start_time; + +ALTER TABLE jam_track_tracks ADD COLUMN preview_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_length BIGINT; +ALTER TABLE jam_track_tracks ADD COLUMN preview_start_time INTEGER; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 47efe3cdf..058e44d1e 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -17,7 +17,7 @@ module JamRuby :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :preview_start_time_raw, as: :admin + :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, as: :admin validates :name, presence: true, uniqueness: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -31,7 +31,6 @@ module JamRuby validates :sales_region, inclusion: {in: [nil] + SALES_REGION} validates_format_of :price, with: /^\d+\.*\d{0,2}$/ validates :version, presence: true - validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true validates :pro_ascap, inclusion: {in: [true, false]} validates :pro_bmi, inclusion: {in: [true, false]} validates :pro_sesac, inclusion: {in: [true, false]} @@ -92,21 +91,6 @@ module JamRuby end end - # create name of the file - def preview_filename - "jam_track_previews/#{self.original_artist}/#{self.name}/preview-44100.ogg" - end - - def has_preview? - !self["preview_url"].nil? - end - # creates a short-lived URL that has access to the object. - # the idea is that this is used when a user who has the rights to this tries to download this JamTrack - # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download - # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:preview_url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) - end def master_track JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index 255cd0337..71ba7fea8 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -7,17 +7,21 @@ module JamRuby # there should only be one Master per JamTrack, but there can be N Track per JamTrack TRACK_TYPE = %w{Track Master} + @@log = Logging.logger[JamTrackTrack] + + mount_uploader :url_48, JamTrackTrackUploader mount_uploader :url_44, JamTrackTrackUploader attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin - attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, as: :admin + attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin attr_accessor :original_audio_s3_path, :skip_uploader validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000} validates :part, length: {maximum: 25} validates :track_type, inclusion: {in: TRACK_TYPE } + validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true validates_uniqueness_of :position, scope: :jam_track_id validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id] # validates :jam_track, presence: true @@ -37,6 +41,24 @@ module JamRuby "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" end + # create name of the file + def preview_filename + filename("#{File.basename(self["url_44"], ".ogg")}-preview.ogg") + end + + def has_preview? + !self["preview_url"].nil? + end + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def preview_sign_url(expiration_time = 120) + s3_manager.sign_url(self[:preview_url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + end + + def manually_uploaded_filename(mounted_as) if track_type == 'Master' filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index c295f1123..61273a2db 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -26,6 +26,7 @@ var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout'; var $startNetworkTestBtn = null; + var $foreverNetworkTestBtn = null; var $testResults = null; var $testScore = null; var $testText = null; @@ -51,6 +52,8 @@ var operatingSystem = null; var PRIME_PUMP_TIME = 1; + var forever = false; + // these try to make it such that we only pass a NetworkTest Pass/Failed one time in a new user flow var trackedPass = false; var lastNetworkFailure = null; @@ -313,6 +316,9 @@ if (success) { $self.triggerHandler(NETWORK_TEST_DONE) + if(forever) { + prepareNetworkTest(); + } } else { $self.triggerHandler(NETWORK_TEST_FAIL) @@ -875,6 +881,7 @@ inGearWizard = _inGearWizard; $startNetworkTestBtn = $step.find('.start-network-test'); + $foreverNetworkTestBtn = $step.find('.forever-network-test') if ($startNetworkTestBtn.length == 0) throw 'no start network test button found in network-test' @@ -889,8 +896,16 @@ $subscore = $step.find('.subscore'); $watchVideo = $step.find('.watch-video'); $startNetworkTestBtn.on('click', function () { + forever = false; prepareNetworkTest(); }); + if(context.JK.currentUserAdmin) { + $foreverNetworkTestBtn.on('click', function() { + forever = true; + prepareNetworkTest(); + }).show(); + } + operatingSystem = context.JK.GetOSAsString(); initializeVideoWatchButton(); diff --git a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index 29d498f69..6916f7c60 100644 --- a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -342,6 +342,11 @@ margin-top: 20px; } + a.forever-network-test { + margin-top:20px; + display:none; + } + .network-test-score { height: 24px; padding: 10px; diff --git a/web/app/views/clients/_network_test.html.haml b/web/app/views/clients/_network_test.html.haml index a61e4e081..0a2e34d64 100644 --- a/web/app/views/clients/_network_test.html.haml +++ b/web/app/views/clients/_network_test.html.haml @@ -16,6 +16,8 @@ %p Then click on the Start Network Test button below. .center %a.button-orange.start-network-test{href:'#'} START NETWORK TEST + %br + %a.button-orange.forever-network-test{href:'#'} THE FOREVER TEST (ADMIN ONLY) .wizard-step-column %h2 Test Results .network-test-results.ftue-box