module JamRuby class Recording < ActiveRecord::Base @@log = Logging.logger[Recording] attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, :jam_track_id, as: :admin has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy has_one :feed, :class_name => "JamRuby::Feed", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id' belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id belongs_to :non_active_music_session, :class_name => "JamRuby::MusicSession", foreign_key: :music_session_id belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id' belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id' accepts_nested_attributes_for :recorded_tracks, :mixes, :claimed_recordings, allow_destroy: true validate :not_already_recording, :on => :create validate :not_still_finalizing_previous, :on => :create validate :not_playback_recording, :on => :create validate :already_stopped_recording validate :only_one_mix before_save :sanitize_active_admin before_create :add_to_feed def add_to_feed feed = Feed.new feed.recording = self end def sanitize_active_admin self.owner_id = nil if self.owner_id == '' self.band_id = nil if self.band_id == '' end def comment_count self.comments.size end def is_jamtrack_recording? !jam_track_id.nil? && parsed_timeline['jam_track_isplaying'] end def parsed_timeline timeline ? JSON.parse(timeline) : {} end def high_quality_mix? has_final_mix end def has_mix? # this is used by the UI to know whether it can show a play button. We prefer a real mix, but a stream mix will do return true if high_quality_mix? has_stream_mix end # this should be a has-one relationship. until this, this is easiest way to get from recording > mix def mix self.mixes[0] if self.mixes.length > 0 end def mix_state mix.state if mix end def mix_error mix.error if mix end def stream_mix quick_mixes.find{|quick_mix| quick_mix.completed && !quick_mix.cleaned } end def mix_state mix.state if mix end def mix_error mix.error if mix end def stream_mix quick_mixes.find{|quick_mix| quick_mix.completed && !quick_mix.cleaned } end # this can probably be done more efficiently, but David needs this asap for a video def grouped_tracks tracks = [] sorted_tracks = self.recorded_tracks.sort { |a,b| a.user.id <=> b.user.id } t = Track.new t.instrument_ids = [] sorted_tracks.each_with_index do |track, index| if index > 0 if sorted_tracks[index-1].user.id != sorted_tracks[index].user.id t = Track.new t.instrument_ids = [] t.instrument_ids << track.instrument.id t.musician = track.user tracks << t else if !t.instrument_ids.include? track.instrument.id t.instrument_ids << track.instrument.id end end else t.musician = track.user t.instrument_ids << track.instrument.id tracks << t end end tracks end def not_already_recording if music_session && music_session.is_recording? errors.add(:music_session, ValidationMessages::ALREADY_BEING_RECORDED) end end # this should be used to cleanup a recording that we detect is no longer running def abort recording.music_session.claimed_recording_id = nil recording.music_session.claimed_recording_initiator_id = nil # double check that there are no claims to this recording before destroying it unless claimed_recordings.length > 0 destroy end end def not_still_finalizing_previous # after a recording is done, users need to keep or discard it. # this checks if the previous recording is still being finalized unless !music_session || music_session.is_recording? previous_recording = music_session.most_recent_recording if previous_recording previous_recording.recorded_tracks.each do |recorded_track| # if at least one user hasn't taken any action yet... if recorded_track.discard.nil? # and they are still around and in this music session still... connection = Connection.find_by_client_id(recorded_track.client_id) if !connection.nil? && connection.music_session == music_session errors.add(:music_session, ValidationMessages::PREVIOUS_RECORDING_STILL_BEING_FINALIZED) break end end end end end end def not_playback_recording if music_session && music_session.is_playing_recording? errors.add(:music_session, ValidationMessages::ALREADY_PLAYBACK_RECORDING) end end def already_stopped_recording if is_done && is_done_was errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING) end end def only_one_mix # we leave mixes as has_many because VRFS-1089 was very hard to do with has_one + cocoon add/remove if mixes.length > 1 errors.add(:mixes, ValidationMessages::ONLY_ONE_MIX) end end def recorded_tracks_for_user(user) unless self.users.exists?(user) raise JamPermissionError, "user was not in this session" end recorded_tracks.where(:user_id => user.id) end def recorded_backing_tracks_for_user(user) unless self.users.exists?(user) raise JamPermissionError, "user was not in this session" end recorded_backing_tracks.where(:user_id => user.id) end def has_access?(user) users.exists?(user) || plays.where("player_id=?", user).count != 0 end # Start recording a session. def self.start(music_session, owner, record_video: false) recording = nil # Use a transaction and lock to avoid races. music_session.with_lock do recording = Recording.new 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) # make quick mixes *before* the audio/video tracks, because this will give them precedence in list_uploads music_session.users.uniq.each do |user| QuickMix.create(recording, user) end music_session.connections.each do |connection| connection.tracks.each do |track| recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) end connection.video_sources.each do |video| recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording) end connection.backing_tracks.each do |backing_track| recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording) end end if music_session.jam_track music_session.jam_track.jam_track_tracks.each do |jam_track_track| recording.recorded_jam_track_tracks << RecordedJamTrackTrack.create_from_jam_track_track(jam_track_track, recording, owner) if jam_track_track.track_type == 'Track' end recording.jam_track = music_session.jam_track recording.jam_track_initiator = music_session.jam_track_initiator end recording.save end end recording end # Stop recording a session def stop # Use a transaction and lock to avoid races. music_session = MusicSession.find_by_id(music_session_id) locker = music_session.nil? ? self : music_session locker.with_lock do self.duration = Time.now - created_at self.is_done = true self.save end self end # Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording. def claim(user, name, description, genre, is_public, upload_to_youtube=false) upload_to_youtube = !!upload_to_youtube # Correct where nil is borking save unless self.users.exists?(user) raise JamPermissionError, "user was not in this session" end claimed_recording = ClaimedRecording.new claimed_recording.user = user claimed_recording.recording = self claimed_recording.name = name claimed_recording.description = description claimed_recording.genre = genre claimed_recording.is_public = is_public claimed_recording.upload_to_youtube = upload_to_youtube self.claimed_recordings << claimed_recording if claimed_recording.save keep(user) end claimed_recording end # the user votes to keep their tracks for this recording def keep(user) recorded_tracks_for_user(user).update_all(:discard => false) Recording.where(:id => id).update_all(:updated_at => Time.now) # updated updated_at for benefit of RecordingsCleaner User.where(:id => user.id).update_all(:first_recording_at => Time.now ) unless user.first_recording_at end # the user votes to discard their tracks for this recording def discard(user) recorded_tracks_for_user(user).update_all(:discard => true) Recording.where(:id => id).update_all(:updated_at => Time.now) # updated updated_at for benefit of RecordingsCleaner # check if all recorded_tracks for this recording are discarded if recorded_tracks.where('discard = false or discard is NULL').length == 0 self.all_discarded = true # the feed won't pick this up; also background cleanup will find these and whack them later self.save(:validate => false) end end # only discard if the user has previously taken no action def discard_if_no_action(user) track = recorded_tracks_for_user(user).first if track.discard.nil? discard(user ) end end # Find out if all the tracks for this recording have been uploaded def uploaded? self.recorded_tracks.each do |recorded_track| return false unless recorded_track.fully_uploaded end return true end def self.list_downloads(user, limit = 100, since = 0) since = 0 unless since || since == '' # guard against nil downloads = [] # That second join is important. It's saying join off of recordings, NOT user. If you take out the # ":recordings =>" part, you'll just get the recorded_tracks that I played. Very different! # we also only allow you to be told about downloads if you have claimed the recording #User.joins(:recordings).joins(:recordings => :recorded_tracks).joins(:recordings => :claimed_recordings) RecordedTrack.joins(:recording).joins(:recording => :claimed_recordings) .order('recorded_tracks.id') .where('recorded_tracks.fully_uploaded = TRUE') .where('recorded_tracks.id > ?', since) .where('all_discarded = false') .where('deleted = false') .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_track| downloads.push( { :type => "recorded_track", :id => recorded_track.client_track_id, :recording_id => recorded_track.recording_id, :length => recorded_track.length, :md5 => recorded_track.md5, :url => recorded_track[:url], :next => recorded_track.id } ) end latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0 Mix.joins(:recording).joins(:recording => :claimed_recordings) .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) .where('all_discarded = false') .where('deleted = false') .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user) .limit(limit).each do |mix| downloads.push( { :type => "mix", :id => mix.id.to_s, :recording_id => mix.recording_id, :length => mix.ogg_length, :md5 => mix.ogg_md5, :url => mix.ogg_url, :created_at => mix.created_at, :next => mix.id } ) end latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0 RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings) .order('recorded_backing_tracks.id') .where('recorded_backing_tracks.fully_uploaded = TRUE') .where('recorded_backing_tracks.id > ?', since) .where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back .where('all_discarded = false') .where('deleted = false') .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track| downloads.push( { :type => "recorded_backing_track", :id => recorded_backing_track.client_track_id, :recording_id => recorded_backing_track.recording_id, :length => recorded_backing_track.length, :md5 => recorded_backing_track.md5, :url => recorded_backing_track[:url], :next => recorded_backing_track.id } ) end latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0 next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max if next_date.nil? next_date = since # echo back to the client the same value they passed in, if there are no results end { 'downloads' => downloads, 'next' => next_date.to_s } end def self.list_uploads(user, limit = 100, since = 0) since = 0 unless since || since == '' # guard against nil uploads = [] # Uploads now include videos in addition to the tracks. # This is accomplished using a SQL UNION query via arel, as follows: # Select fields from track. Note the reorder, which removes # the default scope sort as it b0rks the union. Also note the # alias so that we can differentiate tracks and videos when # processing the results: track_arel = RecordedTrack.select([ :id, :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_track_id, Arel::Nodes::As.new('track', Arel.sql('item_type')) ]).reorder("") # Select fields for video. Note that it must include # the same number of fields as the track in order for # the union to work: vid_arel = RecordedVideo.select([ :id, :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_video_source_id, Arel::Nodes::As.new('video', Arel.sql('item_type')) ]).reorder("") # Select fields for quick mix. Note that it must include # the same number of fields as the track or video in order for # the union to work: quick_mix_arel = QuickMix.select([ :id, :recording_id, :user_id, :ogg_url, :fully_uploaded, :upload_failures, Arel::Nodes::As.new('', Arel.sql('quick_mix_track_id')), Arel::Nodes::As.new('stream_mix', Arel.sql('item_type')) ]).reorder("") # Select fields for quick mix. Note that it must include # the same number of fields as the track or video in order for # the union to work: backing_track_arel = RecordedBackingTrack.select([ :id, :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_track_id, Arel::Nodes::As.new('backing_track', Arel.sql('item_type')) ]).reorder("") # Glue them together: union = track_arel.union(vid_arel) utable = Arel::Nodes::TableAlias.new(union, :recorded_items) arel = track_arel.from(utable) arel = arel.except(:select) # Remove the implicit select created by .from. It # contains an ambigious "id" field: arel = arel.except(:select) arel = arel.select([ "recorded_items.id", :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_track_id, :item_type ]) # And repeat: union_quick = arel.union(quick_mix_arel) utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick) arel = arel.from(utable_quick) arel = arel.except(:select) arel = arel.select([ "recorded_items_quick.id", :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_track_id, :item_type ]) # And repeat for backing track: union_all = arel.union(backing_track_arel) utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all) arel = arel.from(utable_all) arel = arel.except(:select) arel = arel.select([ "recorded_items_all.id", :recording_id, :user_id, :url, :fully_uploaded, :upload_failures, :client_track_id, :item_type ]) # Further joining and criteria for the unioned object: arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \ .where('recorded_items_all.user_id' => user.id) \ .where('recorded_items_all.fully_uploaded = ?', false) \ .where('recorded_items_all.id > ?', since) \ .where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") \ .where("duration IS NOT NULL") \ .where('all_discarded = false') \ .where('deleted = false') \ .order('recorded_items_all.id') \ .limit(limit) # Load into array: arel.each do |recorded_item| if recorded_item.item_type=='video' # A video: uploads << ({ :type => "recorded_video", :client_video_source_id => recorded_item.client_track_id, :recording_id => recorded_item.recording_id, :next => recorded_item.id }) elsif recorded_item.item_type == 'track' # A track: uploads << ({ :type => "recorded_track", :client_track_id => recorded_item.client_track_id, :recording_id => recorded_item.recording_id, :next => recorded_item.id }) elsif recorded_item.item_type == 'stream_mix' uploads << ({ :type => "stream_mix", :recording_id => recorded_item.recording_id, :next => recorded_item.id }) elsif recorded_item.item_type == 'backing_track' uploads << ({ :type => "recorded_backing_track", :recording_id => recorded_item.recording_id, :client_track_id => recorded_item.client_track_id, :next => recorded_item.id }) else end end next_value = uploads.length > 0 ? uploads[-1][:next].to_s : nil if next_value.nil? next_value = since # echo back to the client the same value they passed in, if there are no results end { "uploads" => uploads, "next" => next_value.to_s } end def preconditions_for_mix? recorded_tracks.each do |recorded_track| return false unless recorded_track.fully_uploaded end recorded_backing_tracks.each do |recorded_backing_track| return false unless recorded_backing_track.fully_uploaded end true end # Check to see if all files have been uploaded. If so, kick off a mix. def upload_complete # Don't allow multiple mixes for now. return if self.mixes.length > 0 # FIXME: There's a possible race condition here. If two users complete # uploads at the same time, we'll schedule 2 mixes. self.mixes << Mix.schedule(self) if preconditions_for_mix? save end def is_public? claimed_recordings.where(is_public: true).length > 0 end # meant to be used as a way to 'pluck' a claimed_recording appropriate for user. def candidate_claimed_recording #claimed_recordings.where(is_public: true).first claimed_recordings.first end # returns a ClaimedRecording that the user did not discard def claim_for_user(user, ignore_discarded = false) return nil unless user claim = claimed_recordings.find{|claimed_recording| claimed_recording.user == user } if ignore_discarded claim else claim unless claim && claim.discarded end end def self.when_will_be_discarded? recorded_track_votes = recorded_tracks.map(&:discard) discarded = 0 recorded_track_votes.each do |discard_vote| if discard_vote == nil || discard_vote == true discarded = discarded + 1 end end if recorded_track_votes.length == discarded # all tracks are discarded, figure out due time for deletion # 3 days in seconds - amount of seconds since last updated ((APP_CONFIG.recordings_stale_time * 3600 * 24) - (Time.now - updated_at).to_i).seconds.from_now else return nil end end # finds all discarded recordings that are sufficiently stale (i.e., abandoned by all those involved, and hasn't been mucked with in a while) def self.discarded_and_stale # we count up all tracks for the Recording, and count up all discarded/not-voted-on tracks # if they are equal, and if the recording is stale, let's return it. Recording .joins("INNER JOIN recorded_tracks ON recordings.id = recorded_tracks.recording_id") .joins(%Q{ LEFT OUTER JOIN (SELECT id FROM recorded_tracks WHERE discard IS NULL OR discard = TRUE) AS discard_info ON recorded_tracks.id = discard_info.id }) .group("recordings.id") .having('COUNT(recorded_tracks.id) = COUNT(discard_info.id)') .where("NOW() - recordings.updated_at > '#{APP_CONFIG.recordings_stale_time} day'::INTERVAL") .limit(1000) .readonly(false) end def mark_delete mixes.each do |mix| mix.delete_s3_files end quick_mixes.each do |quick_mix| quick_mix.delete_s3_files end recorded_tracks.each do |recorded_track| recorded_track.delete_s3_files end self.deleted = true self.save(:validate => false) end def add_video_data(data) Recording.where(id: self.id).update_all(external_video_id: data[:video_id]) end def add_timeline(timeline) global = timeline["global"] raise JamArgumentError, "global must be specified" unless global tracks = timeline["tracks"] raise JamArgumentError, "tracks must be specified" unless tracks Recording.where(id: self.id).update_all(timeline: global.to_json) jam_tracks = tracks.select {|track| track["type"] == "jam_track"} jam_tracks.each do |client_jam_track| RecordedJamTrackTrack.where(recording_id: id, jam_track_track_id: client_jam_track["id"]).update_all(timeline: client_jam_track["timeline"].to_json) end end def self.popular_recordings(limit = 100) Recording.select('recordings.id').joins('inner join claimed_recordings ON claimed_recordings.recording_id = recordings.id AND claimed_recordings.is_public = TRUE').where(all_discarded: false).where(is_done: true).where(deleted: false).order('play_count DESC').limit(limit).group('recordings.id') end private def self.validate_user_is_band_member(user, band) unless band.users.exists? user raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end end def self.validate_user_is_creator(user, creator) unless user.id == creator.id raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end def self.validate_user_is_musician(user) unless user.musician? raise JamPermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR end end end end