more recordings work

This commit is contained in:
Mike Slemmer 2013-04-24 23:50:52 -07:00
parent fe222ff13a
commit 8d4fd2aeaf
10 changed files with 288 additions and 265 deletions

View File

@ -26,6 +26,7 @@ require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/uploaders/artifact_uploader"
require "jam_ruby/app/uploaders/perf_data_uploader"
require "jam_ruby/lib/s3_manager"
require "jam_ruby/message_factory"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
@ -61,6 +62,7 @@ require "jam_ruby/models/search"
require "jam_ruby/models/recording"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/mix"
require "jam_ruby/models/claimed_recording"
include Jampb

View File

@ -0,0 +1,61 @@
module JamRuby
class S3Manager
SECRET = "krQP3fKpjAtWkApBEJwJJrCZ"
def self.s3_url(filename)
"s3://#{aws_bucket}/#{filename}"
end
def self.url(filename)
"https://s3.amazonaws.com/#{aws_bucket}/#{filename}"
end
def self.upload_sign(filename, content_md5, upload_id)
hdt = http_date_time
str_to_sign = "PUT\n#{content_md5}\n#{content_type}\n#{hdt}\n/#{aws_bucket}/#{filename}"
signature = Base64.encode64(HMAC::SHA1.digest(aws_secret_key, str_to_sign)).chomp
{ :filename => filename, :signature => signature, :datetime => hdt, :upload_id => upload_id }
end
def self.hashed_filename(type, id)
Digest::SHA1.hexdigest "#{SECRET}_#{type}_#{id}"
end
def self.multipart_upload_start(upload_filename)
s3_bucket.objects[upload_filename].multipart_upload.id
end
def self.multipart_upload_complete(upload_id)
s3_bucket.objects[upload_filename].multipart_uploads[upload_id].upload_complete(:remote_parts)
end
def self.delete(filename)
s3_bucket.objects[filename].delete
end
private
def self.s3_bucket
@s3 ||= AWS::S3.new
@s3.buckets[aws_bucket]
end
def self.aws_bucket
"jamkazam-dev"
end
def self.aws_secret_key
"XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv"
end
def self.content_type
"application/octet-stream"
end
def self.http_date_time
Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
end
end
end

View File

@ -1,16 +1,15 @@
# FIXME:
# Need to pass in the JSON spec for the mix and put that in the migration.
module JamRuby
class Mix < ActiveRecord::Base
MAX_MIX_TIME = 7200 # 2 hours
before_destroy :delete_s3_files
self.primary_key = 'id'
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mix
def self.schedule(recording_id, user_id, description, spec)
def self.schedule(recording_id, user_id, description, manifest)
# This would have made it so you couldn't have more than one mix of a recording+owner
#raise unless self.where(:recording_id => recording_id, :owner_id => user_id).size == 0
# raise unless self.where(:recording_id => recording_id, :owner_id => user_id).size == 0
recording = Recording.find(recording_id)
raise if recording.nil?
raise if recording.owner_id != user_id
@ -19,7 +18,7 @@ module JamRuby
mix.recording_id = recording_id
mix.owner_id = user_id
mix.description = description
mix.spec = spec
mix.manifest = manifest
mix.save
mix
@ -44,14 +43,25 @@ module JamRuby
mix
end
def finish(url)
raise if url.nil?
def finish()
self.completed_at = Time.now
self.url = url
save
end
def s3_url
S3Manager.s3_url(hashed_filename)
end
private
def delete_s3_files
S3Manager.delete(hashed_filename)
end
def hashed_filename
S3Manager.hashed_filename('mix', id)
end
end
end

View File

@ -13,6 +13,8 @@ module JamRuby
validates :sound, :inclusion => {:in => SOUND}
before_destroy :delete_s3_files
# Copy an ephemeral track to create a saved one. Some fields are ok with defaults
def self.create_from_track(track, recording)
recorded_track = self.new
@ -25,17 +27,14 @@ module JamRuby
end
def upload_start
self.upload_id = s3_bucket.objects[filename].multipart_upload.id
self.upload_id = S3Manager.multipart_upload_start(hashed_filename)
save
end
def upload_sign(filename, content_md5)
str_to_sign = "PUT\n#{content_md5}\n#{content_type}\n#{http_date_time}\n/#{aws_bucket}/#{filename}"
signature = Base64.encode64(HMAC::SHA1.digest(aws_secret_key, str_to_sign)).chomp
{ :signature => signature, :datetime => http_date_time, :upload_id => upload_id }
def upload_sign(content_md5)
S3Manager.upload_sign(hashed_filename, content_md5, upload_id)
end
def upload_part_complete(part)
raise JamRuby::JamArgumentError unless part == next_part_to_upload
self.next_part_to_upload = part + 1
@ -43,45 +42,33 @@ module JamRuby
end
def upload_complete
s3_bucket.objects[filename].multipart_uploads[self.upload_id].upload_complete(:remote_parts)
S3Manager.multipart_upload_complete(upload_id)
self.fully_uploaded = true
save
end
def url
S3Manager.url(hashed_filename)
end
# Format: "recording_#{saved_track_id}"
# Format: "recording_#{recorded_track_id}"
# File extension is irrelevant actually.
def self.filename_to_recorded_track_id(filename)
def self.find_by_upload_filename(filename)
matches = /^recording_([\w-]+)$/.match(filename)
return nil unless matches && matches.length > 1
matches[1]
end
private
def s3_bucket
@s3 ||= AWS::S3.new
@s3.buckets[aws_bucket]
end
def filename
"recording_#{self.id}"
end
def aws_bucket
"jamkazam-dev"
end
def aws_secret_key
"XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv"
end
def content_type
"application/octet-stream"
RecordedTrack.find(matches[1])
end
def http_date_time
Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
private
def delete_s3_files
S3Manager.delete(hashed_filename)
end
def hashed_filename
S3Manager.hashed_filename('recorded_track', id)
end
end
end

View File

@ -1,14 +1,20 @@
# A bunch to do here:
# 1. Make it so users "own" recordings
# 2. Add a route for a user to specify if they want to save or discard a recording. If save, then add them to recordings_users (not sure anything else is even doing that now)
# 3. Need a way to get a list of all the recordings you "should" have as a user. That will be all the recorded tracks plus mixes for all recordings you "own"
# 4. I think the notion of the recording's "owner" being who started it can remain.
module JamRuby
class Recording < ActiveRecord::Base
self.primary_key = 'id'
has_and_belongs_to_many :users, :class_name => "JamRuby::User"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording
has_many :users, :through => :claimed_recordings, :class_name => "JamRuby::User"
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recording
has_one :mix, :class_name => "JamRuby::Mix", :inverse_of => :recording
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
# genres
@ -18,10 +24,6 @@ module JamRuby
has_many :favorites, :class_name => "JamRuby::UserFavorite", :foreign_key => "recording_id"
has_many :inverse_favorites, :through => :favorites, :source => :recording, :class_name => "JamRuby::Recording"
validates :name, length: { maximum: 100 }
validates :description, length: { maximum: 200 }
validates :genres, length: { maximum: 3 }
def favorite_count
return self.favorites.size
end
@ -77,11 +79,11 @@ module JamRuby
recording = Recording.new
recording.music_session = music_session
recording.owner = owner
recording.is_public = false
recording.is_downloadable = false
music_session.connections.each do |connection|
recording.users << connection.user
# Note that we do NOT connect the recording to any users at this point.
# That ONLY happens if a user clicks 'save'
# recording.users << connection.user
connection.tracks.each do |track|
RecordedTrack.create_from_track(track, recording)
end
@ -113,8 +115,6 @@ module JamRuby
end
# Stop recording a session
# FIXME: What to do about recordings which are started and then stopped but never get any metadata (not even a name)?
# I'm guessing they should be deleted eventually.
def stop
# Use a transaction and lock to avoid races.
ActiveRecord::Base.transaction do
@ -128,37 +128,43 @@ module JamRuby
music_session.recording = nil
music_session.save
end
end
# Update the metadata for the recording.
def update_fields(params)
unless self.music_session.nil?
raise PermissionError, "recording data cannot be changed while it is being recorded"
end
if params[:name].nil?
raise PermissionError, "recording must have a name" if self.name.nil?
end
self.name = params[:name] unless params[:name].nil?
self.description = params[:description] unless params[:description].nil?
genres = params[:genres]
unless genres.nil?
validate_genres(genres)
self.genres = []
genres.each do |genre_id|
g = Genre.find(genre_id)
self.genres << g
end
end
self.is_public = params[:is_public] unless params[:is_public].nil?
self.is_downloadable = params[:is_downloadable] unless params[:is_downloadable].nil?
self.duration = Time.now - created_at
save
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, genre, is_public, is_downloadable)
if self.users.include?(user)
raise PermissionError, "user already claimed this recording"
end
unless self.recorded_tracks.find { |recorded_track| recorded_track.user == user }
raise PermissionError, "user was not in this session"
end
unless self.music_session.nil?
raise PermissionError, "recording cannot be claimed while it is being recorded"
end
if name.nil? || genre.nil? || is_public.nil? || is_downloadable.nil?
raise PermissionError, "recording must have name, genre and flags"
end
claimed_recording = ClaimedRecording.new
claimed_recording.user = user
claimed_recording.recording = self
claimed_recording.name = name
claimed_recording.genre = genre
claimed_recording.is_public = is_public
claimed_recording.is_downloadable = is_downloadable
self.claimed_recordings << claimed_recording
save
claimed_recording
end
# Find out if all the tracks for this recording have been uploaded
def uploaded?
self.recorded_tracks.each do |recorded_track|
@ -167,6 +173,13 @@ module JamRuby
return true
end
# Discards this recording and schedules deletion of all files associated with it.
def discard
self.destroy
end
=begin
# This is no longer remotely right.
def self.search(query, options = { :limit => 10 })
# only issue search if at least 2 characters are specified
@ -183,92 +196,9 @@ module JamRuby
return Recording.where("description_tsv @@ to_tsquery('jamenglish', ?)", query).limit(options[:limit])
end
def self.save(id, is_public, description, genres, updater_id, owner_id, is_band)
return nil
# Spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+Studio
# This is seriously wrong. A recording needs to be created from inside a session. I'm not sure who owns
# the recording, but I do know that it's affiliated with the session and then should be able to get the band id
# and probably all this other stuff (genre, etc) from that. I need to read the recording spec in more detail
# to figure that out. I'll read it on bart.
#
# The studio spec is the key here. A couple notes:
# * Multiple musicians are all associated with a recording -- everyone who is in the music session when the recording starts
# * Associating a band with the recording is optional
# * The recording needs to be associated with a music_session for sure. Probably also a set of tracks (NOT musicians). Then, on
# recording completion is when you have the track -> saved_track (rename this to recorded_track) transition.
# * The metadata in general is filled in *after* the recording ends. This is important because it means the save method below
# is sort of jacked.
=begin
creator = User.find(updater_id)
if is_band
band = Band.find(owner_id)
validate_user_is_band_member(creator, band)
else
user = User.find(owner_id)
validate_user_is_creator(user, creator)
validate_user_is_musician(user)
end
if id.nil?
validate_genres(genres, false)
recording = Recording.new()
recording.creator_id = updater_id
else
validate_genres(genres, true)
recording = Recording.find(id)
end
recording.updater_id = updater_id
# public flag
unless is_public.nil?
recording.public = is_public
end
# description
unless description.nil?
recording.description = description
end
# genres
unless genres.nil?
ActiveRecord::Base.transaction do
# delete all genres for this recording first
unless recording.id.nil? || recording.id.length == 0
recording.genres.delete_all
end
# loop through each genre in the array and save to the db
genres.each do |genre_id|
g = Genre.find(genre_id)
recording.genres << g
end
end
end
recording.updated_at = Time.now.getutc
# TODO: wrap in transaction with statements below
recording.save
if id.nil?
if is_band
recording.band_recordings << BandRecording.create(band_id: owner_id, recording_id: recording.id)
else
recording.musician_recordings << MusicianRecording.create(user_id: owner_id, recording_id: recording.id)
end
end
return recording
=end
end
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
@ -297,14 +227,5 @@ module JamRuby
raise JamRuby::JamArgumentError, ValidationMessages::GENRE_LIMIT_EXCEEDED
end
end
=begin
def self.delete(id, owner_id, is_band)
if is_band?
JamRuby::Recording.delete_all "(user_id = '#{user_id}' AND follower_id = '#{follower_id}')"
else
end
end
=end
end
end
end

View File

@ -10,8 +10,9 @@ module JamRuby
users = User.search(query, :limit => LIMIT)
bands = Band.search(query, :limit => LIMIT)
recordings = Recording.search(query, :limit => LIMIT)
return Search.new(users + bands + recordings)
# NOTE: I removed recordings from search here. This is because we switched
# to "claimed_recordings" so it's not clear what should be searched.
return Search.new(users + bands)
end
# performs a friend search scoped to a specific user
@ -78,4 +79,4 @@ module JamRuby
end
end
end
end

View File

@ -31,7 +31,8 @@ module JamRuby
# recordings
has_many :owned_recordings, :class_name => "JamRuby::Recording"
has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording"
has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user
# user likers (a musician has likers and may have likes too; fans do not have likers)
has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user

View File

@ -18,7 +18,7 @@ describe Mix do
@mix.recording_id.should == @recording.id
@mix.owner_id.should == @user.id
@mix.description.should == "description"
@mix.spec.should == "{}"
@mix.manifest.should == "{}"
@mix.url.should be_nil
@mix.mix_server.should be_nil
@mix.started_at.should be_nil
@ -44,10 +44,9 @@ describe Mix do
end
it "should record when a mix has finished" do
Mix.find(@mix.id).finish("http://blah")
Mix.find(@mix.id).finish
@mix.reload
@mix.completed_at.should_not be_nil
@mix.url.should == "http://blah"
end
it "should re-run a mix if it was started a long time ago" do

View File

@ -7,7 +7,7 @@ describe RecordedTrack do
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@recording = FactoryGirl.create(:recording, :description => 'description', :owner => @user)
@recording = FactoryGirl.create(:recording, :owner => @user)
end
it "should copy from a regular track properly" do
@ -36,6 +36,17 @@ describe RecordedTrack do
@recorded_track.next_part_to_upload.should == 2
end
it "properly finds a recorded track given its upload filename" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
RecordedTrack.find_by_upload_filename("recording_#{@recorded_track.id}").should == @recorded_track
end
it "gets a url for the track" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_track.url.should == S3Manager.url(S3Manager.hashed_filename("recorded_track", @recorded_track.id))
end
end

View File

@ -23,9 +23,7 @@ describe Recording do
@music_session.recording.should == @recording
@recording.owner_id.should == @user.id
@recording.genres.should == @music_session.genres
@recording.users.length.should == 1
@recording.users.first.should == @user
@recording.description.should be_nil
@recording.users.length.should == 0
@recorded_tracks = RecordedTrack.where(:recording_id => @recording.id)
@recorded_tracks.length.should == 1
@ -61,7 +59,7 @@ describe Recording do
@music_session.recording.should == @recording2
end
it "should attach the recording to all users in a the music session when recording started" do
it "should NOT attach the recording to all users in a the music session when recording started" do
@user2 = FactoryGirl.create(:user)
@connection2 = FactoryGirl.create(:connection, :user => @user2)
@instrument2 = FactoryGirl.create(:instrument, :description => 'a great instrument')
@ -70,81 +68,11 @@ describe Recording do
@music_session.connections << @connection2
@recording = Recording.start(@music_session.id, @user)
@user.recordings.length.should == 1
@user.recordings.first.should == @recording
@user.recordings.length.should == 0
#@user.recordings.first.should == @recording
@user2.recordings.length.should == 1
@user2.recordings.first.should == @recording
end
it "should update fields in a recording properly after it finishes" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre1 = FactoryGirl.create(:genre)
@genre2 = FactoryGirl.create(:genre)
@genre3 = FactoryGirl.create(:genre)
@recording.update_fields :name => "name1", :description => "description", :genres => [@genre1], :is_downloadable => true, :is_public => true
@recording.name.should == "name1"
@recording.description.should == "description"
@recording.genres.length.should == 1
@recording.is_public.should == true
@recording.is_downloadable.should == true
@recording.update_fields :name => "name2", :description => "description2", :genres => [@genre3], :is_downloadable => false, :is_public => false
@recording.name.should == "name2"
@recording.description.should == "description2"
@recording.genres.length.should == 1
@recording.genres.first.should == @genre3
@recording.is_public.should == false
@recording.is_downloadable.should == false
# Use a fake param here to simulate getting no params
@recording.update_fields :fakeparam => "fake"
@recording.name.should == "name2"
@recording.description.should == "description2"
@recording.genres.length.should == 1
@recording.genres.first.should == @genre3
@recording.is_public.should == false
@recording.is_downloadable.should == false
end
it "should error if you update fields in a recording while its ongoing" do
@recording = Recording.start(@music_session.id, @user)
expect { @recording.update_fields :name => "name", :description => "description" }.to raise_error
end
it "should error if you try to assign more than 3 genres or fewer than 1" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre1 = FactoryGirl.create(:genre)
@genre2 = FactoryGirl.create(:genre)
@genre3 = FactoryGirl.create(:genre)
@genre4 = FactoryGirl.create(:genre)
expect { @recording.update_fields :name => "name", :genres => [@genre1, @genre2, @genre3, @genre4] }.to raise_error
expect { @recording.update_fields :genres => [] }.to raise_error
end
it "should error if you set fields on a recording with no name" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@recording.name.should be_nil
expect { @recording.update_fields :description => "description" }.to raise_error
end
it "should delete a recording as expected" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
Recording.delete(@recording.id)
expect { Recording.find(@recording.id) }.to raise_error
@user2.recordings.length.should == 0
#@user2.recordings.first.should == @recording
end
it "should report correctly whether its tracks have been uploaded" do
@ -167,6 +95,108 @@ describe Recording do
expect { RecordedTracks.find(@recorded_track.id) }.to raise_error
end
it "should allow a user to claim a track" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", @genre, true, true)
@recording.reload
@recording.users.length.should == 1
@recording.users.first.should == @user
@user.recordings.length.should == 1
@user.recordings.first.should == @recording
@recording.claimed_recordings.length.should == 1
@claimed_recording = @recording.claimed_recordings.first
@claimed_recording.name.should == "name"
@claimed_recording.genre.should == @genre
@claimed_recording.is_public.should == true
@claimed_recording.is_downloadable.should == true
end
it "should fail if a user who was not in the session claims a recording" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
user2 = FactoryGirl.create(:user)
expect { @recording.claim(user2) }.to raise_error
end
it "should fail if a user tries to claim a recording twice" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", @genre, true, true)
@recording.reload
expect { @recording.claim(@user, "name", @genre, true, true) }.to raise_error
end
it "should allow editing metadata for claimed recordings" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@genre2 = FactoryGirl.create(:genre)
@claimed_recording.update_fields(@user, :name => "name2", :genre => @genre2.id, :is_public => false, :is_downloadable => false)
@claimed_recording.reload
@claimed_recording.name.should == "name2"
@claimed_recording.genre.should == @genre2
@claimed_recording.is_public.should == false
@claimed_recording.is_downloadable.should == false
end
it "should only allow the owner to edit a claimed recording" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@user2 = FactoryGirl.create(:user)
expect { @claimed_recording.update_fields(@user2, "name2") }.to raise_error
end
it "should record the duration of the recording properly" do
@recording = Recording.start(@music_session.id, @user)
@recording.duration.should be_nil
@recording.stop
@recording.reload
@recording.duration.should_not be_nil
# Note: it will be 0 since this was fast. You can see something non-zero by just
# inserting a sleep here.
# puts @recording.duration
end
it "should only destroy a single claimed_recording if there are more than one" do
@user2 = FactoryGirl.create(:user)
@connection2 = FactoryGirl.create(:connection, :user => @user2)
@track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument)
@music_session.connections << @connection2
@music_session.save
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
expect { @claimed_recordign.discard(@user2) }.to raise_error
@claimed_recording = @recording.claim(@user2, "name2", @genre, true, true)
@claimed_recording.discard(@user2)
@recording.reload
@recording.claimed_recordings.length.should == 1
end
it "should destroy the entire recording if there was only one claimed_recording which is discarded" do
@recording = Recording.start(@music_session.id, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@claimed_recording.discard(@user)
expect { Recording.find(@recording.id) }.to raise_error
expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error
end
end