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,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
|