kaede 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/bin/kaede +4 -0
- data/kaede.gemspec +36 -0
- data/kaede.rb.sample +25 -0
- data/kaede.service.sample +16 -0
- data/lib/kaede.rb +12 -0
- data/lib/kaede/channel.rb +4 -0
- data/lib/kaede/cli.rb +81 -0
- data/lib/kaede/config.rb +33 -0
- data/lib/kaede/database.rb +146 -0
- data/lib/kaede/dbus.rb +5 -0
- data/lib/kaede/dbus/generator.rb +32 -0
- data/lib/kaede/dbus/program.rb +125 -0
- data/lib/kaede/dbus/scheduler.rb +28 -0
- data/lib/kaede/notifier.rb +72 -0
- data/lib/kaede/program.rb +39 -0
- data/lib/kaede/recorder.rb +150 -0
- data/lib/kaede/scheduler.rb +174 -0
- data/lib/kaede/syoboi_calendar.rb +39 -0
- data/lib/kaede/updater.rb +61 -0
- data/lib/kaede/version.rb +3 -0
- data/spec/fixtures/vcr/cal_chk/all.yml +6906 -0
- data/spec/fixtures/vcr/cal_chk/days7.yml +3581 -0
- data/spec/kaede/notifier_spec.rb +60 -0
- data/spec/kaede/recorder_spec.rb +176 -0
- data/spec/kaede/scheduler_spec.rb +55 -0
- data/spec/kaede/syoboi_calendar_spec.rb +20 -0
- data/spec/kaede/updater_spec.rb +67 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/tools/assdumper +2 -0
- data/spec/tools/b25 +6 -0
- data/spec/tools/clean-ts +2 -0
- data/spec/tools/recpt1 +8 -0
- data/spec/tools/statvfs +2 -0
- metadata +309 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
require 'kaede/notifier'
|
5
|
+
require 'kaede/program'
|
6
|
+
|
7
|
+
describe Kaede::Notifier do
|
8
|
+
let(:notifier) { described_class.new }
|
9
|
+
let(:duration) { 30 }
|
10
|
+
let(:program) { Kaede::Program.new(1234, 5678, Time.now, Time.now + duration, nil, 19, 9, '6', 0, 'sub', 'title', 'comment') }
|
11
|
+
|
12
|
+
describe '#notify_before_record' do
|
13
|
+
it 'tweets' do
|
14
|
+
expect(notifier).to receive(:tweet).with(/title #6 sub/)
|
15
|
+
notifier.notify_before_record(program)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#notify_after_record' do
|
20
|
+
before do
|
21
|
+
Kaede.configure do |config|
|
22
|
+
config.record_dir = @tmpdir.join('record').tap(&:mkpath)
|
23
|
+
tools = @topdir.join('tools')
|
24
|
+
config.statvfs = tools.join('statvfs')
|
25
|
+
end
|
26
|
+
|
27
|
+
notifier.record_path(program).open('w') {}
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'tweets' do
|
31
|
+
expect(notifier).to receive(:tweet).with(/title #6 sub.*0\.00GB/)
|
32
|
+
notifier.notify_after_record(program)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#notify_exception' do
|
37
|
+
class MyException < Exception
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'tweets' do
|
41
|
+
Kaede.configure do |config|
|
42
|
+
config.twitter_target = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
expect(notifier).to receive(:tweet).with(/MyException で失敗した/)
|
46
|
+
notifier.notify_exception(MyException.new, program)
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when twitter_target is set' do
|
50
|
+
it 'tweets to twitter_target' do
|
51
|
+
Kaede.configure do |config|
|
52
|
+
config.twitter_target = 'eagletmt'
|
53
|
+
end
|
54
|
+
|
55
|
+
expect(notifier).to receive(:tweet).with(/\A@eagletmt .* MyException で失敗した/)
|
56
|
+
notifier.notify_exception(MyException.new, program)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'kaede'
|
4
|
+
require 'kaede/database'
|
5
|
+
require 'kaede/recorder'
|
6
|
+
require 'kaede/updater'
|
7
|
+
|
8
|
+
describe Kaede::Recorder do
|
9
|
+
let(:notifier) { double('Notifier') }
|
10
|
+
let(:recorder) { described_class.new(notifier) }
|
11
|
+
let(:db) { Kaede::Database.new(':memory:') }
|
12
|
+
let(:job) { db.get_jobs.first }
|
13
|
+
let(:program) { db.get_program(job[:pid]) }
|
14
|
+
let(:duration) { 30 }
|
15
|
+
|
16
|
+
let(:formatted_fname) { '5678_1234 title #6 sub (comment) at MX' }
|
17
|
+
let(:record_dir) { @tmpdir.join('record').tap(&:mkpath) }
|
18
|
+
let(:record_path) { recorder.record_path(program) }
|
19
|
+
let(:cache_dir) { @tmpdir.join('cache').tap(&:mkpath) }
|
20
|
+
let(:cache_path) { recorder.cache_path(program) }
|
21
|
+
let(:cache_ass_path) { recorder.cache_ass_path(program) }
|
22
|
+
let(:cabinet_dir) { @tmpdir.join('cabinet').tap(&:mkpath) }
|
23
|
+
let(:cabinet_path) { recorder.cabinet_path(program) }
|
24
|
+
let(:cabinet_ass_path) { recorder.cabinet_ass_path(program) }
|
25
|
+
|
26
|
+
before do
|
27
|
+
db.add_channel(Kaede::Channel.new(nil, 'MX', 9, 19))
|
28
|
+
channel = db.get_channels.first
|
29
|
+
program = Kaede::Program.new(1234, 5678, Time.now, Time.now + duration, nil, 19, 9, '6', 0, 'sub', 'title', 'comment')
|
30
|
+
db.update_program(program, channel)
|
31
|
+
db.update_job(program.pid, Time.now + 5)
|
32
|
+
|
33
|
+
Kaede.configure do |config|
|
34
|
+
config.redis = double('redis')
|
35
|
+
tools = @topdir.join('tools')
|
36
|
+
config.recpt1 = tools.join('recpt1')
|
37
|
+
config.b25 = tools.join('b25')
|
38
|
+
config.assdumper = tools.join('assdumper')
|
39
|
+
config.statvfs = tools.join('statvfs')
|
40
|
+
config.clean_ts = tools.join('clean-ts')
|
41
|
+
config.record_dir = record_dir
|
42
|
+
config.cache_dir = cache_dir
|
43
|
+
config.cabinet_dir = cabinet_dir
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#record' do
|
48
|
+
it 'calls before_record -> do_record -> after_record' do
|
49
|
+
expect(recorder).to receive(:before_record).ordered.with(program)
|
50
|
+
expect(recorder).to receive(:do_record).ordered.with(program)
|
51
|
+
expect(recorder).to receive(:after_record).ordered.with(program)
|
52
|
+
|
53
|
+
expect { recorder.record(db, job[:pid]) }.to output(/Start #{job[:pid]}.*Done #{job[:pid]}/m).to_stdout
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'notifies exception' do
|
57
|
+
e = Class.new(Exception)
|
58
|
+
expect(notifier).to receive(:notify_exception).with(e, program)
|
59
|
+
allow(recorder).to receive(:before_record)
|
60
|
+
allow(recorder).to receive(:do_record).and_raise(e)
|
61
|
+
allow(recorder).to receive(:after_record)
|
62
|
+
|
63
|
+
expect { recorder.record(db, job[:pid]) }.to raise_error(e)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#after_record' do
|
68
|
+
before do
|
69
|
+
record_path.open('w') {}
|
70
|
+
cache_path.open('w') {}
|
71
|
+
cache_ass_path.open('w') { |f| f.puts 'ass' }
|
72
|
+
|
73
|
+
allow(Kaede.config.redis).to receive(:rpush)
|
74
|
+
allow(notifier).to receive(:notify_after_record).with(program)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'calls Notifier#notify_after_record' do
|
78
|
+
expect(notifier).to receive(:notify_after_record).with(program)
|
79
|
+
recorder.after_record(program)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'cleans cached TS' do
|
83
|
+
expect(cache_path).to be_exist
|
84
|
+
expect(cabinet_path).to_not be_exist
|
85
|
+
|
86
|
+
recorder.after_record(program)
|
87
|
+
|
88
|
+
expect(cache_path).to_not be_exist
|
89
|
+
expect(cabinet_path).to be_exist
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'moves ass' do
|
93
|
+
expect(cache_ass_path).to be_exist
|
94
|
+
expect(cabinet_ass_path).to_not be_exist
|
95
|
+
|
96
|
+
recorder.after_record(program)
|
97
|
+
|
98
|
+
expect(cache_ass_path).to_not be_exist
|
99
|
+
expect(cabinet_ass_path).to be_exist
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'enqueues to redis' do
|
103
|
+
expect(Kaede.config.redis).to receive(:rpush).with(Kaede.config.redis_queue, formatted_fname)
|
104
|
+
|
105
|
+
recorder.after_record(program)
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'with empty ass' do
|
109
|
+
before do
|
110
|
+
cache_ass_path.open('w') {}
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'removes ass' do
|
114
|
+
expect(cache_ass_path).to be_exist
|
115
|
+
expect(cabinet_ass_path).to_not be_exist
|
116
|
+
|
117
|
+
recorder.after_record(program)
|
118
|
+
|
119
|
+
expect(cache_ass_path).to_not be_exist
|
120
|
+
expect(cabinet_ass_path).to_not be_exist
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#do_record' do
|
126
|
+
it 'creates raw TS in record dir' do
|
127
|
+
expect(record_path).to_not be_exist
|
128
|
+
recorder.do_record(program)
|
129
|
+
expect(record_path).to be_exist
|
130
|
+
expect(record_path.read.chomp).to eq("#{program.channel_for_recorder} #{duration - 10}")
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'creates b25-decoded TS in cache dir' do
|
134
|
+
expect(cache_path).to_not be_exist
|
135
|
+
recorder.do_record(program)
|
136
|
+
expect(cache_path).to be_exist
|
137
|
+
expect(cache_path.read.chomp).to eq(record_path.read.chomp.reverse)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'creates ass in cache dir' do
|
141
|
+
expect(cache_ass_path).to_not be_exist
|
142
|
+
recorder.do_record(program)
|
143
|
+
expect(cache_ass_path).to be_exist
|
144
|
+
expect(cache_ass_path.read.chomp).to eq(record_path.read.chomp.gsub('1', '2'))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#calculate_duration' do
|
149
|
+
subject { recorder.calculate_duration(program) }
|
150
|
+
|
151
|
+
it 'returns duration' do
|
152
|
+
is_expected.to eq(duration - 10)
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'with NHK (which has no CM)' do
|
156
|
+
before do
|
157
|
+
program.channel_name = 'NHK-G'
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'has additional record time' do
|
161
|
+
is_expected.to eq(duration + Kaede::Updater::JOB_TIME_GAP)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'with MX on Sunday 22:00' do
|
166
|
+
before do
|
167
|
+
program.start_time = Time.local(2014, 4, 6, 22, 00)
|
168
|
+
program.end_time = Time.local(2014, 4, 6, 22, 27)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'has 30min duration even if Syoboi Calendar says 27min' do
|
172
|
+
is_expected.to eq(30*60 - 10)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'timeout'
|
5
|
+
require 'kaede'
|
6
|
+
require 'kaede/database'
|
7
|
+
require 'kaede/recorder'
|
8
|
+
require 'kaede/scheduler'
|
9
|
+
|
10
|
+
describe Kaede::Scheduler do
|
11
|
+
let(:db_file) { Tempfile.open('kaede.db') }
|
12
|
+
let(:db) { Kaede::Database.new(db_file.path) }
|
13
|
+
|
14
|
+
describe '.start' do
|
15
|
+
let(:program) { Kaede::Program.new(1234, 5678, Time.now, Time.now + 30, nil, 19, 9, '5.5', 0, 'sub', 'title', '') }
|
16
|
+
|
17
|
+
before do
|
18
|
+
db.add_channel(Kaede::Channel.new(nil, 'MX', 9, 19))
|
19
|
+
channel = db.get_channels.first
|
20
|
+
db.update_program(program, channel)
|
21
|
+
db.update_job(program.pid, Time.now + 5)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'works' do
|
25
|
+
r, w = IO.pipe
|
26
|
+
allow_any_instance_of(Kaede::Recorder).to receive(:record) { |recorder, db, pid|
|
27
|
+
program = db.get_program(pid)
|
28
|
+
puts "Record #{program.pid}"
|
29
|
+
}
|
30
|
+
pid = fork do
|
31
|
+
r.close
|
32
|
+
$stdout.reopen(w)
|
33
|
+
described_class.setup(db)
|
34
|
+
described_class.start
|
35
|
+
end
|
36
|
+
w.close
|
37
|
+
|
38
|
+
expect(db.get_jobs.size).to eq(1)
|
39
|
+
begin
|
40
|
+
Timeout.timeout(10) do
|
41
|
+
while s = r.gets.chomp
|
42
|
+
if s =~ /\ARecord (\d+)\z/
|
43
|
+
expect($1.to_i).to eq(program.pid)
|
44
|
+
break
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
ensure
|
49
|
+
Process.kill(:QUIT, pid)
|
50
|
+
Process.waitpid(pid)
|
51
|
+
end
|
52
|
+
expect(db.get_jobs.size).to eq(0)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'kaede/syoboi_calendar'
|
3
|
+
|
4
|
+
describe Kaede::SyoboiCalendar do
|
5
|
+
let(:client) { described_class.new }
|
6
|
+
|
7
|
+
describe '#cal_chk' do
|
8
|
+
it 'works' do
|
9
|
+
VCR.use_cassette('cal_chk/all') do
|
10
|
+
programs = client.cal_chk
|
11
|
+
programs.each do |program|
|
12
|
+
program.members.each do |attr|
|
13
|
+
next if [:channel_name, :channel_for_recorder].include?(attr)
|
14
|
+
expect(program[attr]).to_not be_nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'kaede/channel'
|
3
|
+
require 'kaede/database'
|
4
|
+
require 'kaede/syoboi_calendar'
|
5
|
+
require 'kaede/updater'
|
6
|
+
|
7
|
+
describe Kaede::Updater do
|
8
|
+
let(:db) { Kaede::Database.new(':memory:') }
|
9
|
+
let(:syobocal) { Kaede::SyoboiCalendar.new }
|
10
|
+
let(:updater) { described_class.new(db, syobocal) }
|
11
|
+
|
12
|
+
describe '#update' do
|
13
|
+
let(:channel) { Kaede::Channel.new(nil, 'MX', 9, 19) }
|
14
|
+
let(:tracking_tid) { 3225 }
|
15
|
+
|
16
|
+
let(:expected_pid) { 279855 }
|
17
|
+
let(:expected_enqueued_at) { Time.local(2014, 3, 6, 21, 59, 45) }
|
18
|
+
|
19
|
+
around do |example|
|
20
|
+
Timecop.travel(Time.local(2014, 3, 5, 18, 0)) do
|
21
|
+
example.run
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
db.add_channel(channel)
|
27
|
+
db.add_tracking_title(tracking_tid)
|
28
|
+
|
29
|
+
allow(updater).to receive(:reload_scheduler)
|
30
|
+
@orig_stderr = $stderr
|
31
|
+
$stderr = open(File::NULL, 'w')
|
32
|
+
end
|
33
|
+
|
34
|
+
after do
|
35
|
+
$stderr = @orig_stderr
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'inserts new jobs' do
|
39
|
+
VCR.use_cassette('cal_chk/days7') do
|
40
|
+
updater.update
|
41
|
+
end
|
42
|
+
|
43
|
+
jobs = db.get_jobs
|
44
|
+
expect(jobs.size).to eq(1)
|
45
|
+
job = jobs[0]
|
46
|
+
expect(job[:pid]).to eq(expected_pid)
|
47
|
+
expect(job[:enqueued_at]).to eq(expected_enqueued_at)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'updates existing job' do
|
51
|
+
dummy_time = Time.local(2014, 3, 6, 20, 29, 45)
|
52
|
+
|
53
|
+
VCR.use_cassette('cal_chk/days7') do
|
54
|
+
updater.update
|
55
|
+
end
|
56
|
+
db.update_job(expected_pid, dummy_time)
|
57
|
+
job1 = db.get_jobs[0]
|
58
|
+
expect(job1[:enqueued_at]).to eq(dummy_time)
|
59
|
+
VCR.use_cassette('cal_chk/days7') do
|
60
|
+
updater.update
|
61
|
+
end
|
62
|
+
job2 = db.get_jobs[0]
|
63
|
+
expect(job2[:enqueued_at]).to eq(expected_enqueued_at)
|
64
|
+
expect(job2[:pid]).to eq(job1[:pid])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
require 'simplecov'
|
3
|
+
require 'timecop'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'vcr'
|
6
|
+
require 'webmock/rspec'
|
7
|
+
|
8
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
9
|
+
SimpleCov::Formatter::HTMLFormatter,
|
10
|
+
Coveralls::SimpleCov::Formatter,
|
11
|
+
]
|
12
|
+
SimpleCov.start do
|
13
|
+
add_filter Bundler.bundle_path.to_s
|
14
|
+
add_filter File.dirname(__FILE__)
|
15
|
+
|
16
|
+
# Kaede::Scheduler is tested in another process.
|
17
|
+
add_filter 'lib/kaede/scheduler.rb'
|
18
|
+
end
|
19
|
+
|
20
|
+
VCR.configure do |config|
|
21
|
+
config.cassette_library_dir = 'spec/fixtures/vcr'
|
22
|
+
config.hook_into :webmock
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec.configure do |config|
|
26
|
+
# config.profile_examples = 10
|
27
|
+
|
28
|
+
config.order = :random
|
29
|
+
Kernel.srand config.seed
|
30
|
+
|
31
|
+
config.expect_with :rspec do |expectations|
|
32
|
+
expectations.syntax = :expect
|
33
|
+
end
|
34
|
+
|
35
|
+
config.mock_with :rspec do |mocks|
|
36
|
+
mocks.syntax = :expect
|
37
|
+
mocks.verify_partial_doubles = true
|
38
|
+
end
|
39
|
+
|
40
|
+
config.before :each do
|
41
|
+
@topdir = Pathname.new(__FILE__).parent
|
42
|
+
end
|
43
|
+
|
44
|
+
config.around :each do |example|
|
45
|
+
Dir.mktmpdir('kaede') do |dir|
|
46
|
+
@tmpdir = Pathname.new(dir)
|
47
|
+
example.run
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/tools/b25
ADDED
data/spec/tools/clean-ts
ADDED