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.
@@ -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