jeeves-pvr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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