kaede 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Kaede
2
+ module DBus
3
+ DESTINATION = 'cc.wanko.kaede1'
4
+ end
5
+ end
@@ -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