kaede 0.1.0 → 0.2.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 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