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