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,41 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeeves CLI
|
4
|
+
#
|
5
|
+
# == Listings related actions
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2014 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
|
+
#
|
17
|
+
#
|
18
|
+
require 'optplus/nested'
|
19
|
+
|
20
|
+
class ListingCLI < Optplus::NestedParser
|
21
|
+
|
22
|
+
usage "listings [update|clean]"
|
23
|
+
|
24
|
+
description "manage TV listings in Jeeves PVR"
|
25
|
+
|
26
|
+
def before_actions
|
27
|
+
@jeeves_config = @_parent.jeeves_config
|
28
|
+
end
|
29
|
+
|
30
|
+
describe :update, 'update TV listings'
|
31
|
+
help :update, 'download TV listings data and load into Jeeves'
|
32
|
+
|
33
|
+
def update
|
34
|
+
@jeeves_config[:jeeves_uri_timeout] = get_option(:timeout) if option?(:timeout)
|
35
|
+
@jeeves_config[:update_listings] = get_option(:update_listings) if option?(:update_listings)
|
36
|
+
Jeeves.update_listings(@jeeves_config)
|
37
|
+
rescue Jeeves::ListingError => e
|
38
|
+
exit_on_error "Error while updating listings #{e}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeeves Store CLI
|
4
|
+
#
|
5
|
+
# == Jeeves
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2014 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
|
+
#
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'optplus/nested'
|
20
|
+
|
21
|
+
class StoreCLI < Optplus::NestedParser
|
22
|
+
|
23
|
+
include Jeni::IO
|
24
|
+
|
25
|
+
usage "store command"
|
26
|
+
|
27
|
+
description "manage Jeeves file stores"
|
28
|
+
|
29
|
+
def before_actions
|
30
|
+
@jeeves_config = @_parent.jeeves_config
|
31
|
+
@logger = @_parent.logger
|
32
|
+
end
|
33
|
+
|
34
|
+
describe :status, 'show status of Jeeves file stores'
|
35
|
+
help :status, 'display basic information about the file stores',
|
36
|
+
'currently available for Jeeves.'
|
37
|
+
|
38
|
+
def status
|
39
|
+
parts = Array.new
|
40
|
+
devices = Array.new
|
41
|
+
@jeeves_config[:add_partition].each_pair do |path, params|
|
42
|
+
part = Jeeves::Partition.new(path, params)
|
43
|
+
if part.ready? then
|
44
|
+
if devices.include?(part.device) then
|
45
|
+
part.duplicate = true
|
46
|
+
else
|
47
|
+
devices << part.device
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
parts << part.to_a
|
52
|
+
end
|
53
|
+
Jeeves.tabulate(%w{* 4c 4c 4c 8r 8r},
|
54
|
+
%w{Path Mnt Rdy Dup Total Free},
|
55
|
+
parts
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe :list, 'list pre-allocated files in the Jeeves store'
|
60
|
+
help :list, 'list all of the pre-allocated files',
|
61
|
+
'that Jeeves currently knows about. These may',
|
62
|
+
'include files that were not released and those',
|
63
|
+
'that have not been recorded yet.'
|
64
|
+
|
65
|
+
def list
|
66
|
+
jestore = Jeeves::Store.new(@logger, @jeeves_config)
|
67
|
+
if jestore.files <= 0 then
|
68
|
+
puts "There are no files to show"
|
69
|
+
else
|
70
|
+
entries = Array.new
|
71
|
+
colours = Array.new
|
72
|
+
compare_time = Time.now
|
73
|
+
jestore.each_file do |path, params|
|
74
|
+
entries << [path, Jeeves.human_size(params[:space]), params[:date].strftime("%Y-%b-%d %H:%M")]
|
75
|
+
if params[:date] < compare_time then
|
76
|
+
if FileTest.exists?(path) then
|
77
|
+
colours << :yellow
|
78
|
+
else
|
79
|
+
colours << :red
|
80
|
+
end
|
81
|
+
else
|
82
|
+
colours << :green
|
83
|
+
end
|
84
|
+
end
|
85
|
+
Jeeves.tabulate(%w{* *r *c},
|
86
|
+
%w{Path Space Date},
|
87
|
+
entries, colours)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
describe :clean, 'clean old records from the list of files'
|
93
|
+
help :clean, 'deletes any records in the Jeeves file store',
|
94
|
+
'for files whose allocation date has now expired'
|
95
|
+
|
96
|
+
def clean
|
97
|
+
jestore = Jeeves::Store.new(@logger, @jeeves_config)
|
98
|
+
if get_option :delete then
|
99
|
+
# going to get rid of all file lists
|
100
|
+
if ask('Delete all file lists?', :no, 'yn') == :yes then
|
101
|
+
|
102
|
+
jestore.each_partition do |part|
|
103
|
+
if FileTest.exists?(part.file_list) then
|
104
|
+
puts "Deleting #{part.file_list}"
|
105
|
+
FileUtils.rm_f(part.file_list)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
else
|
110
|
+
|
111
|
+
puts "OK, no files have been deleted"
|
112
|
+
|
113
|
+
end
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
# jeeves store clear
|
118
|
+
if jestore.files == 0 then
|
119
|
+
puts "The file list is empty, nothing to clean".yellow
|
120
|
+
return
|
121
|
+
end
|
122
|
+
if get_option :clean_all then
|
123
|
+
if ask('Clean all files from the list?', :no, 'yn') != :no then
|
124
|
+
jestore.clean(true)
|
125
|
+
puts "The file list is now empty".green
|
126
|
+
end
|
127
|
+
else
|
128
|
+
if jestore.stale_files == 0 then
|
129
|
+
puts "There are no files to clean!".yellow
|
130
|
+
return
|
131
|
+
end
|
132
|
+
if ask('Clean files from the list?', :no, 'yn') != :no then
|
133
|
+
jestore.clean
|
134
|
+
puts "Cleaned file list".green
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
describe :init, 'initialise a partition for use by Jeeves'
|
141
|
+
help :init, 'This will set up a partition to be used by Jeeves'
|
142
|
+
|
143
|
+
def init
|
144
|
+
new_store = next_argument_or_error("need to provide a path to initialise")
|
145
|
+
|
146
|
+
unless FileTest.exists?(new_store)
|
147
|
+
exit_on_error "path #{new_store} does not exist"
|
148
|
+
end
|
149
|
+
|
150
|
+
part = Jeeves::Partition.new(new_store)
|
151
|
+
|
152
|
+
if get_option :delete then
|
153
|
+
part.delete_key
|
154
|
+
return
|
155
|
+
end
|
156
|
+
|
157
|
+
force = get_option :force_init
|
158
|
+
puts "Forcing a new key!".red.bold if force
|
159
|
+
key = part.create_key(force)
|
160
|
+
puts ""
|
161
|
+
puts "# add this line to your config file"
|
162
|
+
puts "add_store :path=>'#{part.path}', :key=>'#{key}'"
|
163
|
+
rescue Jeeves::InvalidPartitionKey => err
|
164
|
+
exit_on_error err
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeeves
|
4
|
+
#
|
5
|
+
# == Video CLI
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2014 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
|
+
#
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'optplus/nested'
|
20
|
+
|
21
|
+
class VideosCLI < Optplus::NestedParser
|
22
|
+
|
23
|
+
include Jeni::IO
|
24
|
+
|
25
|
+
usage "videos <command> [<options>]"
|
26
|
+
|
27
|
+
description "Do things with videos, such as load them into Jeeves."
|
28
|
+
|
29
|
+
def before_actions
|
30
|
+
@jeeves_config = @_parent.jeeves_config
|
31
|
+
@logger = @_parent.logger
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
describe :show, 'show information about a Jeeves-ready video'
|
36
|
+
|
37
|
+
def show
|
38
|
+
file = next_argument_or_error "Need to provide a file to show"
|
39
|
+
|
40
|
+
unless FileTest.exists?(file)
|
41
|
+
exit_on_error "File does not exist: #{file}"
|
42
|
+
end
|
43
|
+
|
44
|
+
vid = Jeeves::Video.new(file, @logger, @jeeves_config)
|
45
|
+
tags = vid.get_tags
|
46
|
+
strs = Array.new
|
47
|
+
|
48
|
+
%w{programme subtitle category}.each do |key|
|
49
|
+
strs << [key.capitalize, tags.delete(key)] if tags.has_tag?(key)
|
50
|
+
end
|
51
|
+
|
52
|
+
unless tags.has_tag?('description')
|
53
|
+
exit_on_error "This file has not been tagged for Jeeves"
|
54
|
+
end
|
55
|
+
|
56
|
+
width = 80
|
57
|
+
descs = [tags.delete('description')]
|
58
|
+
if descs[0].length > width then
|
59
|
+
descs = descs[0].scan(/\S.{0,#{width}}\S(?=\s|$)|\S+/)
|
60
|
+
end
|
61
|
+
|
62
|
+
first = true
|
63
|
+
descs.each do |desc|
|
64
|
+
strs << [first ? 'Description' : '', desc]
|
65
|
+
first = false
|
66
|
+
end
|
67
|
+
|
68
|
+
strs << ['','']
|
69
|
+
|
70
|
+
strs << ['Start', Time.at(tags.delete('start').to_i).strftime('%a %-d %b %y, %H:%M')]
|
71
|
+
strs << ['Duration', Jeeves.hrs_mins(tags.delete('duration').to_i)]
|
72
|
+
|
73
|
+
tags.each_tag do |key, value|
|
74
|
+
strs << [key, value]
|
75
|
+
end
|
76
|
+
|
77
|
+
Jeeves.tabulate(%w{* 80}, %w{Key Contents}, strs)
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
describe :load, 'load an existing video into Jeeves'
|
82
|
+
help :load, 'Use load for a file that already has the tags relevant to Jeeves',
|
83
|
+
'and just needs to be added to the database.'
|
84
|
+
|
85
|
+
def load
|
86
|
+
file = next_argument_or_error "Need to provide a file to load"
|
87
|
+
|
88
|
+
unless FileTest.exists?(file)
|
89
|
+
exit_on_error "File does not exist: #{file}"
|
90
|
+
end
|
91
|
+
vid = Jeeves::Video.new(file, @logger, @jeeves_config)
|
92
|
+
|
93
|
+
response = vid.upload_to_jeeves
|
94
|
+
vid.clean_up
|
95
|
+
if response.to_i == 0 then
|
96
|
+
puts "Jeeves OK".green
|
97
|
+
|
98
|
+
else
|
99
|
+
exit_on_error "Jeeves returned unexpected response: #{response}"
|
100
|
+
end
|
101
|
+
rescue Jeeves::JeevesError => e
|
102
|
+
exit_on_error "Error while loading video: #{e}"
|
103
|
+
end
|
104
|
+
|
105
|
+
describe :wrangle, 'tag and load a video into Jeeves'
|
106
|
+
help :load, 'Obtain metadata from Jeeves and tag a video with it',
|
107
|
+
'before loading into Jeeves.'
|
108
|
+
|
109
|
+
def wrangle
|
110
|
+
|
111
|
+
file = next_argument_or_error "Need to provide a file to wrangle"
|
112
|
+
unless FileTest.exists?(file)
|
113
|
+
exit_on_error "File does not exist: #{file}"
|
114
|
+
end
|
115
|
+
pid = next_argument_or_error "Need to provide a programme id to wrangle"
|
116
|
+
vid = Jeeves::Video.new(file, @logger, @jeeves_config)
|
117
|
+
vid.tag_from_prog_id(pid)
|
118
|
+
response = vid.upload_to_jeeves
|
119
|
+
vid.clean_up
|
120
|
+
if response.to_i == 0 then
|
121
|
+
puts "Jeeves OK"
|
122
|
+
|
123
|
+
else
|
124
|
+
exit_on_error "Jeeves returned unexpected response: #{response}"
|
125
|
+
end
|
126
|
+
rescue Jeeves::JeevesError => e
|
127
|
+
exit_on_error "Error while wrangling video: #{e}"
|
128
|
+
end
|
129
|
+
|
130
|
+
describe :orphans, 'find files that have no entry in Jeeves'
|
131
|
+
|
132
|
+
def orphans
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeeves Disk
|
4
|
+
#
|
5
|
+
# == a single area to store files and part of a set of disks making up a store
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2013 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
|
+
#
|
17
|
+
#
|
18
|
+
require 'jeeves/errors'
|
19
|
+
require 'jeeves/utils'
|
20
|
+
|
21
|
+
module Jeeves
|
22
|
+
|
23
|
+
# a unit of storage that can be added into a store
|
24
|
+
class Partition
|
25
|
+
|
26
|
+
#include Jeeves::Utils
|
27
|
+
|
28
|
+
# create a partition, regardless of what state it is in
|
29
|
+
def initialize(path, options={})
|
30
|
+
@path = File.expand_path(path)
|
31
|
+
@key = options[:key]
|
32
|
+
@key_file = File.join(@path, '.jeeves.key')
|
33
|
+
@file_list = File.join(@path, '.jeeves_store.rb')
|
34
|
+
@mount_point = options[:mount_point] || @path
|
35
|
+
|
36
|
+
# assume the disk is not ready
|
37
|
+
@mounted = false
|
38
|
+
@ready = false
|
39
|
+
# check status
|
40
|
+
if FileTest.writable?(@path) then
|
41
|
+
# seems to be there, but is it correct?
|
42
|
+
if FileTest.exists?(@key_file) then
|
43
|
+
# there is a key file so assume it is mounted
|
44
|
+
@mounted = true
|
45
|
+
@ready = key_ok?
|
46
|
+
end
|
47
|
+
else
|
48
|
+
unless @path == @mount_point then
|
49
|
+
# check if mount_point is mounted
|
50
|
+
if FileTest.writable?(@mount_point) then
|
51
|
+
@mounted = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end #if
|
55
|
+
|
56
|
+
@total_space = 0
|
57
|
+
@free_space = 0
|
58
|
+
@device = ''
|
59
|
+
update_space
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# path to the partition
|
64
|
+
attr_reader :path
|
65
|
+
|
66
|
+
# boolean flag to indicate if the partition is mounted
|
67
|
+
# @deprecated use {#mounted?} instead
|
68
|
+
attr_reader :mounted
|
69
|
+
|
70
|
+
# test if the partition is mounted
|
71
|
+
def mounted?
|
72
|
+
return @mounted
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :ready
|
76
|
+
|
77
|
+
# test if the partition is ready to use
|
78
|
+
def ready?
|
79
|
+
return @ready
|
80
|
+
end
|
81
|
+
|
82
|
+
# return the device on which the partition resides (e.g. /dev/sda1)
|
83
|
+
attr_reader :device
|
84
|
+
|
85
|
+
# return the total space in the partition (in 1K blocks)
|
86
|
+
attr_reader :total_space
|
87
|
+
|
88
|
+
# return the free space in the partitions (in 1K blocks)
|
89
|
+
attr_reader :free_space
|
90
|
+
|
91
|
+
# return tha path to where the file list would be or is
|
92
|
+
attr_reader :file_list
|
93
|
+
|
94
|
+
# flag the partition as a duplicate
|
95
|
+
attr_accessor :duplicate
|
96
|
+
|
97
|
+
# return the date of the file_list if it exists or nil otherwise
|
98
|
+
# The returned value is the modification time of the file
|
99
|
+
def file_list_date
|
100
|
+
return nil if @file_list.nil?
|
101
|
+
return File.mtime(@file_list)
|
102
|
+
end
|
103
|
+
|
104
|
+
# test if the file list exists
|
105
|
+
def file_list?
|
106
|
+
return FileTest.exists?(@file_list)
|
107
|
+
end
|
108
|
+
|
109
|
+
# test that the key given at init is the same as the key
|
110
|
+
# on the disk
|
111
|
+
def key_ok?
|
112
|
+
# get the key in the keyfile
|
113
|
+
return false if @key.nil?
|
114
|
+
return false unless File.exists?(@key_file)
|
115
|
+
file_key = File.readlines(@key_file).first.chomp
|
116
|
+
return file_key == @key
|
117
|
+
end
|
118
|
+
|
119
|
+
# return the free space in 1K blocks on the given path
|
120
|
+
def update_space
|
121
|
+
return unless @ready
|
122
|
+
# use df to get the free space for path
|
123
|
+
df_lines = `/bin/df -Pk #{@path}`.split("\n")
|
124
|
+
df_fields = df_lines[1].split
|
125
|
+
@device = df_fields[0]
|
126
|
+
@total_space = df_fields[1].to_i
|
127
|
+
@free_space = df_fields[3].to_i
|
128
|
+
end
|
129
|
+
|
130
|
+
def display(cols)
|
131
|
+
return @path.ljust(cols.shift) +
|
132
|
+
(@mounted ? 'OK' : 'No').ljust(cols.shift) +
|
133
|
+
(@ready ? 'OK': 'No').ljust(cols.shift) +
|
134
|
+
(@duplicate ? '*': '').ljust(cols.shift) +
|
135
|
+
Jeeves.human_size(@total_space).rjust(cols.shift) +
|
136
|
+
Jeeves.human_size(@free_space).rjust(cols.shift)
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_a
|
140
|
+
return [@path,
|
141
|
+
(@mounted ? 'OK' : 'No'),
|
142
|
+
(@ready ? 'OK': 'No'),
|
143
|
+
(@duplicate ? '*': ''),
|
144
|
+
Jeeves.human_size(@total_space),
|
145
|
+
Jeeves.human_size(@free_space)]
|
146
|
+
end
|
147
|
+
|
148
|
+
# create a key and save it to the disk
|
149
|
+
#
|
150
|
+
# This will raise an exception if the key file already exists or cannot be written
|
151
|
+
# but it can be forced to create the key anyway
|
152
|
+
#
|
153
|
+
# @param [Boolean] force the key file to be created if one already exists
|
154
|
+
def create_key(force=false)
|
155
|
+
# check that path is writable
|
156
|
+
unless FileTest.writable?(@path)
|
157
|
+
raise Jeeves::InvalidPartition, "Cannot write to #{path}"
|
158
|
+
end
|
159
|
+
|
160
|
+
if FileTest.exists?(@key_file) && !force then
|
161
|
+
raise Jeeves::InvalidPartitionKey, "Keyfile already exists at #{path}"
|
162
|
+
end
|
163
|
+
|
164
|
+
@key = Digest::SHA1.hexdigest(Time.now.to_s + rand(12341234).to_s)
|
165
|
+
|
166
|
+
File.open(@key_file, 'w') do |kfile|
|
167
|
+
kfile.puts @key
|
168
|
+
end
|
169
|
+
return @key
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|