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
data/lib/kaede/dbus.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'kaede/dbus'
|
3
|
+
|
4
|
+
module Kaede
|
5
|
+
module DBus
|
6
|
+
class Generator
|
7
|
+
def generate_policy(user)
|
8
|
+
Nokogiri::XML::Builder.new do |xml|
|
9
|
+
xml.comment 'Put this policy configuration file into /etc/dbus-1/system.d'
|
10
|
+
xml.doc.create_internal_subset(
|
11
|
+
'busconfig',
|
12
|
+
'-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN',
|
13
|
+
'http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd',
|
14
|
+
)
|
15
|
+
xml.busconfig do
|
16
|
+
xml.policy(user: 'root') do
|
17
|
+
xml.allow(own: DESTINATION)
|
18
|
+
end
|
19
|
+
xml.policy(user: user) do
|
20
|
+
xml.allow(own: DESTINATION)
|
21
|
+
end
|
22
|
+
|
23
|
+
xml.policy(context: 'default') do
|
24
|
+
xml.allow(send_destination: DESTINATION)
|
25
|
+
xml.allow(receive_sender: DESTINATION)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end.to_xml
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'dbus'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Kaede
|
6
|
+
module DBus
|
7
|
+
class Program < ::DBus::Object
|
8
|
+
PATH = '/cc/wanko/kaede1/program'
|
9
|
+
PROPERTY_INTERFACE = 'org.freedesktop.DBus.Properties'
|
10
|
+
INTROSPECT_INTERFACE = 'org.freedesktop.DBus.Introspectable'
|
11
|
+
PROGRAM_INTERFACE = 'cc.wanko.kaede1.Program'
|
12
|
+
|
13
|
+
def initialize(program, enqueued_at)
|
14
|
+
super("#{PATH}/#{program.pid}")
|
15
|
+
@program = program
|
16
|
+
@enqueued_at = enqueued_at
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_xml
|
20
|
+
Nokogiri::XML::Builder.new do |xml|
|
21
|
+
xml.doc.create_internal_subset(
|
22
|
+
'node',
|
23
|
+
'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN',
|
24
|
+
'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd',
|
25
|
+
)
|
26
|
+
xml.node do
|
27
|
+
xml.interface(name: INTROSPECT_INTERFACE) do
|
28
|
+
xml.method_(name: 'Introspect') do
|
29
|
+
xml.arg(name: 'data', direction: 'out', type: 's')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
xml.interface(name: PROPERTY_INTERFACE) do
|
34
|
+
xml.method_(name: 'Get') do
|
35
|
+
xml.arg(name: 'interface', direction: 'in', type: 's')
|
36
|
+
xml.arg(name: 'property', direction: 'in', type: 's')
|
37
|
+
xml.arg(name: 'value', direction: 'out', type: 'v')
|
38
|
+
end
|
39
|
+
|
40
|
+
xml.method_(name: 'GetAll') do
|
41
|
+
xml.arg(name: 'interface', direction: 'in', type: 's')
|
42
|
+
xml.arg(name: 'properties', direction: 'out', type: 'a{sv}')
|
43
|
+
end
|
44
|
+
|
45
|
+
xml.method_(name: 'Set') do
|
46
|
+
xml.arg(name: 'interface', direction: 'in', type: 's')
|
47
|
+
xml.arg(name: 'property', direction: 'in', type: 's')
|
48
|
+
xml.arg(name: 'value', direction: 'in', type: 'v')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
xml.interface(name: PROGRAM_INTERFACE) do
|
53
|
+
properties.each_key do |key|
|
54
|
+
xml.property(name: key, type: 's', access: 'read')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end.to_xml
|
59
|
+
end
|
60
|
+
|
61
|
+
dbus_interface PROPERTY_INTERFACE do
|
62
|
+
dbus_method :Get, 'in interface:s, in property:s, out value:v' do |iface, prop|
|
63
|
+
case iface
|
64
|
+
when PROGRAM_INTERFACE
|
65
|
+
if properties.has_key?(prop)
|
66
|
+
[properties[prop]]
|
67
|
+
else
|
68
|
+
raise_unknown_property!
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise_unknown_property!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
dbus_method :GetAll, 'in interface:s, out properties:a{sv}' do |iface|
|
76
|
+
case iface
|
77
|
+
when PROGRAM_INTERFACE
|
78
|
+
[properties]
|
79
|
+
when PROGRAM_INTERFACE
|
80
|
+
[{}]
|
81
|
+
else
|
82
|
+
unknown_interface!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
dbus_method :Set, 'in interface:s, in property:s, in value:v' do |iface, prop, val|
|
87
|
+
raise_access_denied!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
dbus_interface PROGRAM_INTERFACE do
|
92
|
+
end
|
93
|
+
|
94
|
+
def properties
|
95
|
+
@properties ||= {
|
96
|
+
'Pid' => @program.pid,
|
97
|
+
'Tid' => @program.tid,
|
98
|
+
'StartTime' => @program.start_time.iso8601,
|
99
|
+
'EndTime' => @program.end_time.iso8601,
|
100
|
+
'ChannelName' => @program.channel_name,
|
101
|
+
'ChannelForSyoboi' => @program.channel_for_syoboi,
|
102
|
+
'ChannelForRecorder' => @program.channel_for_recorder,
|
103
|
+
'Count' => @program.count,
|
104
|
+
'StartOffset' => @program.start_offset,
|
105
|
+
'SubTitle' => @program.subtitle,
|
106
|
+
'Title' => @program.title,
|
107
|
+
'Comment' => @program.comment,
|
108
|
+
'EnqueuedAt' => @enqueued_at.iso8601,
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def raise_unknown_interface!
|
113
|
+
raise ::DBus.error('org.freedesktop.DBus.Error.UnknownInterface')
|
114
|
+
end
|
115
|
+
|
116
|
+
def raise_unknown_property!
|
117
|
+
raise ::DBus.error('org.freedesktop.DBus.Error.UnknownProperty')
|
118
|
+
end
|
119
|
+
|
120
|
+
def raise_access_denied!
|
121
|
+
raise ::DBus.error('org.freedesktop.DBus.Error.AccessDenied')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'dbus'
|
2
|
+
|
3
|
+
module Kaede
|
4
|
+
module DBus
|
5
|
+
class Scheduler < ::DBus::Object
|
6
|
+
PATH = '/cc/wanko/kaede1/scheduler'
|
7
|
+
SCHEDULER_INTERFACE = 'cc.wanko.kaede1.Scheduler'
|
8
|
+
|
9
|
+
def initialize(reload_event, stop_event)
|
10
|
+
super(PATH)
|
11
|
+
@reload_event = reload_event
|
12
|
+
@stop_event = stop_event
|
13
|
+
end
|
14
|
+
|
15
|
+
dbus_interface SCHEDULER_INTERFACE do
|
16
|
+
dbus_method :Reload do
|
17
|
+
@reload_event.incr(1)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
dbus_method :Stop do
|
22
|
+
@stop_event.incr(1)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'kaede'
|
3
|
+
|
4
|
+
module Kaede
|
5
|
+
class Notifier
|
6
|
+
def initialize
|
7
|
+
@twitter = Kaede.config.twitter
|
8
|
+
@twitter_target = Kaede.config.twitter_target
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify_before_record(program)
|
12
|
+
tweet("#{format_title(program)}を録画する")
|
13
|
+
end
|
14
|
+
|
15
|
+
def notify_after_record(program)
|
16
|
+
tweet(
|
17
|
+
sprintf(
|
18
|
+
"%sを録画した。ファイルサイズ約%.2fGB。残り約%dGB\n",
|
19
|
+
format_title(program),
|
20
|
+
ts_filesize(program),
|
21
|
+
available_disk,
|
22
|
+
)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def notify_exception(exception, program)
|
27
|
+
msg = "#{program.title}(PID #{program.pid}) の録画中に #{exception.class} で失敗した……"
|
28
|
+
if @twitter_target
|
29
|
+
msg = "@#{@twitter_target} #{msg}"
|
30
|
+
end
|
31
|
+
tweet(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
def format_title(program)
|
35
|
+
buf = "#{program.channel_name}で「#{program.title}"
|
36
|
+
if program.count
|
37
|
+
buf += " ##{program.count}"
|
38
|
+
end
|
39
|
+
buf += " #{program.subtitle}」"
|
40
|
+
buf
|
41
|
+
end
|
42
|
+
|
43
|
+
def ts_filesize(program)
|
44
|
+
in_gigabyte(record_path(program).size.to_f)
|
45
|
+
end
|
46
|
+
|
47
|
+
# FIXME: duplicate
|
48
|
+
def record_path(program)
|
49
|
+
Kaede.config.record_dir.join("#{program.tid}_#{program.pid}.ts")
|
50
|
+
end
|
51
|
+
|
52
|
+
def available_disk
|
53
|
+
_, avail = `#{Kaede.config.statvfs} #{Kaede.config.record_dir}`.chomp.split(/\s/, 2).map(&:to_i)
|
54
|
+
in_gigabyte(avail)
|
55
|
+
end
|
56
|
+
|
57
|
+
def in_gigabyte(size)
|
58
|
+
size / (1024 * 1024 * 1024)
|
59
|
+
end
|
60
|
+
|
61
|
+
def tweet(text)
|
62
|
+
return unless @twitter
|
63
|
+
Thread.start do
|
64
|
+
begin
|
65
|
+
@twitter.update(text)
|
66
|
+
rescue Exception => e
|
67
|
+
$stderr.puts "Failed to tweet: #{text}: #{e.class}: #{e.message}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Kaede
|
4
|
+
class Program < Struct.new(:pid, :tid, :start_time, :end_time, :channel_name, :channel_for_syoboi, :channel_for_recorder, :count, :start_offset, :subtitle, :title, :comment)
|
5
|
+
def self.from_xml(doc)
|
6
|
+
doc.xpath('//progitem').map do |item|
|
7
|
+
prog = self.new
|
8
|
+
prog.pid = item['pid'].to_i
|
9
|
+
prog.tid = item['tid'].to_i
|
10
|
+
prog.start_time = Time.parse(item['sttime'])
|
11
|
+
prog.end_time = Time.parse(item['edtime'])
|
12
|
+
prog.channel_for_syoboi = item['chid'].to_i
|
13
|
+
prog.count = item['count'].to_i
|
14
|
+
prog.start_offset = item['stoffset'].to_i # in second
|
15
|
+
prog.subtitle = item['subtitle']
|
16
|
+
prog.title = item['title']
|
17
|
+
prog.comment = item['progcomment']
|
18
|
+
prog
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def syoboi_url
|
23
|
+
"http://cal.syoboi.jp/tid/#{tid}##{pid}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def formatted_fname
|
27
|
+
@formatted_fname ||= format_fname
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_fname
|
31
|
+
fname = "#{tid}_#{pid} #{title} ##{count} #{subtitle}#{comment.empty? ? '' : " (#{comment})"} at #{channel_name}"
|
32
|
+
fname = fname.gsub('/', '/')
|
33
|
+
if fname.bytesize >= 200
|
34
|
+
fname = "#{tid}_#{pid} #{title} ##{count} at #{channel_name}"
|
35
|
+
end
|
36
|
+
fname
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'open3'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Kaede
|
6
|
+
class Recorder
|
7
|
+
def initialize(notifier)
|
8
|
+
@notifier = notifier
|
9
|
+
end
|
10
|
+
|
11
|
+
def record(db, pid)
|
12
|
+
program = db.get_program(pid)
|
13
|
+
before_record(program)
|
14
|
+
|
15
|
+
puts "Start #{pid} #{program.syoboi_url}"
|
16
|
+
do_record(program)
|
17
|
+
|
18
|
+
program = db.get_program(pid)
|
19
|
+
puts "Done #{pid} #{program.syoboi_url}"
|
20
|
+
after_record(program)
|
21
|
+
rescue Exception => e
|
22
|
+
@notifier.notify_exception(e, program)
|
23
|
+
raise e
|
24
|
+
end
|
25
|
+
|
26
|
+
def record_path(program)
|
27
|
+
Kaede.config.record_dir.join("#{program.tid}_#{program.pid}.ts")
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_path(program)
|
31
|
+
Kaede.config.cache_dir.join("#{program.tid}_#{program.pid}.cache.ts")
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_ass_path(program)
|
35
|
+
Kaede.config.cache_dir.join("#{program.tid}_#{program.pid}.raw.ass")
|
36
|
+
end
|
37
|
+
|
38
|
+
def cabinet_path(program)
|
39
|
+
Kaede.config.cabinet_dir.join("#{program.formatted_fname}.ts")
|
40
|
+
end
|
41
|
+
|
42
|
+
def cabinet_ass_path(program)
|
43
|
+
Kaede.config.cabinet_dir.join("#{program.formatted_fname}.raw.ass")
|
44
|
+
end
|
45
|
+
|
46
|
+
def do_record(program)
|
47
|
+
spawn_recpt1(program)
|
48
|
+
spawn_tail(program)
|
49
|
+
spawn_b25(program)
|
50
|
+
spawn_ass(program)
|
51
|
+
spawn_repeater
|
52
|
+
wait_recpt1
|
53
|
+
finalize
|
54
|
+
end
|
55
|
+
|
56
|
+
def spawn_recpt1(program)
|
57
|
+
path = record_path(program)
|
58
|
+
path.open('w') {}
|
59
|
+
@recpt1_pid = spawn(Kaede.config.recpt1.to_s, program.channel_for_recorder.to_s, calculate_duration(program).to_s, path.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculate_duration(program)
|
63
|
+
duration = (program.end_time - program.start_time).to_i - 10
|
64
|
+
end_datetime = program.end_time.to_datetime
|
65
|
+
if end_datetime.sunday? && end_datetime.hour == 22 && end_datetime.min == 27
|
66
|
+
# For MX
|
67
|
+
duration += 3 * 60
|
68
|
+
elsif program.channel_name =~ /NHK/
|
69
|
+
# For NHK
|
70
|
+
duration += 25
|
71
|
+
end
|
72
|
+
duration
|
73
|
+
end
|
74
|
+
|
75
|
+
def spawn_tail(program)
|
76
|
+
@tail_pipe_r, @tail_pipe_w = IO.pipe
|
77
|
+
@tail_pid = spawn('tail', '-f', record_path(program).to_s, out: @tail_pipe_w)
|
78
|
+
@tail_pipe_w.close
|
79
|
+
end
|
80
|
+
|
81
|
+
def spawn_b25(program)
|
82
|
+
@b25_pipe_r, @b25_pipe_w = IO.pipe
|
83
|
+
@b25_pid = spawn(Kaede.config.b25.to_s, '-v0', '-s1', '-m1', '/dev/stdin', cache_path(program).to_s, in: @b25_pipe_r)
|
84
|
+
@b25_pipe_r.close
|
85
|
+
end
|
86
|
+
|
87
|
+
def spawn_ass(program)
|
88
|
+
@ass_pipe_r, @ass_pipe_w = IO.pipe
|
89
|
+
@ass_pid = spawn(Kaede.config.assdumper.to_s, '/dev/stdin', in: @ass_pipe_r, out: cache_ass_path(program).to_s)
|
90
|
+
@ass_pipe_r.close
|
91
|
+
end
|
92
|
+
|
93
|
+
BUFSIZ = 188 * 16
|
94
|
+
|
95
|
+
def spawn_repeater
|
96
|
+
@repeater_thread = Thread.start do
|
97
|
+
while buf = @tail_pipe_r.read(BUFSIZ)
|
98
|
+
@b25_pipe_w.write(buf)
|
99
|
+
@ass_pipe_w.write(buf)
|
100
|
+
end
|
101
|
+
@tail_pipe_r.close
|
102
|
+
@b25_pipe_w.close
|
103
|
+
@ass_pipe_w.close
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def wait_recpt1
|
108
|
+
Process.waitpid(@recpt1_pid)
|
109
|
+
end
|
110
|
+
|
111
|
+
def finalize
|
112
|
+
Process.kill(:INT, @tail_pid)
|
113
|
+
Process.waitpid(@tail_pid)
|
114
|
+
@repeater_thread.join
|
115
|
+
Process.waitpid(@b25_pid)
|
116
|
+
Process.waitpid(@ass_pid)
|
117
|
+
end
|
118
|
+
|
119
|
+
def before_record(program)
|
120
|
+
@notifier.notify_before_record(program)
|
121
|
+
end
|
122
|
+
|
123
|
+
def after_record(program)
|
124
|
+
@notifier.notify_after_record(program)
|
125
|
+
move_ass_to_cabinet(program)
|
126
|
+
clean_ts(program)
|
127
|
+
enqueue_to_redis(program)
|
128
|
+
FileUtils.rm(cache_path(program).to_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
def move_ass_to_cabinet(program)
|
132
|
+
ass_path = cache_ass_path(program)
|
133
|
+
if ass_path.size == 0
|
134
|
+
ass_path.unlink
|
135
|
+
else
|
136
|
+
FileUtils.mv(ass_path.to_s, cabinet_ass_path(program).to_s)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def clean_ts(program)
|
141
|
+
unless system(Kaede.config.clean_ts.to_s, cache_path(program).to_s, cabinet_path(program).to_s)
|
142
|
+
raise "clean-ts failure: #{program.formatted_fname}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def enqueue_to_redis(program)
|
147
|
+
Kaede.config.redis.rpush(Kaede.config.redis_queue, program.formatted_fname)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|