parent
fd4e526ca2
commit
498660c015
@ -0,0 +1,24 @@
|
||||
class Callbacks::AliyunVodsController < Callbacks::BaseController
|
||||
before_action :check_signature_valid!
|
||||
|
||||
def create
|
||||
Videos::DispatchCallbackService.call(params)
|
||||
render_ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_signature_valid!
|
||||
return if AliyunVod::Sign.verify?(header_signature, header_timestamp)
|
||||
|
||||
render_not_acceptable
|
||||
end
|
||||
|
||||
def header_timestamp
|
||||
request.headers['X-VOD-TIMESTAMP']
|
||||
end
|
||||
|
||||
def header_signature
|
||||
request.headers['X-VOD-SIGNATURE']
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class Callbacks::BaseController < ActionController::Base
|
||||
include RenderHelper
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
end
|
@ -0,0 +1,26 @@
|
||||
class Users::VideoAuthsController < Users::BaseController
|
||||
before_action :private_user_resources!
|
||||
|
||||
def create
|
||||
result = Videos::CreateAuthService.call(observed_user, create_params)
|
||||
render_ok(data: result)
|
||||
rescue Videos::CreateAuthService::Error => ex
|
||||
render_error(ex.message)
|
||||
end
|
||||
|
||||
def update
|
||||
video = observed_user.videos.find_by(uuid: params[:video_id])
|
||||
return render_error('该视频凭证不存在') if video.blank?
|
||||
|
||||
result = AliyunVod::Service.refresh_upload_video(video.uuid)
|
||||
render_ok(data: result)
|
||||
rescue AliyunVod::Error => _
|
||||
render_error('刷新上传凭证失败')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_params
|
||||
params.permit(:title, :file_name, :file_size, :description, :cover_url)
|
||||
end
|
||||
end
|
@ -0,0 +1,58 @@
|
||||
class Users::VideosController < Users::BaseController
|
||||
before_action :private_user_resources!
|
||||
|
||||
helper_method :current_video
|
||||
|
||||
def index
|
||||
videos = Users::VideoQuery.call(observed_user, search_params)
|
||||
|
||||
@count = videos.count
|
||||
@videos = paginate videos
|
||||
end
|
||||
|
||||
def update
|
||||
return render_error('该状态下不能编辑视频信息') unless current_video.published?
|
||||
|
||||
current_video.update!(title: params[:title])
|
||||
|
||||
AliyunVod::Service.update_video_info(current_video.uuid, Title: current_video.title) rescue nil
|
||||
end
|
||||
|
||||
def cancel
|
||||
video = observed_user.videos.find_by(uuid: params[:video_id])
|
||||
return render_not_found if video.blank?
|
||||
return render_error('该状态下不能删除视频') unless video.pending?
|
||||
|
||||
video.destroy!
|
||||
AliyunVod::Service.delete_video([video.uuid]) rescue nil
|
||||
|
||||
render_ok
|
||||
end
|
||||
|
||||
def review
|
||||
params[:status] = 'processing'
|
||||
videos = Users::VideoQuery.call(observed_user, params)
|
||||
|
||||
@count = videos.count
|
||||
@videos = paginate videos
|
||||
end
|
||||
|
||||
def batch_publish
|
||||
Videos::BatchPublishService.call(observed_user, batch_publish_params)
|
||||
render_ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_video
|
||||
@_current_video ||= observed_user.videos.find_by(id: params[:id])
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.permit(:keyword, :sort_by, :sort_direction)
|
||||
end
|
||||
|
||||
def batch_publish_params
|
||||
params.permit(videos: [])
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
module VideoDecorator
|
||||
extend ApplicationDecorator
|
||||
|
||||
display_time_method :published_at, :created_at, :updated_at
|
||||
end
|
@ -0,0 +1,23 @@
|
||||
# 批量发布视频 消息任务
|
||||
class BatchPublishVideoNotifyJob < ApplicationJob
|
||||
queue_as :notify
|
||||
|
||||
def perform(user_id, video_ids)
|
||||
user = User.find_by(id: user_id)
|
||||
return if user.blank?
|
||||
|
||||
attrs = %i[user_id trigger_user_id container_id container_type tiding_type status created_at updated_at]
|
||||
|
||||
same_attrs = {
|
||||
user_id: 1,
|
||||
trigger_user_id: user.id,
|
||||
container_type: 'Video',
|
||||
tiding_type: 'Apply', status: 0
|
||||
}
|
||||
Tiding.bulk_insert(*attrs) do |worker|
|
||||
user.videos.where(id: video_ids).each do |video|
|
||||
worker.add same_attrs.merge(container_id: video.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# 获取阿里云视频信息
|
||||
class GetAliyunVideoInfoJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(vod_video_id)
|
||||
video = Video.find_by(uuid: vod_video_id)
|
||||
return if video.blank? || video.vod_uploading?
|
||||
|
||||
result = AliyunVod::Service.get_play_info(video.uuid)
|
||||
cover_url = result.dig('VideoBase', 'CoverURL')
|
||||
file_url = (result.dig('PlayInfoList', 'PlayInfo') || []).first&.[]('PlayURL')
|
||||
|
||||
video.cover_url = cover_url if cover_url.present? && video.cover_url.blank?
|
||||
video.file_url = file_url if file_url.present?
|
||||
video.save!
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
module AliyunVod
|
||||
class << self
|
||||
attr_accessor :access_key_id, :access_key_secret, :base_url, :callback_url, :signature_key
|
||||
end
|
||||
end
|
@ -0,0 +1,2 @@
|
||||
class AliyunVod::Error < StandardError
|
||||
end
|
@ -0,0 +1,8 @@
|
||||
module AliyunVod::Service
|
||||
extend AliyunVod::Service::Base
|
||||
|
||||
extend AliyunVod::Service::VideoUpload
|
||||
extend AliyunVod::Service::VideoProcess
|
||||
extend AliyunVod::Service::VideoManage
|
||||
extend AliyunVod::Service::VideoPlay
|
||||
end
|
@ -0,0 +1,40 @@
|
||||
# 视频管理
|
||||
module AliyunVod::Service::VideoManage
|
||||
# 修改视频信息
|
||||
def update_video_info(video_id, **opts)
|
||||
params = {
|
||||
Action: 'UpdateVideoInfo',
|
||||
VideoId: video_id
|
||||
}.merge(base_params)
|
||||
|
||||
params = opts.merge(params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# 获取视频信息
|
||||
def get_video_info(video_id)
|
||||
params = {
|
||||
Action: 'GetVideoInfo',
|
||||
VideoId: video_id
|
||||
}.merge(base_params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# 删除视频信息
|
||||
def delete_video(video_ids)
|
||||
params = {
|
||||
Action: 'DeleteVideo',
|
||||
VideoIds: video_ids.join(',')
|
||||
}.merge(base_params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# 视频播放
|
||||
module AliyunVod::Service::VideoPlay
|
||||
# 获取视频播放地址
|
||||
# https://help.aliyun.com/document_detail/56124.html?spm=a2c4g.11186623.6.715.4d7e2d52dU1CTK
|
||||
def get_play_info(video_id, **opts)
|
||||
params = {
|
||||
Action: 'GetPlayInfo',
|
||||
VideoId: video_id
|
||||
}.merge(base_params)
|
||||
|
||||
params = opts.merge(params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# 视频处理
|
||||
module AliyunVod::Service::VideoProcess
|
||||
# 提交媒体截图作业
|
||||
def submit_snapshot_job(video_id, **opts)
|
||||
params = {
|
||||
Action: 'SubmitSnapshotJob',
|
||||
VideoId: video_id
|
||||
}.merge(base_params)
|
||||
params = opts.merge(params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
raise AliyunVod::Error, '提交媒体截图作业失败' if result['SnapshotJob'].blank?
|
||||
|
||||
result
|
||||
end
|
||||
end
|
@ -0,0 +1,33 @@
|
||||
# 视频上传
|
||||
module AliyunVod::Service::VideoUpload
|
||||
# 获取视频上传地址和凭证
|
||||
def create_upload_video(title, filename, **opts)
|
||||
params = {
|
||||
Action: 'CreateUploadVideo',
|
||||
Title: title,
|
||||
FileName: filename
|
||||
}.merge(base_params)
|
||||
|
||||
params = opts.merge(params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
raise AliyunVod::Error, '获取上传凭证失败' if result['UploadAddress'].blank?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# 刷新视频上传凭证
|
||||
def refresh_upload_video(video_id)
|
||||
params = {
|
||||
Action: 'RefreshUploadVideo',
|
||||
VideoId: video_id
|
||||
}.merge(base_params)
|
||||
|
||||
result = request(:post, params)
|
||||
|
||||
raise AliyunVod::Error, '刷新上传凭证失败' if result['UploadAddress'].blank?
|
||||
|
||||
result
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
module AliyunVod::Sign
|
||||
# https://help.aliyun.com/document_detail/44434.html?spm=a2c4g.11186623.2.16.354c7853oqlhMb&/#SignatureNonce
|
||||
def self.generate(params, **opts)
|
||||
method = opts[:method] || 'POST'
|
||||
key = opts[:key] || AliyunVod.access_key_secret + '&'
|
||||
digest = OpenSSL::Digest.new('sha1')
|
||||
|
||||
str = params_to_string(params)
|
||||
str = percent_encode(str)
|
||||
str = "#{method}&%2F&#{str}"
|
||||
|
||||
Base64.encode64(OpenSSL::HMAC.digest(digest, key, str)).gsub(/\n/, '')
|
||||
end
|
||||
|
||||
def self.verify?(signature, timestamp)
|
||||
content = "#{AliyunVod.callback_url}|#{timestamp}|#{AliyunVod.signature_key}"
|
||||
our_signature = Digest::MD5.hexdigest(content)
|
||||
ActiveSupport::SecurityUtils.secure_compare(signature, our_signature)
|
||||
end
|
||||
|
||||
def self.params_to_string(params)
|
||||
params.sort.map { |k, v| "#{percent_encode(k)}=#{percent_encode(v)}" }.join('&')
|
||||
end
|
||||
|
||||
def self.percent_encode(str)
|
||||
return '' if str.blank?
|
||||
CGI::escape(str.to_s).gsub('/\+/','%20').gsub('/\*/','%2A').gsub('/%7E/','~')
|
||||
end
|
||||
|
||||
def self.format_params(params)
|
||||
params.each_with_object({}) do |arr, obj|
|
||||
obj[arr[0]] = arr[1].is_a?(Hash) ? parse_hash_to_str(arr[1]) : arr[1]
|
||||
end
|
||||
end
|
||||
|
||||
def self.parse_hash_to_str(hash)
|
||||
hash.each_with_object({}) do |h, obj|
|
||||
obj[h[0]] = h[1].is_a?(Hash) ? parse_hash_to_str(h[1].clone) : h[1].to_s
|
||||
end.to_json
|
||||
end
|
||||
end
|
@ -0,0 +1,36 @@
|
||||
class Video < ApplicationRecord
|
||||
include AASM
|
||||
|
||||
belongs_to :user
|
||||
|
||||
has_many :video_applies, dependent: :destroy
|
||||
has_one :processing_video_apply, -> { where(status: :pending) }, class_name: 'VideoApply'
|
||||
|
||||
aasm(:status) do
|
||||
state :pending, initial: true
|
||||
state :processing
|
||||
state :refused
|
||||
state :published
|
||||
|
||||
event :apply_publish do
|
||||
transitions from: :pending, to: :processing
|
||||
end
|
||||
|
||||
event :refuse do
|
||||
transitions from: :processing, to: :refused
|
||||
end
|
||||
|
||||
event :publish do
|
||||
transitions from: :processing, to: :published, guard: :vod_uploaded?
|
||||
end
|
||||
end
|
||||
|
||||
aasm(:vod_status, namespace: :vod) do
|
||||
state :uploading, initial: true
|
||||
state :uploaded
|
||||
|
||||
event :upload_success do
|
||||
transitions from: :uploading, to: :uploaded
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,19 @@
|
||||
class VideoApply < ApplicationRecord
|
||||
include AASM
|
||||
|
||||
belongs_to :video
|
||||
|
||||
aasm(:status) do
|
||||
state :pending, initial: true
|
||||
state :refused
|
||||
state :agreed
|
||||
|
||||
event :refuse do
|
||||
transitions from: :pending, to: :refused
|
||||
end
|
||||
|
||||
event :agree do
|
||||
transitions from: :pending, to: :agreed
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,3 @@
|
||||
class ApplicationQuery
|
||||
include Callable
|
||||
end
|
@ -0,0 +1,28 @@
|
||||
class Users::VideoQuery < ApplicationQuery
|
||||
include CustomSortable
|
||||
|
||||
sort_columns :published_at, :title, default_by: :published_at, default_direction: :desc
|
||||
|
||||
attr_reader :user, :params
|
||||
|
||||
def initialize(user, params)
|
||||
@user = user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
videos = user.videos.published
|
||||
|
||||
videos =
|
||||
case params[:status]
|
||||
when 'published' then videos.published
|
||||
when 'processing' then videos.processing
|
||||
else videos.published
|
||||
end
|
||||
|
||||
keyword = params[:keyword].to_s.strip
|
||||
videos = videos.where('title LIKE ?', "%#{keyword}%") if keyword.present?
|
||||
|
||||
custom_sort(videos, params[:sort_by], params[:sort_direction])
|
||||
end
|
||||
end
|
@ -0,0 +1,35 @@
|
||||
class Videos::AgreeApplyService < ApplicationService
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
attr_reader :video_apply, :video, :user
|
||||
|
||||
def initialize(video_apply, user)
|
||||
@video_apply = video_apply
|
||||
@video = video_apply.video
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
raise Error, '该状态下不能进行此操作' unless video_apply.may_agree? && video.may_publish?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
video_apply.agree!
|
||||
|
||||
video.published_at = Time.now
|
||||
video.publish
|
||||
video.save!
|
||||
|
||||
# 将消息改为已处理
|
||||
Tiding.where(container_id: video.id, container_type: 'Video', tiding_type: 'Apply', status: 0).update_all(status: 1)
|
||||
notify_video_author!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_video_author!
|
||||
Tiding.create!(user_id: video.user_id, trigger_user_id: 1,
|
||||
container_id: video.id, container_type: 'Video',
|
||||
tiding_type: 'System', status: 1)
|
||||
end
|
||||
end
|
@ -0,0 +1,31 @@
|
||||
class Videos::BatchPublishService < ApplicationService
|
||||
attr_reader :user, :params
|
||||
|
||||
def initialize(user, params)
|
||||
@user = user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
video_params = Array.wrap(params[:videos]).compact
|
||||
return if video_params.blank?
|
||||
|
||||
video_ids = []
|
||||
ActiveRecord::Base.transaction do
|
||||
video_params.each do |param|
|
||||
video = user.videos.find_by(uuid: param[:video_id])
|
||||
next if video.blank? || video.processing_video_apply.present?
|
||||
|
||||
video.title = param[:title].to_s.strip.presence || video.title
|
||||
video.apply_publish
|
||||
video.save!
|
||||
|
||||
video.video_applies.create!
|
||||
|
||||
video_ids << video.id
|
||||
end
|
||||
end
|
||||
|
||||
BatchPublishVideoNotifyJob.perform_later(user.id, video_ids) if video_ids.present?
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
class Videos::CreateAuthService < ApplicationService
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
attr_reader :user, :params
|
||||
|
||||
def initialize(user, params)
|
||||
@user = user
|
||||
@params = params.clone
|
||||
end
|
||||
|
||||
def call
|
||||
validate!
|
||||
|
||||
result = upload_video_result
|
||||
|
||||
Video.create!(user: user, uuid: result['VideoId'], title: title, cover_url: params[:cover_url])
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def title
|
||||
@_title ||= params.delete(:title).to_s.strip
|
||||
end
|
||||
|
||||
def filename
|
||||
@_filename ||= params.delete(:file_name).to_s.strip
|
||||
end
|
||||
|
||||
def validate!
|
||||
raise Error, '视频标题不能为空' if title.blank?
|
||||
raise Error, '源文件名不能为空' if filename.blank?
|
||||
end
|
||||
|
||||
def upload_video_result
|
||||
AliyunVod::Service.create_upload_video(title, filename, params)
|
||||
rescue AliyunVod::Error => _
|
||||
raise Error, '获取视频上传凭证失败'
|
||||
end
|
||||
end
|
@ -0,0 +1,25 @@
|
||||
class Videos::DispatchCallbackService < ApplicationService
|
||||
attr_reader :video, :params
|
||||
|
||||
def initialize(params)
|
||||
@video = Video.find_by(uuid: params[:VideoId])
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
return if video.blank?
|
||||
|
||||
# TODO:: 拆分事件分发
|
||||
case params['EventType']
|
||||
when 'FileUploadComplete' then
|
||||
video.file_url = params['FileUrl']
|
||||
video.upload_success
|
||||
video.save!
|
||||
|
||||
GetAliyunVideoInfoJob.perform_later(video.uuid)
|
||||
end
|
||||
|
||||
rescue => ex
|
||||
Util.logger_error(ex)
|
||||
end
|
||||
end
|
@ -0,0 +1,38 @@
|
||||
class Videos::RefuseApplyService < ApplicationService
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
attr_reader :video_apply, :video, :user, :params
|
||||
|
||||
def initialize(video_apply, user, params)
|
||||
@video_apply = video_apply
|
||||
@video = video_apply.video
|
||||
@user = user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
reason = params[:reason].to_s.strip
|
||||
raise Error, '原因不能为空' if reason.blank?
|
||||
raise Error, '该状态下不能进行此操作' unless video_apply.may_refuse?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
video_apply.reason = reason
|
||||
video_apply.refuse
|
||||
video_apply.save!
|
||||
|
||||
video.refuse!
|
||||
|
||||
# 将消息改为已处理
|
||||
Tiding.where(container_id: video.id, container_type: 'Video', tiding_type: 'Apply', status: 0).update_all(status: 1)
|
||||
notify_video_author!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_video_author!
|
||||
Tiding.create!(user_id: video.user_id, trigger_user_id: 1,
|
||||
container_id: video.id, container_type: 'Video',
|
||||
tiding_type: 'System', status: 2, extra: video_apply.reason)
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
json.extract! video, :id, :title, :cover_url, :file_url
|
||||
|
||||
json.published_at video.display_published_at
|
||||
json.created_at video.display_created_at
|
||||
json.updated_at video.display_updated_at
|
@ -0,0 +1,2 @@
|
||||
json.count @count
|
||||
json.videos @videos, partial: 'video', as: :video
|
@ -0,0 +1,7 @@
|
||||
json.count @count
|
||||
json.videos do
|
||||
json.array! @video.each do |video|
|
||||
json.partial! 'video', video: video
|
||||
json.file_url nil
|
||||
end
|
||||
end
|
@ -0,0 +1 @@
|
||||
json.partial! 'video', video: current_video
|
@ -0,0 +1,15 @@
|
||||
defaults: &defaults
|
||||
access_key_id: 'test'
|
||||
access_key_secret: 'test'
|
||||
base_url: 'http://vod.cn-shanghai.aliyuncs.com'
|
||||
callback_url: 'http://47.96.87.25:48080/api/callbacks/aliyun_vod.json'
|
||||
signature_key: 'test12345678'
|
||||
|
||||
development:
|
||||
<<: *defaults
|
||||
|
||||
test:
|
||||
<<: *defaults
|
||||
|
||||
production:
|
||||
<<: *defaults
|
@ -0,0 +1,6 @@
|
||||
config = Rails.application.config_for(:aliyun_vod)
|
||||
AliyunVod.access_key_id = config['access_key_id']
|
||||
AliyunVod.access_key_secret = config['access_key_secret']
|
||||
AliyunVod.base_url = config['base_url'] || 'http://vod.cn-shanghai.aliyuncs.com'.freeze
|
||||
AliyunVod.callback_url = config['callback_url']
|
||||
AliyunVod.signature_key = config['signature_key']
|
@ -0,0 +1,20 @@
|
||||
class CreateVideos < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :videos do |t|
|
||||
t.references :user, index: true, type: :integer
|
||||
t.string :title
|
||||
t.string :uuid, comment: 'Aliyun媒体ID'
|
||||
t.string :cover_url, comment: '视频封面'
|
||||
t.string :file_url, comment: '视频地址'
|
||||
|
||||
t.string :status
|
||||
t.string :vod_status, comment: 'Aliyun媒体状态'
|
||||
|
||||
t.datetime :published_at, index: true
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index :uuid, unique: true
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
class CreateVideoApplies < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :video_applies do |t|
|
||||
t.references :video, index: true, type: :integer
|
||||
t.string :status
|
||||
t.string :reason
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in new issue