jeeves-pvr 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|