Initial import from our internal version of the Panda HQ application

This commit is contained in:
Damien Tanner 2008-06-05 21:41:11 +01:00
Родитель 90b613c2ae
Коммит 7ae041ee22
88 изменённых файлов: 2571 добавлений и 0 удалений

6
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
*.log
*.pid
uuid.state
.DS_Store
database.yml
schema.rb

9
README
Просмотреть файл

@ -0,0 +1,9 @@
Panda is a Merb app which runs on a special EC2 instance to encode videos for you. Uploaded videos are stored on S3 with a small amount of info kept in SimpleDB. The REST API makes it easy to integrate user video uploading into your web application.
How does Panda work?
1. Video is uploaded to panda
2. Panda checks the video's metadata, uploads the raw file to S3 and adds it to the encoding queue
3. The encoder application picks the encoding job off the queue when it's free and encodes the video to all possible formats
4. Panda sends a callback to your web application notifying you the video has been encoded
5. You use the S3 url of the encoding you want to show to your users

124
Rakefile Normal file
Просмотреть файл

@ -0,0 +1,124 @@
require 'rubygems'
Gem.clear_paths
Gem.path.unshift(File.join(File.dirname(__FILE__), "gems"))
require 'rake'
require 'rake/rdoctask'
require 'rake/testtask'
require 'spec/rake/spectask'
require 'fileutils'
require File.dirname(__FILE__)+'/config/boot.rb'
require Merb::framework_root+'/tasks'
include FileUtils
# Set these before any dependencies load
# otherwise the ORM may connect to the wrong env
Merb.root = File.dirname(__FILE__)
Merb.environment = ENV['MERB_ENV'] if ENV['MERB_ENV']
# Get Merb plugins and dependencies
require File.dirname(__FILE__)+'/config/dependencies.rb'
Merb::Plugins.rakefiles.each {|r| require r }
#desc "Packages up Merb."
#task :default => [:package]
desc "load merb_init.rb"
task :merb_init do
# deprecated - here for BC
Rake::Task['merb_env'].invoke
end
task :uninstall => [:clean] do
sh %{sudo gem uninstall #{NAME}}
end
desc 'Run unit tests'
Rake::TestTask.new('test_unit') do |t|
t.libs << 'test'
t.pattern = 'test/unit/*_test.rb'
t.verbose = true
end
desc 'Run functional tests'
Rake::TestTask.new('test_functional') do |t|
t.libs << 'test'
t.pattern = 'test/functional/*_test.rb'
t.verbose = true
end
desc 'Run all tests'
Rake::TestTask.new('test') do |t|
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc "Run all specs"
Spec::Rake::SpecTask.new('specs') do |t|
t.spec_opts = ["--format", "specdoc", "--colour"]
t.spec_files = Dir['spec/**/*_spec.rb'].sort
end
desc "Run all model specs"
Spec::Rake::SpecTask.new('model_specs') do |t|
t.spec_opts = ["--format", "specdoc", "--colour"]
t.spec_files = Dir['spec/models/**/*_spec.rb'].sort
end
desc "Run all controller specs"
Spec::Rake::SpecTask.new('controller_specs') do |t|
t.spec_opts = ["--format", "specdoc", "--colour"]
t.spec_files = Dir['spec/controllers/**/*_spec.rb'].sort
end
desc "Run a specific spec with TASK=xxxx"
Spec::Rake::SpecTask.new('spec') do |t|
t.spec_opts = ["--format", "specdoc", "--colour"]
t.libs = ['lib', 'server/lib' ]
t.spec_files = ["spec/merb/#{ENV['TASK']}_spec.rb"]
end
desc "Run all specs output html"
Spec::Rake::SpecTask.new('specs_html') do |t|
t.spec_opts = ["--format", "html"]
t.libs = ['lib', 'server/lib' ]
t.spec_files = Dir['spec/**/*_spec.rb'].sort
end
desc "RCov"
Spec::Rake::SpecTask.new('rcov') do |t|
t.spec_opts = ["--format", "specdoc", "--colour"]
t.spec_files = Dir['spec/**/*_spec.rb'].sort
t.libs = ['lib', 'server/lib' ]
t.rcov = true
end
desc 'Run all tests, specs and finish with rcov'
task :aok do
sh %{rake rcov}
sh %{rake spec}
end
unless Gem.cache.search("haml").empty?
namespace :haml do
desc "Compiles all sass files into CSS"
task :compile_sass do
gem 'haml'
require 'sass'
puts "*** Updating stylesheets"
Sass::Plugin.update_stylesheets
puts "*** Done"
end
end
end
##############################################################################
# SVN
##############################################################################
desc "Add new files to subversion"
task :svn_add do
system "svn status | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\ /g' | xargs svn add"
end

Просмотреть файл

@ -0,0 +1,53 @@
class Accounts < Application
provides :html
before :require_login, :only => [:dashboard, :show, :edit, :update]
def new
@account = Account.new
@account.email = params[:email] if params[:email]
if request.post?
if invite = Invite.find(:first, :conditions => ["email = ? and account_id IS NULL and approved IS NOT NULL",params[:account][:email]])
@account = Account.new(params[:account])
if @account.save!
invite.update_attribute(:account_id, @account.id)
session[:account_id] = @account.id
redirect "/"
end
else
render :text => "No invite for that email address."
end
else
render :layout => "auth"
end
end
def dashboard
@queued_videos = @account.queued_videos
@recently_completed_videos = @account.recently_completed_videos
render
end
def show
render
end
def edit
render
end
def update
@account.update_attribute(:name, params[:account][:name])
@account.update_attribute(:email, params[:account][:email])
@account.update_attribute(:upload_redirect_url, params[:account][:upload_redirect_url])
@account.update_attribute(:state_update_url, params[:account][:state_update_url])
unless params[:account][:password].blank?
@account.password = params[:account][:password]
@account.password_confirmation = params[:account][:password_confirmation]
@account.save!
end
redirect "/accounts"
end
end

Просмотреть файл

@ -0,0 +1,45 @@
# all your other controllers should inherit from this one to share code.
class Application < Merb::Controller
provides :html
# Auth plugin
attr_accessor :account
def index
redirect "/dashboard"
end
private
def require_login
case (params[:format] || "html")
when "html"
Merb.logger.info("AUTH DEBUG: session[:account_id]: #{session[:account_id]}")
@account = Account.find(session[:account_id]) if session[:account_id]
Merb.logger.info("AUTH DEBUG: @account:")
Merb.logger.info(@account.to_yaml)
throw :halt, redirect("/login") unless @account
when "xml", "yaml"
@account = Account.find_by_token(params[:account_key])
throw :halt, render('', :status => 401) unless @account
else
throw :halt, render('', :status => 401)
end
end
# Ensure the request is coming from one of our ec2 instances
def require_internal_auth
# if Merb.environment == "development"
# @ec2_instance = Ec2.find(:first, :conditions => {:amazon_id => "test"})
# end
unless @ec2_instance = Ec2.find(:first, :conditions => {:address => request.remote_ip})
throw :halt, render('', :status => 401)
end
# Rog.log :info, "Identified Encoder##{@ec2_instance.id} with IP #{request.remote_ip}"
end
def set_video
unless @video = Video.find_by_token(params[:id])
throw :halt, render('', :status => 404)
end
end
end

31
app/controllers/auth.rb Normal file
Просмотреть файл

@ -0,0 +1,31 @@
class Auth < Application
provides :html
# Auth plugin
def login
@account = Account.new
if request.post?
Merb.logger.info("AUTH DEBUG: Got post")
Merb.logger.info(params[:account].to_yaml)
if @account = Account.authenticate(params[:account])
Merb.logger.info("AUTH DEBUG: Account.authenticate returned valid account")
session[:account_id] = @account.id
Merb.logger.info("AUTH DEBUG: session[:account_id] = #{@account.id}")
redirect "/"
else
@account = Account.new(:login => params[:account][:login])
@notice = "Your username or password was incorrect."
Merb.logger.info("AUTH DEBUG: Invalid auth")
end
end
render
end
def logout
session[:account_id] = nil
redirect "/"
end
end

Просмотреть файл

@ -0,0 +1,13 @@
class Exceptions < Application
# handle NotFound exceptions (404)
def not_found
render :format => :html
end
# handle NotAcceptable exceptions (406)
def not_acceptable
render :format => :html
end
end

Просмотреть файл

@ -0,0 +1,18 @@
class Invites < Application
def create
@invite = Invite.new(:email => params[:invite][:email])
return render :text => "Email already invited" if Invite.find_by_email(@invite.email)
if @invite.save!
send_mail(InviteMailer, :notification, {
:from => EMAIL_SENDER,
:to => @invite.email,
:subject => "Thanks for signing up for the Panda beta"
})
redirect "http://pandastream.com/thanks"
else
return render :text => "Invalid email"
end
end
end

49
app/controllers/jobs.rb Normal file
Просмотреть файл

@ -0,0 +1,49 @@
class Jobs < Application
provides :yaml
before :require_internal_auth
before :set_job, :only => [:done]
# Called whenever an encoding is complete
def done
Rog.log :info, "Encoder##{@ec2_instance.id} sent internal request for completed job #{@job.id}"
@job.result = params[:result] # Save the raw result for furture debugging or some such
job_data = YAML.load(params[:result])
@job.status = "done"
@job.encoding_time = job_data[:encoding_time]
@job.save
# Update status of video and its encodings
job_data[:video][:encodings].each {|e| Encoding.find(e[:id]).change_status(e[:status]) }
@job.video.change_status(:done)
@job.video.send_status
end
def next
# Rog.log :info, "Encoder##{@ec2_instance.id}: Internal request for job from #{request.remote_ip}"
# Rog.log :info, "EC2##{@ec2_instance.id}: Internal request for job"
# Find the next job in the queue
job = Job.find_next_job
# Tell the instance to call again later if there's nothing todo
unless job
# Rog.log :info, "EC2##{@ec2_instance.id}: No jobs, telling the instance to call again later"
return {:command => :wait}.to_yaml
end
# TODO: Shutdown or start a new instance depending on the size of the queue
# return {:command => :shutdown} if it's the right thing to do
# Assign to the ec2 instance that requested it and change state to assigned
job.assign_to_ec2(@ec2_instance)
Rog.log :info, "Assigning job#{job.id} to encoder Encoder##{@ec2_instance.id}"
return {:job => job.job_response}.to_yaml
end
private
def set_job
unless @job = Job.find(params[:id])
throw :halt, render('', :status => 404)
end
end
end

105
app/controllers/videos.rb Normal file
Просмотреть файл

@ -0,0 +1,105 @@
class Videos < Application
provides :html, :xml, :yaml # Allow before filters to accept all formats, which are then futher refined in each action
before :require_login, :only => [:index, :create]
# before :require_internal_auth, :only => [:valid,:uploaded]
before :set_video, :only => [:show, :valid, :uploaded]
def index
provides :html, :xml, :yaml
# @videos = AWS::S3::Bucket.find('pandavision').objects
@videos = @account.videos.find(:all, :order => "created_at desc")
case content_type
when :html
render :layout => :accounts
when :xml
{:videos => @videos.map {|v| v.show_response }}.to_simple_xml
when :yaml
{:videos => @videos.map {|v| v.show_response }}.to_yaml
end
end
def show
provides :html, :xml, :yaml
case content_type
when :html
@account = Account.find(session[:account_id]) if session[:account_id]
if @account
render :layout => :accounts
else
redirect("/login")
end
when :xml
@video.show_response.to_simple_xml
when :yaml
@video.show_response.to_yaml
end
end
# Just for our testing
def new
provides :html
render :layout => "simple"
end
def create
provides :html, :xml, :yaml
@video = @account.videos.create
Rog.log :info, "#{@video.token}: Created video"
case content_type
when :html
redirect @video.upload_form_url
when :xml
headers.merge!({'Location'=> "/videos/#{@video.token}"})
@video.create_response.to_simple_xml
when :yaml
headers.merge!({'Location'=> "/videos/#{@video.token}"})
puts @video.create_response.to_yaml
@video.create_response.to_yaml
end
end
# Internal API
def valid
provides :yaml
Rog.log :info, "#{params[:id]}: Internal request for validity of video"
if @video.empty?
Rog.log :info, "#{params[:id]}: Response: 200"
render('', :status => 200)
else
Rog.log :info, "#{params[:id]}: Response: 404"
render('', :status => 404)
end
end
# Called when an uploader instance receives a new file
# First we check that there is a video id with no previously uploaded file
# If we were expecting this file, we create the jobs to encode it to varoius formats
# Then we reply with the url that the customer wants user's to be redirected to after uploading a video
def uploaded
provides :yaml
Rog.log :info, "#{params[:id]}: Internal request to confirm upload for video"
# Don't allow files to be uploaded to a video id more than once
unless @video.empty?
Rog.log :info, "#{params[:id]}: Whoops, this doesn't look like a valid upload"
Rog.log :info, "#{params[:id]}: Response: 404"
render('', :status => 404) and return
end
@video.filename = params[:filename]
@video.save_metadata(YAML.load(params[:metadata]))
@video.save
@video.add_encodings
job = @video.add_to_queue
Rog.log :info, "#{params[:id]}: Video added to queue (job id: #{job.id})"
# Tell the uploader where to redirect the client to
headers.merge!({'Location'=> @video.account.upload_redirect_url_or_default})
Rog.log :info, "#{params[:id]}: Response: 200"
render('', :status => 200)
end
end

Просмотреть файл

@ -0,0 +1,18 @@
module Merb
module GlobalHelpers
def nav_item(name, url=nil)
if url.class == Hash
url_str = '/'+url[:controller].to_s
url_str += '/'+url[:action].to_s if url.include?(:action)
url_str += '/'+url[:actions].first.to_s if url.include?(:actions) and url[:actions].first.to_s != "index"
elsif url.class == String
url_str = url
end
%(<li><a href="#{url ? url_str : '/'+name}">#{name.humanize}</a></li>)
end
def notice
%(<div class="notice">#{@notice}</div>) if @notice
end
end
end

Просмотреть файл

@ -0,0 +1,8 @@
class InviteMailer < Merb::MailController
def notification
render_mail
end
def approved
render_mail
end
end

Просмотреть файл

@ -0,0 +1,14 @@
Hi,
Welcome to the Panda beta!
You can sign up for your account at: http://hq.pandastream.com/signup (make sure you use the same email that this invite was sent to)
Once you have your login details you'll probably want to have a read through the Getting started (using Ruby on Rails) guide here: http://pandastream.com/docs/getting_started
If you're interested in getting to know the API in more depth, the docs are here: http://pandastream.com/docs
During the beta period we won't be charging for use, however videos will be cut to 3 minutes and removed after 1 week. If you would like to use Panda in production please get in contact (panda@new-bamboo.co.uk) with us!
Thanks,
Team Panda.

Просмотреть файл

@ -0,0 +1,12 @@
Hi!
Thanks for signing up for the Panda beta. We will be sending out invites every few days so sit tight.
In the meantime don't forget to have a read through the Getting started guide here: http://pandastream.com/docs/getting_started
If you're interested in getting to know the API in more depth, the docs are here: http://pandastream.com/docs
Let is know if you have any questions or suggestions!
Thanks,
Team Panda.

Просмотреть файл

@ -0,0 +1 @@
<%= catch_content :layout %>

Просмотреть файл

@ -0,0 +1 @@
<%= catch_content :layout %>

68
app/models/account.rb Normal file
Просмотреть файл

@ -0,0 +1,68 @@
class Account < ActiveRecord::Base
attr_accessor :password # Virtual attribute for the unencrypted password
attr_accessor :password_confirmation
validates_presence_of :login, :email
validates_presence_of :password #,:if => :password_required?
validates_presence_of :password_confirmation #,:if => :password_required?
validates_length_of :password, :within => 4..40 #,:if => :password_required?
validates_confirmation_of :password #,:if => :password_required?
validates_length_of :login, :within => 3..40
validates_length_of :email, :within => 3..100
validates_uniqueness_of :login, :email, :case_sensitive => false
validates_format_of(:email,
:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
:message=>"is invalid")
before_save :encrypt_password
before_create :set_token
has_many :videos
has_many :jobs, :through => :videos
belongs_to :format
def set_token
self.token = UUID.new
end
def upload_redirect_url_or_default
self.upload_redirect_url.blank? ? "http://#{PANDA_UPLOAD_DOMAIN}/videos/done" : self.upload_redirect_url
end
def recent_videos
self.videos.find(:all, :order => "created_at desc", :limit => 25)
end
# def all_completed_videos
# self.videos.find(:all, :conditions => "status = 'done'", :order => "created_at desc")
# end
def recently_completed_videos
self.videos.find(:all, :conditions => "status = 'done'", :order => "created_at desc", :limit => 5)
end
def queued_videos
self.videos.find(:all, :conditions => "status = 'queued' or status = 'processing'", :order => "created_at asc")
end
# Auth plugin
def self.authenticate(params)
return nil unless u = find_by_login(params[:login]) # need to get the salt
puts "#{u.crypted_password} | #{encrypt(params[:password], u.salt)}"
u && (u.crypted_password == encrypt(params[:password], u.salt)) ? u : nil
end
def self.encrypt(password, salt)
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
end
protected
def encrypt_password
return if password.blank?
self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
self.crypted_password = self.class.encrypt(password, self.salt)
end
end

3
app/models/ec2.rb Normal file
Просмотреть файл

@ -0,0 +1,3 @@
class Ec2 < ActiveRecord::Base
has_many :jobs
end

76
app/models/encoding.rb Normal file
Просмотреть файл

@ -0,0 +1,76 @@
class Encoding < ActiveRecord::Base
belongs_to :video
belongs_to :quality
has_many :notifications
before_create :copy_metadata
before_create :set_pending_status
def filename
"#{self.token}.#{self.container}"
end
def full_path
File.join(ENCODED_DIR,self.filename)
end
def url
# FIXME: only return the flv encoding
"http://#{PANDA_VIDEOS_DOMAIN}/#{self.filename}"
end
def embed_html
%(<embed src="http://#{PANDA_VIDEOS_DOMAIN}/flvplayer.swf" width="#{self.width}" height="#{self.height}" allowfullscreen="true" allowscriptaccess="always" flashvars="&displayheight=#{self.height}&file=#{self.url}&width=#{self.width}&height=#{self.height}" />)
end
def resolution
self.width ? "#{self.width}x#{self.height}" : nil
end
def video_bitrate_in_bits
self.video_bitrate * 1024
end
def audio_bitrate_in_bits
self.audio_bitrate * 1024
end
def copy_metadata
[:width, :height, :container, :fps, :video_bitrate, :audio_bitrate].each do |x|
self.send("#{x}=", self.quality.send(x))
end
end
def show_response
{
:id => self.token,
:width => self.width,
:height => self.height,
:resolution => self.resolution,
:duration => self.duration,
:format => self.quality.format.code,
:quality => self.quality.quality,
:status => self.status,
:filename => self.filename
}
end
def job_response
hash = {}
[:id, :token, :filename, :status, :width, :height, :resolution, :container, :fps, :video_bitrate, :video_bitrate_in_bits, :audio_bitrate, :audio_bitrate_in_bits].each do |x|
hash[x] = self.send(x)
end
hash[:format] = self.quality.format.code
hash[:quality] = self.quality.quality
return hash
end
def change_status(st)
self.update_attribute(:status, st.to_s)
end
def set_pending_status
self.status = 'queued'
end
end

3
app/models/format.rb Normal file
Просмотреть файл

@ -0,0 +1,3 @@
class Format < ActiveRecord::Base
has_many :qualities, :order => "position asc"
end

19
app/models/invite.rb Normal file
Просмотреть файл

@ -0,0 +1,19 @@
class Invite < ActiveRecord::Base
belongs_to :account
validates_presence_of :email
validates_length_of :email, :within => 3..100
validates_uniqueness_of :email, :case_sensitive => false
validates_format_of(:email,
:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
:message=>"is invalid")
def approve!
self.update_attribute(:approved, Time.now)
InviteMailer.new.dispatch_and_deliver(:approved, {
:from => EMAIL_SENDER,
:to => self.email,
:subject => "Welcome to the Panda beta"
})
end
end

30
app/models/job.rb Normal file
Просмотреть файл

@ -0,0 +1,30 @@
class Job < ActiveRecord::Base
belongs_to :video
belongs_to :ec2
before_create :set_default_status
def set_default_status
self.status = 'queued'
end
def job_response
{
:id => self.id,
:video => self.video.job_response
}
end
def assign_to_ec2(ec2_instance)
self.ec2_id = ec2_instance.id
self.status = 'processing'
self.save
self.video.change_status(:processing)
self.video.send_status
end
def self.find_next_job
self.find(:first, :conditions => "status = 'queued'", :order => "created_at asc")
end
end

Просмотреть файл

@ -0,0 +1,3 @@
class Notification < ActiveRecord::Base
belongs_to :encoding
end

3
app/models/quality.rb Normal file
Просмотреть файл

@ -0,0 +1,3 @@
class Quality < ActiveRecord::Base
belongs_to :format
end

0
app/models/resolution.rb Normal file
Просмотреть файл

4
app/models/upload.rb Normal file
Просмотреть файл

@ -0,0 +1,4 @@
# class Upload < ActiveRecord::Base
# attr_accessor :bitrate
# attr_accessor :resolution
# end

141
app/models/video.rb Normal file
Просмотреть файл

@ -0,0 +1,141 @@
class Video < ActiveRecord::Base
belongs_to :account
has_many :encodings, :dependent => :destroy
has_many :jobs, :dependent => :destroy
before_create :set_default_status
before_create :set_token
before_destroy :delete_s3_files
def set_default_status
self.status = 'empty'
end
def empty?
self.status == "empty"
end
def set_token
self.token = UUID.new
end
def default_flv_url
self.encodings.first.url
end
def full_raw_path
File.join(RAW_DIR,self.token)
end
def duration_str
s = (self.duration || 0) / 1000
"#{sprintf("%02d", s/60)}:#{sprintf("%02d", s%60)}"
end
def resolution
self.width ? "#{self.width}x#{self.height}" : nil
end
def upload_form_url
%(http://#{PANDA_UPLOAD_DOMAIN}:#{PANDA_UPLOAD_PORT}/videos/#{self.token}/form)
end
def show_response
{:video => {
:id => self.token,
:resolution => self.resolution,
:duration => self.duration,
:status => self.status,
:encodings => self.encodings.map {|e| e.show_response}
}
}
end
def create_response
{:video => {
:id => self.token
}
}
end
def job_response
{:token => self.token,
:status => self.status,
:encodings => self.encodings.map {|e| e.job_response}
}
end
def change_status(st)
self.update_attribute(:status, st.to_s)
end
def add_to_queue
job = self.jobs.create
self.change_status(:queued)
self.send_status
return job
end
# TODO: Use notifications daemon
def send_status
url = self.account.state_update_url.gsub(/\$id/,self.token)
# params = {"video" => self.show_response.to_yaml}
Rog.log :info, "Sending status update of video##{self.token} to client (#{self.account.login}): #{url}"
begin
ressult = Net::HTTP.get_response(URI.parse(url))
puts "--> #{result.code} #{result.message} (#{result.body.length})"
puts "WOULD FETCH URL NOW: #{url}"
rescue
puts "Couldn't connect to #{url}"
# TODO: Send back a nice error if we can't connect to the client
end
end
def save_metadata(metadata)
[:width, :height, :duration, :container, :fps, :video_codec, :video_bitrate, :audio_codec, :audio_sample_rate].each do |x|
self.send("#{x}=", metadata[x])
end
end
def add_encoding_for_quality(quality, force=nil)
# Only create an encoding if it doesn't already exist for this format
unless Encoding.find(:first, :conditions => {:video_id => self.id, :quality_id => quality.id})
if force == :force or self.width >= quality.width
status = "queued"
Encoding.create(:token => UUID.new, :video_id => self.id, :quality_id => quality.id, :status => status)
end
end
end
def add_encodings
# TODO: Only add formats which the user has added to their account
Format.find(:all).each do |format|
qualities = format.qualities
# We always encode to the lowest quality (which will be the first, as they are ordered)
self.add_encoding_for_quality(qualities.shift, :force)
qualities.each do |quality|
self.add_encoding_for_quality(quality)
end
end
end
def delete_s3_files
S3RawVideoObject.delete(self.token)
self.encodings.each do |e|
S3VideoObject.delete(e.filename)
end
end
# def upload_and_encode(filename, format)
# encoding = self.encodings.create(:format_id => format.id)
# Send to uploading queue
# message = {:filename => filename, :token => self.token, :encoding => encoding.hash_for_queue}
# puts "Adding to upload queue"
# puts message.to_yaml
# Queue.up.send_message message.to_yaml
# end
end

Просмотреть файл

@ -0,0 +1 @@
<%= catch_content :layout %>

Просмотреть файл

@ -0,0 +1,21 @@
<div class="dashboard">
<% if @account.upload_redirect_url.blank? or @account.state_update_url.blank? %>
<div class="notice">
<p>You haven't set an upload redirect or state update url. <a href="/accounts">Edit your account details</a> and set these to allow Panda to communicate with your application.</p>
</div>
<% end %>
<div id="new_video">
<h2><a href="#" onclick="window.open('/videos/new?account_token=<%= @account.token %>', 'panda_upload', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=395,height=35');return false;">Upload a test video</a></h2>
</div>
<h2>Queued videos</h2>
<%= partial "videos/list", :videos => @queued_videos %>
<h2>Recently completed videos</h2>
<%= partial "videos/list", :videos => @recently_completed_videos %>
<div class="more_videos"><a href="/videos">View all videos</a></div>
</div>

Просмотреть файл

@ -0,0 +1,18 @@
<% form_for :account, :action => "/accounts", :method => :put do %>
<h2>Account details</h2>
<p><%= text_control :name, :label => "Name" %></p>
<p><%= text_control :email, :label => "Email" %></p>
<h2>Change password</h2>
<p><%= password_control :password, :label => "Password" %></p>
<p><%= password_control :password_confirmation, :label => "Confirm password" %></p>
<h2>Settings</h2>
<p><%= text_control :upload_redirect_url, :label => "Upload redirect url", :size => 50 %></p>
<p><%= text_control :state_update_url, :label => "State update url", :size => 50 %></p>
<%= submit_button "Save changes" %>
<% end %>

Просмотреть файл

@ -0,0 +1,14 @@
<h2>Sign up to Panda</h2>
<div class="notice">You must sign up with the same email address your invite was sent to.</div>
<% form_for :account, :action => "/signup" do %>
<p><%= text_control :name, :label => "Name" %></p>
<p><%= text_control :login, :label => "Login" %></p>
<p><%= text_control :email, :label => "Email" %></p>
<p><%= password_control :password, :label => "Password" %></p>
<p><%= password_control :password_confirmation, :label => "Confirm password" %></p>
<%= submit_button "Sign up" %>
<% end %>

Просмотреть файл

@ -0,0 +1,29 @@
<h2>Account details</h2>
<div class="account_details">
<h3>Name</h3>
<p class="note">Company, organisation or individual's name.</p>
<p class="value"><%= @account.name %></p>
<h3>Email</h3>
<p class="note">Email address to send notifications and error reports to.</p>
<p class="value"><%= @account.email %></p>
<h3>Account key</h3>
<p class="note">Your secret key used to authenticate API calls.</p>
<p class="value"><%= @account.token %></p>
</div>
<h2>Settings</h2>
<div class="account_details">
<h3>Upload redirect url</h3>
<p class="note">After uploading a video using the form served by Panda, the user will be redirect to this url. Typically you should display a confirmation that the video has been uploaded and will take several minutes to process.</p>
<p class="value"><%= @account.upload_redirect_url %></p>
<h3>State update url</h3>
<p class="note">Along each stage of the video upload process, Panda will report back to you the current state of a video. The variables $token and $state will be replaced with the video's token and it's new state: uploaded, encoding, error, encoding_error or done.</p>
<p class="value"><%= @account.state_update_url %></p>
</div>
<div class="edit_details"><a href="/accounts/edit">Edit details</a></div>

Просмотреть файл

@ -0,0 +1,6 @@
<%= notice %>
<% form_for :account, :action => "/login", :method => :post do %>
<p><%= text_control :login, :label => "Username" %></p>
<p><%= password_control :password, :label => "Password" %></p>
<%= submit_button "Login" %>
<% end %>

Просмотреть файл

@ -0,0 +1,3 @@
<h2>Documentation</h2>
<p>Coming soon to a browser near you!</p>

Просмотреть файл

@ -0,0 +1,207 @@
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title><%= @exception.name.humanize %></title>
<style type="text/css" media="screen">
body {
font-family:arial;
font-size:11px;
}
h1 {
font-size:48px;
letter-spacing:-4px;
margin:0;
line-height:36px;
color:#333;
}
h1 sup {
font-size: 0.5em;
}
h1 sup.error_500, h1 sup.error_400 {
color:#990E05;
}
h1 sup.error_100, h1 sup.error_200 {
color:#00BF10;
}
h1 sup.error_300 {
/* pretty sure you cant 'see' status 300
errors but if you could I think they
would be blue */
color:#1B2099;
}
h2 {
font-size:36px;
letter-spacing:-3px;
margin:0;
line-height:28px;
color:#444;
}
a, a:visited {
color:#00BF10;
}
.internalError {
width:800px;
margin:50px auto;
}
.header {
border-bottom:10px solid #333;
margin-bottom:1px;
background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7");
padding:20px;
}
table.trace {
width:100%;
font-family:courier, monospace;
letter-spacing:-1px;
border-collapse: collapse;
border-spacing:0;
}
table.trace tr td{
padding:0;
height:26px;
font-size:13px;
vertical-align:middle;
}
table.trace tr.file{
border-top:2px solid #fff;
background-color:#F3F3F3;
}
table.trace tr.source {
background-color:#F8F8F8;
display:none;
}
table.trace .open tr.source {
display:table-row;
}
table.trace tr.file td.expand {
width:23px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAXCAIAAABvSEP3AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdVJREFUeNqMVL+TwUAYxaRIOlEhlZHGDAUzzOQ61+AqXMV1lJSU7q/QRqm8KFUcJTNn5qJkaPyoKKVz7y4mF8na5Kt29tt9+/Z97/u81+vVQ4r9frdarS6Xi7ETDIZisRjxMGPfmk4niNPpZE+xLAugbPaZ53nzvtfMBe/3+/3dbuehBrAKhZdUKkVAWa9Xsiybv0CPZDJZLr/qa5/BwgwRjYqOKIvFYjQa/aNommZh0Ww2K5UqzwfoQOPxaLPZ3FAmk0+7lplMpt1u53J5OpBOR0eZEE9wHJfP5zud93g88QhluwWbjW+5VOmKBgKBer3eaDTDYeGBQF8+x7rqIYoiPgixWJazpA6HA+MSxRArkUgMh0M409g8Ho8+9wYxxCqVSq1W26EDHGM2m4HOHQrEc38f/Yn7cLmlIRhBENzcx8cVRZnPZ/YUep2BWkjTIfA+PKVpZAXR5QxsjiqCKvGEqqp443w+0dvy17swqD0HB3S73V5PpkNg1qBqt8kwGCjmPkinM0QJbIoEa7U6UG6ToVgs4V9G2g0ESoP5Aoi7KYX5oCgf8IKbkvn9/mr1LRQKESamzgJy0g0tSZIuB3nuGqRU9Vv9C4sKkUhEkp4soxvxI8AAhWrrtXa3X8EAAAAASUVORK5CYII=);
background-position:top left;
background-repeat:no-repeat;
}
table.trace .open tr.file td.expand {
width:19px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXZJREFUeNrslK1ywkAUhcMOBomEOiSdqLxEBJX0NaijOsjyHGGmCGyQQYaiiiw4gktkcOmZbpsuuzQ/M5XnqJ2d3S/n3nM3rTzPLUP7/Tt0+pLcGQwG3W53OLyHzPMtjYL7q9UqSRLrD4E1Gj1orCvKYuFHUWTVkOM44/HjDcp8/lL4r6NerzeZPMm1KFw0QkDn83m5fP2lHA4fNQvRtNvtjsfDd0WzmSfb2e/fdTqdOvdh/HLJZLOn0+d2HJ+KRGzbdl23EpFlmed5cp2maRzHQq1lvQ5KMi6EUZBGfup6E1pTfd+vrGW7jbQ2C9hTt9BpqNyIWaAwAy6xg2eBz5iRC/NomiZhGN5sqmnkauo0BUGgVQoBjQ80oCACgNQdZHfTYBkF2mxCtWWAqunWpahxIDUt3QYUxIFQpJHyIWpXjinabKbbwItMHT+NyjchrP8QKaSQQgoppJBCCimkkEIKKaSQQgoppJBCCimkkEIKKaSo+hRgAEFD17X08O2NAAAAAElFTkSuQmCC);
background-position:top left;
background-repeat:no-repeat;
}
table.trace tr.source td.collapse {
width:19px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAVxJREFUeNrs0zFygkAUBmBlUkgJHdABlQwVkVJKKUxBYWbkALTxMJwhltyDFkss03IF8pudIcwaDaDl/6pd2P327b7d+eHwMXs4lNkzggoVKlSoUKFChQoVKlSoUKFChQoVKlSoUKFChQqVEYqm6ft9+qiSJEkYho7jTlcw2fd9NOI4nq4gEdFwXXe1Cqco63VkWVbXRTqLhTpOwQRpF7quR1E0TgGhqvLKUFCyoQqG/rks3O6kZKW/eRFpevOCoGTXVTcMQ5EyxyDEkML1c5RzuZOICIyXqn7JBVez6282MWrx731HOv2qB8Hri2lamNk0DfpVVdV1Peodappmmua8bdvzuc7zfNprzrLMth1FnGh/X8MjCAIQv/cFz/+65PcDh7rbvYv2ZUfdj+PxsyzLgVl0hKwgTqeqKApx2LeOc7t98zyv/1FWOgvx9RPii23bmL9cetJ8Ed8CDAC6aFW8bCzFhwAAAABJRU5ErkJggg==);
background-position:bottom left;
background-repeat:no-repeat;
background-color:#6F706F;
}
table.trace tr td.path {
padding-left:10px;
}
table.trace tr td.code {
padding-left:35px;
white-space: pre;
line-height:9px;
padding-bottom:10px;
}
table.trace tr td.code em {
font-weight:bold;
color:#00BF10;
}
table.trace tr td.code .more {
color:#666;
}
table.trace tr td.line {
width:30px;
text-align:right;
padding-right:4px;
}
.footer {
margin-top:5px;
font-size:11px;
color:#444;
text-align:right;
}
</style>
</head>
<body>
<div class="internalError">
<div class="header">
<h1><%= @exception.name.humanize %> <sup class="error_<%= @exception.class::STATUS %>"><%= @exception.class::STATUS %></sup></h1>
<% if show_details = ::Merb::Server.config[:exception_details] -%>
<h2><%= @exception.message %></h2>
<% else -%>
<h2>Sorry about that...</h2>
<% end -%>
<h3>Parameters</h3>
<ul>
<% controller.params[:original_params].each do |param, value| %>
<li><strong><%= param %>:</strong> <%= value.inspect %></li>
<% end %>
<%= "<li>None</li>" if controller.params[:original_params].empty? %>
</ul>
<h3>Session</h3>
<ul>
<% controller.params[:original_session].each do |param, value| %>
<li><strong><%= param %>:</strong> <%= value.inspect %></li>
<% end %>
<%= "<li>None</li>" if controller.params[:original_session].empty? %>
</ul>
<h3>Cookies</h3>
<ul>
<% controller.params[:original_cookies].each do |param, value| %>
<li><strong><%= param %>:</strong> <%= value.inspect %></li>
<% end %>
<%= "<li>None</li>" if controller.params[:original_cookies].empty? %>
</ul>
</div>
<% if show_details %>
<table class="trace">
<% @exception.backtrace.each_with_index do |line, index| %>
<tbody class="close">
<tr class="file">
<td class="expand">
</td>
<td class="path">
<%= (line.match(/^([^:]+)/)[1] rescue 'unknown').sub(/\/((opt|usr)\/local\/lib\/(ruby\/)?(gems\/)?(1.8\/)?(gems\/)?|.+\/app\/)/, '') %> in "<strong><%= line.match(/:in `(.+)'$/)[1] rescue '?' %></strong>"
</td>
<td class="line">
<a href="txmt://open?url=file://<%=file = (line.match(/^([^:]+)/)[1] rescue 'unknown')%>&amp;line=<%= lineno = line.match(/:([0-9]+):/)[1] rescue '?' %>"><%=lineno%></a>
</td>
</tr>
<tr class="source">
<td class="collapse">
</td>
<td class="code" colspan="2"><% (__caller_lines__(file, lineno, 5) rescue []).each do |llineno, lcode, lcurrent| %>
<a href="txmt://open?url=file://<%=file%>&amp;line=<%=llineno%>"><%= llineno %></a><%='<em>' if llineno==lineno.to_i %><%= lcode.size > 90 ? lcode[0..90]+'<span class="more">......</span>' : lcode %><%='</em>' if llineno==lineno.to_i %>
<% end %>
</td>
</tr>
</tbody>
<% end %>
</table>
<script type="text/javascript" charset="utf-8">
// swop the open & closed classes
els = document.getElementsByTagName('td');
for(i=0; i<els.length; i++){
if(els[i].className=='expand' || els[i].className=='collapse'){
els[i].onclick = function(e){
tbody = this.parentNode.parentNode;
if(tbody.className=='open'){
tbody.className='closed';
}else{
tbody.className='open';
}
}
}
}
</script>
<% end %>
<div class="footer">
lots of love, from <a href="#">merb</a>
</div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,38 @@
<div id="container">
<div id="header-container">
<img src="/images/merb.jpg">
<!-- <h1>Mongrel + Erb</h1> -->
<h2>pocket rocket web framework</h2>
<hr />
</div>
<div id="left-container">
<h3>Exception:</h3>
<p><%= params[:exception] %></p>
</div>
<div id="main-container">
<h3>Why am I seeing this page?</h3>
<p>Merb couldn't find an appropriate content_type to return,
based on what you said was available via provides() and
what the client requested. For more information, visit
http://merbivore.com/fixing_406_issues
</p>
<h3>Where can I find help?</h3>
<p>If you have any questions or if you can't figure something out, please take a
look at our <a href="http://merb.devjavu.com/"> project development page</a> or,
feel free to come chat at irc.freenode.net, channel #merb.</p>
<h3>How do I edit this page?</h3>
<p>You can change what people see when this happens byy editing <tt>app/views/exceptions/not_found.html.erb</tt>.</p>
</div>
<div id="footer-container">
<hr />
<div class="left"></div>
<div class="right">&copy; 2007 the merb dev team</div>
<p>&nbsp;</p>
</div>
</div>

Просмотреть файл

@ -0,0 +1,40 @@
<div id="container">
<div id="header-container">
<img src="/images/merb.jpg">
<!-- <h1>Mongrel + Erb</h1> -->
<h2>pocket rocket web framework</h2>
<hr />
</div>
<div id="left-container">
<h3>Exception:</h3>
<p><%= params[:exception] %></p>
</div>
<div id="main-container">
<h3>Welcome to Merb!</h3>
<p>Merb is a light-weight MVC framework written in Ruby. We hope you enjoy it.</p>
<h3>Where can I find help?</h3>
<p>If you have any questions or if you can't figure something out, please take a
look at our <a href="http://merb.devjavu.com/"> project development page</a> or,
feel free to come chat at irc.freenode.net, channel #merb.</p>
<h3>How do I edit this page?</h3>
<p>You're seeing this page because you need to edit the following files:
<ul>
<li>config/merb.yml <strong><em>(optional)</em></strong></li>
<li>config/router.rb <strong><em>(recommended)</em></strong></li>
<li>app/views/exceptions/not_found.html.erb <strong><em>(recommended)</em></strong></li>
<li>app/views/layout/application.html.erb <strong><em>(change this layout)</em></strong></li>
</ul>
</p>
</div>
<div id="footer-container">
<hr />
<div class="left"></div>
<div class="right">&copy; 2007 the merb dev team</div>
<p>&nbsp;</p>
</div>
</div>

Просмотреть файл

@ -0,0 +1 @@
<%# Form is in panda_site %>

Просмотреть файл

@ -0,0 +1,35 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Panda</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" charset="utf-8" />
<script type="text/javascript" charset="utf-8" src="/javascripts/jquery.js"></script>
<script type="text/javascript" charset="utf-8" src="/javascripts/swfobject.js"></script>
<%= catch_content :head %>
</head>
<body>
<div id="container">
<div id="header" class="clearfix">
<div id="account">
<h2><%= @account.name %></h2>
<p><a href="/logout">Logout</a></p>
</div>
<h1><a href="/"><img src="/images/panda_logo.gif" alt="Panda" /></a></h1>
<ul id="nav" class="clearfix">
<%= nav_item "dashboard", "/" %>
<%= nav_item "videos" %>
<%= nav_item "account", {:controller => "accounts", :actions => [:index,:edit]} %>
<li><a href="http://pandastream.com/docs" target="_blank">Documentation</a></li>
</ul>
</div>
<div id="main">
<%= catch_content :for_layout %>
</div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,23 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Panda</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" charset="utf-8" />
<script type="text/javascript" charset="utf-8" src="/javascripts/jquery.js"></script>
<script type="text/javascript" charset="utf-8" src="/javascripts/swfobject.js"></script>
<%= catch_content :head %>
</head>
<body class="auth">
<div id="container">
<div id="header" class="clearfix">
<h1><a href="/"><img src="/images/panda_logo.gif" alt="Panda" /></a></h1>
</div>
<div id="main">
<%= catch_content :for_layout %>
</div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
<title>Panda</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<%= catch_content :for_layout %>
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
<% throw_content(:head) do %>
<script type="text/javascript" charset="utf-8">
$(function() {
jQuery.nginxUploadProgress({uuid: "<%= params[:id] %>"});
});
</script>
<% end %>
<div id="pandaloader">
<div id="uploading">
<div id="progress" class="bar">
<div id="progressbar">&nbsp;</div>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,8 @@
<p>
<label>Video format</label>
<select name="panda_video[max_format]">
<% Format.find_all.each do |f| %>
<option value="<%= f.id %>"><%= f.full_name %></option>
<% end %>
</select>
</p>

Просмотреть файл

@ -0,0 +1,19 @@
<table id="videos">
<tr>
<th>Filename</th>
<th>Duration</th>
<th>Status</th>
<th>Created at</th>
</tr>
<% videos.each do |v| %>
<tr>
<td>
<a href="/videos/<%= v.token %>" class="filename"><%= v.filename || "No filename" %></a>
<span class="token"><%= v.token %></span>
</td>
<td><%= v.duration_str %></td>
<td class="<%= v.status %>"><%= v.status %></td>
<td><%= v.created_at.strftime("%a, %d %b %Y %H:%M:%S") %></td>
</tr>
<% end %>
</table>

Просмотреть файл

@ -0,0 +1,3 @@
<h2>Videos</h2>
<%= partial :list, :videos => @videos %>

Просмотреть файл

@ -0,0 +1,4 @@
<form action="/videos" method="post">
<input type="submit" value="Continue to upload form">
<input type="hidden" name="panda_video[account_token]" id="panda_video_account_token" value="<%= params[:account_token] %>">
</form>

Просмотреть файл

@ -0,0 +1,29 @@
<h2><%= @video.filename %></h2>
<dl>
<dt>ID:</dt>
<dd><%= @video.token %></dd>
<dt>Status:</dt>
<dd><%= @video.status %></dd>
<dt>Duration:</dt>
<dd><%= @video.duration_str %></dd>
</dl>
<% @video.encodings.each do |enc| %>
<h3><%= enc.quality.format.name %> (<%= enc.quality.quality %>)</h3>
<div class="video">
<% if enc.status == "success" %>
<%= enc.embed_html %>
<% elsif enc.status == "error" %>
<div class="response">There was an error encoding to this format. We have been notified and will look into the issue immediately.</div>
<% else %>
<div class="response">Once uploaded and encoded the video will be displayed here.</div>
<% end %>
</div>
<div class="embed_code">
<h4>Embed code</h4>
<textarea cols="50" rows="6"><%= enc.embed_html %></textarea>
</div>
<% end %>

Просмотреть файл

@ -0,0 +1,15 @@
Merb.logger.info("Loaded DEVELOPMENT Environment...")
Merb::Config.use { |c|
c[:exception_details] = true
c[:reload_classes] = true
c[:reload_time] = 0.5
c[:log_auto_flush ] = true
}
PANDA_HOME = File.join(Merb.root, '..', 'panda_ec2', 'panda')
PANDA_LOG_SERVER = "127.0.0.1"
PANDA_DOMAIN = "127.0.0.1"
PANDA_PORT = 4000
PANDA_UPLOAD_DOMAIN = "127.0.0.1"
PANDA_UPLOAD_PORT = 4001
PANDA_VIDEOS_DOMAIN = "videos.pandastream.com"

Просмотреть файл

@ -0,0 +1,15 @@
Merb.logger.info("Loaded PRODUCTION Environment...")
Merb::Config.use { |c|
c[:exception_details] = false
c[:reload_classes] = false
c[:log_level] = :error
c[:log_file] = Merb.log_path + "/production.log"
}
PANDA_HOME = "/mnt/panda"
PANDA_LOG_SERVER = "127.0.0.1"
PANDA_DOMAIN = "hq.pandastream.com"
PANDA_PORT = 80
PANDA_UPLOAD_DOMAIN = "upload.pandastream.com"
PANDA_UPLOAD_PORT = 80
PANDA_VIDEOS_DOMAIN = "videos.pandastream.com"

Просмотреть файл

@ -0,0 +1 @@
puts "Loaded TEST Environment..."

55
config/init.rb Normal file
Просмотреть файл

@ -0,0 +1,55 @@
# Make the app's "gems" directory a place where gems are loaded from
Gem.clear_paths
Gem.path.unshift(Merb.root / "gems")
# Make the app's "lib" directory a place where ruby files get "require"d from
$LOAD_PATH.unshift(Merb.root / "lib")
Merb::Config.use do |c|
### Sets up a custom session id key, if you want to piggyback sessions of other applications
### with the cookie session store. If not specified, defaults to '_session_id'.
# c[:session_id_key] = '_session_id'
c[:session_secret_key] = '4d5e9b90d9e92c236a2300d718059aef3a9b9cbe'
c[:session_store] = 'cookie'
end
use_orm :activerecord
dependencies 'merb_helpers', 'merb-mailer', 'uuid', 'to_simple_xml', 'rog'
# Not sure why dependencies won't load AWS::S3
require 'aws/s3'
Merb::BootLoader.after_app_loads do
# Panda specific
unless Merb.environment == "test"
require File.join(Merb.root, '..', 'aws_connect')
AWS::S3::Base.establish_connection!(
:access_key_id => ACCESS_KEY_ID,
:secret_access_key => SECRET_ACCESS_KEY
)
Rog.prefix = "HQ"
Rog.host = PANDA_LOG_SERVER
Rog.port = 3333
Rog.log :info, "Panda HQ app awake"
Merb::Mailer.config = {
:host=>'localhost',
:domain => 'pandastream.com',
:port=>'25'
# :user=>'',
# :pass=>'',
# :auth=>:plain # :plain, :login, :cram_md5, the default is no auth
}
end
end
EMAIL_SENDER = "Panda <info@pandastream.com>"

64
config/merb.yml Normal file
Просмотреть файл

@ -0,0 +1,64 @@
---
# Hostname or IP address to bind to.
:host: 0.0.0.0
# Port merb runs on or starting port for merb cluster.
:port: "4001"
# Set if your app will be hosted in some dir other than the root
#:path_prefix: "/my_app"
# In development mode your app's files are reloaded whenever Merb detects a
# change. Templates are parsed each time and not cached. In production mode
# templates are cached, as well as all your classes
:environment: development
# Uncomment if you have more than one ORM or if you need to be specific about
# which memory store to use. Built-in options are: memory, cookie, or mem_cache
:session_store: memory
#:memory_session_ttl: 3600 # one hour
# A secret key is required when using the 'cookie' session store (default),
# change this value to something unique to your application and keep it private
:session_secret_key: PANDA_MERB91626923692367236784223
# Uncomment to use the merb upload progress. The 'path match' will be treated as
# a regex for any URLs that should be considered for upload monitoring.
#:upload_path_match: /files/\d
#:upload_frequency: 3
# Uncomment to cache templates in dev mode. Templates are cached
# automatically in production mode.
#:cache_templates: true
# Uncomment and set this if you want to run a drb server for upload progress
# or other drb services.
#:drb_server_port: 32323
# If you want to protect some or all of your app with HTTP basic auth then
# uncomment the following and fill in your credentials you want it to use.
# You will then need to set a 'before' filter in a controller. For example:
# before :basic_authentication
#:basic_auth:
# :username: ezra
# :password: test
# :domain: localhost
# Uncomment this if you want merb to daemonize when you start it. You can also
# just use merb -d for the same effect. Don't uncomment this if you use the
# cluster option.
#:daemonize: true
# Uncomment this to set the number of members in your merb cluster. Don't set
# this and :daemonize: at the same time.
#:cluster: 3
# Uncomment this if you want to force merb to show full InternalServerError
# details, even when in production mode
:exception_details: true
# It is often useful to use a differant layout from 'application' for errors
# set this to the layout template (or :none) that you want to use by default
#:exception_layout: :none

1
config/rack.rb Normal file
Просмотреть файл

@ -0,0 +1 @@
run Merb::Rack::Application.new

49
config/router.rb Normal file
Просмотреть файл

@ -0,0 +1,49 @@
# Merb::Router is the request routing mapper for the merb framework.
#
# You can route a specific URL to a controller / action pair:
#
# r.match("/contact").
# to(:controller => "info", :action => "contact")
#
# You can define placeholder parts of the url with the :symbol notation. These
# placeholders will be available in the params hash of your controllers. For example:
#
# r.match("/books/:book_id/:action").
# to(:controller => "books")
#
# Or, use placeholders in the "to" results for more complicated routing, e.g.:
#
# r.match("/admin/:module/:controller/:action/:id").
# to(:controller => ":module/:controller")
#
# You can also use regular expressions, deferred routes, and many other options.
# See merb/specs/merb/router.rb for a fairly complete usage sample.
Merb.logger.info("Compiling routes...")
Merb::Router.prepare do |r|
# RESTful routes
r.resources :invites
# External API
r.resources :videos, {:member => {:valid => :get, :uploaded => :post}}
r.resource :accounts
r.match("/account").to(:controller => "accounts", :action => "index")
r.match("/signup").to(:controller => "accounts", :action => "new")
r.match("/login").to(:controller => "auth", :action => "login")
r.match("/logout").to(:controller => "auth", :action => "logout")
r.match("/docs").to(:controller => "docs", :action => "index")
# Internal API
r.resources :uploads
r.resources :jobs, {:member => {:done => :post}, :collection => {:next => :get}}
# This is the default route for /:controller/:action/:id
# This is fine for most cases. If you're heavily using resource-based
# routes, you may want to comment/remove this line to prevent
# clients from calling your create or destroy actions with a GET
# r.default_routes
r.match("/").to(:controller => "accounts", :action => "dashboard")
end

36
lib/rog.rb Normal file
Просмотреть файл

@ -0,0 +1,36 @@
# == Synopsis
#
# Simple remote debug class
#
# == Author
# Stefan Saasen s@juretta.com
#
# == Copyright
# Copyright (c) 2005 juretta.com Stefan Saasen
# Licensed under the same terms as Ruby.
# == Version
# Version 0.1 ($Id: logger.rb 5 2006-01-01 12:51:04Z stefan $)
require 'socket'
require 'singleton'
require 'timeout'
class Rog
include Singleton
cattr_writer :port, :host, :prefix
attr :session
def self.log(level, msg)
begin
Timeout::timeout(1) do
@session = TCPSocket.new(@@host, @@port)
@session.puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + \
" " + "[" + level.to_s.upcase + "] #{@@prefix}: " + msg + "\n"
@session.close
end
rescue => e
return false
end
true
end
end

23
lib/to_simple_xml.rb Normal file
Просмотреть файл

@ -0,0 +1,23 @@
class Hash
def to_simple_xml
hash_to_xml_string(self)
end
private
def hash_to_xml_string(h)
s = ""
h.each do |k,v|
s += "<#{k}>"
if v.class == Hash
s += hash_to_xml_string(v)
elsif v.class == Array
v.each {|i| s += hash_to_xml_string(i) }
else
s += v.to_s
end
s += "</#{k}>"
end
return s
end
end

5
public/crossdomain.xml Normal file
Просмотреть файл

@ -0,0 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" />
</cross-domain-policy>

Двоичные данные
public/flvplayer.swf Executable file

Двоичный файл не отображается.

Двоичные данные
public/images/merb.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 39 KiB

Двоичные данные
public/images/panda_logo.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Просмотреть файл

11
public/javascripts/jquery.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,54 @@
interval = null;
function openProgressBar() {
/* generate random progress-id */
uuid = "";
for (i = 0; i < 32; i++) {
uuid += Math.floor(Math.random() * 16).toString(16);
}
/* patch the form-action tag to include the progress-id */
document.getElementById("upload").action="/uploader/upload?X-Progress-ID=" + uuid;
/* call the progress-updater every 1000ms */
interval = window.setInterval(
function () {
fetch(uuid);
},
1000
);
// show the progress bar
$('#uploader').hide();
$('#uploading').show();
}
function fetch(uuid) {
req = new XMLHttpRequest();
req.open("GET", "/progress", 1);
req.setRequestHeader("X-Progress-ID", uuid);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
/* poor-man JSON parser */
var upload = eval(req.responseText);
document.getElementById('tp').innerHTML = upload.state;
/* change the width if the inner progress-bar */
if (upload.state == 'done' || upload.state == 'uploading') {
bar = document.getElementById('progressbar');
w = 300 * upload.received / upload.size;
bar.style.width = w + 'px';
$('#progressbar').show();
}
/* we are done, stop the interval */
if (upload.state == 'done') {
window.clearTimeout(interval);
$('#uploading').hide();
$('#weredone').show();
}
}
}
}
req.send(null);
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

6
public/merb.fcgi Normal file
Просмотреть файл

@ -0,0 +1,6 @@
#!/usr/bin/env ruby
ARGV=["-F"]
require 'merb/server'
Merb::Server.run

Просмотреть файл

@ -0,0 +1,241 @@
/* hax */
.clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix {
zoom: 1;
}
body {
font-family: "Lucida Grande", Arial, Verdana, sans-serif;
font-size: 12px;
color: #333;
background-color: #fff;
}
* {
margin: 0px;
padding: 0px;
text-decoration: none;
}
img {
border: none;
}
a, a:visited {
color: #86b514;
}
h2 {
font-size: 1.5em;
font-weight: normal;
color: #444;
margin-top: 1em;
}
h3 {
clear: left;
margin-top: 1em;
font-size: 1.2em;
}
h4 {
margin-top: 0.5em;
font-size: 1.1em;
}
dt,dd {
margin-bottom: 0.2em;
}
dt {
font-weight: bold;
float: left;
width: 5.5em;
padding-right: 0.5em;
text-align: right;
}
/* LAYOUT */
#container {
margin: auto;
width: 800px;
}
#main h2, #main h3 {
margin-bottom: 0.3em;
}
/* FORMS */
label {
display: block;
margin-top: 7px;
margin-bottom: 3px;
}
input {
font-size: 1.1em;
padding: 2px;
}
button {
padding: 2px 3px;
margin-top: 10px;
font-size: 1.1em;
color: #fff;
background: #95c229;
border: 1px solid #76a500;
}
/* FLASH NOTICES */
.notice {
border: 1px solid #dedede;
background: #eee;
padding: 5px;
}
/* HEADER */
#account {
float: right;
text-align: right;
margin-top: 5px;
}
#account h2 {
font-size: 1.2em;
font-weight: bold;
}
#header {
border-bottom: 1px solid #dedede;
margin-bottom: 10px;
}
#nav {
list-style-type: none;
}
#nav li {
float: left;
}
#nav a {
color: #999;
display: block;
padding: 4px 7px;
}
#nav a:hover {
color: #444;
background: #efefef;
}
/* DASHBOARD */
#new_video {
border: 1px solid #dedede;
background: #f9f9f9;
float: right;
padding: 5px;
width: 11em;
text-align: center;
margin-top: 23px;
}
#new_video h2 {
margin: 0px;
font-size: 1.1em;
}
.more_videos {
margin-top: 7px;
font-weight: bold;
font-size: 0.9em;
}
/* VIDEOS LIST */
#videos {
width: 100%;
border-collapse: collapse;
}
.dashboard #videos, .more_videos {
width: 630px;
}
#videos th, #videos td {
padding: 3px 4px;
}
#videos th {
background: #eee;
color: #888;
font-size: 0.9em;
text-align: left;
}
#videos tr {
border-bottom: 1px solid #dedede;
}
#videos .filename {
font-weight: bold;
display: block;
}
#videos .token {
font-size: 0.8em;
color: #555;
}
#videos .error, #videos .encoding_error {
font-weight: bold;
color: #f90203;
}
#videos .done {
font-weight: bold;
color: #86b514;
}
/* SHOW VIDEOS */
.video {
float: left;
margin-top: 10px;
margin-bottom: 10px;
padding: 5px;
background: #eee;
}
.video .response {
width: 320px;
height: 160px;
padding: 5px;
padding-top: 80px;
text-align: center;
font-size: 0.9em;
}
.embed_code {
clear: left;
}
/* ACCOUNT */
.account_details h3 {
font-size: 1em;
margin-top: 10px;
margin-bottom: 1px;
}
.account_details .note {
font-size: 0.9em;
margin-bottom: 7px;
}
.account_details .value {
display: inline;
background: #f9f9f9;
border: 1px solid #ddd;
padding: 2px;
}
.edit_details {
font-weight: bold;
border-top: 1px solid #dedede;
margin-top: 15px;
padding-top: 5px;
}

Просмотреть файл

@ -0,0 +1,119 @@
body {
font-family: Arial, Verdana, sans-serif;
font-size: 12px;
background-color: #fff;
}
* {
margin: 0px;
padding: 0px;
text-decoration: none;
}
html {
height: 100%;
margin-bottom: 1px;
}
#container {
width: 80%;
text-align: left;
background-color: #fff;
margin-right: auto;
margin-left: auto;
}
#header-container {
width: 100%;
padding-top: 15px;
}
#header-container h1, #header-container h2 {
margin-left: 6px;
margin-bottom: 6px;
}
.spacer {
width: 100%;
height: 15px;
}
hr {
border: 0px;
color: #ccc;
background-color: #cdcdcd;
height: 1px;
width: 100%;
text-align: left;
}
h1 {
font-size: 28px;
color: #c55;
background-color: #fff;
font-family: Arial, Verdana, sans-serif;
font-weight: 300;
}
h2 {
font-size: 15px;
color: #999;
font-family: Arial, Verdana, sans-serif;
font-weight: 300;
background-color: #fff;
}
h3 {
color: #4d9b12;
font-size: 15px;
text-align: left;
font-weight: 300;
padding: 5px;
margin-top: 5px;
}
#left-container {
float: left;
width: 250px;
background-color: #FFFFFF;
color: black;
}
#left-container h3 {
color: #c55;
}
#main-container {
margin: 5px 5px 5px 260px;
padding: 15px;
border-left: 1px solid silver;
min-height: 400px;
}
p {
color: #000;
background-color: #fff;
line-height: 20px;
padding: 5px;
}
a {
color: #4d9b12;
background-color: #fff;
text-decoration: none;
}
a:hover {
color: #4d9b12;
background-color: #fff;
text-decoration: underline;
}
#footer-container {
clear: both;
font-size: 12px;
font-family: Verdana, Arial, sans-serif;
}
.right {
float: right;
font-size: 100%;
margin-top: 5px;
color: #999;
background-color: #fff;
}
.left {
float: left;
font-size: 100%;
margin-top: 5px;
color: #999;
background-color: #fff;
}
#main-container ul {
margin-left: 3.0em;
}

Просмотреть файл

@ -0,0 +1,22 @@
class AddModelAccounts < ActiveRecord::Migration
def self.up
create_table :accounts do |t|
t.column :name, :string
t.column :login, :string
t.column :email, :string
t.column :crypted_password, :string, :limit => 40
t.column :salt, :string, :limit => 40
t.column :remember_token, :string
t.column :remember_token_expires_at, :datetime
t.column :token, :string
t.column :upload_redirect_url, :string
t.column :state_update_url, :string
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :accounts
end
end

Просмотреть файл

@ -0,0 +1,48 @@
class AddModelVideos < ActiveRecord::Migration
def self.up
create_table :videos do |t|
t.column :account_id, :integer
t.column :token, :string
t.column :filename, :string
t.column :resolution, :string
t.column :duration, :integer
t.column :container, :string
t.column :fps, :string
t.column :video_codec, :string
t.column :video_bitrate, :integer
t.column :audio_codec, :string
t.column :audio_sample_rate, :integer
t.column :status, :string # NULL or 'uploaded'
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
create_table :encodings do |t|
t.column :video_id, :integer
t.column :format_id, :integer
t.column :duration, :integer # For free accounts we might restrict the duration of encodings
# Copied from Format for safe keeping
t.column :resolution, :string
t.column :container, :string
t.column :fps, :string
t.column :video_codec, :string
t.column :video_bitrate, :integer
t.column :audio_codec, :string
t.column :audio_sample_rate, :integer
t.column :status, :string # 'encoding', 'error' or 'done'
t.column :encoding_time, :integer # Time it took to encode the video in seconds
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :videos
drop_table :encodings
end
end

Просмотреть файл

@ -0,0 +1,23 @@
class AddModelFormats < ActiveRecord::Migration
def self.up
create_table :formats do |t|
t.column :name, :string
t.column :quality, :string # low, med, hi, hd
t.column :resolution, :string
t.column :container, :string # flv, mp4, mov
t.column :fps, :string
t.column :video_codec, :string
t.column :video_bitrate, :integer
t.column :audio_codec, :string
t.column :audio_sample_rate, :integer
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :formats
end
end

Просмотреть файл

@ -0,0 +1,32 @@
class FormatChanges < ActiveRecord::Migration
def self.up
remove_column :formats, :name
remove_column :formats, :resolution
add_column :formats, :width, :integer
add_column :formats, :height, :integer
add_column :formats, :position, :integer
add_column :formats, :format_id, :integer
rename_table :formats, :qualities
remove_column :videos, :resolution
add_column :videos, :width, :integer
add_column :videos, :height, :integer
remove_column :encodings, :resolution
add_column :encodings, :width, :integer
add_column :encodings, :height, :integer
create_table :formats do |t|
t.column :name, :string
t.column :format, :string
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
rename_column :encodings, :format_id, :quality_id
end
def self.down
end
end

Просмотреть файл

@ -0,0 +1,8 @@
class AddEncodingToken < ActiveRecord::Migration
def self.up
add_column :encodings, :token, :string
end
def self.down
end
end

Просмотреть файл

@ -0,0 +1,8 @@
class RenameFormats < ActiveRecord::Migration
def self.up
rename_column :formats, :format, :code
end
def self.down
end
end

Просмотреть файл

@ -0,0 +1,18 @@
class AddModelJobs < ActiveRecord::Migration
def self.up
create_table :jobs do |t|
t.column :video_id, :integer
t.column :ec2_id, :integer
t.column :status, :string
t.column :result, :text
t.column :force, :boolean # Force re-encoding of already encoded files
t.column :encoding_time, :integer
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :jobs
end
end

Просмотреть файл

@ -0,0 +1,16 @@
class AddModelNotifications < ActiveRecord::Migration
def self.up
create_table :notifications do |t|
t.column :encoding_id, :integer
t.column :tries, :integer
t.column :response, :string
t.column :state, :string # success, error (contacting client)
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :jobs
end
end

Просмотреть файл

@ -0,0 +1,17 @@
class AddModelEc2s < ActiveRecord::Migration
def self.up
create_table :ec2s do |t|
t.column :amazon_id, :string
t.column :address, :string
t.column :instance_type, :string
t.column :started_at, :datetime
t.column :shutdown_at, :datetime
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :jobs
end
end

Просмотреть файл

@ -0,0 +1,10 @@
class ChangeQualities < ActiveRecord::Migration
def self.up
remove_column :qualities, :video_codec
remove_column :qualities, :audio_codec
rename_column :qualities, :audio_sample_rate, :audio_bitrate
end
def self.down
end
end

Просмотреть файл

@ -0,0 +1,10 @@
class ChangeEncodings < ActiveRecord::Migration
def self.up
remove_column :encodings, :video_codec
remove_column :encodings, :audio_codec
rename_column :encodings, :audio_sample_rate, :audio_bitrate
end
def self.down
end
end

Просмотреть файл

@ -0,0 +1,15 @@
class AddModelInvites < ActiveRecord::Migration
def self.up
create_table :invites do |t|
t.column :email, :string
t.column :approved, :datetime
t.column :account_id, :integer
t.column :updated_at, :datetime
t.column :created_at, :datetime
end
end
def self.down
drop_table :invites
end
end

28
script/destroy Executable file
Просмотреть файл

@ -0,0 +1,28 @@
#!/usr/bin/env ruby
APP_ROOT = File.join(File.dirname(__FILE__), '..')
begin
require 'rubigen'
rescue LoadError
require 'rubygems'
require 'rubigen'
end
require File.join(File.dirname(__FILE__), "..", 'config', 'boot')
require (APP_ROOT / "config" / "merb_init" )
module Kernel
undef dependency if defined?(Kernel.dependency)
end
# Make the App's local gems available
Gem.clear_paths
Gem.path.unshift(APP_ROOT / "gems")
require 'rubigen/scripts/destroy'
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
# Default is to use rspec generators in gems. To change this to
# Test::Unit use [:merb, :test_unit]
RubiGen::Base.use_component_sources! Merb::GENERATOR_SCOPE
RubiGen::Scripts::Destroy.new.run(ARGV)

28
script/generate Executable file
Просмотреть файл

@ -0,0 +1,28 @@
#!/usr/bin/env ruby
APP_ROOT = File.join(File.dirname(__FILE__), '..')
begin
require 'rubigen'
rescue LoadError
require 'rubygems'
require 'rubigen'
end
require File.join(File.dirname(__FILE__), "..", 'config', 'boot')
require (APP_ROOT / "config" / "merb_init" )
module Kernel
undef dependency if defined?(Kernel.dependency)
end
# Make the App's local gems available
Gem.clear_paths
Gem.path.unshift(APP_ROOT / "gems")
require 'rubigen/scripts/generate'
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
# Default is to use rspec generators in gems. To change this to
# Test::Unit use [:merb, :test_unit]
RubiGen::Base.use_component_sources! Merb::GENERATOR_SCOPE
RubiGen::Scripts::Generate.new.run(ARGV)

13
script/stop_merb Executable file
Просмотреть файл

@ -0,0 +1,13 @@
#!/usr/bin/env ruby
require 'fileutils'
pids=[]
port_or_star = ARGV[0] || '*'
Dir[File.dirname(__FILE__)+"/../log/merb.#{port_or_star}.pid"].each do |f|
pid = IO.read(f).chomp.to_i
puts "killing PID: #{pid}"
Process.kill(9, pid)
FileUtils.rm f
end

Просмотреть файл

@ -0,0 +1,103 @@
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
# describe "Videos Controller", "index action" do
# before(:each) do
# @controller = Videos.build(fake_request)
# @controller.dispatch('index')
# end
#
# it "should return video details in yaml" do
#
# end
# end
#
# describe Videos, "show action" do
# before(:each) do
# # @controller = Videos.build(fake_request)
# # @controller[:params][:id] = "123"
# # @controller.dispatch('show')
# end
#
# it "should return video details in yaml" do
# # controller.stub!(:render)
# # puts body.to_yaml
# # puts status
# # puts headers
# # puts controller.instance_variables
# video = Video.create
# Video.should_receive(:find_by_token).with(video.token)
# get("/videos/#{video.token}.yaml")
# # puts controller.inspect
# # puts response.inspect
# # status.should == 404
# controller.should be_success
# # puts @controller.methods.sort
# end
# end
describe Videos, "valid action" do
before(:each) do
@video = mock(Video)
@video.stub!(:token).and_return("123")
end
it "should return 200 if video is empty" do
@video.should_receive(:empty?).and_return(true)
Video.should_receive(:find_by_token).with("123").and_return(@video)
get("/videos/123/valid.yaml")
status.should == 200
end
it "should return 404 if video is not empty" do
@video.should_receive(:empty?).and_return(false)
Video.should_receive(:find_by_token).with("123").and_return(@video)
get("/videos/123/valid.yaml")
status.should == 404
end
end
describe Videos, "process action" do
before(:each) do
@video = mock(Video)
@video.stub!(:token).and_return("123")
@filename = "vid.avi"
@raw_filename = "raw.avi"
end
def setup_video
@video.should_receive(:filename=).with("vid.avi")
@video.should_receive(:empty?).and_return(true)
@video.should_receive(:save_metadata).with({:metadata => :here})
@video.should_receive(:save)
@video.should_receive(:add_encodings)
@video.should_receive(:add_to_queue).and_return(OpenStruct.new(:id => 999))
end
it "should return 200, add video to queue and set location header" do
setup_video
Video.should_receive(:find_by_token).with("123").and_return(@video)
@video.stub!(:account).and_return(OpenStruct.new(:upload_redirect_url => "http://mysite.com/videos/done"))
post("/videos/123/uploaded.yaml", {:filename => "vid.avi", :metadata => {:metadata => :here}.to_yaml})
status.should == 200
headers['Location'].should == "http://mysite.com/videos/done"
end
it "should return 200, add video to queue but not set location header if account.upload_redirect_url is blank" do
setup_video
Video.should_receive(:find_by_token).with("123").and_return(@video)
@video.stub!(:account).and_return(OpenStruct.new(:upload_redirect_url => ""))
post("/videos/123/uploaded.yaml", {:filename => "vid.avi", :metadata => {:metadata => :here}.to_yaml})
status.should == 200
headers['Location'].should_not == "http://mysite.com/videos/done"
end
it "should return 404 if video is not empty" do
@video.should_receive(:empty?).and_return(false)
Video.should_receive(:find_by_token).with("123").and_return(@video)
post("/videos/123/uploaded.yaml")
status.should == 404
end
end

Просмотреть файл

@ -0,0 +1,7 @@
require File.join( File.dirname(__FILE__), "..", "spec_helper" )
describe Account do
it "should have specs"
end

Просмотреть файл

@ -0,0 +1,7 @@
require File.join( File.dirname(__FILE__), "..", "spec_helper" )
describe Format do
it "should have specs"
end

10
spec/models/job_spec.rb Normal file
Просмотреть файл

@ -0,0 +1,10 @@
require File.join( File.dirname(__FILE__), "..", "spec_helper" )
describe Job do
it "should should be next in queue if latest job" do
video = Video.create
job = video.add_to_queue
Job.find_next_job.should == job
end
end

49
spec/models/video_spec.rb Normal file
Просмотреть файл

@ -0,0 +1,49 @@
require File.join( File.dirname(__FILE__), "..", "spec_helper" )
describe Video do
before :each do
@video = Video.new
end
it "should have token after create" do
@video.save
@video.token.should_not be_nil
end
it "should have default status of empty after create" do
@video.save
@video.status.should == "empty"
end
it "should return 00:00 duration string for nil duration" do
@video.duration_str.should == "00:00"
end
it "should return correct duration string" do
@video = Video.new
@video.duration = 5586000
@video.duration_str.should == "93:06"
end
it "should be added to the job queue" do
@video.save
@video.should_receive(:send_status)
job = @video.add_to_queue
job.status.should == "queued"
job.video_id.should == @video.id
end
it "should add encoding" do
f = Format.create(:name => "Flash video", :code => "flv")
quality = Quality.create(:format_id => f.id, :quality => "sd", :container => "flv", :width => 320, :height => 240, :position => 0)
# Video only added if width is at least that of the quality
@video.width = 320
@video.save
@video.add_encoding_for_quality(quality)
enc = @video.encodings.first
enc.quality_id.should == quality.id
enc.status.should == "queued"
end
end

15
spec/spec_helper.rb Normal file
Просмотреть файл

@ -0,0 +1,15 @@
$TESTING=true
require File.join(File.dirname(__FILE__), "..", 'config', 'boot')
Merb.environment="test"
require File.join(Merb.root, 'config', 'merb_init')
require 'merb/test/helper'
require 'merb/test/rspec'
Spec::Runner.configure do |config|
config.include(Merb::Test::Helper)
config.include(Merb::Test::RspecMatchers)
end
### METHODS BELOW THIS LINE SHOULD BE EXTRACTED TO MERB ITSELF