Skip to content

Commit

Permalink
add option to create broadcast for default show if no mapping is found
Browse files Browse the repository at this point in the history
  • Loading branch information
codez committed Apr 3, 2017
1 parent 23379dd commit 5ca6b65
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 24 deletions.
5 changes: 3 additions & 2 deletions app/services/import/broadcast_mapping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ module Import
# A simple data holder for a single broadcast.
class BroadcastMapping

attr_reader :show, :broadcast
attr_accessor :show
attr_reader :broadcast

delegate :started_at, :finished_at, :duration,
to: :broadcast, allow_nil: true
Expand Down Expand Up @@ -75,7 +76,7 @@ def overlaps?(recording)

def fetch_show(attrs = {})
Show.where(name: attrs.fetch(:name)).first_or_initialize.tap do |show|
show.attributes = attrs
show.details ||= attrs[:details]
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def build_mappings
end

def show_instances
@show_instances ||= recordings.present? ? fetch_show_instances : []
recordings.present? ? fetch_show_instances : []
end

def build_broadcast_mapping(instance)
Expand Down
61 changes: 52 additions & 9 deletions app/services/import/broadcast_mapping/builder/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,34 @@ module Builder
# corresponding recordings.
class Base

DURATION_TOLERANCE = 5.minutes

include Loggable

attr_reader :recordings

def initialize(recordings)
check_intervals(recordings)
@recordings = recordings.sort_by(&:started_at)
@unmapped_recordings = @recordings.clone
end

def run
build_mappings.each { |m| add_corresponding_recordings(m) }.tap do |_|
warn_for_unmapped_recordings
return [] if recordings.blank?

build_allover_mappings.tap do |mappings|
mappings.each { |m| add_corresponding_recordings(m) }
end
end

private

def build_allover_mappings
fill_gaps_with_default_show(build_mappings,
recordings.first.started_at,
recordings.last.finished_at)
.compact
end

def build_mappings
raise(NotImplementedError)
end
Expand All @@ -41,16 +51,49 @@ def check_intervals(recordings)

def add_corresponding_recordings(mapping)
recordings.each do |r|
if mapping.add_recording_if_overlapping(r)
@unmapped_recordings.delete(r)
mapping.add_recording_if_overlapping(r)
end
end

def fill_gaps_with_default_show(mappings, cut, fin)
[].tap do |all|
mappings.each do |m|
if m.started_at > cut + DURATION_TOLERANCE
all << handle_broadcast_gap(cut, m.started_at)
end
all << m
cut = m.finished_at
end
all << handle_broadcast_gap(cut, fin) if fin > cut + DURATION_TOLERANCE
end
end

def handle_broadcast_gap(started_at, finished_at)
at_period = "from #{I18n.l(started_at)} to #{I18n.l(finished_at, format: :time)}"
if default_show
warn("Creating default broadcast #{at_period}.")
build_default_mapping(started_at, finished_at)
else
warn("No broadcast found #{at_period}.")
nil
end
end

def build_default_mapping(started_at, finished_at)
Import::BroadcastMapping.new.tap do |mapping|
mapping.show = default_show
mapping.assign_broadcast(
label: default_show.name,
started_at: started_at,
finished_at: finished_at
)
end
end

def warn_for_unmapped_recordings
if @unmapped_recordings.present?
warn("No corresponding broadcasts found for the following recordings:\n" +
@unmapped_recordings.collect(&:path).join("\n"))
def default_show
@default_show ||= begin
id = Rails.application.secrets.import_default_show_id
id.present? && Show.find(id)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,15 @@ def build_broadcast_mapping(broadcast_recordings)

def assign_show(mapping, broadcast_recordings)
mapping.assign_show(
name: fetch_show_name(broadcast_recordings.first),
details: nil
name: fetch_show_name(broadcast_recordings.first)
)
end

def assign_broadcast(mapping, broadcast_recordings)
mapping.assign_broadcast(
label: fetch_show_name(broadcast_recordings.first),
details: nil,
started_at: broadcast_recordings.first.started_at,
finished_at: broadcast_recordings.last.finished_at,
people: nil
finished_at: broadcast_recordings.last.finished_at
)
end

Expand Down
4 changes: 4 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,7 @@ de:
must_exist: muss definiert sein.
downgrade_action:
delete_must_be_last: Löschen darf erst am Ende aller Aktionen geschehen.

time:
formats:
time: "%H:%M:%S"
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ en:
must_not_be_deleted: must not be deleted
downgrade_action:
delete_must_be_last: delete must be the last action

time:
formats:
time: "%H:%M:%S"
1 change: 1 addition & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ production:
days_to_keep_imported: <%= ENV['DAYS_TO_KEEP_IMPORTED'] %>
days_to_finish_import: <%= ENV['DAYS_TO_FINISH_IMPORT'] %>
parallel_transcodings: <%= ENV['PARALLEL_TRANSCODINGS'] || 1 %>
import_default_show_id: <%= ENV['IMPORT_DEFAULT_SHOW_ID'] %>
ssl: <%= ENV['RAAR_SSL'] == 'true' %>
host_name: <%= ENV['RAAR_HOST_NAME'] %>
base_path: <%= ENV['RAAR_BASE_PATH'] %>
Expand Down
1 change: 1 addition & 0 deletions doc/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ An easy way to manage these values is to create a `~/.env` file with several `VA
| IMPORT_DIRECTORIES | A comma-separated list of directories where the original recordings to import are found. | - |
| DAYS_TO_KEEP_IMPORTED | Number of days to keep the original recordings, before they are deleted. Recordings are never deleted if left empty. | - |
| DAYS_TO_FINISH_IMPORT | Number of days before a warning is produced because of unimported recordings. No warnings are generated if left empty. | - |
| IMPORT_DEFAULT_SHOW_ID | ID of the show record to use when no other broadcast mapping is found for a given period. Leave empty to generate no broadcasts if no mappings are found. | - |
| PARALLEL_TRANSCODINGS | Number of threads to use for audio transcoding. | 1 |
| AUDIO_PROCESSOR | Name of the audio processor class to use. | Ffmpeg |
| BROADCAST_MAPPING_BUILDER | Name of the broadcast mapping builder class to use. | AirtimeDb |
Expand Down
2 changes: 1 addition & 1 deletion doc/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The following steps are performed during the import process. The respective Clas

1. The import is started (`Import.run`).
1. Find all recordings in the `IMPORT_DIRECTORIES` (`Import::Recording::Finder#pending`).
1. Based on the timestamps given in the recording file names, map the recordings to their respective broadcasts (`Import::BroadcastMapping::Builder#run`). Different strategies would be possible by implementing different `Import::BroadcastMapping::Builder`s. Currently, this data is fetched directly for an Airtime database (`Import::BroadcastMapping::Builder::AirtimeDb`).
1. Based on the timestamps given in the recording file names, map the recordings to their respective broadcasts (`Import::BroadcastMapping::Builder#run`). Different strategies would be possible by implementing different `Import::BroadcastMapping::Builder`s. Currently, this data is fetched directly for an Airtime database (`Import::BroadcastMapping::Builder::AirtimeDb`). If no mappings are found but a `IMPORT_DEFAULT_SHOW_ID` is defined, broadcasts mappings for this show are created.
1. For each broadcast mapping, do the following (`Import::Importer#run`):
1. If the recordings do not cover the entire broadcast duration, cancel and retry later.
1. Select the best recording for a given time (`Import::Recording::Chooser#best`).
Expand Down
19 changes: 13 additions & 6 deletions test/integration/import_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ class ImportTest < ActiveSupport::TestCase

teardown :clear_archive_dir

teardown do
Rails.application.secrets.import_default_show_id = nil
end

# Travis has ffmpeg 0.8.17, which reports "Unknown input format: 'lavfi'"
unless ENV['TRAVIS']
test 'imports recordings as broadcasts' do
Rails.application.secrets.import_default_show_id = shows(:klangbecken).id
Time.zone.stubs(today: Time.local(2013, 6, 19),
now: Time.local(2013, 6, 19, 11))
# build dummy recordings and broadcasts with a duration of two minutes
Expand All @@ -21,7 +26,7 @@ class ImportTest < ActiveSupport::TestCase
.with(Import::Recording::UnimportedWarning.new(Import::Recording::File.new(@f1)))
ExceptionNotifier
.expects(:notify_exception)
.with(Import::Recording::TooShortError.new(Import::Recording::File.new(@f4)), instance_of(Hash))
.with(Import::Recording::TooShortError.new(Import::Recording::File.new(@f5)), instance_of(Hash))

assert_difference('Show.count', 1) do
assert_difference('Broadcast.count', 2) do
Expand All @@ -47,13 +52,15 @@ def build_recording_files
touch('2013-06-19T080000+0200_060_imported.mp3')
touch('2013-06-19T090000+0200_060_imported.mp3')
@f1 = file('2013-06-10T100000+0200_002.mp3') # old unimported
@f2 = file('2013-06-19T100600+0200_002.mp3')
@f3 = file('2013-06-19T100800+0200_002.mp3')
@f4 = file('2013-06-19T101000+0200_002.mp3')
@f2 = file('2013-06-19T095800+0200_002.mp3')
@f3 = file('2013-06-19T100600+0200_002.mp3')
@f4 = file('2013-06-19T100800+0200_002.mp3')
@f5 = file('2013-06-19T101000+0200_002.mp3')
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f1, 120)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f2, 120)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f3, 130)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f4, 110)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f3, 120)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f4, 130)
AudioGenerator.new.silent_file(AudioFormat.new('mp3', 320, 2), @f5, 110)
end

def build_airtime_entries
Expand Down
79 changes: 79 additions & 0 deletions test/services/import/broadcast_mapping/builder/airtime_db_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class Import::BroadcastMapping::Builder::AirtimeDbTest < ActiveSupport::TestCase
include RecordingHelper
include AirtimeHelper

teardown do
Rails.application.secrets.import_default_show_id = nil
end

test 'returns no mappings without recordings' do
builder = new_builder([])
assert_equal [], builder.run
Expand Down Expand Up @@ -144,6 +148,81 @@ class Import::BroadcastMapping::Builder::AirtimeDbTest < ActiveSupport::TestCase
map.recordings.collect(&:path)
end

test 'logs warnings for unmapped recordings' do
recordings = build_recordings('2016-01-01T080000+0100_060.mp3',
'2016-01-01T090000+0100_060.mp3',
'2016-01-01T100000+0100_060.mp3')
morgen = Airtime::Show.create!(name: 'Morgen')
morgen.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 7),
ends: Time.zone.local(2016, 1, 1, 8, 30),
created: Time.zone.now)
morgen.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 9),
ends: Time.zone.local(2016, 1, 1, 10, 30),
created: Time.zone.now)

Import::BroadcastMapping::Builder::AirtimeDb.any_instance
.expects(:warn)
.with('No broadcast found from Fri, 01 Jan 2016 08:30:00 +0100 to 09:00:00.')
Import::BroadcastMapping::Builder::AirtimeDb.any_instance
.expects(:warn)
.with('No broadcast found from Fri, 01 Jan 2016 10:30:00 +0100 to 11:00:00.')

builder = new_builder(recordings)
mappings = builder.run

assert_equal 2, mappings.size
end

test 'creates broadcast for default show for unmapped recordings' do
Rails.application.secrets.import_default_show_id = shows(:klangbecken).id
recordings = build_recordings('2016-01-01T080000+0100_060.mp3',
'2016-01-01T090000+0100_060.mp3',
'2016-01-01T100000+0100_060.mp3')
morgen = Airtime::Show.create!(name: 'Morgen')
morgen.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 7),
ends: Time.zone.local(2016, 1, 1, 8, 30),
created: Time.zone.now)
morgen.show_instances.create!(starts: Time.zone.local(2016, 1, 1, 9),
ends: Time.zone.local(2016, 1, 1, 9, 30),
created: Time.zone.now)

Import::BroadcastMapping::Builder::AirtimeDb.any_instance
.expects(:warn)
.with('Creating default broadcast from Fri, 01 Jan 2016 08:30:00 +0100 to 09:00:00.')
Import::BroadcastMapping::Builder::AirtimeDb.any_instance
.expects(:warn)
.with('Creating default broadcast from Fri, 01 Jan 2016 09:30:00 +0100 to 11:00:00.')

builder = new_builder(recordings)
mappings = builder.run

assert_equal 4, mappings.size
assert_equal shows(:klangbecken).id, mappings.second.show.id
assert_equal Time.zone.local(2016, 1, 1, 8, 30), mappings.second.started_at
assert_equal Time.zone.local(2016, 1, 1, 9), mappings.second.finished_at
assert_equal shows(:klangbecken).id, mappings.last.show.id
assert_equal Time.zone.local(2016, 1, 1, 9, 30), mappings.last.started_at
assert_equal Time.zone.local(2016, 1, 1, 11), mappings.last.finished_at
end

test 'creates broadcast for default show for unmapped recordings for entire duration' do
Rails.application.secrets.import_default_show_id = shows(:klangbecken).id
recordings = build_recordings('2016-01-01T080000+0100_060.mp3',
'2016-01-01T090000+0100_060.mp3')

Import::BroadcastMapping::Builder::AirtimeDb.any_instance
.expects(:warn)
.with('Creating default broadcast from Fri, 01 Jan 2016 08:00:00 +0100 to 10:00:00.')

builder = new_builder(recordings)
mappings = builder.run

assert_equal 1, mappings.size
assert_equal shows(:klangbecken).id, mappings.first.show.id
assert_equal Time.zone.local(2016, 1, 1, 8), mappings.first.started_at
assert_equal Time.zone.local(2016, 1, 1, 10), mappings.first.finished_at
end

private

def new_builder(recordings)
Expand Down
10 changes: 10 additions & 0 deletions test/services/import/broadcast_mapping_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ class Import::BroadcastMappingTest < ActiveSupport::TestCase
assert_equal shows(:info), show
end

test '#assign_show_attrs uses an existing show and keeps details' do
shows(:info).update!(details: 'RaBe Info')
mapping.assign_show(name: 'Info', details: 'Info')

assert_equal 'Info', show.name
assert_equal 'RaBe Info', show.details
assert_equal profiles(:important), show.profile
assert_equal shows(:info), show
end

test '#assign_broadcast_attrs creates a new broadcast' do
mapping.assign_show(name: 'Info')
mapping.assign_broadcast(started_at: Time.local(2016, 1, 1, 11),
Expand Down

0 comments on commit 5ca6b65

Please sign in to comment.