jam-cloud/ruby/lib/jam_ruby/models/recording.rb

795 lines
28 KiB
Ruby

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.id)
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.id)
raise JamPermissionError, "user was not in this session"
end
recorded_backing_tracks.where(:user_id => user.id)
end
def has_access?(user)
return false if user.nil?
users.exists?(user.id) || attached_with_lesson(user) #|| plays.where("player_id=?", user).count != 0
end
def attached_with_lesson(user)
ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.user_id = ?', user.id).count > 0 ||
ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.target_user_id = ?', user.id).count > 0
end
# creates a recording, and then claims it in one shot.
def self.create_immediately(owner, params)
recording = Recording.new
recording.music_session = nil
recording.owner = owner
recording.band = nil
recording.immediate = true # immediate in practice means 'the ios app uploaded this'
recording.video = params[:record_video]
if recording.save
QuickMix.create(recording, owner)
recording.recorded_tracks << RecordedTrack.create_for_immediate(owner, recording)
recording.save
end
if recording.errors.any?
return recording
end
recording.stop
if recording.errors.any?
return recording
end
recording.reload
claim = recording.claim(owner, params[:name], params[:description], Genre.find_by_id(params[:genre]), params[:is_public], upload_to_youtube = params[:upload_to_youtube])
if claim.errors.any?
return claim
end
recording
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
if !self.users.exists?(user.id)
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(Arel::Nodes.build_quoted('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(Arel::Nodes.build_quoted('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::Nodes.build_quoted(''), Arel.sql('quick_mix_track_id')),
Arel::Nodes::As.new(Arel::Nodes.build_quoted('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(Arel::Nodes.build_quoted('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)
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"])
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.id
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