jeeves-pvr 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 +15 -0
- data/Bugs.rdoc +6 -0
- data/History.txt +49 -0
- data/LICENCE.rdoc +159 -0
- data/README.md +60 -0
- data/bin/jeeves +122 -0
- data/bin/jeeves-install +27 -0
- data/bin/jeeves-old +375 -0
- data/bin/jeeves.wrapper +26 -0
- data/etc/jerbil/jeeves.rb +46 -0
- data/lib/jeeves.rb +68 -0
- data/lib/jeeves/config.rb +141 -0
- data/lib/jeeves/errors.rb +70 -0
- data/lib/jeeves/listings.rb +116 -0
- data/lib/jeeves/parser/listings.rb +41 -0
- data/lib/jeeves/parser/store.rb +167 -0
- data/lib/jeeves/parser/videos.rb +136 -0
- data/lib/jeeves/partition.rb +174 -0
- data/lib/jeeves/scheduler/base.rb +209 -0
- data/lib/jeeves/scheduler/old_base.rb +148 -0
- data/lib/jeeves/store.rb +544 -0
- data/lib/jeeves/tags.rb +329 -0
- data/lib/jeeves/utils.rb +124 -0
- data/lib/jeeves/version.rb +13 -0
- data/lib/jeeves/video.rb +79 -0
- metadata +176 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Scheduler
|
4
|
+
#
|
5
|
+
# == Base class
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2011 Robert Sharp
|
9
|
+
# License:: Open Software Licence v3.0
|
10
|
+
#
|
11
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
12
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
13
|
+
# and in the file copyright.txt. Under the terms of this licence, all derivative works
|
14
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
15
|
+
#
|
16
|
+
# A base class for the Jeeves Tuner and Scheduler
|
17
|
+
#
|
18
|
+
# This class needs to be inherited to create specific subclasses for different tuners
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'jerbil/jerbil_service/base'
|
22
|
+
require 'jerbil/jerbil_service/support'
|
23
|
+
require 'jerbil/support'
|
24
|
+
|
25
|
+
require 'jeeves/config'
|
26
|
+
require 'jeeves/errors'
|
27
|
+
|
28
|
+
require 'open3'
|
29
|
+
|
30
|
+
#
|
31
|
+
# == Jeeves Service
|
32
|
+
#
|
33
|
+
# Synopsis of the whole service
|
34
|
+
#
|
35
|
+
module Jscheduler
|
36
|
+
|
37
|
+
# add generic class methods
|
38
|
+
extend JerbilService::Support
|
39
|
+
|
40
|
+
# inherit the Jeeves config file
|
41
|
+
class Config < Jeeves::Config; end
|
42
|
+
|
43
|
+
# === Jeeves Service class
|
44
|
+
#
|
45
|
+
# Description of how the service class works
|
46
|
+
#
|
47
|
+
class Base < JerbilService::Base
|
48
|
+
|
49
|
+
# Document the constructor as required.
|
50
|
+
#
|
51
|
+
# pkey:: string - private key that must be used for
|
52
|
+
# supervision calls (e.g. stop_callback)
|
53
|
+
# options:: hash of options, preferably as created by Jeckyl. See documentation
|
54
|
+
# on JerbilService::Base for assumed options re logging etc
|
55
|
+
#
|
56
|
+
# Note that this method should be super'd before any local initialization
|
57
|
+
#
|
58
|
+
def initialize(pkey, options)
|
59
|
+
|
60
|
+
@tuner = options[:tuner]
|
61
|
+
@device = '/dev/video0' # set this to the device from which the tuner streams
|
62
|
+
|
63
|
+
# do things that cannot be done when $SAFE > 0
|
64
|
+
|
65
|
+
# the symbol should be the app name and must correspond to a service in /etc/services
|
66
|
+
super(:jschedule, pkey, options)
|
67
|
+
|
68
|
+
# DRb is now working, this process may have daemonized and $SAFE = 1
|
69
|
+
|
70
|
+
# the current priority, an integer where the higher the number the higher the priority
|
71
|
+
@priority = 0
|
72
|
+
|
73
|
+
# save the pid for the tuner process if there is one
|
74
|
+
@tuner_pid = nil
|
75
|
+
|
76
|
+
@logger.verbose("Started scheduler for tuner #{@tuner}")
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# define additional class methods to make things happen
|
82
|
+
#
|
83
|
+
|
84
|
+
# tune_receiver is needed for each receiver to prepare that receiver for
|
85
|
+
# a given channel.
|
86
|
+
#
|
87
|
+
# calls init_receiver to prepare the receiver itself
|
88
|
+
# and then sets a timer to protect the tuner
|
89
|
+
def tune_receiver(priority, params={})
|
90
|
+
|
91
|
+
# do not tune anything if there is a higher priority tune in progress
|
92
|
+
raise Jeeves::TunerBusy if @priority > 0 && priority <= @priority
|
93
|
+
|
94
|
+
# do the specific stuff for the given tuner and get back its pid (or not if there
|
95
|
+
# is not process involved)
|
96
|
+
self.stop_tuner(@tuner_pid) unless @tuner_pid.nil?
|
97
|
+
|
98
|
+
@tuner_pid = self.init_tuner(params)
|
99
|
+
|
100
|
+
if params[:duration] then
|
101
|
+
# need to do this for a given time
|
102
|
+
@priority = priority
|
103
|
+
@thread = Thread.new do
|
104
|
+
# use Jeckyl to ensure this is sensible!
|
105
|
+
sleep(params[:duration])
|
106
|
+
# now clear up
|
107
|
+
@priority = 0
|
108
|
+
self.stop_tuner(@tuner_pid)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
# no duration, so tune indefinitely
|
112
|
+
# BUT what if there is a priority? Ignore it!
|
113
|
+
@priority = 0
|
114
|
+
end
|
115
|
+
#raise NotImplementedError
|
116
|
+
# needs to do what ever the tuner requires to tune to the given channel
|
117
|
+
# For Drb tuners this may involve a process running continuously, which
|
118
|
+
# can be ended either after a certain time or when another call is made
|
119
|
+
# to tune the receiver.
|
120
|
+
|
121
|
+
# params include:
|
122
|
+
#
|
123
|
+
# * an identity for the channel as required by the tuner
|
124
|
+
# * a priority - to allow recordings to overide casual viewing, for example
|
125
|
+
# * a duration that enforces the priority and may also hold the tuner on.
|
126
|
+
end
|
127
|
+
|
128
|
+
# this will record the current receiver settings to a given file. It needs
|
129
|
+
# to know the device from which to record and will then use FFMPEG to do
|
130
|
+
# the recording
|
131
|
+
def record(filename)
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
# play the current tuned channel on the primary X display
|
136
|
+
def play
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
# stream the current tuned channel over the net using netcat
|
141
|
+
def stream
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# this will create an at job to record a programme at some time in the future
|
146
|
+
def schedule(params={})
|
147
|
+
[:filename, :channel, :start, :duration].each do |param|
|
148
|
+
raise Jeeves::MissingParam, "Schedule method must specify #{param}" unless params.has_key?(param)
|
149
|
+
end
|
150
|
+
mode = params[:mode] || :avi
|
151
|
+
raise InvalidMode, "Tuner #{@tuner} does not have mode: #{mode}" unless Schedulers[@tuner].has_key?(mode)
|
152
|
+
scheduler = Schedulers[@tuner][mode]
|
153
|
+
|
154
|
+
startParam = params[:start].strftime("%H:%M %b %d")
|
155
|
+
|
156
|
+
command = "#{scheduler} #{params[:channel]} #{params[:duration]} #{params[:filename]}"
|
157
|
+
|
158
|
+
@logger.debug "About to schedule: #{command}"
|
159
|
+
|
160
|
+
# need open3 to get the full communication with at
|
161
|
+
inpipe, outpipe, errpipe = Open3.popen3("/usr/bin/at #{startParam}")
|
162
|
+
inpipe.puts command
|
163
|
+
inpipe.close
|
164
|
+
response = errpipe.readlines
|
165
|
+
outpipe.close
|
166
|
+
errpipe.close
|
167
|
+
|
168
|
+
# get the job id and return it to the caller
|
169
|
+
jobnum = nil
|
170
|
+
job_re = /job (\d+) at/
|
171
|
+
response.each do |resp|
|
172
|
+
@logger.debug "Response includes: " + resp
|
173
|
+
if results = job_re.match(resp) then
|
174
|
+
@logger.debug "Recognised Job: " + results[1]
|
175
|
+
jobnum = results[1].clone
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
@logger.info "Scheduled recording on #{params[:channel]} at #{startParam} for #{params[:duration]} with job no: #{jobnum}"
|
180
|
+
# set a wol timer here
|
181
|
+
|
182
|
+
return jobnum
|
183
|
+
rescue Jeeves::JeevesError
|
184
|
+
raise
|
185
|
+
rescue
|
186
|
+
@logger.exception($!)
|
187
|
+
return '0'
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# deletes the at job with the given job number
|
192
|
+
# assume that the given jobnum is valid
|
193
|
+
def delete_schedule(jobnum)
|
194
|
+
begin
|
195
|
+
@logger.info "Removing job: #{jobnum}"
|
196
|
+
system("/usr/bin/atrm #{jobnum.to_s}")
|
197
|
+
rescue
|
198
|
+
@logger.exception($!)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# do not redefine the superclass methods unless you are absolutely sure you know
|
203
|
+
# what you are doing.
|
204
|
+
|
205
|
+
protected
|
206
|
+
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeeves Scheduler
|
4
|
+
#
|
5
|
+
# == Schedule Jeeves related tasks from recordings to shutdowns and wakeups
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2011 Robert Sharp
|
9
|
+
# License:: Open Software Licence v3.0
|
10
|
+
#
|
11
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
12
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
13
|
+
# and in the file copyright.txt. Under the terms of this licence, all derivative works
|
14
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
15
|
+
#
|
16
|
+
# This scheduler will schedule at jobs and use wolly to schedule wakeups. It is a Jerbil
|
17
|
+
# service.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'jerbil/jerbil_service/base'
|
21
|
+
require 'jerbil/jerbil_service/support'
|
22
|
+
require 'jerbil/support'
|
23
|
+
|
24
|
+
require 'jeeves/config'
|
25
|
+
require 'jeeves/errors'
|
26
|
+
|
27
|
+
require 'open3'
|
28
|
+
|
29
|
+
#
|
30
|
+
# == Jeeves Service
|
31
|
+
#
|
32
|
+
# Synopsis of the whole service
|
33
|
+
#
|
34
|
+
module Jscheduler
|
35
|
+
|
36
|
+
# add generic class methods
|
37
|
+
extend JerbilService::Support
|
38
|
+
|
39
|
+
# inherit the Jeeves config file
|
40
|
+
class Config < Jeeves::Config; end
|
41
|
+
|
42
|
+
# === Jeeves Service class
|
43
|
+
#
|
44
|
+
# Description of how the service class works
|
45
|
+
#
|
46
|
+
class Service < JerbilService::Base
|
47
|
+
|
48
|
+
# Document the constructor as required.
|
49
|
+
#
|
50
|
+
# pkey:: string - private key that must be used for
|
51
|
+
# supervision calls (e.g. stop_callback)
|
52
|
+
# options:: hash of options, preferably as created by Jeckyl. See documentation
|
53
|
+
# on JerbilService::Base for assumed options re logging etc
|
54
|
+
#
|
55
|
+
def initialize(pkey, options)
|
56
|
+
|
57
|
+
@tuner = options[:tuner]
|
58
|
+
raise InvalidTuner("Tuner is not recognized: #{@tuner}") unless Schedulers.has_key?(@tuner)
|
59
|
+
|
60
|
+
# do things that cannot be done when $SAFE > 0
|
61
|
+
|
62
|
+
# the symbol should be the app name and must correspond to a service in /etc/services
|
63
|
+
super(:jschedule, pkey, options)
|
64
|
+
|
65
|
+
# DRb is now working, this process may have daemonized and $SAFE = 1
|
66
|
+
|
67
|
+
@logger.verbose("Started scheduler for tuner #{@tuner}")
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# define additional class methods to make things happen
|
73
|
+
#
|
74
|
+
|
75
|
+
def schedule(params={})
|
76
|
+
[:filename, :channel, :start, :duration].each do |param|
|
77
|
+
raise Jeeves::MissingParam, "Schedule method must specify #{param}" unless params.has_key?(param)
|
78
|
+
end
|
79
|
+
mode = params[:mode] || :avi
|
80
|
+
raise InvalidMode, "Tuner #{@tuner} does not have mode: #{mode}" unless Schedulers[@tuner].has_key?(mode)
|
81
|
+
scheduler = Schedulers[@tuner][mode]
|
82
|
+
|
83
|
+
startParam = params[:start].strftime("%H:%M %b %d")
|
84
|
+
|
85
|
+
command = "#{scheduler} #{params[:channel]} #{params[:duration]} #{params[:filename]}"
|
86
|
+
|
87
|
+
@logger.debug "About to schedule: #{command}"
|
88
|
+
|
89
|
+
# need open3 to get the full communication with at
|
90
|
+
inpipe, outpipe, errpipe = Open3.popen3("/usr/bin/at #{startParam}")
|
91
|
+
inpipe.puts command
|
92
|
+
inpipe.close
|
93
|
+
response = errpipe.readlines
|
94
|
+
outpipe.close
|
95
|
+
errpipe.close
|
96
|
+
|
97
|
+
# get the job id and return it to the caller
|
98
|
+
jobnum = nil
|
99
|
+
job_re = /job (\d+) at/
|
100
|
+
response.each do |resp|
|
101
|
+
@logger.debug "Response includes: " + resp
|
102
|
+
if results = job_re.match(resp) then
|
103
|
+
@logger.debug "Recognised Job: " + results[1]
|
104
|
+
jobnum = results[1].clone
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
@logger.info "Scheduled recording on #{params[:channel]} at #{startParam} for #{params[:duration]} with job no: #{jobnum}"
|
109
|
+
# set a wol timer here
|
110
|
+
|
111
|
+
return jobnum
|
112
|
+
rescue Jeeves::JeevesError
|
113
|
+
raise
|
114
|
+
rescue
|
115
|
+
@logger.exception($!)
|
116
|
+
return '0'
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# deletes the at job with the given job number
|
121
|
+
# assume that the given jobnum is valid
|
122
|
+
def delete_schedule(jobnum)
|
123
|
+
begin
|
124
|
+
@logger.info "Removing job: #{jobnum}"
|
125
|
+
system("/usr/bin/atrm #{jobnum.to_s}")
|
126
|
+
rescue
|
127
|
+
@logger.exception($!)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# do not redefine the superclass methods unless you are absolutely sure you know
|
132
|
+
# what you are doing.
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
Schedulers = {
|
137
|
+
:dvb=>{
|
138
|
+
:avi=>'/usr/local/bin/virea-dvb-record.sh',
|
139
|
+
:ts=>'/usr/local/bin/virea-dvb-record-ts.sh'
|
140
|
+
},
|
141
|
+
:pvr=>{
|
142
|
+
:avi=>'/usr/local/bin/virea-pvr-record.sh',
|
143
|
+
:ts=>'/usr/local/bin/virea-pvr-record-ts.sh'
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
data/lib/jeeves/store.rb
ADDED
@@ -0,0 +1,544 @@
|
|
1
|
+
#
|
2
|
+
# Author:: R.J.Sharp
|
3
|
+
# Email:: robert(a)osburn-sharp.ath.cx
|
4
|
+
# Copyright:: Copyright (c) 2013
|
5
|
+
# License:: Open Software Licence v3.0
|
6
|
+
#
|
7
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
8
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
9
|
+
# and in the file LICENCE. Under the terms of this licence, all derivative works
|
10
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
11
|
+
#
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'jerbil/jerbil_service/support'
|
15
|
+
|
16
|
+
|
17
|
+
require 'jellog'
|
18
|
+
|
19
|
+
require 'yaml'
|
20
|
+
require 'open-uri'
|
21
|
+
require 'digest'
|
22
|
+
|
23
|
+
require 'jeeves/partition'
|
24
|
+
require 'jeeves/errors'
|
25
|
+
require 'jeeves/version'
|
26
|
+
require 'jeeves/utils'
|
27
|
+
|
28
|
+
|
29
|
+
module Jeeves
|
30
|
+
|
31
|
+
# Check if this is really needed...
|
32
|
+
#extend JerbilService::Support
|
33
|
+
|
34
|
+
# === Jeeves Store class
|
35
|
+
#
|
36
|
+
# Store manages the allocation of video files for use by Jeeves. Multiple
|
37
|
+
# locations can be added to a store and Jestore distributes the video files
|
38
|
+
# across these in proportion to their size. Files are pre-allocated when a
|
39
|
+
# recording is scheduled and then released when the recording completes.
|
40
|
+
# Store also notifies Jeeves when a video is released.
|
41
|
+
#
|
42
|
+
# NOTE: Store assumes it has all of the space on a given partition. For example,
|
43
|
+
# if you define two stores that are actually on the same partition then Store
|
44
|
+
# will report twice the space that is actually available! Probably needs fixing to
|
45
|
+
# detect duplicate partitions at some point.
|
46
|
+
#
|
47
|
+
# Store is not an active daemon but just an interface. It uses file locking to
|
48
|
+
# avoid any conflicts with multiple versions running across the network.
|
49
|
+
#
|
50
|
+
class Store
|
51
|
+
|
52
|
+
# create an interface to Store.
|
53
|
+
#
|
54
|
+
# creates lookup tables but does not fill them at this stage
|
55
|
+
#
|
56
|
+
# @param [Jeckyl::Config] options hash as defined in {Jeeves:Config}
|
57
|
+
def initialize(logger, options)
|
58
|
+
|
59
|
+
# do things that cannot be done when $SAFE > 0
|
60
|
+
|
61
|
+
@store_params = options[:add_partition]
|
62
|
+
@store_errors = options[:store_errors]
|
63
|
+
@partitions = Array.new
|
64
|
+
@logger = logger
|
65
|
+
@file_list_path = nil
|
66
|
+
# lookup of device to free space
|
67
|
+
@space_lup = Hash.new
|
68
|
+
# lookup of path to device
|
69
|
+
@dev_lup = Hash.new
|
70
|
+
|
71
|
+
check_partitions
|
72
|
+
|
73
|
+
@base_rate = options[:base_data_rate]
|
74
|
+
@jeeves_url = options[:jeeves_url]
|
75
|
+
|
76
|
+
# hash of allocated files to space allocated
|
77
|
+
@file_list = Hash.new
|
78
|
+
|
79
|
+
# file basenames and date to check if files have already been
|
80
|
+
# allocated
|
81
|
+
@file_lup = Hash.new
|
82
|
+
|
83
|
+
if @file_list_path.nil? then
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
# ??
|
90
|
+
@alloc_lup = Hash.new
|
91
|
+
@total_space = 0
|
92
|
+
@total_alloc = 0
|
93
|
+
|
94
|
+
options[:unsafe] = true
|
95
|
+
|
96
|
+
|
97
|
+
# set up my own logger
|
98
|
+
|
99
|
+
|
100
|
+
@logger.debug "Added stores: #{@partitions.inspect}"
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
# return the free space (as a human readable string) in the system, or
|
105
|
+
# on the partition for the given path.
|
106
|
+
#
|
107
|
+
# @param [String] path to the
|
108
|
+
def free_space(path='')
|
109
|
+
|
110
|
+
get_and_lock_file_list do
|
111
|
+
update_space
|
112
|
+
fspace = 0
|
113
|
+
if path.empty? then
|
114
|
+
fspace = @total_space - @total_alloc
|
115
|
+
else
|
116
|
+
dev, space = free_space(path)
|
117
|
+
fspace = space - @alloc_lup[dev]
|
118
|
+
end
|
119
|
+
return Jeeves.human_size(fspace)
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# allocate space for a video file with the given name, start time and duration
|
126
|
+
#
|
127
|
+
# use scale to adjust the default data rate if a tuner produces more or less.
|
128
|
+
# If the file is already allocated then do not allocate it again.
|
129
|
+
# Note that where a file already exists with the same name (for some reason?)
|
130
|
+
# a random key will be added, but the original name is remembered in case
|
131
|
+
# it is later used again.
|
132
|
+
#
|
133
|
+
# @param [String] name of the file, which should be a basename only
|
134
|
+
# @param [Integer] duration of the proposed recording in seconds
|
135
|
+
# @param [Time] for_when to allocate the space
|
136
|
+
def allocate(name, duration, for_when, scale=1.0)
|
137
|
+
space = 0
|
138
|
+
path = ''
|
139
|
+
dev = ''
|
140
|
+
|
141
|
+
get_and_lock_file_list do
|
142
|
+
|
143
|
+
# check if there is already a file for this programme
|
144
|
+
if @file_lup.has_key?(name) && @file_lup[name][:date] == for_when then
|
145
|
+
# we already have it so don't allocate again
|
146
|
+
@logger.info "File is already allocated: #{name} for #{for_when.strftime('%d-%b-%y %H:%M')}"
|
147
|
+
path = @file_lup[name][:path]
|
148
|
+
break
|
149
|
+
end
|
150
|
+
|
151
|
+
update_space # work out what space there is
|
152
|
+
|
153
|
+
# how much do we need
|
154
|
+
space = (duration * @base_rate * scale).to_i
|
155
|
+
|
156
|
+
# get the best drive for it
|
157
|
+
path = get_path(name, space)
|
158
|
+
|
159
|
+
#path = File.join(@partitions.first.path, name)
|
160
|
+
|
161
|
+
# does it already exist?
|
162
|
+
if file_exists_or_is_allocated?(path) then
|
163
|
+
# yes - so rename it
|
164
|
+
path = Jeeves.rename(path) {|f| file_exists_or_is_allocated?(f)}
|
165
|
+
|
166
|
+
end
|
167
|
+
# save it to the list
|
168
|
+
@file_list[path] = {:space=>space, :date=>for_when + duration}
|
169
|
+
@file_lup[name] = {:date=>for_when, :path=>path}
|
170
|
+
@total_alloc += space
|
171
|
+
|
172
|
+
@logger.info "Allocating #{space} kbytes for #{path} on #{dev}"
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
return path
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# release allocated space, either because the file has been created
|
181
|
+
# or is no longer required
|
182
|
+
#
|
183
|
+
# @param [String] path to the file to be released
|
184
|
+
def release(path)
|
185
|
+
@logger.verbose "Attempting to release #{path}"
|
186
|
+
deleted = false
|
187
|
+
# open the list file to lock it for the duration
|
188
|
+
get_and_lock_file_list do
|
189
|
+
|
190
|
+
if @file_list.has_key?(path) then
|
191
|
+
# remove the space from the record of allocated space
|
192
|
+
@total_alloc -= @file_list[path][:space]
|
193
|
+
@file_list.delete(path)
|
194
|
+
@file_lup.delete(File.basename(path))
|
195
|
+
@logger.info "Deleted: #{path} from file_list"
|
196
|
+
deleted = true
|
197
|
+
else
|
198
|
+
@logger.verbose "Did not find #{path} to release"
|
199
|
+
end
|
200
|
+
|
201
|
+
end # auto closes the file
|
202
|
+
return deleted
|
203
|
+
end
|
204
|
+
|
205
|
+
def release_and_notify(path)
|
206
|
+
|
207
|
+
unless self.release(path)
|
208
|
+
# the file was never allocated? what
|
209
|
+
# to do now?
|
210
|
+
@logger.verbose "Trying to notify for a file that was not allocated: #{path}"
|
211
|
+
end
|
212
|
+
|
213
|
+
data = ''
|
214
|
+
@logger.info "Notifying Jeeves for #{path}"
|
215
|
+
open("#{@jeeves_url}/videos/new_from_file.txt?path=#{path}") do |resp|
|
216
|
+
resp.each_line do |line|
|
217
|
+
data << line
|
218
|
+
end
|
219
|
+
end
|
220
|
+
@logger.verbose "Jeeves returned #{data}"
|
221
|
+
|
222
|
+
return data == '0'
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
# return block for each allocated file with the path and a hash
|
227
|
+
# containing :space (in Kbytes) and :date when recording expected
|
228
|
+
def each_file(&block)
|
229
|
+
get_and_lock_file_list do
|
230
|
+
@file_list.each_pair do |key, value|
|
231
|
+
block.call(key, value)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# return true if there are no entries allocated
|
237
|
+
def empty?
|
238
|
+
empty = true
|
239
|
+
get_and_lock_file_list do
|
240
|
+
empty = @file_list.empty?
|
241
|
+
end
|
242
|
+
return empty
|
243
|
+
end
|
244
|
+
|
245
|
+
# return the number of entries in the file list
|
246
|
+
def files
|
247
|
+
len = 0
|
248
|
+
get_and_lock_file_list do
|
249
|
+
len = @file_list.length
|
250
|
+
end
|
251
|
+
return len
|
252
|
+
end
|
253
|
+
|
254
|
+
# return the number of stale files
|
255
|
+
#
|
256
|
+
# stale files are old and have no real file on the system
|
257
|
+
#
|
258
|
+
def stale_files
|
259
|
+
len = 0
|
260
|
+
get_and_lock_file_list do
|
261
|
+
@file_list.each_pair do |path, params|
|
262
|
+
len += 1 if params[:date] < Time.now
|
263
|
+
end
|
264
|
+
end
|
265
|
+
return len
|
266
|
+
end
|
267
|
+
|
268
|
+
# clear all allocated files etc. Should only do this if you are certain there
|
269
|
+
# are no outstanding recordings etc
|
270
|
+
def clean(all=false)
|
271
|
+
get_and_lock_file_list do
|
272
|
+
if all then
|
273
|
+
@file_list = Hash.new
|
274
|
+
else
|
275
|
+
@file_list.each_pair do |path, params|
|
276
|
+
if params[:date] < Time.now then
|
277
|
+
# already passed so lets clear it
|
278
|
+
@file_list.delete(path)
|
279
|
+
@file_list.delete(File.basename(path))
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end # if all
|
283
|
+
end # lock flie list
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
|
288
|
+
# check that the specified partitions exist etc and add them to the store
|
289
|
+
#
|
290
|
+
# internal method called during initialization to set up the store.
|
291
|
+
# Checks that the partitions specified are properly mounted and ready
|
292
|
+
# (that the keys given match the partition), and then removes
|
293
|
+
# any duplicates that are on the same disk partition.
|
294
|
+
#
|
295
|
+
# It also looks for the file_list on each partition and if it finds it
|
296
|
+
# then it uses it and sets the partition as the master (has priority when
|
297
|
+
# removing duplicates). If there are two or more file_lists found then
|
298
|
+
# the most recent one is used. If there are none then one is created
|
299
|
+
# on the largest partition.
|
300
|
+
#
|
301
|
+
def check_partitions
|
302
|
+
|
303
|
+
@store_params.each_pair do |path, options|
|
304
|
+
begin
|
305
|
+
part = Jeeves::Partition.new(path, options)
|
306
|
+
if part.mounted? then
|
307
|
+
# path is OK, check it has been initialised with the given key
|
308
|
+
if part.ready? then
|
309
|
+
# it has, does it have a file_
|
310
|
+
|
311
|
+
@logger.info "Added #{path} to Jeeves Store"
|
312
|
+
@partitions << part
|
313
|
+
|
314
|
+
else
|
315
|
+
raise Jeeves::InvalidPartitionKey, "#{part.path} does not have a valid key"
|
316
|
+
end
|
317
|
+
else
|
318
|
+
raise Jeeves::InvalidPartition, "#{part.path} does not exist or is not mounted"
|
319
|
+
end
|
320
|
+
rescue Jeeves::InvalidPartition, Jeeves::InvalidPartitionKey => err
|
321
|
+
msg = "Jeeves Error with partition: #{err}"
|
322
|
+
case @store_errors
|
323
|
+
when :ignore
|
324
|
+
@logger.system msg
|
325
|
+
when :warn
|
326
|
+
@logger.warn msg
|
327
|
+
when :fatal
|
328
|
+
@logger.fatal msg + ", Aborting"
|
329
|
+
raise
|
330
|
+
end
|
331
|
+
end # begin block
|
332
|
+
end # do loop
|
333
|
+
|
334
|
+
if @partitions.length == 0 then
|
335
|
+
raise Jeeves::StoreError, "There are no partitions available"
|
336
|
+
else
|
337
|
+
@logger.verbose "There are #{@partitions.length} partitions specified"
|
338
|
+
end
|
339
|
+
|
340
|
+
# find the best file list!
|
341
|
+
master = nil
|
342
|
+
@partitions.each do |part|
|
343
|
+
if part.file_list? then
|
344
|
+
if master.nil? then
|
345
|
+
# no master yet so set this one to be it
|
346
|
+
master = part
|
347
|
+
else
|
348
|
+
# master exists but ignore it if its not the newest
|
349
|
+
master = part unless master.file_list_date > part.file_list_date
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
if master.nil?
|
355
|
+
@logger.verbose "No master partition found"
|
356
|
+
else
|
357
|
+
@logger.verbose "Master Partition set to #{master.path}"
|
358
|
+
end
|
359
|
+
|
360
|
+
# remove duplicates, giving precedence to the one with the most recent
|
361
|
+
# file list (master)
|
362
|
+
devices = Hash.new
|
363
|
+
@partitions.each do |part|
|
364
|
+
if devices.has_key?(part.device) then
|
365
|
+
# got one already
|
366
|
+
if part == master then
|
367
|
+
# but this one is better
|
368
|
+
old_part = devices[part.device]
|
369
|
+
devices[part.device] = part
|
370
|
+
@logger.verbose "Duplicate partition replaced: #{old_part.path}"
|
371
|
+
else
|
372
|
+
# duplicate, so ignore it
|
373
|
+
@logger.verbose "Duplicate partition: #{part.path}"
|
374
|
+
end
|
375
|
+
else
|
376
|
+
# not already there so save it...
|
377
|
+
devices[part.device] = part
|
378
|
+
@logger.verbose "Keeping unique partition #{part.path} on #{part.device}"
|
379
|
+
end
|
380
|
+
end # do
|
381
|
+
|
382
|
+
@partitions = devices.values
|
383
|
+
|
384
|
+
update_space
|
385
|
+
|
386
|
+
if master.nil? then
|
387
|
+
# did not find a partition with a file list so create one
|
388
|
+
# the first partition should be the largest
|
389
|
+
# not found a filelist, so create one
|
390
|
+
@logger.debug "No file list found"
|
391
|
+
@file_list_path = @partitions.first.file_list
|
392
|
+
save_file_list
|
393
|
+
@logger.debug "Created file list at #{@file_list_path}"
|
394
|
+
else
|
395
|
+
@file_list_path = master.file_list
|
396
|
+
end
|
397
|
+
|
398
|
+
@logger.info "Using file list: #{@file_list_path}"
|
399
|
+
|
400
|
+
end
|
401
|
+
|
402
|
+
def each_partition
|
403
|
+
@partitions.each do |part|
|
404
|
+
yield part
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
private
|
409
|
+
# wrapper around the file locking business
|
410
|
+
def get_and_lock_file_list(&block)
|
411
|
+
File.open(@file_list_path, 'r+') do |flist|
|
412
|
+
flist.flock(File::LOCK_EX)
|
413
|
+
begin
|
414
|
+
saved_data = YAML.load(flist)
|
415
|
+
unless saved_data[:version_major] == Jeeves::Version.split('.').first
|
416
|
+
raise VersionError, "Saved data is incompatible with current version"
|
417
|
+
end
|
418
|
+
load_saved_data(saved_data)
|
419
|
+
|
420
|
+
# do whatever
|
421
|
+
yield
|
422
|
+
|
423
|
+
# empty the file before re-writing it
|
424
|
+
flist.truncate(0)
|
425
|
+
flist.rewind
|
426
|
+
YAML::dump(save_data, flist)
|
427
|
+
ensure
|
428
|
+
flist.flock(File::LOCK_UN)
|
429
|
+
end
|
430
|
+
end # auto closes the file
|
431
|
+
|
432
|
+
end
|
433
|
+
|
434
|
+
# update the free space records
|
435
|
+
def update_space
|
436
|
+
@total_space = 0
|
437
|
+
# get actual free space from df
|
438
|
+
@partitions.each do |part|
|
439
|
+
part.update_space # in case it has been a while?
|
440
|
+
#dev, space = free_space(store)
|
441
|
+
@logger.debug "Updating space for store #{part.path}: #{part.free_space}"
|
442
|
+
@dev_lup[part.path] = part.device
|
443
|
+
@space_lup[part.device] = part.free_space
|
444
|
+
@total_space += part.free_space
|
445
|
+
end
|
446
|
+
# now deduct stuff that has been allocated
|
447
|
+
if @file_list then
|
448
|
+
@file_list.each_pair do |key, value|
|
449
|
+
@logger.debug "Deducting allocated file: #{key}, #{value}"
|
450
|
+
# dev = @dev_lup[File.dirname(key)]
|
451
|
+
# @space_lup[dev] -= value
|
452
|
+
end
|
453
|
+
end
|
454
|
+
# now sort the partitions by free space
|
455
|
+
@partitions.sort! {|x,y| x.free_space <=> y.free_space}
|
456
|
+
|
457
|
+
end
|
458
|
+
|
459
|
+
# return the free space in 1K blocks on the given path
|
460
|
+
# def free_space(path)
|
461
|
+
# # use df to get the free space for path
|
462
|
+
# df_lines = `/bin/df -Pk #{path}`.split("\n")
|
463
|
+
# @logger.debug "df yeilds: " + df_lines.inspect
|
464
|
+
# df_fields = df_lines[1].split
|
465
|
+
# return [df_fields[0], df_fields[3].to_i]
|
466
|
+
# end
|
467
|
+
#
|
468
|
+
|
469
|
+
# @file_list contains a list of file data for requests made but
|
470
|
+
# where files have not yet been created. Need to save
|
471
|
+
# this information to disk to retain over restarts
|
472
|
+
#
|
473
|
+
# load a saved file list if there was one
|
474
|
+
def load_file_list
|
475
|
+
if FileTest.exists?(@file_list_path) then
|
476
|
+
@file_list = File.open(@file_list_path) {|yf| YAML.load(yf)}
|
477
|
+
end
|
478
|
+
|
479
|
+
# update allocated space
|
480
|
+
@total_alloc = 0
|
481
|
+
@file_list.each do |file, space|
|
482
|
+
#dev, dev_space = free_space(File.dirname(file))
|
483
|
+
@total_alloc += space
|
484
|
+
end
|
485
|
+
|
486
|
+
end
|
487
|
+
|
488
|
+
# load the saved data into instance variables
|
489
|
+
def load_saved_data(saved_data)
|
490
|
+
@file_list = saved_data[:file_list] || Hash.new
|
491
|
+
@file_lup = saved_data[:file_lup] || Hash.new
|
492
|
+
end
|
493
|
+
|
494
|
+
def save_data
|
495
|
+
saved_data = Hash.new
|
496
|
+
saved_data[:version_major] = Jeeves::Version.split('.').first
|
497
|
+
saved_data[:file_list] = @file_list || Hash.new
|
498
|
+
saved_data[:file_lup] = @file_lup || Hash.new
|
499
|
+
@logger.debug "Saving #{saved_data.inspect}"
|
500
|
+
return saved_data
|
501
|
+
end
|
502
|
+
|
503
|
+
# save the file list
|
504
|
+
def save_file_list
|
505
|
+
File.open(@file_list_path, 'w') do |yf|
|
506
|
+
yf.flock(File::LOCK_EX)
|
507
|
+
begin
|
508
|
+
YAML::dump(save_data, yf)
|
509
|
+
ensure
|
510
|
+
yf.flock(File::LOCK_UN)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# find the best place to put the file
|
516
|
+
def get_path(name, space)
|
517
|
+
best_dev = @space_lup.sort {|a,b| b[1] <=> a[1]}.first[0]
|
518
|
+
@logger.debug "#{best_dev} has the most space left"
|
519
|
+
path = @dev_lup.key(best_dev)
|
520
|
+
return File.join(path, name)
|
521
|
+
end
|
522
|
+
|
523
|
+
def file_exists_or_is_allocated?(path)
|
524
|
+
File.exists?(path) || @file_list.has_key?(path)
|
525
|
+
end
|
526
|
+
|
527
|
+
def rename(path)
|
528
|
+
dir_name = File.dirname(path)
|
529
|
+
extname = File.extname(path)
|
530
|
+
basename = File.basename(path, extname)
|
531
|
+
|
532
|
+
while file_exists?(path)
|
533
|
+
# oh dear, got one already. need to add something to the name
|
534
|
+
randname = (rand * 10000).to_i.to_s
|
535
|
+
basename = basename + '_' + randname + extname
|
536
|
+
path = File.join(dir_name, basename)
|
537
|
+
end
|
538
|
+
|
539
|
+
return path
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
end
|
544
|
+
end
|