Save track files in new Sound model:

* Consolidate migrations
* Add Sound model. Each sound can belong to a track and contains
  informations about one sound file.
This commit is contained in:
Thibault Jouan 2011-09-15 21:08:24 +00:00
parent f49a5a3f67
commit 27550fd14e
26 changed files with 265 additions and 216 deletions

View File

@ -0,0 +1,6 @@
class SoundsController < ApplicationController
def show
sound = Sound.find params[:id]
send_file sound.path, :type => sound.mime_type
end
end

View File

@ -8,19 +8,11 @@ class TracksController < ApplicationController
end end
def create def create
@track = Track.new(:name => params[:track][:name]) @track = Track.new params[:track]
if @track.save_with_file( if @track.save
params[:track][:file],
params[:track][:file].content_type
)
redirect_to @track redirect_to @track
else else
render :new render :new
end end
end end
def download
track = Track.find params[:id]
send_file track.filepath, :type => track.mime_type
end
end end

18
app/models/sound.rb Normal file
View File

@ -0,0 +1,18 @@
require 'fileutils'
class Sound < ActiveRecord::Base
belongs_to :track
validates_presence_of :sha256
validates_presence_of :mime_type
def path
"#{Rails.root}/data/sounds/#{sha256}"
end
def file=(file)
self.sha256 = Digest::SHA256.file(file.path).hexdigest
FileUtils.cp file.path, path
self.mime_type = file.content_type
end
end

View File

@ -1,19 +1,20 @@
class Track < ActiveRecord::Base class Track < ActiveRecord::Base
validates_presence_of :name has_many :sounds
validates_presence_of :mime_type
validates_presence_of :sha256
def filepath attr_accessible :name, :file
"#{Rails.root}/data/tracks/#{sha256}"
validates_presence_of :name
def file=(file)
sounds.build({:file => file})
end end
def save_with_file(file, mime_type) def sound
self.sha256 = Digest::SHA256.file(file.path).hexdigest sounds.first
self.mime_type = mime_type end
File.open(filepath, 'w') do |f|
f.write file.read def sound?
end sounds.any?
save!
end end
def self.latest def self.latest

View File

@ -1,3 +1,4 @@
%h1= @track.name %h1= @track.name
%audio{:src => download_track_path(@track), :controls => true, :autoplay => true} - if @track.sound?
Your browser does not support the audio element %audio{:src => sound_path(@track.sound), :controls => true, :autoplay => true}
Your browser does not support the audio element

View File

@ -1,4 +1,6 @@
Scube::Application.routes.draw do Scube::Application.routes.draw do
resources :sounds, :only => [:show]
resources :users, :only => [:new, :create] resources :users, :only => [:new, :create]
resources :sessions, :only => [:new, :create] resources :sessions, :only => [:new, :create]

View File

@ -1,13 +0,0 @@
class CreatePlaylists < ActiveRecord::Migration
def self.up
create_table :playlists do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :playlists
end
end

View File

@ -1,13 +0,0 @@
class CreateTracks < ActiveRecord::Migration
def self.up
create_table :tracks do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :tracks
end
end

View File

@ -1,9 +0,0 @@
class AddMimeTypeFieldToTracks < ActiveRecord::Migration
def self.up
add_column :tracks, :mime_type, :string
end
def self.down
remove_column :tracks, :mime_type
end
end

View File

@ -1,9 +0,0 @@
class AddSha256FieldToTracks < ActiveRecord::Migration
def self.up
add_column :tracks, :sha256, :string
end
def self.down
remove_column :tracks, :sha256
end
end

View File

@ -1,16 +0,0 @@
class AddSessionsTable < ActiveRecord::Migration
def self.up
create_table :sessions do |t|
t.string :session_id, :null => false
t.text :data
t.timestamps
end
add_index :sessions, :session_id
add_index :sessions, :updated_at
end
def self.down
drop_table :sessions
end
end

View File

@ -1,14 +0,0 @@
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :email
t.string :password
t.timestamps
end
end
def self.down
drop_table :users
end
end

View File

@ -1,11 +0,0 @@
class AddPasswordHashToUsers < ActiveRecord::Migration
def self.up
add_column :users, :password_hash, :string
remove_column :users, :password
end
def self.down
remove_column :users, :password_hash
add_column :users, :password, :string
end
end

View File

@ -1,9 +0,0 @@
class AddUserIdToPlaylists < ActiveRecord::Migration
def self.up
add_column :playlists, :user_id, :integer
end
def self.down
remove_column :playlists, :user_id
end
end

View File

@ -1,9 +0,0 @@
class AddEmailUniqueIndexToUsers < ActiveRecord::Migration
def self.up
add_index :users, :email, :unique => true
end
def self.down
drop_index :users, :email
end
end

View File

@ -0,0 +1,48 @@
class InitialSchema < ActiveRecord::Migration
def up
create_table :playlists do |t|
t.integer :user_id
t.string :name
t.timestamps
end
create_table :sessions do |t|
t.string :session_id, :null => false
t.text :data
t.timestamps
end
add_index :sessions, :session_id
add_index :sessions, :updated_at
create_table :tracks do |t|
t.string :name
t.timestamps
end
create_table :users do |t|
t.string :email
t.string :password_hash
t.timestamps
end
add_index :users, :email, :unique => true
end
def down
drop_index :sessions, :session_id
drop_index :sessions, :updated_at
drop_index :users, :email
drop_table :playlists
drop_table :sessions
drop_table :tracks
drop_table :users
end
end

View File

@ -0,0 +1,15 @@
class CreateSounds < ActiveRecord::Migration
def up
create_table :sounds do |t|
t.integer :track_id
t.string :sha256
t.string :mime_type
t.timestamps
end
end
def down
drop_table :sounds
end
end

View File

@ -10,13 +10,13 @@
# #
# It's strongly recommended to check this file into your version control system. # It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20110913195421) do ActiveRecord::Schema.define(:version => 20110916003929) do
create_table "playlists", :force => true do |t| create_table "playlists", :force => true do |t|
t.integer "user_id"
t.string "name" t.string "name"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "user_id"
end end
create_table "sessions", :force => true do |t| create_table "sessions", :force => true do |t|
@ -29,19 +29,25 @@ ActiveRecord::Schema.define(:version => 20110913195421) do
add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id"
add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at" add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at"
create_table "sounds", :force => true do |t|
t.integer "track_id"
t.string "sha256"
t.string "mime_type"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "tracks", :force => true do |t| create_table "tracks", :force => true do |t|
t.string "name" t.string "name"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "mime_type"
t.string "sha256"
end end
create_table "users", :force => true do |t| create_table "users", :force => true do |t|
t.string "email" t.string "email"
t.string "password_hash"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "password_hash"
end end
add_index "users", ["email"], :name => "index_users_on_email", :unique => true add_index "users", ["email"], :name => "index_users_on_email", :unique => true

View File

@ -0,0 +1,25 @@
require 'spec_helper'
describe SoundsController do
before do
controller.current_user = Factory.create(:user)
end
describe 'GET show' do
let(:sound) { Factory.create(:sound) }
def do_show
get :show, :id => sound.id
end
it 'sends the sound file as the response body' do
do_show
response.body.should == File.read(sound.path)
end
it 'sets the sound file mime-type as the response content-type' do
do_show
response.content_type.should == sound.mime_type
end
end
end

View File

@ -13,20 +13,6 @@ describe TracksController do
end end
end end
describe 'GET download' do
let(:track) { Factory.create(:track) }
it 'streams the requested track' do
get :download, :id => track.id.to_s
response.should be_success
end
it 'returns the track mime-type as content-type' do
get :download, :id => track.id.to_s
response.content_type.should == track.mime_type
end
end
describe 'GET new' do describe 'GET new' do
it 'assigns a new track as @track' do it 'assigns a new track as @track' do
get :new get :new
@ -35,48 +21,46 @@ describe TracksController do
end end
describe 'POST create' do describe 'POST create' do
let(:track) { mock_model(Track).as_null_object } let(:track) { mock_model(Track).as_null_object }
let(:file) { let(:attributes) { Factory.attributes_for(:track).stringify_keys }
fixture_file_upload("#{Rails.root}/spec/fixtures/test.mp3", 'audio/mpeg')
}
before { Track.stub(:new).and_return(track) }
it 'creates a new track' do before do
attributes = Factory.attributes_for(:track) Track.stub(:new).and_return(track)
Track.should_receive(:new).
with({:name => attributes[:name]}).
and_return(track)
post :create, :track => {
:name => attributes[:name],
:file => file
}
end end
it 'saves the track with a file' do def do_create
track.should_receive(:save_with_file). post :create, :track => attributes
with(file, 'audio/mpeg') end
post :create, :track => { :file => file }
it 'creates a new track' do
Track.should_receive(:new).with(attributes)
do_create
end
it 'saves the track' do
track.should_receive :save
do_create
end end
context 'when the track saves successfully' do context 'when the track saves successfully' do
it 'redirects to the track page' do it 'redirects to the track page' do
post :create, :track => { :file => file } do_create
response.should redirect_to(track) response.should redirect_to(track)
end end
end end
context 'when the track fails to save' do context 'when the track fails to save' do
before do before do
track.stub(:save_with_file).and_return(false) track.stub(:save).and_return(false)
end end
it 'assigns the track as @track' do it 'assigns the track as @track' do
post :create, :track => { :file => file } do_create
assigns[:track].should == track assigns[:track].should == track
end end
it 'renders the new template' do it 'renders the new template' do
post :create, :track => { :file => file } do_create
response.should render_template('new') response.should render_template('new')
end end
end end

View File

@ -1,16 +1,24 @@
def build_sound_file
file = File.new("#{Rails.root}/spec/fixtures/test.mp3")
file.stub(:content_type => 'audio/mpeg')
file
end
FactoryGirl.define do FactoryGirl.define do
factory :playlist do factory :playlist do
name 'Electro' name 'Electro'
user user
end end
factory :sound do
file { build_sound_file }
end
factory :track do factory :track do
name 'Mega song' name 'Mega song'
mime_type 'audio/ogg'
sha256 '94a5486a69a7261da350c57f9e5a1eaa789e08752cfc56a1989976a6ad82f7a8'
after_create do |t| factory :track_with_sound do
t.save_with_file(File.new("#{Rails.root}/spec/fixtures/test.mp3"), 'audio/mpeg') file { build_sound_file }
end end
end end

View File

@ -28,17 +28,15 @@ feature 'Tracks' do
end end
scenario 'plays track' do scenario 'plays track' do
track = Factory.create(:track, :name => 'Mega song') track = Factory.create(:track_with_sound)
file = File.new("#{Rails.root}/spec/fixtures/test.mp3")
track.save_with_file(file, 'audio/mpeg')
visit track_path(track) visit track_path(track)
page.should have_xpath "//audio[@src='#{download_track_path(track)}']" page.should have_xpath "//audio[@src='#{sound_path(track.sound)}']"
visit find('audio')[:src] visit find('audio')[:src]
end end
after do after do
`rm -f #{Rails.root}/data/tracks/*` `rm -f #{Rails.root}/data/sounds/*`
end end
end end

46
spec/models/sound_spec.rb Normal file
View File

@ -0,0 +1,46 @@
require 'spec_helper'
describe Sound do
subject { sound }
let(:sound) { Factory.build(:sound) }
context 'with valid attributes' do
it { should be_valid }
end
context 'when sha256 empty' do
before { sound.sha256 = '' }
it { should_not be_valid }
end
context 'when mime_type empty' do
before { sound.mime_type = '' }
it { should_not be_valid }
end
describe '#path' do
it 'returns the sound file path based on the SHA256 digest' do
sound.path.should == "#{Rails.root}/data/sounds/#{sound.sha256}"
end
end
describe '#file=' do
let (:file) { Factory.attributes_for(:sound)[:file] }
it 'saves the file SHA256 digest' do
sound.sha256.should == Digest::SHA256.file(file.path).hexdigest
end
it 'copies the file to #path' do
File.read(sound.path).should == file.read
end
it 'saves the file MIME type' do
sound.mime_type.should == 'audio/mpeg'
end
end
after do
`rm -f #{Rails.root}/data/sounds/*`
end
end

View File

@ -1,7 +1,7 @@
require 'spec_helper' require 'spec_helper'
describe Track do describe Track do
subject { track } subject { track }
let(:track) { Factory.build(:track) } let(:track) { Factory.build(:track) }
context 'with valid attributes' do context 'with valid attributes' do
@ -13,47 +13,49 @@ describe Track do
it { should_not be_valid } it { should_not be_valid }
end end
context 'when mime_type empty' do describe '#file=' do
before { track.mime_type = '' } it 'builds a new related sound with the file' do
it { should_not be_valid } file = Factory.attributes_for(:track_with_sound)[:file]
end sounds = mock('sounds association proxy')
track.stub(:sounds => sounds)
context 'when sha256 empty' do sounds.should_receive(:build).with({:file => file})
before { track.sha256 = '' } track.file = file
it { should_not be_valid }
end
describe '#filepath' do
it 'returns the path to the track file' do
track.filepath.should == "#{Rails.root}/data/tracks/#{track.sha256}"
end end
end end
describe '#save_with_file' do describe '#sounds' do
let(:file) { File.new("#{Rails.root}/spec/fixtures/test.mp3") } it 'responds to sound' do
track.should respond_to(:sounds)
end
end
it 'calls save' do describe '#sound' do
Track.any_instance.should_receive(:save!) context 'with a sound' do
track.save_with_file(file, 'audio/ogg') before do
track.sounds << Factory.create(:sound)
end
it 'returns a sound' do
track.sound.should be_a(Sound)
end
end
end
describe '#sound?' do
context 'without a sound' do
it 'returns false' do
track.sound?.should be_false
end
end end
it 'saves the file content' do context 'with a sound' do
track.save_with_file(file, 'audio/ogg') before do
File.read(track.filepath).should == File.read(file.path) track.sounds << Factory.create(:sound)
end end
it 'saves the file SHA256 digest' do it 'returns true' do
track.save_with_file(file, 'audio/ogg') track.sound?.should be_true
track.sha256.should == Digest::SHA256.file(file.path).hexdigest end
end
it 'saves the file mime type' do
track.save_with_file(file, 'audio/mpeg')
track.mime_type.should == 'audio/mpeg'
end
after do
`rm -f #{Rails.root}/data/tracks/*`
end end
end end

View File

@ -1,9 +1,7 @@
require 'spec_helper' require 'spec_helper'
describe 'tracks/show.html.haml' do describe 'tracks/show.html.haml' do
let(:track) do let(:track) { Factory.create(:track) }
mock_model('Track', :name => 'Mega song')
end
before do before do
assign :track, track assign :track, track
@ -14,7 +12,9 @@ describe 'tracks/show.html.haml' do
rendered.should have_selector('h1', :text => 'Mega song') rendered.should have_selector('h1', :text => 'Mega song')
end end
context 'audio tag' do context 'when track has a sound' do
let(:track) { Factory.create(:track_with_sound) }
it 'provides an audio stream for the track' do it 'provides an audio stream for the track' do
render render
rendered.should have_selector('audio[src]') rendered.should have_selector('audio[src]')