diff --git a/app/controllers/tracks_controller.rb b/app/controllers/tracks_controller.rb index 12e7229..7496b38 100644 --- a/app/controllers/tracks_controller.rb +++ b/app/controllers/tracks_controller.rb @@ -9,10 +9,10 @@ class TracksController < ApplicationController def create @track = Track.new(:name => params[:track][:name]) - if params[:track][:file] - @track.uploaded_file = params[:track][:file] - end - if @track.save + if @track.save_with_file( + params[:track][:file].tempfile, + params[:track][:file].content_type + ) redirect_to @track else render :new @@ -20,5 +20,8 @@ class TracksController < ApplicationController end def stream + track = Track.find params[:id] + self.content_type = track.mime_type + self.response_body = Streamer.new(track.filepath) end end diff --git a/app/models/track.rb b/app/models/track.rb index 96bac19..389ee57 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -1,17 +1,18 @@ class Track < ActiveRecord::Base validates_presence_of :name + validates_presence_of :mime_type + validates_presence_of :sha256 - after_create :save_file - - def uploaded_file=(file) - @file = file + def filepath + "#{Rails.root}/data/tracks/#{sha256}" end - def save_file - if @file - File.open("#{Rails.root}/data/tracks/#{id}", 'w') do |f| - f.write @file.tempfile.read - end + def save_with_file(file, mime_type) + self.sha256 = Digest::SHA256.file(file.path).hexdigest + self.mime_type = mime_type + File.open(filepath, 'w') do |f| + f.write file.read end + save! end end diff --git a/app/views/tracks/show.html.haml b/app/views/tracks/show.html.haml index aaad844..6fd03e1 100644 --- a/app/views/tracks/show.html.haml +++ b/app/views/tracks/show.html.haml @@ -1,3 +1,3 @@ %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 diff --git a/config/application.rb b/config/application.rb index 937563e..70f0b74 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,7 @@ module Scube # -- all .rb files in that directory are automatically loaded. # 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). # :all can be used as a placeholder for all plugins not explicitly named. diff --git a/db/migrate/20110725110405_add_mime_type_field_to_tracks.rb b/db/migrate/20110725110405_add_mime_type_field_to_tracks.rb new file mode 100644 index 0000000..d49ef93 --- /dev/null +++ b/db/migrate/20110725110405_add_mime_type_field_to_tracks.rb @@ -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 diff --git a/db/migrate/20110726101228_add_sha256_field_to_tracks.rb b/db/migrate/20110726101228_add_sha256_field_to_tracks.rb new file mode 100644 index 0000000..f181212 --- /dev/null +++ b/db/migrate/20110726101228_add_sha256_field_to_tracks.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index daaba49..e84458b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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| t.string "name" @@ -22,6 +22,8 @@ ActiveRecord::Schema.define(:version => 20110713182005) do t.string "name" t.datetime "created_at" t.datetime "updated_at" + t.string "mime_type" + t.string "sha256" end end diff --git a/features/data/test.mp3 b/features/fixtures/test.mp3 similarity index 100% rename from features/data/test.mp3 rename to features/fixtures/test.mp3 diff --git a/features/step_definitions/playlists_step.rb b/features/step_definitions/playlists_step.rb index 57ea2c1..cb02b48 100644 --- a/features/step_definitions/playlists_step.rb +++ b/features/step_definitions/playlists_step.rb @@ -1,3 +1,3 @@ Given /^a playlist named "([^"]*)"$/ do |name| - Playlist.create!(:name => name) + Factory.create(:playlist, :name => name) end diff --git a/features/step_definitions/tracks_step.rb b/features/step_definitions/tracks_step.rb index fc60296..f8edd6c 100644 --- a/features/step_definitions/tracks_step.rb +++ b/features/step_definitions/tracks_step.rb @@ -1,5 +1,7 @@ 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 Then /^I should see an audio player$/ do @@ -8,4 +10,6 @@ end Then /^it should provide an audio stream for "([^"]*)"$/ do |name| page.should have_xpath "//audio[@src='#{stream_track_path(@track)}']" + get find('audio')[:src] + last_response.status.should == 200 end diff --git a/features/tracks.feature b/features/tracks.feature index 0f8bf93..0774b99 100644 --- a/features/tracks.feature +++ b/features/tracks.feature @@ -13,7 +13,7 @@ Feature: Tracks Given I am on the playlists page When I follow "Add a track" 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" Then I should see "Mega song" within "h1" diff --git a/lib/streamer.rb b/lib/streamer.rb new file mode 100644 index 0000000..436c25c --- /dev/null +++ b/lib/streamer.rb @@ -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 diff --git a/spec/controllers/tracks_controller_spec.rb b/spec/controllers/tracks_controller_spec.rb index e4311cc..95b49cb 100644 --- a/spec/controllers/tracks_controller_spec.rb +++ b/spec/controllers/tracks_controller_spec.rb @@ -9,6 +9,25 @@ describe TracksController do 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 it 'assigns a new track as @track' do get :new @@ -18,48 +37,52 @@ describe TracksController do describe 'POST create' do 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) } it 'creates a new track' do + attributes = Factory.attributes_for(:track) Track.should_receive(:new). - with(Factory.attributes_for(:track)). + with({:name => attributes[:name]}). and_return(track) - post :create, :track => Factory.attributes_for(:track) + post :create, :track => { + :name => attributes[:name], + :file => file + } end - it 'saves the track' do - track.should_receive(:save) - post :create, :track => {} - end - - it 'saves the file uploaded for the track' do - file = mock(Rack::Test::UploadedFile) - track.should_receive(:uploaded_file=).with(file) + it 'saves the track with a file' do + track.should_receive(:save_with_file). + with(file.tempfile, 'audio/ogg') post :create, :track => { :file => file } end context 'when the track saves successfully' 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) end end context 'when the track fails to save' do before do - track.stub(:save).and_return(false) + track.stub(:save_with_file).and_return(false) end it 'assigns the track as @track' do - post :create, :track => {} + post :create, :track => { :file => file } assigns[:track].should == track end it 'renders the new template' do - post :create, :track => {} + post :create, :track => { :file => file } response.should render_template('new') end end - end end diff --git a/spec/data/test.mp3 b/spec/data/test.mp3 new file mode 100644 index 0000000..7cdcb1f Binary files /dev/null and b/spec/data/test.mp3 differ diff --git a/spec/factories.rb b/spec/factories.rb index f714180..bca6527 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -5,5 +5,7 @@ FactoryGirl.define do factory :track do name 'Mega song' + mime_type 'audio/ogg' + sha256 '94a5486a69a7261da350c57f9e5a1eaa789e08752cfc56a1989976a6ad82f7a8' end end diff --git a/spec/lib/streamer_spec.rb b/spec/lib/streamer_spec.rb new file mode 100644 index 0000000..ff683f1 --- /dev/null +++ b/spec/lib/streamer_spec.rb @@ -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 diff --git a/spec/models/track_spec.rb b/spec/models/track_spec.rb index 9ce782b..82e2019 100644 --- a/spec/models/track_spec.rb +++ b/spec/models/track_spec.rb @@ -9,30 +9,51 @@ describe Track do end context 'when name empty' do - before do - track.name = '' - end - + before { track.name = '' } it { should_not be_valid } end - describe '#uploaded_file=' do - let(:track) { Track.new :name => 'Mega song' } + context 'when mime_type empty' do + before { track.mime_type = '' } + it { should_not be_valid } + end - it 'saves an uploaded file' do - filepath = "#{Rails.root}/spec/fixtures/test.mp3" - file = mock(Rack::Test::UploadedFile) - file.stub( - :tempfile => File.new(filepath), - :content_type => 'audio/mpeg' - ) - track.uploaded_file = file - track.save - File.read("#{Rails.root}/data/tracks/#{track.id.to_s}").should == File.read(filepath) + context 'when sha256 empty' do + before { track.sha256 = '' } + 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 + + 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 after do - `rm -rf #{Rails.root}/data/tracks/*` + `rm -f #{Rails.root}/data/tracks/*` end end end diff --git a/spec/views/tracks/show.html.haml_spec.rb b/spec/views/tracks/show.html.haml_spec.rb index dd12c95..f30097e 100644 --- a/spec/views/tracks/show.html.haml_spec.rb +++ b/spec/views/tracks/show.html.haml_spec.rb @@ -20,6 +20,16 @@ describe 'tracks/show.html.haml' do rendered.should have_selector('audio[src]') 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 render rendered.should have_selector('audio',