kaede 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|