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
data/lib/jeeves/tags.rb
ADDED
@@ -0,0 +1,329 @@
|
|
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
|
+
# [requires go here]
|
14
|
+
# ffmpeg -itsoffset -4 -i test.avi -vcodec mjpeg -vframes 1 -an -f rawvideo -s 320x240 test.jpg
|
15
|
+
#
|
16
|
+
require 'jeeves/errors'
|
17
|
+
require 'jeeves/utils'
|
18
|
+
|
19
|
+
require 'nokogiri'
|
20
|
+
require 'open-uri'
|
21
|
+
|
22
|
+
# = Jetags
|
23
|
+
#
|
24
|
+
# Helper class to create and manipulate tag data to be included in or extracted from matroska files
|
25
|
+
#
|
26
|
+
module Jeeves
|
27
|
+
|
28
|
+
class Tagger
|
29
|
+
|
30
|
+
Fingerprint = "JeevesTags"
|
31
|
+
|
32
|
+
# create a tags objects
|
33
|
+
def initialize(options = {})
|
34
|
+
|
35
|
+
#@jeeves_tv_url = options[:jeeves_tv_url] || raise(MissingOption, "Need to have a jeeves tv url")
|
36
|
+
|
37
|
+
@tag_data = nil
|
38
|
+
@tag_hash = Hash.new
|
39
|
+
@tag_hash[:fingerprint] = Fingerprint
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# provided for backward compatibility - use new_from_remote_id
|
44
|
+
def self.new_from_prog_id(prog_id, listing_url)
|
45
|
+
|
46
|
+
tags = self.new
|
47
|
+
|
48
|
+
tags.load_tags_from_prog(prog_id, listing_url)
|
49
|
+
|
50
|
+
return tags
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
# get tags from a remote service with the id, route and url
|
55
|
+
# #{url}/#{route}/#{id}.xml
|
56
|
+
# which should provide an XML document with Simple nodes and
|
57
|
+
# Name and String node contents.
|
58
|
+
#
|
59
|
+
# <Tags>
|
60
|
+
# <Tag>
|
61
|
+
# <Simple>
|
62
|
+
# <Name>name</Name>
|
63
|
+
# <String>value</String>
|
64
|
+
# </Simple>
|
65
|
+
# <Simple>
|
66
|
+
# ...
|
67
|
+
# </Simple>
|
68
|
+
# </Tag>
|
69
|
+
# </Tags>
|
70
|
+
#
|
71
|
+
def self.new_from_remote_id(id, route, url)
|
72
|
+
|
73
|
+
tags = self.new
|
74
|
+
|
75
|
+
tags.load_tags_from_remote(id, route, url)
|
76
|
+
|
77
|
+
return tags
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.new_from_file(pathname)
|
82
|
+
tags = self.new
|
83
|
+
|
84
|
+
tags.load_tags_from_file(pathname)
|
85
|
+
|
86
|
+
return tags
|
87
|
+
end
|
88
|
+
|
89
|
+
# get tags for the given programme from jeeves-tv and
|
90
|
+
# embed them in the given video file, which incidentally becomes an mkv
|
91
|
+
# WARNING - mkvmerge, used here to tag stuff, makes a big hash of mpegts
|
92
|
+
# and often seems to screw the audio up completely. Probably not worth trying this one!
|
93
|
+
#
|
94
|
+
def write_to_file(path)
|
95
|
+
|
96
|
+
# check that the path exists and get its basename
|
97
|
+
unless File.readable?(path)
|
98
|
+
raise FileError, "Cannot read: #{path}"
|
99
|
+
end
|
100
|
+
dirname = File.expand_path(File.dirname(path))
|
101
|
+
bname = File.basename(path, File.extname(path))
|
102
|
+
mkv_path = File.join(dirname, bname + '.mkv')
|
103
|
+
unless mkv_path != path
|
104
|
+
raise FileError, "Source file is already matroska"
|
105
|
+
end
|
106
|
+
|
107
|
+
xml_path = File.join(dirname, bname + '.xml')
|
108
|
+
|
109
|
+
# get the tags from jeeves-tv, wherever that might be and save to a hidden file
|
110
|
+
|
111
|
+
cmd = "/usr/bin/wget #{@jeeves_tv_url}/pid/#{prog_id}.xml -O #{xml_path}"
|
112
|
+
unless system(cmd)
|
113
|
+
raise XMLError, "Failed to get xml data: #{$?}. Command line was: #{cmd}"
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
unless File.exists?(xml_path)
|
118
|
+
raise XMLError, "XML file: #{xml_path} was not created"
|
119
|
+
end
|
120
|
+
|
121
|
+
# merge the tags with the video file to create a matroska file
|
122
|
+
|
123
|
+
cmd = "/usr/bin/mkvmerge --global-tags #{xml_path} #{path} -o #{mkv_path}"
|
124
|
+
|
125
|
+
unless system(cmd)
|
126
|
+
raise MkvError, "Failed to create mkv: #{$?}, Command line was: #{cmd}"
|
127
|
+
end
|
128
|
+
return true
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# provide all tag data as key, value pairs
|
133
|
+
def each_tag(&block)
|
134
|
+
@tag_hash.each do |tag, value|
|
135
|
+
block.call(tag, value)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# return all the tags as a string of key=value pairs
|
140
|
+
def to_a
|
141
|
+
|
142
|
+
tags_to_exclude = %w[priority tuner]
|
143
|
+
|
144
|
+
tag_ary = Array.new
|
145
|
+
|
146
|
+
@tag_hash.each do |tag, value|
|
147
|
+
next if tags_to_exclude.include?(tag)
|
148
|
+
tag_ary << tag.to_s + '=' + value
|
149
|
+
end
|
150
|
+
|
151
|
+
return tag_ary
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_s
|
155
|
+
strings = ''
|
156
|
+
each_tag do |tag, value|
|
157
|
+
strings << " #{tag}: #{value}\n"
|
158
|
+
end
|
159
|
+
return strings
|
160
|
+
end
|
161
|
+
|
162
|
+
alias :metadata :to_a
|
163
|
+
|
164
|
+
# return the value of a given tag
|
165
|
+
def [](tag)
|
166
|
+
return @tag_hash[tag]
|
167
|
+
end
|
168
|
+
|
169
|
+
# create a new tag
|
170
|
+
def []=(tag, value)
|
171
|
+
@tag_hash[tag] = value
|
172
|
+
end
|
173
|
+
|
174
|
+
def delete(tag)
|
175
|
+
@tag_hash.delete(tag)
|
176
|
+
end
|
177
|
+
|
178
|
+
def has_tag?(tag)
|
179
|
+
@tag_hash.has_key?(tag)
|
180
|
+
end
|
181
|
+
|
182
|
+
def jeeves?
|
183
|
+
@tag_hash.has_key?(:fingerprint) && @tag_hash[:fingerprint] == Fingerprint
|
184
|
+
end
|
185
|
+
|
186
|
+
#protected
|
187
|
+
|
188
|
+
# get tags direct from programme listing
|
189
|
+
def load_tags_from_prog(prog_id, url)
|
190
|
+
@tag_data = String.new
|
191
|
+
my_url = "#{url}/pid/#{prog_id}.xml"
|
192
|
+
open(my_url) do |xml|
|
193
|
+
xml.each_line {|line| @tag_data << line}
|
194
|
+
end
|
195
|
+
self.xml_to_hash
|
196
|
+
#rescue
|
197
|
+
#raise UrlError, "Url failed to open: #{my_url}"
|
198
|
+
end
|
199
|
+
|
200
|
+
# get tags from a remote service on the given url and route
|
201
|
+
# #{url}/#{route}/#{id}.xml
|
202
|
+
#
|
203
|
+
# should replace load_tags_from_prog
|
204
|
+
def load_tags_from_remote(id, route, url)
|
205
|
+
@tag_data = String.new
|
206
|
+
my_url = "#{url}/#{route}/#{id}.xml"
|
207
|
+
#puts "URL: #{my_url}"
|
208
|
+
open(my_url) do |xml|
|
209
|
+
xml.each_line do |line|
|
210
|
+
#puts line
|
211
|
+
@tag_data << line
|
212
|
+
end
|
213
|
+
end
|
214
|
+
self.xml_to_hash
|
215
|
+
#puts @tag_hash.inspect
|
216
|
+
end
|
217
|
+
|
218
|
+
# extract tags from an mkv video and return as an xml string
|
219
|
+
def load_tags_from_file(path)
|
220
|
+
unless File.readable?(path)
|
221
|
+
raise FileError, "Cannot read: #{path}"
|
222
|
+
end
|
223
|
+
dirname = File.expand_path(File.dirname(path))
|
224
|
+
bname = File.basename(path, File.extname(path))
|
225
|
+
xml_path = File.join(dirname, bname + '.xml')
|
226
|
+
|
227
|
+
@tag_data = `/usr/bin/mkvextract tags #{path}`
|
228
|
+
|
229
|
+
self.xml_to_hash
|
230
|
+
|
231
|
+
# unless system(cmd)
|
232
|
+
# raise MkvError, "Failed to extract tags: #{$?}, command line: #{cmd}"
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# unless File.exists?(xml_path)
|
236
|
+
# raise XMLError, "XML file: #{xml_path} was not created"
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# @tag_data = File.readlines(xml_path)
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
def load_tags_from_imdb(url)
|
244
|
+
doc = Nokogiri::HTML(open(url))
|
245
|
+
|
246
|
+
name = doc.at_css('*[itemprop="name"]').content
|
247
|
+
@tag_hash[:programme] = name.gsub("\n", '').gsub(/\([0-9]+\)/, '')
|
248
|
+
@tag_hash[:duration] = doc.at_css('*[itemprop="duration"]').content.to_i * 60
|
249
|
+
@tag_hash[:date] = doc.at_css('*[itemprop="datePublished"]').content.match(/.*([0-9]{4,4})/)[1]
|
250
|
+
@tag_hash[:description] = doc.at_css('*[itemprop="description"]').content.gsub("\n", '')
|
251
|
+
@tag_hash[:source] = 'IMDB'
|
252
|
+
@tag_hash[:category] = 'Film'
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
# copy a file and apply the given tags to it
|
257
|
+
def apply_tags_to_file(filename)
|
258
|
+
if FileTest.exists?(filename) then
|
259
|
+
source = funique(filename)
|
260
|
+
FileUtils.mv(filename, source)
|
261
|
+
dn, bn, en = fsplit(filename)
|
262
|
+
target = File.join(dn, bn + ".mkv")
|
263
|
+
ffargs = "-i #{source} -c:v copy -c:a copy -f matroska -y".split(' ')
|
264
|
+
self.metadata.each do |tag|
|
265
|
+
ffargs << "-metadata"
|
266
|
+
ffargs << tag
|
267
|
+
end
|
268
|
+
ffargs << target
|
269
|
+
system("/usr/bin/ffmpeg", *ffargs)
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
# copy a bit of a file and apply the given tags to it
|
276
|
+
def apply_to_proxy(filename)
|
277
|
+
if FileTest.exists?(filename) then
|
278
|
+
self['path'] = filename
|
279
|
+
target = Jeeves.rename(filename)
|
280
|
+
dn, bn, en = fsplit(target)
|
281
|
+
target = File.join(dn, bn + ".mkv")
|
282
|
+
ffargs = "-i #{filename} -v quiet -t 10 -f matroska -y".split(' ')
|
283
|
+
self.metadata.each do |tag|
|
284
|
+
ffargs << "-metadata"
|
285
|
+
ffargs << tag
|
286
|
+
end
|
287
|
+
ffargs << target
|
288
|
+
unless system("/usr/bin/ffmpeg", *ffargs)
|
289
|
+
raise Jeeves::MkvError, "ffmpeg failed with args: #{ffargs.join(' ')}"
|
290
|
+
end
|
291
|
+
return target
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
def xml_to_hash
|
297
|
+
xml_tags = Nokogiri::XML(@tag_data)
|
298
|
+
|
299
|
+
@tag_hash = Hash.new # ensure you are not assuming this comes from Jeeves
|
300
|
+
|
301
|
+
xml_tags.xpath('//Simple').each do |tag|
|
302
|
+
@tag_hash[tag.at_xpath('Name').content.downcase] = tag.at_xpath('String').content
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
|
307
|
+
def funique(filename)
|
308
|
+
dirname, basename, extname = fsplit(filename)
|
309
|
+
newfilename = File.join(dirname, basename + extname)
|
310
|
+
while FileTest.exists?(newfilename)
|
311
|
+
# oh dear, got one already. need to add something to the name
|
312
|
+
randname = (rand * 10000).to_i.to_s
|
313
|
+
basename = basename + '_' + randname + extname
|
314
|
+
newfilename = File.join(dirname, basename)
|
315
|
+
end
|
316
|
+
|
317
|
+
return newfilename
|
318
|
+
|
319
|
+
end
|
320
|
+
|
321
|
+
def fsplit(filename)
|
322
|
+
dname = File.dirname(filename)
|
323
|
+
extname = File.extname(filename)
|
324
|
+
basename = File.basename(filename, extname)
|
325
|
+
return [dname, basename, extname]
|
326
|
+
end
|
327
|
+
|
328
|
+
end
|
329
|
+
end
|
data/lib/jeeves/utils.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Utilities for Jeeves
|
4
|
+
#
|
5
|
+
# == human_size etc
|
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 'colored'
|
19
|
+
|
20
|
+
module Jeeves
|
21
|
+
|
22
|
+
Kb = 1 * 1000
|
23
|
+
Mb = Kb * Kb
|
24
|
+
Gb = Mb * Kb
|
25
|
+
Tb = Gb * Kb
|
26
|
+
|
27
|
+
def Jeeves.human_size(size_in_k)
|
28
|
+
size = size_in_k * Kb
|
29
|
+
case
|
30
|
+
when size < Mb then '%.1f KB' % (size_in_k)
|
31
|
+
when size < Gb then '%.1f MB' % (size_in_k / Kb)
|
32
|
+
when size < Tb then '%.1f GB' % (size_in_k / Mb)
|
33
|
+
else
|
34
|
+
'%.1f TB' % (size_in_k / Gb)
|
35
|
+
end.sub('.0', '')
|
36
|
+
rescue
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def Jeeves.hrs_mins(secs)
|
41
|
+
mins = secs / 60
|
42
|
+
hours = mins /60
|
43
|
+
mins -= hours * 60
|
44
|
+
mstr = "%02d" % mins
|
45
|
+
units = hours > 1 ? 'hrs' : 'hr'
|
46
|
+
return "#{hours}#{units}#{mstr}"
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def Jeeves.tabulate(cols, header, data, colours=nil)
|
51
|
+
|
52
|
+
data_s = Array.new
|
53
|
+
data.each do |row|
|
54
|
+
row_s = []
|
55
|
+
row.each do |cell|
|
56
|
+
row_s << cell.to_s
|
57
|
+
end
|
58
|
+
data_s << row_s
|
59
|
+
end
|
60
|
+
|
61
|
+
data = data_s
|
62
|
+
|
63
|
+
methods = Array.new(cols.length, :ljust)
|
64
|
+
method_lup = {'l'=>:ljust, 'r'=>:rjust, 'c'=>:center}
|
65
|
+
|
66
|
+
col_re = /^(\*|[0-9]+)([lrc]{0,1})$/
|
67
|
+
|
68
|
+
# need to get column widths from header and data
|
69
|
+
cols.each_index do |col_index|
|
70
|
+
if tokens = col_re.match(cols[col_index]) then
|
71
|
+
if tokens[1] == '*' then
|
72
|
+
# work out the column width
|
73
|
+
cols[col_index] = header[col_index].length
|
74
|
+
data.each do |row|
|
75
|
+
cols[col_index] = row[col_index].length if row[col_index].length >= cols[col_index]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
cols[col_index] = tokens[1].to_i
|
79
|
+
end
|
80
|
+
# and add the justification method
|
81
|
+
#puts tokens.inspect
|
82
|
+
methods[col_index] = method_lup[tokens[2]] unless tokens[2] == ''
|
83
|
+
end # if
|
84
|
+
end # cols.each_index block
|
85
|
+
|
86
|
+
#puts cols.inspect
|
87
|
+
fstr = String.new
|
88
|
+
header.each_index {|hdi| fstr << (sprintf header[hdi].send(methods[hdi], cols[hdi]).bold + ' ')}
|
89
|
+
puts fstr
|
90
|
+
|
91
|
+
data.each_index do |row_ind|
|
92
|
+
fstr = String.new
|
93
|
+
row = data[row_ind]
|
94
|
+
row.each_index {|ri| fstr << (sprintf row[ri].send(methods[ri], cols[ri]) + ' ')}
|
95
|
+
fstr = fstr.send(colours[row_ind]) unless colours.nil? || colours[row_ind].nil?
|
96
|
+
puts fstr
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
# rename a file by appending a random string to the basename
|
102
|
+
#
|
103
|
+
def Jeeves.rename(path)
|
104
|
+
dir_name = File.dirname(path)
|
105
|
+
extname = File.extname(path)
|
106
|
+
root_basename = File.basename(path, extname)
|
107
|
+
|
108
|
+
loop_count = 0 # catch a loop
|
109
|
+
|
110
|
+
while block_given? ? yield(path) : FileTest.exists?(path)
|
111
|
+
loop_count += 1
|
112
|
+
path = nil
|
113
|
+
break if loop_count > 10
|
114
|
+
# oh dear, got one already. need to add something to the name
|
115
|
+
randname = Kernel.rand(10000).to_s
|
116
|
+
basename = root_basename + '_' + randname + extname
|
117
|
+
path = File.join(dir_name, basename)
|
118
|
+
end
|
119
|
+
|
120
|
+
return path
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|