Implement track/stream

* Add Streamer class
* Use FactoryGirl for factories
* Add sha256 field to tracks
* Add mime_type field to tracks
This commit is contained in:
Thibault Jouan 2011-07-23 16:30:51 +00:00
parent 47fa969617
commit 2f6a447416
18 changed files with 166 additions and 51 deletions

View File

@ -9,10 +9,10 @@ class TracksController < ApplicationController
def create def create
@track = Track.new(:name => params[:track][:name]) @track = Track.new(:name => params[:track][:name])
if params[:track][:file] if @track.save_with_file(
@track.uploaded_file = params[:track][:file] params[:track][:file].tempfile,
end params[:track][:file].content_type
if @track.save )
redirect_to @track redirect_to @track
else else
render :new render :new
@ -20,5 +20,8 @@ class TracksController < ApplicationController
end end
def stream def stream
track = Track.find params[:id]
self.content_type = track.mime_type
self.response_body = Streamer.new(track.filepath)
end end
end end

View File

@ -1,17 +1,18 @@
class Track < ActiveRecord::Base class Track < ActiveRecord::Base
validates_presence_of :name validates_presence_of :name
validates_presence_of :mime_type
validates_presence_of :sha256
after_create :save_file def filepath
"#{Rails.root}/data/tracks/#{sha256}"
def uploaded_file=(file)
@file = file
end end
def save_file def save_with_file(file, mime_type)
if @file self.sha256 = Digest::SHA256.file(file.path).hexdigest
File.open("#{Rails.root}/data/tracks/#{id}", 'w') do |f| self.mime_type = mime_type
f.write @file.tempfile.read File.open(filepath, 'w') do |f|
end f.write file.read
end end
save!
end end
end end

View File

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

View File

@ -13,7 +13,7 @@ module Scube
# -- all .rb files in that directory are automatically loaded. # -- all .rb files in that directory are automatically loaded.
# Custom directories with classes and modules you want to be autoloadable. # Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras) config.autoload_paths += %W(#{config.root}/lib)
# Only load the plugins named here, in the order given (default is alphabetical). # Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named. # :all can be used as a placeholder for all plugins not explicitly named.

View File

@ -0,0 +1,9 @@
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

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

View File

@ -10,7 +10,7 @@
# #
# 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 => 20110713182005) do ActiveRecord::Schema.define(:version => 20110726101228) do
create_table "playlists", :force => true do |t| create_table "playlists", :force => true do |t|
t.string "name" t.string "name"
@ -22,6 +22,8 @@ ActiveRecord::Schema.define(:version => 20110713182005) do
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
end end

View File

@ -1,3 +1,3 @@
Given /^a playlist named "([^"]*)"$/ do |name| Given /^a playlist named "([^"]*)"$/ do |name|
Playlist.create!(:name => name) Factory.create(:playlist, :name => name)
end end

View File

@ -1,5 +1,7 @@
Given /^a track named "([^"]*)"$/ do |name| Given /^a track named "([^"]*)"$/ do |name|
@track = Track.create!(:name => name) @track = Factory.create(:track, :name => name)
file = File.new("#{Rails.root}/features/fixtures/test.mp3")
@track.save_with_file(file, 'audio/mpeg')
end end
Then /^I should see an audio player$/ do Then /^I should see an audio player$/ do
@ -8,4 +10,6 @@ end
Then /^it should provide an audio stream for "([^"]*)"$/ do |name| Then /^it should provide an audio stream for "([^"]*)"$/ do |name|
page.should have_xpath "//audio[@src='#{stream_track_path(@track)}']" page.should have_xpath "//audio[@src='#{stream_track_path(@track)}']"
get find('audio')[:src]
last_response.status.should == 200
end end

View File

@ -13,7 +13,7 @@ Feature: Tracks
Given I am on the playlists page Given I am on the playlists page
When I follow "Add a track" When I follow "Add a track"
And I fill in "Name" with "Mega song" And I fill in "Name" with "Mega song"
And I attach the file "features/data/test.mp3" to "File" And I attach the file "features/fixtures/test.mp3" to "File"
And I press "Upload" And I press "Upload"
Then I should see "Mega song" within "h1" Then I should see "Mega song" within "h1"

12
lib/streamer.rb Normal file
View File

@ -0,0 +1,12 @@
class Streamer
def initialize(file)
@file = file
end
def each
f = File.new(@file, 'r')
while data = f.read(4096) do
yield data
end
end
end

View File

@ -9,6 +9,25 @@ describe TracksController do
end end
end end
describe 'GET stream' do
let(:track) { Factory.create(:track) }
it 'streams the requested track' do
get :stream, :id => track.id.to_s
response.should be_success
end
it 'creates a streamer instance' do
Streamer.should_receive(:new).with(track.filepath)
get :stream, :id => track.id.to_s
end
it 'returns the track mime-type as content-type' do
get :stream, :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
@ -18,48 +37,52 @@ describe TracksController do
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) {
file = mock(Rack::Test::UploadedFile)
file.stub(:tempfile => File.new("#{Rails.root}/spec/fixtures/test.mp3"))
file.stub(:content_type => 'audio/ogg')
file
}
before { Track.stub(:new).and_return(track) } before { Track.stub(:new).and_return(track) }
it 'creates a new track' do it 'creates a new track' do
attributes = Factory.attributes_for(:track)
Track.should_receive(:new). Track.should_receive(:new).
with(Factory.attributes_for(:track)). with({:name => attributes[:name]}).
and_return(track) and_return(track)
post :create, :track => Factory.attributes_for(:track) post :create, :track => {
:name => attributes[:name],
:file => file
}
end end
it 'saves the track' do it 'saves the track with a file' do
track.should_receive(:save) track.should_receive(:save_with_file).
post :create, :track => {} with(file.tempfile, 'audio/ogg')
end
it 'saves the file uploaded for the track' do
file = mock(Rack::Test::UploadedFile)
track.should_receive(:uploaded_file=).with(file)
post :create, :track => { :file => file } post :create, :track => { :file => file }
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 => Factory.attributes_for(:track) post :create, :track => { :file => file }
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).and_return(false) track.stub(:save_with_file).and_return(false)
end end
it 'assigns the track as @track' do it 'assigns the track as @track' do
post :create, :track => {} post :create, :track => { :file => file }
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 => {} post :create, :track => { :file => file }
response.should render_template('new') response.should render_template('new')
end end
end end
end end
end end

BIN
spec/data/test.mp3 Normal file

Binary file not shown.

View File

@ -5,5 +5,7 @@ FactoryGirl.define do
factory :track do factory :track do
name 'Mega song' name 'Mega song'
mime_type 'audio/ogg'
sha256 '94a5486a69a7261da350c57f9e5a1eaa789e08752cfc56a1989976a6ad82f7a8'
end end
end end

19
spec/lib/streamer_spec.rb Normal file
View File

@ -0,0 +1,19 @@
require 'spec_helper'
describe Streamer do
describe '#each' do
let(:stream) { Streamer.new("#{Rails.root}/spec/fixtures/test.mp3") }
it 'returns file content' do
chunks = ''
stream.each { |c| chunks << c }
chunks.should == File.read("#{Rails.root}/spec/fixtures/test.mp3")
end
it 'returns content in multiple chunks' do
count = 0
stream.each { count += 1 }
count.should be >= 2
end
end
end

View File

@ -9,30 +9,51 @@ describe Track do
end end
context 'when name empty' do context 'when name empty' do
before do before { track.name = '' }
track.name = ''
end
it { should_not be_valid } it { should_not be_valid }
end end
describe '#uploaded_file=' do context 'when mime_type empty' do
let(:track) { Track.new :name => 'Mega song' } before { track.mime_type = '' }
it { should_not be_valid }
end
it 'saves an uploaded file' do context 'when sha256 empty' do
filepath = "#{Rails.root}/spec/fixtures/test.mp3" before { track.sha256 = '' }
file = mock(Rack::Test::UploadedFile) it { should_not be_valid }
file.stub( end
:tempfile => File.new(filepath),
:content_type => 'audio/mpeg' describe '#filepath' do
) it 'returns the path to the track file' do
track.uploaded_file = file track.filepath.should == "#{Rails.root}/data/tracks/#{track.sha256}"
track.save end
File.read("#{Rails.root}/data/tracks/#{track.id.to_s}").should == File.read(filepath) end
describe '#save_with_file' do
let(:file) { File.new("#{Rails.root}/spec/fixtures/test.mp3") }
it 'calls save' do
Track.any_instance.should_receive(:save!)
track.save_with_file(file, 'audio/ogg')
end
it 'saves the file content' do
track.save_with_file(file, 'audio/ogg')
File.read(track.filepath).should == File.read(file.path)
end
it 'saves the file SHA256 digest' do
track.save_with_file(file, 'audio/ogg')
track.sha256.should == Digest::SHA256.file(file.path).hexdigest
end
it 'saves the file mime type' do
track.save_with_file(file, 'audio/mpeg')
track.mime_type.should == 'audio/mpeg'
end end
after do after do
`rm -rf #{Rails.root}/data/tracks/*` `rm -f #{Rails.root}/data/tracks/*`
end end
end end
end end

View File

@ -20,6 +20,16 @@ describe 'tracks/show.html.haml' do
rendered.should have_selector('audio[src]') rendered.should have_selector('audio[src]')
end end
it 'provides controls' do
render
rendered.should have_selector('audio[controls]')
end
it 'has autoplay activated' do
render
rendered.should have_selector('audio[autoplay]')
end
it 'displays a text fallback for UA without support' do it 'displays a text fallback for UA without support' do
render render
rendered.should have_selector('audio', rendered.should have_selector('audio',