Initial commit
This commit is contained in:
38
app/services/amqp/amqp_service.rb
Normal file
38
app/services/amqp/amqp_service.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Amqp
|
||||
class AmqpService
|
||||
include Singleton
|
||||
|
||||
def queue(queue_name, options = {})
|
||||
@queue ||= {}
|
||||
@queue[queue_name] ||= channel.queue queue_name, options
|
||||
end
|
||||
|
||||
def exchange(exchange_name, exchange_type, options = {})
|
||||
@exchange ||= {}
|
||||
@exchange[exchange_name] ||= channel.exchange exchange_name, options.merge(type: exchange_type)
|
||||
end
|
||||
|
||||
def default_exchange
|
||||
channel.default_exchange
|
||||
end
|
||||
|
||||
private
|
||||
def channel
|
||||
@channel ||= connection.create_channel
|
||||
end
|
||||
|
||||
def connection
|
||||
@connection ||= begin
|
||||
Bunny.new(amqp_config).tap do |conn|
|
||||
conn.start
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def amqp_config
|
||||
AppConfig.amqp
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/services/auth/auth_token_service.rb
Normal file
48
app/services/auth/auth_token_service.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'auth/token_service'
|
||||
|
||||
# Manages tokens for this App
|
||||
module Auth
|
||||
class AuthTokenService
|
||||
include Singleton
|
||||
|
||||
def refresh
|
||||
@tokens = mutex.synchronize { obtain_tokens }
|
||||
end
|
||||
|
||||
def access_token
|
||||
ensure_tokens
|
||||
@tokens[:access_token]
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_tokens
|
||||
@tokens ||= mutex.synchronize { obtain_tokens }
|
||||
end
|
||||
|
||||
def obtain_tokens
|
||||
token_source.fetch('client_credentials', client_id, client_secret)
|
||||
end
|
||||
|
||||
def mutex
|
||||
@mutex ||= Mutex.new
|
||||
end
|
||||
|
||||
def token_source
|
||||
@token_source ||= Auth::TokenService.new(auth_url)
|
||||
end
|
||||
|
||||
def client_id
|
||||
AppConfig.auth.client_id
|
||||
end
|
||||
|
||||
def client_secret
|
||||
AppConfig.auth.client_secret
|
||||
end
|
||||
|
||||
def auth_url
|
||||
URI(AppConfig.auth.url)
|
||||
end
|
||||
end
|
||||
end
|
||||
111
app/services/log/log.rb
Normal file
111
app/services/log/log.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Log
|
||||
class Log
|
||||
class LogFormatter < Logger::Formatter
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
super() # superclass does not take parameters
|
||||
end
|
||||
|
||||
def call(severity, timestamp, progname, msg)
|
||||
super(severity, timestamp, progname, "#{log_tags}#{msg}")
|
||||
end
|
||||
|
||||
def log_tags
|
||||
tags = formatted_tags.join ' '
|
||||
tags.blank? ? '' : "[#{tags}] "
|
||||
end
|
||||
|
||||
private
|
||||
def formatted_tags
|
||||
elems = [Thread.current[:name].to_s]
|
||||
elems += (options[:tags] || []).map { |e| e.is_a?(Proc) ? e.call : e }
|
||||
elems.reject(&:blank?)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :options # default = {}
|
||||
|
||||
# Options may include:
|
||||
# tags: an array of static elements and/or callable Procs to include in log output
|
||||
# filename: specifies filename for log in Rails log directory
|
||||
# stream: specifies stream for log output (included for test support)
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= begin
|
||||
new_logger = Logger.new log_destination
|
||||
new_logger.level = numeric_log_level
|
||||
new_logger.formatter = LogFormatter.new options
|
||||
new_logger
|
||||
end
|
||||
end
|
||||
|
||||
def numeric_log_level
|
||||
AppConfig.numeric_log_level
|
||||
end
|
||||
|
||||
# Returns filename for log or stream object if specified in options.
|
||||
def log_destination
|
||||
options[:stream].present? ? options[:stream] : log_filepath
|
||||
end
|
||||
|
||||
def log_filepath
|
||||
@log_filepath ||= log_dir.join log_filename
|
||||
end
|
||||
|
||||
def log_filename
|
||||
options[:filename].present? ? options[:filename].to_s : "#{Rails.env.to_s}.log"
|
||||
end
|
||||
|
||||
def log_dir
|
||||
@log_dir ||= begin
|
||||
dir = Rails.root.join 'log'
|
||||
FileUtils.mkdir_p dir
|
||||
dir
|
||||
end
|
||||
end
|
||||
|
||||
# Same semantics as Log.error except uses current instance of logger.
|
||||
def error(msg, exception = nil, options = {})
|
||||
Log.log_error logger, msg, exception, options
|
||||
end
|
||||
|
||||
# Same semantics as Log.raise_error except uses current instance of logger.
|
||||
def raise_error(msg, exception, options = {})
|
||||
error msg, exception, options
|
||||
raise exception
|
||||
end
|
||||
|
||||
class << self
|
||||
# Logs and re-raises exception.
|
||||
def raise_error (msg, exception, options = {})
|
||||
error msg, exception, options
|
||||
raise exception
|
||||
end
|
||||
|
||||
# Logs error with exception backtrace detail if available. Always uses Rails.logger.
|
||||
#
|
||||
# Options may include:
|
||||
# no_backtrace: true suppresses backtrace
|
||||
#
|
||||
# Returns formatted msg.
|
||||
def error(msg, exception = nil, options = {})
|
||||
log_error Rails.logger, msg, exception, options
|
||||
end
|
||||
|
||||
# Same semantics as Log.error except uses specified logger.
|
||||
def log_error(logger, msg, exception = nil, options = {})
|
||||
message = exception.nil? ? msg : "Exception #{exception.class.name}: #{exception.inspect} #{msg}"
|
||||
logger.error message
|
||||
logger.error exception.backtrace if exception.try(:backtrace) && !options[:no_backtrace]
|
||||
message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/services/log/loggable.rb
Normal file
11
app/services/log/loggable.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'logging'
|
||||
|
||||
module Log
|
||||
module Loggable
|
||||
def logger
|
||||
Logging.instance.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/services/log/logging.rb
Normal file
11
app/services/log/logging.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'singleton'
|
||||
|
||||
module Log
|
||||
class Logging
|
||||
include Singleton
|
||||
|
||||
attr_accessor :logger
|
||||
end
|
||||
end
|
||||
13
app/services/schedule_pipeline/errors.rb
Normal file
13
app/services/schedule_pipeline/errors.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
|
||||
module SchedulePipeline
|
||||
class Errors
|
||||
include Log::Loggable
|
||||
|
||||
def push(error)
|
||||
logger.error error
|
||||
end
|
||||
end
|
||||
end
|
||||
56
app/services/schedule_pipeline/fetch_schedule.rb
Normal file
56
app/services/schedule_pipeline/fetch_schedule.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vendors/broad_sign/broad_sign_tokens'
|
||||
require_relative '../vendors/broad_sign/broad_sign_fetch_schedule'
|
||||
require_relative '../vendors/vistar/vistar_tokens'
|
||||
require_relative '../vendors/vistar/vistar_fetch_schedule'
|
||||
|
||||
module SchedulePipeline
|
||||
class FetchSchedule
|
||||
include Log::Loggable
|
||||
|
||||
def initialize(in_queue, out_queue, errors)
|
||||
@in_queue = in_queue
|
||||
@out_queue = out_queue
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
def start
|
||||
@in_queue.subscribe { |msg| process_msg(msg) }
|
||||
end
|
||||
|
||||
def process_msg(msg)
|
||||
logger.debug("fetch schedule: #{msg}")
|
||||
vendor = msg[:vendor]
|
||||
vendor_fetch = for_vendor(vendor)
|
||||
unless vendor_fetch
|
||||
@errors.push("FetchSchedule: Unknown vendor #{vendor}")
|
||||
return
|
||||
end
|
||||
|
||||
params = msg[:params]
|
||||
fetched = vendor_fetch.call(params)
|
||||
unless fetched
|
||||
@errors.push("FetchSchedule: No ads returned from vendor #{vendor}")
|
||||
return
|
||||
end
|
||||
Models::ScheduleProcessMsg.new(vendor, params[:player], fetched).push(@out_queue) if fetched
|
||||
true
|
||||
rescue StandardError => e
|
||||
@errors.push(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
def for_vendor(vendor)
|
||||
vendor_map[vendor&.to_sym]
|
||||
end
|
||||
|
||||
def vendor_map
|
||||
@vendor_map ||= {
|
||||
broad_sign: Vendors::BroadSign::BroadSignFetchSchedule.new(Vendors::BroadSign::BroadSignTokens.instance),
|
||||
vistar: Vendors::Vistar::VistarFetchSchedule.new(Vendors::Vistar::VistarTokens.instance)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/services/schedule_pipeline/models/schedule.rb
Normal file
37
app/services/schedule_pipeline/models/schedule.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SchedulePipeline
|
||||
module Models
|
||||
class Schedule
|
||||
attr_reader :name, :vendor, :player, :start_time, :items
|
||||
|
||||
def initialize(name, vendor, player, start_time, items)
|
||||
@name = name
|
||||
@vendor = vendor
|
||||
@player = player
|
||||
@start_time = start_time
|
||||
@items = items
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
name: name,
|
||||
vendor: vendor,
|
||||
player: player,
|
||||
start_time: start_time,
|
||||
items: items.collect(&:to_hash)
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
def self.from_hash(input)
|
||||
self.new(
|
||||
input[:name],
|
||||
input[:vendor],
|
||||
input[:player],
|
||||
input[:start_time],
|
||||
input[:items].collect { |i| ScheduleItem.from_hash(i) }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
app/services/schedule_pipeline/models/schedule_fetch_msg.rb
Normal file
26
app/services/schedule_pipeline/models/schedule_fetch_msg.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SchedulePipeline
|
||||
module Models
|
||||
class ScheduleFetchMsg
|
||||
attr_reader :vendor, :params
|
||||
|
||||
def initialize(vendor, params)
|
||||
@vendor = vendor
|
||||
@params = params
|
||||
end
|
||||
|
||||
def send(queue)
|
||||
queue.push(serialized_msg)
|
||||
end
|
||||
|
||||
private
|
||||
def serialized_msg
|
||||
{
|
||||
vendor: @vendor,
|
||||
params: @params
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/services/schedule_pipeline/models/schedule_item.rb
Normal file
31
app/services/schedule_pipeline/models/schedule_item.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SchedulePipeline
|
||||
module Models
|
||||
class ScheduleItem
|
||||
attr_reader :duration, :content_key, :pop_data
|
||||
|
||||
def initialize(duration, content_key, pop_data)
|
||||
@duration = duration
|
||||
@content_key = content_key
|
||||
@pop_data = pop_data
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
duration: @duration,
|
||||
content_key: @content_key,
|
||||
pop_data: @pop_data
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
def self.from_hash(input)
|
||||
self.new(
|
||||
input[:duration],
|
||||
input[:content_key],
|
||||
input[:pop_data]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SchedulePipeline
|
||||
module Models
|
||||
class ScheduleProcessMsg
|
||||
attr_reader :vendor, :player, :vendor_schedule
|
||||
|
||||
def initialize(vendor, player, schedule)
|
||||
@vendor = vendor
|
||||
@player = player
|
||||
@vendor_schedule = schedule.with_indifferent_access
|
||||
end
|
||||
|
||||
def push(queue)
|
||||
queue.push(to_hash)
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
vendor: @vendor,
|
||||
player: @player,
|
||||
vendor_schedule: @vendor_schedule
|
||||
}
|
||||
end
|
||||
|
||||
def self.from_hash(input)
|
||||
self.new(
|
||||
input[:vendor],
|
||||
input[:player],
|
||||
input[:vendor_schedule],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
app/services/schedule_pipeline/process_schedule.rb
Normal file
85
app/services/schedule_pipeline/process_schedule.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vle/vle_settings'
|
||||
require_relative '../vle/vle_create_asset'
|
||||
require_relative '../vle/vle_ingest_asset'
|
||||
require_relative '../vendors/broad_sign/broad_sign_transform_schedule'
|
||||
require_relative '../vendors/vistar/vistar_transform_schedule'
|
||||
require_relative '../auth/auth_token_service'
|
||||
require 'auth/client/request/auth_aware_request'
|
||||
|
||||
module SchedulePipeline
|
||||
class ProcessSchedule
|
||||
include Log::Loggable
|
||||
|
||||
def initialize(in_queue, out_queue, errors, tokens = Auth::AuthTokenService.instance)
|
||||
@in_queue = in_queue
|
||||
@out_queue = out_queue
|
||||
@errors = errors
|
||||
@tokens = tokens
|
||||
end
|
||||
|
||||
def start
|
||||
@in_queue.subscribe do |msg|
|
||||
msg = Models::ScheduleProcessMsg.from_hash(msg) if msg.is_a? Hash
|
||||
process_msg(msg)
|
||||
end
|
||||
end
|
||||
|
||||
def process_msg(msg)
|
||||
logger.info("Process Schedule")
|
||||
|
||||
vendor = msg.vendor
|
||||
schedule_transform = for_vendor(vendor)
|
||||
unless schedule_transform
|
||||
@errors.push("ProcessSchedule: Unknown vendor #{vendor}")
|
||||
return
|
||||
end
|
||||
|
||||
content_map = []
|
||||
msg.vendor_schedule[:contents].each do |content|
|
||||
# TODO: optimization: first check whether the content has already been ingested into VLE
|
||||
vle_asset = create_asset(content)[:asset]
|
||||
content_map << vle_asset
|
||||
ingest_content(content, vle_asset[:meta][:presigned_url])
|
||||
end
|
||||
|
||||
schedule = schedule_transform.call(vendor, msg.player, msg.vendor_schedule, content_map)
|
||||
@out_queue.push(schedule)
|
||||
true
|
||||
rescue StandardError => e
|
||||
@errors.push(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
def create_asset(content)
|
||||
sanitized_name = content[:name].gsub(/\s+/, '_')
|
||||
Auth::Client::Request::AuthAwareRequest.new(
|
||||
Vle::VleCreateAsset.new(
|
||||
{
|
||||
name: sanitized_name,
|
||||
file: sanitized_name,
|
||||
project_id: Vle::VleSettings.instance.target_project
|
||||
}
|
||||
),
|
||||
@tokens
|
||||
).call
|
||||
end
|
||||
|
||||
def ingest_content(content, destination)
|
||||
Vle::VleIngestAsset.new.call(content, destination)
|
||||
end
|
||||
|
||||
def for_vendor(vendor)
|
||||
vendor_map[vendor&.to_sym]
|
||||
end
|
||||
|
||||
def vendor_map
|
||||
@vendor_map ||= {
|
||||
broad_sign: Vendors::BroadSign::BroadSignTransformSchedule.new,
|
||||
vistar: Vendors::Vistar::VistarTransformSchedule.new
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/services/schedule_pipeline/publish_schedule.rb
Normal file
41
app/services/schedule_pipeline/publish_schedule.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vle/vle_vendor_schedule'
|
||||
require_relative 'models/schedule'
|
||||
require_relative '../auth/auth_token_service'
|
||||
require 'auth/client/request/auth_aware_request'
|
||||
|
||||
module SchedulePipeline
|
||||
class PublishSchedule
|
||||
include Log::Loggable
|
||||
|
||||
def initialize(queue, errors, tokens = Auth::AuthTokenService.instance)
|
||||
@queue = queue
|
||||
@errors = errors
|
||||
@tokens = tokens
|
||||
end
|
||||
|
||||
def start
|
||||
@queue.subscribe do |msg|
|
||||
msg = Models::Schedule.from_hash(msg) if msg.is_a? Hash
|
||||
process_msg(msg)
|
||||
end
|
||||
end
|
||||
|
||||
def process_msg(msg)
|
||||
logger.info("Publishing schedule: #{msg.name}...")
|
||||
Auth::Client::Request::AuthAwareRequest.new(
|
||||
Vle::VleVendorSchedule.new(
|
||||
msg.vendor,
|
||||
msg.player,
|
||||
{ name: msg.name, start_time: msg.start_time, items: msg.to_hash[:items] }
|
||||
),
|
||||
@tokens
|
||||
).call
|
||||
logger.info("Published schedule: #{msg.name}")
|
||||
rescue StandardError => e
|
||||
@errors.push(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
32
app/services/schedule_pipeline/queue/bunny_queue.rb
Normal file
32
app/services/schedule_pipeline/queue/bunny_queue.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../amqp/amqp_service'
|
||||
|
||||
module SchedulePipeline
|
||||
module Queue
|
||||
class BunnyQueue
|
||||
def initialize(destination)
|
||||
@destination = destination
|
||||
end
|
||||
|
||||
def push(msg)
|
||||
amqp.default_exchange.publish(msg.to_json, routing_key: @destination)
|
||||
end
|
||||
|
||||
def subscribe(&block)
|
||||
amqp_queue.subscribe do |_delivery_info, _metadata, payload|
|
||||
block.call(JSON.parse(payload).with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def amqp
|
||||
Amqp::AmqpService.instance
|
||||
end
|
||||
|
||||
def amqp_queue
|
||||
@queue ||= amqp.queue(@destination.to_s, auto_delete: false, durable: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/services/schedule_pipeline/queue/bunny_queue_factory.rb
Normal file
15
app/services/schedule_pipeline/queue/bunny_queue_factory.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'bunny_queue'
|
||||
|
||||
module SchedulePipeline
|
||||
module Queue
|
||||
class BunnyQueueFactory
|
||||
include Singleton
|
||||
|
||||
def for_name(name)
|
||||
BunnyQueue.new(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/services/schedule_pipeline/schedule_pipeline.rb
Normal file
44
app/services/schedule_pipeline/schedule_pipeline.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative 'errors'
|
||||
require_relative 'publish_schedule'
|
||||
require_relative 'process_schedule'
|
||||
require_relative 'fetch_schedule'
|
||||
|
||||
module SchedulePipeline
|
||||
# Schedule Processing Pipeline
|
||||
class SchedulePipeline
|
||||
include Log::Loggable
|
||||
|
||||
def initialize(queue_factory)
|
||||
errors = Errors.new
|
||||
processed_schedules_queue = queue_factory.for_name(:processed_schedules)
|
||||
publish_schedule = PublishSchedule.new(processed_schedules_queue, errors)
|
||||
unprocessed_schedules_queue = queue_factory.for_name(:unprocessed_schedules)
|
||||
process_schedule = ProcessSchedule.new(unprocessed_schedules_queue, processed_schedules_queue, errors)
|
||||
fetch_schedule_queue = queue_factory.for_name(:fetch_vendor_schedules)
|
||||
fetch_schedule = FetchSchedule.new(fetch_schedule_queue, unprocessed_schedules_queue, errors)
|
||||
@queues = [processed_schedules_queue, unprocessed_schedules_queue, fetch_schedule_queue]
|
||||
@stages = [publish_schedule, process_schedule, fetch_schedule]
|
||||
end
|
||||
|
||||
def start
|
||||
stages.each(&:start)
|
||||
logger.info "Started Schedule Pipeline"
|
||||
end
|
||||
|
||||
def stop
|
||||
stages.reverse_each(&:stop)
|
||||
end
|
||||
|
||||
private
|
||||
def stages
|
||||
@stages
|
||||
end
|
||||
|
||||
def queues
|
||||
@queues
|
||||
end
|
||||
end
|
||||
end
|
||||
57
app/services/vendors/broad_sign/broad_sign_fetch_schedule.rb
vendored
Normal file
57
app/services/vendors/broad_sign/broad_sign_fetch_schedule.rb
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
require_relative '../errors/schedule_fetch_error'
|
||||
require 'net/http'
|
||||
|
||||
module Vendors
|
||||
module BroadSign
|
||||
class BroadSignFetchSchedule
|
||||
include Log::Loggable
|
||||
|
||||
AIR_DOMAIN = 'air.broadsign.com'
|
||||
PATH = 'playlist/v1/generate'
|
||||
|
||||
def initialize(tokens)
|
||||
@tokens = tokens
|
||||
end
|
||||
|
||||
def call(params)
|
||||
params = ActiveSupport::HashWithIndifferentAccess.new(
|
||||
screen: '1',
|
||||
duration: 3600
|
||||
).merge(params_whitelist(params))
|
||||
logger.info "BroadSign fetch schedule request for player #{params[:player]}"
|
||||
url = URI.join("https://#{AIR_DOMAIN}", PATH)
|
||||
res = Net::HTTP.post(
|
||||
url,
|
||||
{
|
||||
player_identifier: params[:player].to_s,
|
||||
screen_identifier: params[:screen]&.to_s,
|
||||
duration: "#{params[:duration]}s"
|
||||
}.to_json,
|
||||
{
|
||||
"Authorization": "Bearer #{@tokens.auth_token}",
|
||||
"Content-Type": 'application/json'
|
||||
}
|
||||
)
|
||||
case res
|
||||
when Net::HTTPSuccess
|
||||
JSON.parse(res.body).with_indifferent_access
|
||||
else
|
||||
logger.error "Error fetching BroadSign Air schedule: #{res.code}"
|
||||
raise Vendors::Errors::ScheduleFetchError, { player: params[:player], response_code: res.code }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def params_whitelist(params)
|
||||
params.slice(
|
||||
:player,
|
||||
:screen,
|
||||
:duration
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/services/vendors/broad_sign/broad_sign_tokens.rb
vendored
Normal file
15
app/services/vendors/broad_sign/broad_sign_tokens.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
|
||||
module Vendors
|
||||
module BroadSign
|
||||
class BroadSignTokens
|
||||
include Singleton
|
||||
|
||||
def auth_token
|
||||
AppConfig.get_mandatory 'vendors.broad_sign.token'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
47
app/services/vendors/broad_sign/broad_sign_transform_schedule.rb
vendored
Normal file
47
app/services/vendors/broad_sign/broad_sign_transform_schedule.rb
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
require_relative '../../schedule_pipeline/models/schedule'
|
||||
require_relative '../../schedule_pipeline/models/schedule_item'
|
||||
require_relative '../errors/schedule_transform_error'
|
||||
|
||||
module Vendors
|
||||
module BroadSign
|
||||
class BroadSignTransformSchedule
|
||||
include Log::Loggable
|
||||
|
||||
def call(vendor, player, schedule, content_map)
|
||||
logger.debug("BroadSign schedule transform for vendor #{vendor}, player #{player}")
|
||||
start_time = nil
|
||||
|
||||
# for performance reasons, here we truncate the items list to the first N items where N is the number of contents
|
||||
# in the schedule. This assumes the schedule round-robins content!
|
||||
truncate_schedule(schedule, content_map.size)
|
||||
|
||||
items = schedule[:items].collect do |item|
|
||||
start_time ||= item[:startTime]
|
||||
SchedulePipeline::Models::ScheduleItem.new(
|
||||
item[:duration],
|
||||
content_map[item[:contentIndex] || 0][:id],
|
||||
# TODO: build full POP reporting url using this token
|
||||
item[:token]
|
||||
)
|
||||
end
|
||||
raise Vendors::Errors::ScheduleTransformError, 'Schedule without a start time' if start_time.nil?
|
||||
|
||||
SchedulePipeline::Models::Schedule.new(
|
||||
"BroadSign schedule for player #{player}",
|
||||
vendor,
|
||||
player,
|
||||
start_time,
|
||||
items
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def truncate_schedule(schedule, size)
|
||||
schedule[:items] = schedule[:items].first(size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
7
app/services/vendors/errors/content_ingest_error.rb
vendored
Normal file
7
app/services/vendors/errors/content_ingest_error.rb
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vendors
|
||||
module Errors
|
||||
class ContentIngestError < StandardError; end
|
||||
end
|
||||
end
|
||||
7
app/services/vendors/errors/schedule_fetch_error.rb
vendored
Normal file
7
app/services/vendors/errors/schedule_fetch_error.rb
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vendors
|
||||
module Errors
|
||||
class ScheduleFetchError < StandardError; end
|
||||
end
|
||||
end
|
||||
7
app/services/vendors/errors/schedule_publish_error.rb
vendored
Normal file
7
app/services/vendors/errors/schedule_publish_error.rb
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vendors
|
||||
module Errors
|
||||
class SchedulePublishError < StandardError; end
|
||||
end
|
||||
end
|
||||
7
app/services/vendors/errors/schedule_transform_error.rb
vendored
Normal file
7
app/services/vendors/errors/schedule_transform_error.rb
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vendors
|
||||
module Errors
|
||||
class ScheduleTransformError < StandardError; end
|
||||
end
|
||||
end
|
||||
93
app/services/vendors/vistar/vistar_fetch_ad.rb
vendored
Normal file
93
app/services/vendors/vistar/vistar_fetch_ad.rb
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
require_relative '../errors/schedule_fetch_error'
|
||||
require 'net/http'
|
||||
|
||||
module Vendors
|
||||
module Vistar
|
||||
class VistarFetchAd
|
||||
include Log::Loggable
|
||||
|
||||
PATH = 'api/v1/get_ad/json'
|
||||
DEFAULT_SUPPORTED_MEDIA = %w[
|
||||
application/x-shockwave-dynamic-flash
|
||||
application/x-shockwave-flash
|
||||
image/jpeg
|
||||
image/png
|
||||
video/mp4
|
||||
video/mpeg
|
||||
video/mpg
|
||||
video/quicktime
|
||||
video/webm
|
||||
video/x-flv
|
||||
video/x-ms-wmv
|
||||
video/x-msvideo
|
||||
]
|
||||
|
||||
def initialize(tokens)
|
||||
@tokens = tokens
|
||||
end
|
||||
|
||||
def call(params)
|
||||
logger.info "Vistar fetch Ad request"
|
||||
res = Net::HTTP.post(
|
||||
Vendors::Vistar::VistarSettings.instance.vistar_url(PATH),
|
||||
body(params),
|
||||
{
|
||||
"Content-Type": 'application/json'
|
||||
}
|
||||
)
|
||||
case res
|
||||
when Net::HTTPSuccess
|
||||
JSON.parse(res.body).with_indifferent_access
|
||||
else
|
||||
logger.error "Error fetching Vistar Ad: #{res.code}"
|
||||
raise Vendors::Errors::ScheduleFetchError, { response_code: res.code }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def body(params)
|
||||
display_area = (params[:display_area] || [{}]).each_with_index.collect do |item, idx|
|
||||
{
|
||||
id: "display-#{idx}",
|
||||
width: 1080,
|
||||
height: 1920,
|
||||
supported_media: DEFAULT_SUPPORTED_MEDIA,
|
||||
static_duration: 8
|
||||
}.merge(item)
|
||||
end
|
||||
params_whitelist(params).merge({
|
||||
network_id: Vendors::Vistar::VistarSettings.instance.network_id,
|
||||
api_key: "#{@tokens.api_key}",
|
||||
direct_connection: false,
|
||||
display_area: display_area
|
||||
}).to_json
|
||||
end
|
||||
|
||||
def params_whitelist(params)
|
||||
params.slice(
|
||||
:device_id,
|
||||
:venue_id,
|
||||
:display_time,
|
||||
:device_attribute,
|
||||
:name,
|
||||
:display_area,
|
||||
:id,
|
||||
:width,
|
||||
:height,
|
||||
:allow_audio,
|
||||
:supported_media,
|
||||
:min_duration,
|
||||
:max_duration,
|
||||
:order_id,
|
||||
:max_file_size_bytes,
|
||||
:static_duration,
|
||||
:latitude,
|
||||
:longitude
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/services/vendors/vistar/vistar_fetch_schedule.rb
vendored
Normal file
37
app/services/vendors/vistar/vistar_fetch_schedule.rb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
module Vendors
|
||||
module Vistar
|
||||
class VistarFetchSchedule
|
||||
def initialize(tokens)
|
||||
@tokens = tokens
|
||||
end
|
||||
|
||||
def call(params)
|
||||
fetch = fetch_ad(params)
|
||||
return nil unless fetch && fetch[:advertisement]
|
||||
|
||||
ad = fetch[:advertisement][0]
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
name: "vistar_asset_#{ad[:asset_id]}",
|
||||
url: ad[:asset_url]
|
||||
}
|
||||
],
|
||||
startTime: Time.at(ad[:display_time]).to_datetime,
|
||||
items: [
|
||||
{
|
||||
contentIndex: 0,
|
||||
duration: "#{ad[:length_in_seconds]}s",
|
||||
pop_url: ad[:proof_of_play_url]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
def fetch_ad(params)
|
||||
Vendors::Vistar::VistarFetchAd.new(@tokens).call(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/services/vendors/vistar/vistar_settings.rb
vendored
Normal file
20
app/services/vendors/vistar/vistar_settings.rb
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vendors
|
||||
module Vistar
|
||||
class VistarSettings
|
||||
include Singleton
|
||||
|
||||
def vistar_url(path)
|
||||
URI::join(
|
||||
AppConfig.get_mandatory('vendors.vistar.base_url'),
|
||||
path
|
||||
)
|
||||
end
|
||||
|
||||
def network_id
|
||||
AppConfig.get_mandatory('vendors.vistar.network_id')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/services/vendors/vistar/vistar_tokens.rb
vendored
Normal file
15
app/services/vendors/vistar/vistar_tokens.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
|
||||
module Vendors
|
||||
module Vistar
|
||||
class VistarTokens
|
||||
include Singleton
|
||||
|
||||
def api_key
|
||||
AppConfig.get_mandatory 'vendors.vistar.api_key'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/services/vendors/vistar/vistar_transform_schedule.rb
vendored
Normal file
35
app/services/vendors/vistar/vistar_transform_schedule.rb
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../log/loggable'
|
||||
require_relative '../../schedule_pipeline/models/schedule'
|
||||
require_relative '../../schedule_pipeline/models/schedule_item'
|
||||
require_relative '../errors/schedule_transform_error'
|
||||
|
||||
module Vendors
|
||||
module Vistar
|
||||
class VistarTransformSchedule
|
||||
include Log::Loggable
|
||||
|
||||
def call(vendor, player, schedule, content_map)
|
||||
logger.debug("Vistar schedule transform for vendor #{vendor}, player #{player}")
|
||||
start_time = schedule[:startTime]
|
||||
items = schedule[:items].collect do |item|
|
||||
SchedulePipeline::Models::ScheduleItem.new(
|
||||
item[:duration],
|
||||
content_map[item[:contentIndex] || 0][:id],
|
||||
item[:pop_url]
|
||||
)
|
||||
end
|
||||
raise Vendors::Errors::ScheduleTransformError, 'Schedule without a start time' if start_time.nil?
|
||||
|
||||
SchedulePipeline::Models::Schedule.new(
|
||||
"Vistar schedule for player #{player}",
|
||||
vendor,
|
||||
player,
|
||||
start_time,
|
||||
items
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
52
app/services/vle/vle_create_asset.rb
Normal file
52
app/services/vle/vle_create_asset.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vendors/errors/content_ingest_error'
|
||||
require_relative 'vle_settings'
|
||||
|
||||
module Vle
|
||||
class VleCreateAsset
|
||||
include Log::Loggable
|
||||
|
||||
PATH = 'v1/assets'
|
||||
|
||||
def initialize(asset)
|
||||
@asset = asset
|
||||
end
|
||||
|
||||
def url
|
||||
Vle::VleSettings.instance.central_url(PATH)
|
||||
end
|
||||
|
||||
def call(access_token)
|
||||
logger.info("ONEX create asset: #{asset}")
|
||||
res = Net::HTTP.post(
|
||||
url,
|
||||
{
|
||||
file: asset[:file],
|
||||
name: asset[:name],
|
||||
project_id: asset[:project_id],
|
||||
description_short: "vendor-scheduler-service content: #{asset[:name]}"
|
||||
}.to_json,
|
||||
{
|
||||
"Authorization": "Bearer #{access_token}",
|
||||
"Content-Type": 'application/json'
|
||||
}
|
||||
)
|
||||
case res
|
||||
when Net::HTTPSuccess
|
||||
JSON.parse(res.body).with_indifferent_access
|
||||
when Net::HTTPUnauthorized
|
||||
raise Auth::Client::Errors::Unauthorized, 'Unauthorized'
|
||||
else
|
||||
logger.error "Error creating asset in VLE: #{res.code}"
|
||||
raise Vendors::Errors::ContentIngestError
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def asset
|
||||
@asset
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/services/vle/vle_ingest_asset.rb
Normal file
35
app/services/vle/vle_ingest_asset.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rest-client'
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vendors/errors/content_ingest_error'
|
||||
|
||||
module Vle
|
||||
class VleIngestAsset
|
||||
include Log::Loggable
|
||||
|
||||
def call(content, destination)
|
||||
logger.info("ONEX asset ingest: #{content}")
|
||||
tmp_file = Tempfile.new('vendor-scheduler-service-content-ingest')
|
||||
begin
|
||||
source = content[:url] || content[:uri]
|
||||
IO.copy_stream(URI.parse(source).open, tmp_file.path)
|
||||
logger.debug("Content retrieved. Now starting ONEX ingest...")
|
||||
res = RestClient.put(destination, tmp_file, { 'Content-Type': "binary/octet-stream" })
|
||||
case res.code
|
||||
when 200 #Net::HTTPSuccess
|
||||
logger.debug("ONEX Asset ingested.")
|
||||
true
|
||||
else
|
||||
logger.error "Error ingesting content into ONEX: POST returned #{res.code}"
|
||||
raise Vendors::Errors::ContentIngestError
|
||||
end
|
||||
rescue StandardError => e
|
||||
logger.error "Error ingesting content into ONEX: #{e.message}"
|
||||
raise Vendors::Errors::ContentIngestError
|
||||
ensure
|
||||
tmp_file.close!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/services/vle/vle_settings.rb
Normal file
25
app/services/vle/vle_settings.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vle
|
||||
class VleSettings
|
||||
include Singleton
|
||||
|
||||
def target_project
|
||||
AppConfig.safe_get('onex.target_project_id') || '1'
|
||||
end
|
||||
|
||||
def central_url(path)
|
||||
URI::join(
|
||||
AppConfig.safe_get('onex.central_url') || 'http://localhost:5070',
|
||||
path
|
||||
)
|
||||
end
|
||||
|
||||
def scheduler_url(path)
|
||||
URI::join(
|
||||
AppConfig.safe_get('onex.scheduler_url') || 'http://localhost:5040',
|
||||
path
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
46
app/services/vle/vle_vendor_schedule.rb
Normal file
46
app/services/vle/vle_vendor_schedule.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../log/loggable'
|
||||
require_relative '../vendors/errors/schedule_publish_error'
|
||||
|
||||
module Vle
|
||||
class VleVendorSchedule
|
||||
include Log::Loggable
|
||||
|
||||
PATH = 'v1/vendor_schedule'
|
||||
|
||||
def initialize(vendor, player, schedule)
|
||||
@vendor = vendor
|
||||
@player = player
|
||||
@schedule = schedule
|
||||
end
|
||||
|
||||
def url
|
||||
Vle::VleSettings.instance.scheduler_url(PATH)
|
||||
end
|
||||
|
||||
def call(access_token)
|
||||
res = Net::HTTP.post(
|
||||
url,
|
||||
{
|
||||
vendor: @vendor,
|
||||
player: @player,
|
||||
schedule: @schedule
|
||||
}.to_json,
|
||||
{
|
||||
"Authorization": "Bearer #{access_token}",
|
||||
"Content-Type": 'application/json'
|
||||
}
|
||||
)
|
||||
case res
|
||||
when Net::HTTPSuccess
|
||||
JSON.parse(res.body).with_indifferent_access
|
||||
when Net::HTTPUnauthorized
|
||||
raise Auth::Client::Errors::Unauthorized, 'Unauthorized'
|
||||
else
|
||||
logger.error "Error (#{res.code}) scheduling vendor content in ONEX: #{res.body}"
|
||||
raise Vendors::Errors::SchedulePublishError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user