kaede 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 763474045525030a9ac8d0ef492f763f325dee9b
4
- data.tar.gz: 03949c0dfd3f2c3e6c7764b36c08c2c9943d79e7
3
+ metadata.gz: 0698a5193297bca2036eaaaf8bab6af7751653c9
4
+ data.tar.gz: 79fd8fab1adfe6bd0e31406daed1af3438881e90
5
5
  SHA512:
6
- metadata.gz: a513df57390c5a072726e9c9aeeaffe62400f3fe69dd0a2d91f191d2bf8d058edda6b0c7487d8168ce48eadd20345c1dc69a5a2d0937021966d80217c774e8cc
7
- data.tar.gz: b5dc2c6b26e45d3b81e7bf34b72b1ac161d2b63567a8684e55b49b38b274af1aab7d04d7298861441cf30514627691b1b78ae802ede2378405b5623de21967b0
6
+ metadata.gz: 5d3760d621577b1afb34a8c610cbeeb9f8e9dcc8c7be6e7a8eb81b7b370a02a6077f7715dcf57bff2887a33f001b300f6161ff93d2b9dc3fc7b6588f2bf8b123
7
+ data.tar.gz: a6b336df58a7627d596ea0aac04fcb79ab222acbae432d05a9a72e8549673e06cf362eb2dcfed8fec22f58a4323addc4855a827b91d463305a47e33aa85bca0d
@@ -2,13 +2,18 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
- - 2.1.1
5
+ - 2.1.2
6
6
  - ruby-head
7
+ env:
8
+ - DB=sqlite
9
+ - DB=postgres
7
10
  before_install:
8
11
  - export NOKOGIRI_USE_SYSTEM_LIBRARIES=1
9
12
  before_script:
10
13
  - bundle exec bin/kaede dbus-policy $USER > kaede.conf
11
14
  - sudo mv kaede.conf /etc/dbus-1/system.d/kaede.conf
15
+ - psql -c "CREATE ROLE kaede WITH LOGIN" -U postgres
16
+ - psql -c "CREATE DATABASE kaede_test WITH OWNER = kaede ENCODING = 'UTF-8' TEMPLATE = template0" -U postgres
12
17
  matrix:
13
18
  allow_failures:
14
19
  - rvm: ruby-head
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Kaede
2
+ [![Gem Version](https://badge.fury.io/rb/kaede.svg)](http://badge.fury.io/rb/kaede)
2
3
  [![Build Status](https://api.travis-ci.org/eagletmt/kaede.svg)](https://travis-ci.org/eagletmt/kaede)
3
4
  [![Coverage Status](https://coveralls.io/repos/eagletmt/kaede/badge.png)](https://coveralls.io/r/eagletmt/kaede)
4
5
  [![Code Climate](https://codeclimate.com/github/eagletmt/kaede.png)](https://codeclimate.com/github/eagletmt/kaede)
@@ -21,7 +22,7 @@ Or install it yourself as:
21
22
 
22
23
  ## Usage
23
24
  ### Requirements
24
- - sqlite3
25
+ - SQLite3 or PostgreSQL
25
26
  - redis
26
27
  - dbus
27
28
  - recpt1
@@ -39,6 +40,8 @@ sudo mv kaede.conf /etc/dbus-1/system.d/kaede.conf
39
40
 
40
41
  cp kaede.rb.sample kaede.rb
41
42
  vim kaede.rb
43
+ gem install pg # gem install sqlite3
44
+ kaede db-prepare
42
45
 
43
46
  cp kaede.service.sample kaede.service
44
47
  vim kaede.service
@@ -20,17 +20,19 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler"
22
22
  spec.add_development_dependency "coveralls"
23
+ spec.add_development_dependency "pg"
23
24
  spec.add_development_dependency "rake"
24
- spec.add_development_dependency "rspec", "~> 3.0.0.beta2"
25
+ spec.add_development_dependency "rspec", ">= 3.0.0"
25
26
  spec.add_development_dependency "simplecov"
27
+ spec.add_development_dependency "sqlite3"
26
28
  spec.add_development_dependency "timecop"
27
29
  spec.add_development_dependency "vcr"
28
30
  spec.add_development_dependency "webmock"
29
- spec.add_dependency "ruby-dbus"
30
31
  spec.add_dependency "nokogiri"
31
32
  spec.add_dependency "redis"
33
+ spec.add_dependency "ruby-dbus"
34
+ spec.add_dependency "sequel"
32
35
  spec.add_dependency "sleepy_penguin"
33
- spec.add_dependency "sqlite3"
34
36
  spec.add_dependency "thor"
35
37
  spec.add_dependency "twitter"
36
38
  end
@@ -5,7 +5,7 @@ Description=Kaede Recorder
5
5
  WorkingDirectory=/home/eagletmt/work/kaede
6
6
  User=eagletmt
7
7
  ExecStart=/usr/bin/bundle exec bin/kaede scheduler -c kaede.rb
8
- ExecReload=/usr/bin/kill -HUP $MAINPID
8
+ ExecReload=/usr/bin/dbus-send --print-reply --system --dest=cc.wanko.kaede1 /cc/wanko/kaede1/scheduler cc.wanko.kaede1.Scheduler.Reload
9
9
  ExecStop=/usr/bin/dbus-send --print-reply --system --dest=cc.wanko.kaede1 /cc/wanko/kaede1/scheduler cc.wanko.kaede1.Scheduler.Stop
10
10
  Restart=always
11
11
  KillMode=none
@@ -16,7 +16,7 @@ module Kaede
16
16
  require 'kaede/scheduler'
17
17
  load_config
18
18
 
19
- db = Kaede::Database.new(Kaede.config.database_path)
19
+ db = Kaede::Database.new(Kaede.config.database_url)
20
20
  Kaede::Scheduler.setup(db)
21
21
  Kaede::Scheduler.start
22
22
  end
@@ -37,7 +37,7 @@ module Kaede
37
37
  require 'kaede/channel'
38
38
  load_config
39
39
 
40
- db = Kaede::Database.new(Kaede.config.database_path)
40
+ db = Kaede::Database.new(Kaede.config.database_url)
41
41
  db.add_channel(Channel.new(nil, name, options[:recorder], options[:syoboi]))
42
42
  end
43
43
 
@@ -46,7 +46,7 @@ module Kaede
46
46
  require 'kaede/database'
47
47
  load_config
48
48
 
49
- db = Kaede::Database.new(Kaede.config.database_path)
49
+ db = Kaede::Database.new(Kaede.config.database_url)
50
50
  db.add_tracking_title(tid.to_i)
51
51
  end
52
52
 
@@ -57,7 +57,7 @@ module Kaede
57
57
  require 'kaede/updater'
58
58
  load_config
59
59
 
60
- db = Kaede::Database.new(Kaede.config.database_path)
60
+ db = Kaede::Database.new(Kaede.config.database_url)
61
61
  syobocal = Kaede::SyoboiCalendar.new
62
62
  Kaede::Updater.new(db, syobocal).update
63
63
  end
@@ -69,6 +69,15 @@ module Kaede
69
69
  puts DBus::Generator.new.generate_policy(user)
70
70
  end
71
71
 
72
+ desc 'db-prepare', 'Create tables'
73
+ def db_prepare
74
+ require 'kaede/database'
75
+ load_config
76
+
77
+ db = Kaede::Database.new(Kaede.config.database_url)
78
+ db.prepare_tables
79
+ end
80
+
72
81
  private
73
82
 
74
83
  def load_config
@@ -3,9 +3,9 @@ require 'redis'
3
3
 
4
4
  module Kaede
5
5
  class Config
6
- attr_accessor :redis, :redis_queue, :twitter, :twitter_target
6
+ attr_accessor :database_url, :redis, :redis_queue, :twitter, :twitter_target
7
7
 
8
- path_attrs = [:b25, :recpt1, :assdumper, :clean_ts, :statvfs, :database_path, :record_dir, :cache_dir, :cabinet_dir]
8
+ path_attrs = [:b25, :recpt1, :assdumper, :clean_ts, :statvfs, :record_dir, :cache_dir, :cabinet_dir]
9
9
  attr_reader *path_attrs
10
10
  path_attrs.each do |attr|
11
11
  define_method("#{attr}=") do |arg|
@@ -20,7 +20,7 @@ module Kaede
20
20
  self.clean_ts = '/usr/bin/clean-ts'
21
21
  self.statvfs = '/usr/bin/statvfs'
22
22
  basedir = Pathname.new(ENV['HOME']).join('kaede')
23
- self.database_path = basedir.join('kaede.db')
23
+ self.database_url = "sqlite://#{basedir.join('kaede.db')}"
24
24
  self.record_dir = basedir.join('records')
25
25
  self.cache_dir = basedir.join('cache')
26
26
  self.cabinet_dir = basedir.join('cabinet')
@@ -1,5 +1,5 @@
1
1
  require 'forwardable'
2
- require 'sqlite3'
2
+ require 'sequel'
3
3
  require 'kaede/channel'
4
4
  require 'kaede/program'
5
5
 
@@ -9,74 +9,56 @@ module Kaede
9
9
  def_delegators :@db, :transaction
10
10
 
11
11
  def initialize(path)
12
- @db = SQLite3::Database.new(path.to_s)
13
- @db.send(:set_boolean_pragma, 'foreign_keys', true)
14
- prepare_tables
12
+ @db = Sequel.connect(path)
15
13
  end
16
14
 
17
15
  def prepare_tables
18
- @db.execute_batch <<-SQL
19
- CREATE TABLE IF NOT EXISTS channels (
20
- id integer PRIMARY KEY AUTOINCREMENT,
21
- name varchar(255) NOT NULL UNIQUE,
22
- for_recorder integer NOT NULL UNIQUE,
23
- for_syoboi integer NOT NULL UNIQUE
24
- );
25
- CREATE TABLE IF NOT EXISTS programs (
26
- pid integer PRIMARY KEY ON CONFLICT REPLACE,
27
- tid integer NOT NULL,
28
- start_time datetime NOT NULL,
29
- end_time datetime NOT NULL,
30
- channel_id integer NOT NULL,
31
- count varchar(16),
32
- start_offset integer NOT NULL,
33
- subtitle varchar(255),
34
- title varchar(255),
35
- comment varchar(255),
36
- FOREIGN KEY(channel_id) REFERENCES channels(id)
37
- );
38
- CREATE TABLE IF NOT EXISTS jobs (
39
- pid integer PRIMARY KEY,
40
- enqueued_at datetime NOT NULL,
41
- finished_at datetime,
42
- created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
- FOREIGN KEY(pid) REFERENCES programs(pid)
44
- );
45
- CREATE TABLE IF NOT EXISTS tracking_titles (
46
- tid integer NOT NULL UNIQUE,
47
- created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
48
- );
49
- SQL
50
- end
51
- private :prepare_tables
52
-
53
- DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
54
-
55
- def to_db_datetime(time)
56
- time.utc.strftime(DATETIME_FORMAT)
57
- end
58
-
59
- def from_db_datetime(str)
60
- Time.parse("#{str} UTC").localtime
61
- end
62
-
63
- def current_timestamp
64
- to_db_datetime(Time.now)
16
+ @db.create_table?(:channels) do
17
+ primary_key :id
18
+ String :name, size: 255, null: false, unique: true
19
+ Integer :for_recorder, null: false, unique: true
20
+ Integer :for_syoboi, null: false, unique: true
21
+ end
22
+ @db.create_table?(:programs) do
23
+ primary_key :pid
24
+ Integer :tid, null: false
25
+ DateTime :start_time, null: false
26
+ DateTime :end_time, null: false
27
+ foreign_key :channel_id, :channels
28
+ String :count, size: 16, null: false
29
+ Integer :start_offset, null: false
30
+ String :subtitle, size: 255
31
+ String :title, size: 255
32
+ String :comment, size: 255
33
+ end
34
+ @db.create_table?(:jobs) do
35
+ foreign_key :pid, :programs, primary_key: true
36
+ DateTime :enqueued_at, null: false
37
+ DateTime :finished_at
38
+ DateTime :created_at, null: false
39
+ end
40
+ @db.create_table?(:tracking_titles) do
41
+ Integer :tid, primary_key: true
42
+ DateTime :created_at, null: false
43
+ end
65
44
  end
66
- private :current_timestamp
67
45
 
68
46
  def get_jobs
69
- @db.execute('SELECT pid, enqueued_at FROM jobs WHERE finished_at IS NULL AND enqueued_at >= ? ORDER BY enqueued_at', [current_timestamp]).map do |pid, enqueued_at|
70
- { pid: pid, enqueued_at: from_db_datetime(enqueued_at) }
71
- end
47
+ @db.from(:jobs).select(:pid, :enqueued_at).where(finished_at: nil).where(Sequel.qualify(:jobs, :enqueued_at) >= Time.now).order(:enqueued_at).to_a
72
48
  end
73
49
 
74
50
  def update_job(pid, enqueued_at)
75
- @db.execute('INSERT OR REPLACE INTO jobs (pid, enqueued_at, created_at) VALUES (?, ?, ?)', [pid, to_db_datetime(enqueued_at), current_timestamp])
51
+ @db.transaction do
52
+ if @db.from(:jobs).where(pid: pid).select(1).first
53
+ @db.from(:jobs).where(pid: pid).update(enqueued_at: enqueued_at)
54
+ else
55
+ @db.from(:jobs).insert(pid: pid, enqueued_at: enqueued_at, created_at: Time.now)
56
+ end
57
+ end
76
58
  end
77
59
 
78
60
  def delete_job(pid)
79
- @db.execute('DELETE FROM jobs WHERE pid = ?', pid)
61
+ @db.from(:jobs).where(pid: pid).delete
80
62
  end
81
63
 
82
64
  def get_program(pid)
@@ -84,62 +66,69 @@ CREATE TABLE IF NOT EXISTS tracking_titles (
84
66
  end
85
67
 
86
68
  def get_programs(pids)
87
- rows = @db.execute(<<-SQL)
88
- SELECT pid, tid, start_time, end_time, channels.name, for_syoboi, for_recorder, count, start_offset, subtitle, title, comment
89
- FROM programs
90
- INNER JOIN channels ON programs.channel_id = channels.id
91
- WHERE programs.pid IN (#{pids.join(', ')})
92
- SQL
93
69
  programs = {}
94
- rows.each do |row|
95
- program = Program.new(*row)
96
- program.start_time = from_db_datetime(program.start_time)
97
- program.end_time = from_db_datetime(program.end_time)
70
+ @db.from(:programs).inner_join(:channels, [[channel_id: :id]]).where(pid: pids).each do |row|
71
+ program = Program.new(
72
+ row[:pid],
73
+ row[:tid],
74
+ row[:start_time],
75
+ row[:end_time],
76
+ row[:name],
77
+ row[:for_syoboi],
78
+ row[:for_recorder],
79
+ row[:count],
80
+ row[:start_offset],
81
+ row[:subtitle],
82
+ row[:title],
83
+ row[:comment],
84
+ )
98
85
  programs[program.pid] = program
99
86
  end
100
87
  programs
101
88
  end
102
89
 
103
90
  def mark_finished(pid)
104
- @db.execute('UPDATE jobs SET finished_at = ? WHERE pid = ?', [current_timestamp, pid])
91
+ @db.from(:jobs).where(pid: pid).update(finished_at: Time.now)
105
92
  end
106
93
 
107
94
  def get_channels
108
- @db.execute('SELECT * FROM channels').map do |row|
109
- Channel.new(*row)
95
+ @db.from(:channels).map do |row|
96
+ Channel.new(row[:id], row[:name], row[:for_recorder], row[:for_syoboi])
110
97
  end
111
98
  end
112
99
 
113
100
  def add_channel(channel)
114
- @db.execute('INSERT INTO channels (name, for_recorder, for_syoboi) VALUES (?, ?, ?)', [channel.name, channel.for_recorder, channel.for_syoboi])
101
+ @db.from(:channels).insert(name: channel.name, for_recorder: channel.for_recorder, for_syoboi: channel.for_syoboi)
115
102
  end
116
103
 
117
104
  def update_program(program, channel)
118
- row = [
119
- program.pid,
120
- program.tid,
121
- to_db_datetime(program.start_time),
122
- to_db_datetime(program.end_time),
123
- channel.id,
124
- program.count,
125
- program.start_offset,
126
- program.subtitle,
127
- program.title,
128
- program.comment,
129
- ]
130
- @db.execute(<<-SQL, row)
131
- INSERT INTO programs (pid, tid, start_time, end_time, channel_id, count, start_offset, subtitle, title, comment)
132
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
133
- SQL
105
+ attributes = {
106
+ tid: program.tid,
107
+ start_time: program.start_time,
108
+ end_time: program.end_time,
109
+ channel_id: channel.id,
110
+ count: program.count,
111
+ start_offset: program.start_offset,
112
+ subtitle: program.subtitle,
113
+ title: program.title,
114
+ comment: program.comment,
115
+ }
116
+ @db.transaction do
117
+ if @db.from(:programs).where(pid: program.pid).select(1).first
118
+ @db.from(:programs).where(pid: program.pid).update(attributes)
119
+ else
120
+ @db.from(:programs).insert(attributes.merge(pid: program.pid))
121
+ end
122
+ end
134
123
  end
135
124
 
136
125
  def add_tracking_title(tid)
137
- @db.execute('INSERT INTO tracking_titles (tid, created_at) VALUES (?, ?)', [tid, current_timestamp])
126
+ @db.from(:tracking_titles).insert(tid: tid, created_at: Time.now)
138
127
  end
139
128
 
140
129
  def get_tracking_titles
141
- @db.execute('SELECT tid FROM tracking_titles').map do |row|
142
- row[0]
130
+ @db.from(:tracking_titles).select(:tid).map do |row|
131
+ row[:tid]
143
132
  end
144
133
  end
145
134
  end
@@ -0,0 +1,73 @@
1
+ # Based on DBus::Main in ruby-dbus gem.
2
+ # Stop the main loop as quick as possible.
3
+ require 'sleepy_penguin'
4
+
5
+ module Kaede
6
+ module DBus
7
+ class Main
8
+ def initialize
9
+ @buses = Hash.new
10
+ @quit_event = SleepyPenguin::EventFD.new(0, :SEMAPHORE)
11
+ end
12
+
13
+ def <<(bus)
14
+ @buses[bus.message_queue.socket] = bus
15
+ end
16
+
17
+ def quit
18
+ @quit_event.incr(1)
19
+ end
20
+
21
+ def loop
22
+ flush_buffers
23
+
24
+ epoll = prepare_epoll
25
+ begin
26
+ while !@buses.empty?
27
+ epoll.wait do |events, io|
28
+ if io == @quit_event
29
+ io.value
30
+ return
31
+ end
32
+ handle_socket(io)
33
+ end
34
+ end
35
+ ensure
36
+ epoll.close
37
+ end
38
+ end
39
+
40
+ def handle_socket(socket)
41
+ bus = @buses[socket]
42
+ begin
43
+ bus.message_queue.buffer_from_socket_nonblock
44
+ rescue EOFError, SystemCallError
45
+ @buses.delete(socket) # this bus died
46
+ return
47
+ end
48
+ while message = bus.message_queue.message_from_buffer_nonblock
49
+ bus.process(message)
50
+ end
51
+ end
52
+
53
+ def flush_buffers
54
+ # before blocking, empty the buffers
55
+ # https://bugzilla.novell.com/show_bug.cgi?id=537401
56
+ @buses.each_value do |b|
57
+ while m = b.message_queue.message_from_buffer_nonblock
58
+ b.process(m)
59
+ end
60
+ end
61
+ end
62
+
63
+ def prepare_epoll
64
+ SleepyPenguin::Epoll.new.tap do |epoll|
65
+ epoll.add(@quit_event, [:IN])
66
+ @buses.each_key do |socket|
67
+ epoll.add(socket, [:IN])
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,6 +1,7 @@
1
1
  require 'dbus'
2
2
  require 'nokogiri'
3
3
  require 'time'
4
+ require 'kaede/dbus/properties'
4
5
 
5
6
  module Kaede
6
7
  module DBus
@@ -16,6 +17,10 @@ module Kaede
16
17
  @enqueued_at = enqueued_at
17
18
  end
18
19
 
20
+ include Properties
21
+ properties_for PROGRAM_INTERFACE, :properties
22
+ define_properties
23
+
19
24
  def to_xml
20
25
  Nokogiri::XML::Builder.new do |xml|
21
26
  xml.doc.create_internal_subset(
@@ -30,64 +35,11 @@ module Kaede
30
35
  end
31
36
  end
32
37
 
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
38
+ xml_for_properties(xml)
57
39
  end
58
40
  end.to_xml
59
41
  end
60
42
 
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
43
  dbus_interface PROGRAM_INTERFACE do
92
44
  end
93
45
 
@@ -108,18 +60,6 @@ module Kaede
108
60
  'EnqueuedAt' => @enqueued_at.iso8601,
109
61
  }
110
62
  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
63
  end
124
64
  end
125
65
  end
@@ -0,0 +1,96 @@
1
+ module Kaede
2
+ module DBus
3
+ module Properties
4
+ PROPERTY_INTERFACE = 'org.freedesktop.DBus.Properties'
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def properties_method
12
+ @properties_method ||= {}
13
+ end
14
+
15
+ def properties_for(iface, method_sym)
16
+ self.properties_method[iface] = method_sym
17
+ end
18
+
19
+ def define_properties
20
+ dbus_interface PROPERTY_INTERFACE do
21
+ dbus_method :Get, 'in interface:s, in property:s, out value:v' do |iface, prop|
22
+ get_property(iface, prop)
23
+ end
24
+
25
+ dbus_method :GetAll, 'in interface:s, out properties:a{sv}' do |iface|
26
+ get_properties(iface)
27
+ end
28
+
29
+ dbus_method :Set, 'in interface:s, in property:s, in value:v' do |iface, prop, val|
30
+ raise_access_denied!
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def xml_for_dbus_properties(xml)
37
+ xml.interface(name: PROPERTY_INTERFACE) do
38
+ xml.method_(name: 'GetAll') do
39
+ xml.arg(name: 'interface', direction: 'in', type: 's')
40
+ xml.arg(name: 'properties', direction: 'out', type: 'a{sv}')
41
+ end
42
+
43
+ helper = lambda do |verb, dir|
44
+ xml.method_(name: verb) do
45
+ xml.arg(name: 'interface', direction: 'in', type: 's')
46
+ xml.arg(name: 'property', direction: 'in', type: 's')
47
+ xml.arg(name: 'value', direction: dir, type: 'v')
48
+ end
49
+ end
50
+ helper.call('Get', 'out')
51
+ helper.call('Set', 'in')
52
+ end
53
+ end
54
+
55
+ def xml_for_properties(xml)
56
+ xml_for_dbus_properties(xml)
57
+ self.class.properties_method.each do |iface, method_sym|
58
+ xml.interface(name: iface) do
59
+ send(method_sym).each_key do |key|
60
+ xml.property(name: key, type: 's', access: 'read')
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def get_property(iface, prop)
67
+ props = get_properties(iface).first
68
+ if props.has_key?(prop)
69
+ [props[prop]]
70
+ else
71
+ raise_unknown_property!
72
+ end
73
+ end
74
+
75
+ def get_properties(iface)
76
+ if sym = self.class.properties_method[iface]
77
+ [send(sym)]
78
+ else
79
+ unknown_interface!
80
+ end
81
+ end
82
+
83
+ def raise_unknown_interface!
84
+ raise ::DBus.error('org.freedesktop.DBus.Error.UnknownInterface')
85
+ end
86
+
87
+ def raise_unknown_property!
88
+ raise ::DBus.error('org.freedesktop.DBus.Error.UnknownProperty')
89
+ end
90
+
91
+ def raise_access_denied!
92
+ raise ::DBus.error('org.freedesktop.DBus.Error.AccessDenied')
93
+ end
94
+ end
95
+ end
96
+ end
@@ -31,6 +31,22 @@ module Kaede
31
31
  tweet(msg)
32
32
  end
33
33
 
34
+ def notify_duration_error(program, got_duration)
35
+ msg = sprintf('%s (PID:%d) の長さが%g秒しか無いようだが……', format_title(program), program.pid, got_duration)
36
+ if @twitter_target
37
+ msg = "@#{@twitter_target} #{msg}"
38
+ end
39
+ tweet(msg)
40
+ end
41
+
42
+ def notify_redo_error(program)
43
+ msg = "再実行にも失敗した…… (PID:#{program.pid})"
44
+ if @twitter_target
45
+ msg = "@#{@twitter_target} #{msg}"
46
+ end
47
+ tweet(msg)
48
+ end
49
+
34
50
  def format_title(program)
35
51
  buf = "#{program.channel_name}で「#{program.title}"
36
52
  if program.count
@@ -1,6 +1,7 @@
1
1
  require 'date'
2
- require 'open3'
3
2
  require 'fileutils'
3
+ require 'json'
4
+ require 'open3'
4
5
 
5
6
  module Kaede
6
7
  class Recorder
@@ -90,7 +91,7 @@ module Kaede
90
91
  @ass_pipe_r.close
91
92
  end
92
93
 
93
- BUFSIZ = 188 * 16
94
+ BUFSIZ = 188 * 1024
94
95
 
95
96
  def spawn_repeater
96
97
  @repeater_thread = Thread.start do
@@ -122,6 +123,9 @@ module Kaede
122
123
 
123
124
  def after_record(program)
124
125
  @notifier.notify_after_record(program)
126
+ unless verify_duration(program, cache_path(program))
127
+ redo_ts_process(program)
128
+ end
125
129
  move_ass_to_cabinet(program)
126
130
  clean_ts(program)
127
131
  enqueue_to_redis(program)
@@ -143,8 +147,50 @@ module Kaede
143
147
  end
144
148
  end
145
149
 
150
+ def redo_ts_process(program)
151
+ unless system(Kaede.config.b25.to_s, '-v0', '-s1', '-m1', record_path(program).to_s, cache_path(program).to_s)
152
+ @notifier.notify_redo_error(program)
153
+ return false
154
+ end
155
+ unless system(Kaede.config.assdumper.to_s, record_path(program).to_s, out: cache_ass_path(program).to_s)
156
+ @notifier.notify_redo_error(program)
157
+ return false
158
+ end
159
+ unless verify_duration(program, cache_path(program))
160
+ @notifier.notify_redo_error(program)
161
+ return false
162
+ end
163
+ true
164
+ end
165
+
146
166
  def enqueue_to_redis(program)
147
167
  Kaede.config.redis.rpush(Kaede.config.redis_queue, program.formatted_fname)
148
168
  end
169
+
170
+ ALLOWED_DURATION_ERROR = 20
171
+
172
+ def verify_duration(program, path)
173
+ expected_duration = calculate_duration(program)
174
+ json = ffprobe(path)
175
+ got_duration = json['duration'].to_f
176
+ if (got_duration - expected_duration).abs < ALLOWED_DURATION_ERROR
177
+ true
178
+ else
179
+ @notifier.notify_duration_error(program, got_duration)
180
+ false
181
+ end
182
+ end
183
+
184
+ class FFprobeError < StandardError
185
+ end
186
+
187
+ def ffprobe(path)
188
+ outbuf, errbuf, status = Open3.capture3('ffprobe', '-show_format', '-print_format', 'json', path.to_s)
189
+ if status.success?
190
+ JSON.parse(outbuf)['format']
191
+ else
192
+ raise FFprobeError.new("ffprobe exited with #{status.exitstatus}: #{errbuf}")
193
+ end
194
+ end
149
195
  end
150
196
  end
@@ -2,6 +2,7 @@ require 'dbus'
2
2
  require 'thread'
3
3
  require 'sleepy_penguin'
4
4
  require 'kaede/dbus'
5
+ require 'kaede/dbus/main'
5
6
  require 'kaede/dbus/program'
6
7
  require 'kaede/dbus/scheduler'
7
8
  require 'kaede/notifier'
@@ -12,7 +13,7 @@ module Kaede
12
13
 
13
14
  def setup(db)
14
15
  @db = db
15
- setup_signals
16
+ prepare_events
16
17
  $stdout.sync = true
17
18
  $stderr.sync = true
18
19
  @recorder_queue = Queue.new
@@ -23,11 +24,13 @@ module Kaede
23
24
 
24
25
  POISON = Object.new
25
26
 
26
- def setup_signals
27
+ def prepare_events
27
28
  @reload_event = SleepyPenguin::EventFD.new(0, :SEMAPHORE)
28
-
29
29
  @stop_event = SleepyPenguin::EventFD.new(0, :SEMAPHORE)
30
- trap(:QUIT) { @stop_event.incr(1) }
30
+ end
31
+
32
+ def fire_stop
33
+ @stop_event.incr(1)
31
34
  end
32
35
 
33
36
  def start_recorder_waiter
@@ -50,18 +53,29 @@ module Kaede
50
53
  @recorder_waiter.join
51
54
  end
52
55
 
53
- def start_epoll
54
- epoll = SleepyPenguin::Epoll.new
55
- epoll.add(@reload_event, [:IN])
56
- epoll.add(@stop_event, [:IN])
57
-
56
+ def prepare_timerfds
58
57
  @timerfds = {}
59
58
  @db.get_jobs.each do |job|
60
59
  tfd = SleepyPenguin::TimerFD.new(:REALTIME)
61
60
  tfd.settime(:ABSTIME, 0, job[:enqueued_at].to_i)
62
- epoll.add(tfd, [:IN])
63
61
  @timerfds[tfd.fileno] = [tfd, job[:pid]]
64
62
  end
63
+ @timerfds
64
+ end
65
+
66
+ def prepare_epoll
67
+ prepare_timerfds
68
+ SleepyPenguin::Epoll.new.tap do |epoll|
69
+ epoll.add(@reload_event, [:IN])
70
+ epoll.add(@stop_event, [:IN])
71
+ @timerfds.each_value do |tfd, _|
72
+ epoll.add(tfd, [:IN])
73
+ end
74
+ end
75
+ end
76
+
77
+ def start_epoll
78
+ epoll = prepare_epoll
65
79
  puts "Loaded #{@timerfds.size} schedules"
66
80
  start_dbus
67
81
 
@@ -99,7 +113,13 @@ module Kaede
99
113
  def start_dbus
100
114
  bus = ::DBus.system_bus
101
115
  service = bus.request_service(DBus::DESTINATION)
116
+ dbus_export_programs(service)
117
+ service.export(DBus::Scheduler.new(@reload_event, @stop_event))
118
+
119
+ @dbus_thread = start_dbus_loop(bus)
120
+ end
102
121
 
122
+ def dbus_export_programs(service)
103
123
  programs = @db.get_programs(@timerfds.values.map { |_, pid| pid })
104
124
  @timerfds.each_value do |tfd, pid|
105
125
  _, value = tfd.gettime
@@ -116,16 +136,16 @@ module Kaede
116
136
  end
117
137
  end
118
138
  end
139
+ end
119
140
 
120
- service.export(DBus::Scheduler.new(@reload_event, @stop_event))
121
-
122
- @dbus_main = ::DBus::Main.new
141
+ def start_dbus_loop(bus)
142
+ @dbus_main = DBus::Main.new
123
143
  @dbus_main << bus
124
- @dbus_thread = Thread.start do
144
+ Thread.start do
125
145
  max_retries = 10
126
146
  retries = 0
127
147
  begin
128
- @dbus_main.run
148
+ @dbus_main.loop
129
149
  rescue ::DBus::Connection::NameRequestError => e
130
150
  puts "#{e.class}: #{e.message}"
131
151
  if retries < max_retries
@@ -1,3 +1,3 @@
1
1
  module Kaede
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -8,7 +8,7 @@ require 'kaede/updater'
8
8
  describe Kaede::Recorder do
9
9
  let(:notifier) { double('Notifier') }
10
10
  let(:recorder) { described_class.new(notifier) }
11
- let(:db) { Kaede::Database.new(':memory:') }
11
+ let(:db) { Kaede::Database.new(DatabaseHelper.database_url) }
12
12
  let(:job) { db.get_jobs.first }
13
13
  let(:program) { db.get_program(job[:pid]) }
14
14
  let(:duration) { 30 }
@@ -24,12 +24,15 @@ describe Kaede::Recorder do
24
24
  let(:cabinet_ass_path) { recorder.cabinet_ass_path(program) }
25
25
 
26
26
  before do
27
+ db.prepare_tables
27
28
  db.add_channel(Kaede::Channel.new(nil, 'MX', 9, 19))
28
29
  channel = db.get_channels.first
29
30
  program = Kaede::Program.new(1234, 5678, Time.now, Time.now + duration, nil, 19, 9, '6', 0, 'sub', 'title', 'comment')
30
31
  db.update_program(program, channel)
31
32
  db.update_job(program.pid, Time.now + 5)
32
33
 
34
+ allow(recorder).to receive(:verify_duration)
35
+
33
36
  Kaede.configure do |config|
34
37
  config.redis = double('redis')
35
38
  tools = @topdir.join('tools')
@@ -72,6 +75,7 @@ describe Kaede::Recorder do
72
75
 
73
76
  allow(Kaede.config.redis).to receive(:rpush)
74
77
  allow(notifier).to receive(:notify_after_record).with(program)
78
+ allow(recorder).to receive(:verify_duration).and_return(true)
75
79
  end
76
80
 
77
81
  it 'calls Notifier#notify_after_record' do
@@ -173,4 +177,30 @@ describe Kaede::Recorder do
173
177
  end
174
178
  end
175
179
  end
180
+
181
+ describe '#verify_duration' do
182
+ let(:duration) { 1800 }
183
+ let(:got_duration) { 1789.21 }
184
+
185
+ before do
186
+ # Unstub verify_duration
187
+ allow(recorder).to receive(:verify_duration).and_call_original
188
+
189
+ allow(recorder).to receive(:ffprobe).and_return('duration' => got_duration)
190
+ end
191
+
192
+ it 'verifies the duration of recorded MPEG2-TS' do
193
+ expect(recorder.verify_duration(program, double('path to MPEG2-TS'))).to eq(true)
194
+ end
195
+
196
+ context 'when the duration is too short' do
197
+ let(:got_duration) { 1750.45 }
198
+
199
+ it 'notifies the verification error' do
200
+ expect(notifier).to receive(:notify_duration_error)
201
+
202
+ expect(recorder.verify_duration(program, double('path to MPEG2-TS'))).to eq(false)
203
+ end
204
+ end
205
+ end
176
206
  end
@@ -6,10 +6,14 @@ require 'kaede'
6
6
  require 'kaede/database'
7
7
  require 'kaede/recorder'
8
8
  require 'kaede/scheduler'
9
+ require 'kaede/dbus'
9
10
 
10
11
  describe Kaede::Scheduler do
11
- let(:db_file) { Tempfile.open('kaede.db') }
12
- let(:db) { Kaede::Database.new(db_file.path) }
12
+ let(:db) { Kaede::Database.new(DatabaseHelper.database_url) }
13
+
14
+ before do
15
+ db.prepare_tables
16
+ end
13
17
 
14
18
  describe '.start' do
15
19
  let(:program) { Kaede::Program.new(1234, 5678, Time.now, Time.now + 30, nil, 19, 9, '5.5', 0, 'sub', 'title', '') }
@@ -22,32 +26,23 @@ describe Kaede::Scheduler do
22
26
  end
23
27
 
24
28
  it 'works' do
25
- r, w = IO.pipe
29
+ q = Queue.new
26
30
  allow_any_instance_of(Kaede::Recorder).to receive(:record) { |recorder, db, pid|
27
- program = db.get_program(pid)
28
- puts "Record #{program.pid}"
31
+ q.push(pid)
29
32
  }
30
- pid = fork do
31
- r.close
32
- $stdout.reopen(w)
33
- described_class.setup(db)
33
+ described_class.setup(db)
34
+ expect(db.get_jobs.size).to eq(1)
35
+ thread = Thread.start do
34
36
  described_class.start
35
37
  end
36
- w.close
37
38
 
38
- expect(db.get_jobs.size).to eq(1)
39
39
  begin
40
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
41
+ expect(q.pop).to eq(program.pid)
47
42
  end
48
43
  ensure
49
- Process.kill(:QUIT, pid)
50
- Process.waitpid(pid)
44
+ described_class.fire_stop
45
+ thread.join
51
46
  end
52
47
  expect(db.get_jobs.size).to eq(0)
53
48
  end
@@ -5,10 +5,14 @@ require 'kaede/syoboi_calendar'
5
5
  require 'kaede/updater'
6
6
 
7
7
  describe Kaede::Updater do
8
- let(:db) { Kaede::Database.new(':memory:') }
8
+ let(:db) { Kaede::Database.new(DatabaseHelper.database_url) }
9
9
  let(:syobocal) { Kaede::SyoboiCalendar.new }
10
10
  let(:updater) { described_class.new(db, syobocal) }
11
11
 
12
+ before do
13
+ db.prepare_tables
14
+ end
15
+
12
16
  describe '#update' do
13
17
  let(:channel) { Kaede::Channel.new(nil, 'MX', 9, 19) }
14
18
  let(:tracking_tid) { 3225 }
@@ -1,4 +1,5 @@
1
1
  require 'coveralls'
2
+ require 'sequel'
2
3
  require 'simplecov'
3
4
  require 'timecop'
4
5
  require 'tmpdir'
@@ -12,9 +13,6 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
12
13
  SimpleCov.start do
13
14
  add_filter Bundler.bundle_path.to_s
14
15
  add_filter File.dirname(__FILE__)
15
-
16
- # Kaede::Scheduler is tested in another process.
17
- add_filter 'lib/kaede/scheduler.rb'
18
16
  end
19
17
 
20
18
  VCR.configure do |config|
@@ -41,6 +39,13 @@ RSpec.configure do |config|
41
39
  @topdir = Pathname.new(__FILE__).parent
42
40
  end
43
41
 
42
+ config.before :suite do
43
+ DatabaseHelper.clean
44
+ end
45
+ config.after :each do
46
+ DatabaseHelper.clean
47
+ end
48
+
44
49
  config.around :each do |example|
45
50
  Dir.mktmpdir('kaede') do |dir|
46
51
  @tmpdir = Pathname.new(dir)
@@ -48,3 +53,29 @@ RSpec.configure do |config|
48
53
  end
49
54
  end
50
55
  end
56
+
57
+ module DatabaseHelper
58
+ module_function
59
+
60
+ def database_url
61
+ if ENV['DB'] == 'postgres'
62
+ "postgres://localhost/kaede_test?user=kaede"
63
+ else
64
+ 'sqlite://kaede.db'
65
+ end
66
+ end
67
+
68
+ def clean
69
+ db = Sequel.connect(database_url)
70
+ [
71
+ :tracking_titles,
72
+ :jobs,
73
+ :programs,
74
+ :channels,
75
+ ].each do |table|
76
+ if db.table_exists?(table)
77
+ db.from(table).delete
78
+ end
79
+ end
80
+ end
81
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaede
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kohei Suzuki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-05 00:00:00.000000000 Z
11
+ date: 2014-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -56,16 +70,16 @@ dependencies:
56
70
  name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - "~>"
73
+ - - ">="
60
74
  - !ruby/object:Gem::Version
61
- version: 3.0.0.beta2
75
+ version: 3.0.0
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - "~>"
80
+ - - ">="
67
81
  - !ruby/object:Gem::Version
68
- version: 3.0.0.beta2
82
+ version: 3.0.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: simplecov
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: timecop
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -123,7 +151,7 @@ dependencies:
123
151
  - !ruby/object:Gem::Version
124
152
  version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
- name: ruby-dbus
154
+ name: nokogiri
127
155
  requirement: !ruby/object:Gem::Requirement
128
156
  requirements:
129
157
  - - ">="
@@ -137,7 +165,7 @@ dependencies:
137
165
  - !ruby/object:Gem::Version
138
166
  version: '0'
139
167
  - !ruby/object:Gem::Dependency
140
- name: nokogiri
168
+ name: redis
141
169
  requirement: !ruby/object:Gem::Requirement
142
170
  requirements:
143
171
  - - ">="
@@ -151,7 +179,7 @@ dependencies:
151
179
  - !ruby/object:Gem::Version
152
180
  version: '0'
153
181
  - !ruby/object:Gem::Dependency
154
- name: redis
182
+ name: ruby-dbus
155
183
  requirement: !ruby/object:Gem::Requirement
156
184
  requirements:
157
185
  - - ">="
@@ -165,7 +193,7 @@ dependencies:
165
193
  - !ruby/object:Gem::Version
166
194
  version: '0'
167
195
  - !ruby/object:Gem::Dependency
168
- name: sleepy_penguin
196
+ name: sequel
169
197
  requirement: !ruby/object:Gem::Requirement
170
198
  requirements:
171
199
  - - ">="
@@ -179,7 +207,7 @@ dependencies:
179
207
  - !ruby/object:Gem::Version
180
208
  version: '0'
181
209
  - !ruby/object:Gem::Dependency
182
- name: sqlite3
210
+ name: sleepy_penguin
183
211
  requirement: !ruby/object:Gem::Requirement
184
212
  requirements:
185
213
  - - ">="
@@ -247,7 +275,9 @@ files:
247
275
  - lib/kaede/database.rb
248
276
  - lib/kaede/dbus.rb
249
277
  - lib/kaede/dbus/generator.rb
278
+ - lib/kaede/dbus/main.rb
250
279
  - lib/kaede/dbus/program.rb
280
+ - lib/kaede/dbus/properties.rb
251
281
  - lib/kaede/dbus/scheduler.rb
252
282
  - lib/kaede/notifier.rb
253
283
  - lib/kaede/program.rb