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