module JamRuby class Recording < ActiveRecord::Base self.primary_key = 'id' attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, 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 :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 :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 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 has_mix? self.mixes.length > 0 && self.mixes.first.completed 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 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 PermissionError, "user was not in this session" end recorded_tracks.where(:user_id => user.id) end def has_access?(user) users.exists?(user) end # Start recording a session. def self.start(music_session, owner) 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 if recording.save GoogleAnalyticsEvent.report_band_recording(recording.band) music_session.connections.each do |connection| connection.tracks.each do |track| recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) end end 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) unless self.users.exists?(user) raise PermissionError, "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 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) 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) # 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 # 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('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[-1][:next] if downloads.length > 0 Mix.joins(:recording).joins(:recording => :claimed_recordings) .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) .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[-1][:next] if downloads.length > 0 if !latest_mix.nil? && !latest_recorded_track.nil? next_date = [latest_mix, latest_recorded_track].max elsif latest_mix.nil? next_date = latest_recorded_track else next_date = latest_mix end 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 = [] RecordedTrack .joins(:recording) .where(:user_id => user.id) .where(:fully_uploaded => false) .where('recorded_tracks.id > ?', since) .where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") .where("duration IS NOT NULL") .where('all_discarded = false') .order('recorded_tracks.id') .limit(limit).each do |recorded_track| uploads.push({ :type => "recorded_track", :client_track_id => recorded_track.client_track_id, :recording_id => recorded_track.recording_id, :next => recorded_track.id }) 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 # 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. raise JamRuby::JamArgumentError unless 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. recorded_tracks.each do |recorded_track| return unless recorded_track.fully_uploaded end self.mixes << Mix.schedule(self) 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 private def self.validate_user_is_band_member(user, band) unless band.users.exists? user raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end end def self.validate_user_is_creator(user, creator) unless user.id == creator.id raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end def self.validate_user_is_musician(user) unless user.musician? raise PermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR end end end end