ruby-mythtv 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1.0 (2008-06-08)
2
+
3
+ * Initial release onto GitHub/Rubyforge
data/License.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008 Nick Ludlam
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.txt ADDED
@@ -0,0 +1,68 @@
1
+ = Ruby-mythtv
2
+
3
+ == Description
4
+
5
+ A pure Ruby implementation of the MythTV Backend protocol to allow interaction with a MythTV server. Features include browsing and streaming of recordings, and thumbnail generation. See http://github.com/nickludlam/ruby-mythtv for more details.
6
+
7
+ == Requirements
8
+
9
+ This gem does not yet support multiple protocol versions, so it requires an up-to-date installation of MythTV v0.21, and specifically implements protocol version 40. For more information on the history of the MythTV protocol, see http://www.mythtv.org/wiki/index.php/Protocol
10
+
11
+ == Install
12
+
13
+ $ gem sources -a http://gems.github.com/ (only required once)
14
+ $ gem install nickludlam-ruby-mythtv
15
+
16
+ == Source
17
+
18
+ The ruby-mythtv source is available on GitHub at
19
+
20
+ http://github.com/nickludlam/ruby-mythtv
21
+
22
+ and can be cloned from
23
+
24
+ git://github.com/nickludlam/ruby-mythtv.git
25
+
26
+ == Basic usage
27
+
28
+ require 'ruby-mythtv'
29
+
30
+ # Connect to the server
31
+ backend = MythTV::Backend.new(:host => 'mythtv.localdomain')
32
+
33
+ # Get an array of recordings
34
+ recordings = backend.query_recordings
35
+
36
+ # Generate a thumbnail of the most recent recording, at 60 seconds in from the start
37
+ preview_thumbnail = @backend.preview_image(recordings[0], :secs_in => 60)
38
+ File.open('preview_thumbnail.png', 'w') { |f| f.write(preview_thumbnail) }
39
+
40
+ == Author
41
+
42
+ Written in 2008 by Nick Ludlam <nick@recoil.org>
43
+
44
+ == License
45
+
46
+ Copyright (c) 2008 Nick Ludlam
47
+
48
+ Permission is hereby granted, free of charge, to any person
49
+ obtaining a copy of this software and associated documentation
50
+ files (the "Software"), to deal in the Software without
51
+ restriction, including without limitation the rights to use,
52
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
53
+ copies of the Software, and to permit persons to whom the
54
+ Software is furnished to do so, subject to the following
55
+ conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
62
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
63
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
64
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
65
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
66
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
67
+ OTHER DEALINGS IN THE SOFTWARE.
68
+
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ Gem::manage_gems
3
+ require 'rake/gempackagetask'
4
+ require 'rake/testtask'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = %q{ruby-mythtv}
8
+ s.version = "0.1.0"
9
+
10
+ s.specification_version = 2 if s.respond_to? :specification_version=
11
+
12
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
13
+ s.authors = ["Nick Ludlam"]
14
+ s.date = %q{2008-06-08}
15
+ s.description = %q{Ruby implementation of the MythTV communication protocol}
16
+ s.email = %q{nick@recoil.org}
17
+ s.extra_rdoc_files = ["History.txt", "License.txt", "README.txt"]
18
+ s.files = ["History.txt", "License.txt", "README.txt", "Rakefile", "lib/ruby-mythtv.rb", "lib/mythtv/backend.rb", "lib/mythtv/recording.rb", "test/test_backend.rb", "test/test_helper.rb"]
19
+ s.has_rdoc = true
20
+ s.homepage = %q{http://github.com/nickludlam/ruby-mythtv/}
21
+ s.rdoc_options = ["--main", "README.txt"]
22
+ s.require_paths = ["lib"]
23
+ s.rubyforge_project = %q{ruby-mythtv}
24
+ s.rubygems_version = %q{0.1.0}
25
+ s.summary = %q{Ruby implementation of the MythTV backend protocol}
26
+ s.test_files = ["test/test_backend.rb", "test/test_helper.rb"]
27
+ end
28
+
29
+ Rake::GemPackageTask.new(spec) do |pkg|
30
+ pkg.need_tar = true
31
+ end
32
+
33
+ task :build => "pkg/#{spec.name}-#{spec.version}.gem" do
34
+ puts "Generated latest version"
35
+ end
36
+
37
+ desc "Run basic unit tests"
38
+ Rake::TestTask.new("test_units") do |t|
39
+ t.pattern = 'test/test_*.rb'
40
+ t.verbose = true
41
+ t.warning = true
42
+ end
43
+
44
+ desc "Run unit tests as default"
45
+ task :default => :test_units
@@ -0,0 +1,416 @@
1
+ require 'socket'
2
+ require 'uri'
3
+ require 'net/http'
4
+
5
+ module MythTV
6
+
7
+ # Raised when we get a response that isn't what we expect
8
+ class CommunicationError < RuntimeError
9
+ end
10
+
11
+ # Raised when we have a protocol version mismatch
12
+ class ProcolError < RuntimeError
13
+ end
14
+
15
+ # Raised when a method is passed incomplete initialisation information
16
+ class ArgumentError < RuntimeError
17
+ end
18
+
19
+ class Backend
20
+ include Socket::Constants
21
+
22
+ # Our current protocol implementation. TODO: Consider how we support
23
+ # multiple protocol versions within a single gem. In theory this is
24
+ # just a case of limiting the number of attr_accessors that are
25
+ # class_eval'd onto MythTV::Recording, and bumping the number below
26
+ MYTHTV_PROTO_VERSION = 40
27
+
28
+ # The currently defined field separator in responses
29
+ FIELD_SEPARATOR = "[]:[]"
30
+
31
+ # The payload size we request from the backend when performing a filetransfer
32
+ TRANSFER_BLOCKSIZE = 65535
33
+
34
+ attr_reader :host,
35
+ :port,
36
+ :status_port,
37
+ :connection_type,
38
+ :filetransfer_port,
39
+ :filetransfer_size,
40
+ :socket
41
+
42
+ # Open the socket, make a protocol check, and announce we'd like an interactive
43
+ # session with the backend server
44
+ def initialize(options = {})
45
+ default_options = { :port => 6543, :status_port => 6544, :connection_type => :playback }
46
+ options = default_options.merge(options)
47
+
48
+ raise ArgumentError, "You must specify a :host key and value to initialize()" unless options.has_key?(:host)
49
+
50
+ @host = options[:host]
51
+ @port = options[:port]
52
+ @status_port = options[:status_port]
53
+
54
+ @socket = TCPSocket.new(@host, @port)
55
+
56
+ check_proto
57
+
58
+ if options[:connection_type] == :playback
59
+ announce_playback()
60
+ elsif options[:connection_type] == :filetransfer
61
+ announce_filetransfer(options[:filename])
62
+ else
63
+ raise ArgumentError, "Unknown connection type '#{options[:connection_type]}'"
64
+ end
65
+ end
66
+
67
+ ############################################################################
68
+ # COMMAND WRAPPERS #########################################################
69
+
70
+ # Tell the backend we speak a specific version of the protocol. Raise
71
+ # an error if the backend does not accept that version.
72
+ def check_proto
73
+ send("MYTH_PROTO_VERSION #{MYTHTV_PROTO_VERSION}")
74
+ response = recv
75
+ unless response[0] == "ACCEPT" && response[1] == MYTHTV_PROTO_VERSION.to_s
76
+ close
77
+ raise ProcolError, response.join(": ")
78
+ end
79
+ end
80
+
81
+ # Announce ourselves as a Playback connection.
82
+ # http://www.mythtv.org/wiki/index.php/Myth_Protocol_Command_ANN for details
83
+ def announce_playback
84
+ client_hostname = Socket.gethostname
85
+
86
+ # We don't want to receive broadcast events for this connection
87
+ want_events = "0"
88
+
89
+ send("ANN Playback #{client_hostname} #{want_events}")
90
+ response = recv
91
+
92
+ unless response[0] == "OK"
93
+ close
94
+ raise CommunicationError, response.join(": ")
95
+ else
96
+ @connection_type = :playback # Not currently used, but may be in later versions
97
+ end
98
+ end
99
+
100
+ # Announce ourselves as a FileTransfer connection.
101
+ # http://www.mythtv.org/wiki/index.php/Myth_Protocol_Command_ANN for details
102
+ def announce_filetransfer(filename = nil)
103
+ raise ArgumentError, "you must specify a filename" if filename.nil?
104
+
105
+ client_hostname = Socket.gethostname
106
+
107
+ filename = "/" + filename if filename[0] != "/" # Ensure leading slash
108
+
109
+ send("ANN FileTransfer #{client_hostname}#{FIELD_SEPARATOR}#{filename}")
110
+ response = recv
111
+
112
+ # Should get back something like:
113
+ # OK[]:[]<socket number>[]:[]<file size high 32 bits>[]:[]<file size low 32 bits>
114
+ unless response[0] == "OK"
115
+ close
116
+ raise CommunicationError, response.join(": ")
117
+ else
118
+ @filetransfer_port = response[1]
119
+ @filetransfer_size = [response[3].to_i, response[2].to_i].pack("ll").unpack("Q")[0]
120
+ @connection_type = :filetransfer # Not currently used, but may be in later versions
121
+ end
122
+ end
123
+
124
+ # Simple method to query the load of the backend server. Returns a hash with keys for
125
+ # :one_minute, :five_minute and :fifteen_minute
126
+ def query_load
127
+ send("QUERY_LOAD")
128
+ response = recv
129
+ { :one_minute => response[0].to_f, :five_minute => response[1].to_f, :fifteen_minute => response[2].to_f }
130
+ end
131
+
132
+ # List all recordings stored on the backend. You can filter via the storagegroup property,
133
+ # and this defaults to /Default/, to list the recordings, rather than any which are from
134
+ # LiveTV sessions.
135
+ #
136
+ # Returns an array of MythTV::Recording objects
137
+ def query_recordings(options = {})
138
+ default_options = { :filter => { :storagegroup => /Default/ } }
139
+ options = default_options.merge(options)
140
+
141
+ send("QUERY_RECORDINGS Play")
142
+ response = recv
143
+
144
+ recording_count = response.shift.to_i
145
+ recordings = []
146
+
147
+ while recording_count > 0
148
+ recording_array = response.slice!(0, Recording::RECORDINGS_ELEMENTS.length)
149
+
150
+ recording = Recording.new(recording_array)
151
+
152
+ options[:filter].each_pair do |k, v|
153
+ recordings.push(recording) if recording.send(k) =~ v
154
+ end
155
+
156
+ recording_count -= 1
157
+ end
158
+
159
+ recordings = recordings.sort_by { |r| r.startts }
160
+ recordings.reverse!
161
+ end
162
+
163
+ # This method will return the next free recorder that the backend has available to it
164
+ # TODO: Fix up the checking of response. Does it return an IP or address in element 1?
165
+ def get_next_free_recorder
166
+ send("GET_NEXT_FREE_RECORDER#{FIELD_SEPARATOR}-1")
167
+ response = recv
168
+
169
+ # If we have a recorder free, return the recorder id, otherwise false
170
+ response[0] == "-1" ? false : response[0].to_i
171
+ end
172
+
173
+ # This will trigger the backend to start recording Live TV from a certain channel.
174
+ # TODO: This is currently buggy, so avoid until it's fixed in a later release
175
+ def spawn_live_tv(recorder_id, start_channel = 1)
176
+ client_hostname = Socket.gethostname
177
+ spawn_time = Time.now.strftime("%y-%m-%dT%H:%M:%S")
178
+ chain_id = "livetv-#{client_hostname}-#{spawn_time}"
179
+
180
+ query_recorder(recorder_id, "SPAWN_LIVETV", [chain_id, 0, "#{start_channel}"])
181
+ response = recv
182
+
183
+ # If we have an "OK" back, then return the chain_id, otherwise return false
184
+ response[0] == "OK" ? chain_id : false
185
+ end
186
+
187
+ # This method returns an array of recording objects which describe which programmes
188
+ # are to be recorded as far as the current EPG data extends
189
+ def query_scheduled
190
+ send("QUERY_GETALLSCHEDULED")
191
+ response = recv
192
+
193
+ recording_count = response.shift.to_i
194
+ recordings = []
195
+
196
+ while recording_count > 0
197
+ recording_array = response.slice!(0, Recording::RECORDINGS_ELEMENTS.length)
198
+ recordings << Recording.new(recording_array)
199
+ recording_count -= 1
200
+ end
201
+
202
+ recordings = recordings.sort_by { |r| r.startts }
203
+ recordings.reverse!
204
+ end
205
+
206
+ # Wrap the QUERY_MEMSTATS backend command. Returns a hash with keys for
207
+ # :used_memory, :free_memory, :total_swap and :free_swap
208
+ def query_memstats
209
+ send("QUERY_MEMSTATS")
210
+ response = recv
211
+
212
+ # We expect to get back 4 elements only for this method
213
+ raise CommunicationError, "Unexpected response from QUERY_MEMSTATS: #{response.join(":")}" if response.length != 4
214
+
215
+ { :used_memory => response[0].to_i, :free_memory => response[1].to_i, :total_swap => response[2].to_i, :free_swap => response[3].to_i }
216
+ end
217
+
218
+ # Wrap the QUERY_UPTIME backend command. Return a single integer
219
+ def query_uptime
220
+ send("QUERY_UPTIME")
221
+ response = recv
222
+
223
+ # We expect to get back 1 element only for this method
224
+ raise CommunicationError, "Unexpected response from QUERY_UPTIME: #{response.join(":")}" if response.length != 1
225
+
226
+ response[0].to_i
227
+ end
228
+
229
+ # This is used when transfering files from the backend. It requests that the next block of data
230
+ # be sent to the socket, ready for us to recieve
231
+ def query_filetransfer_transfer_block(sock_num, size)
232
+ query = "QUERY_FILETRANSFER #{sock_num}#{FIELD_SEPARATOR}REQUEST_BLOCK#{FIELD_SEPARATOR}#{size}"
233
+ send(query)
234
+ end
235
+
236
+ # Tell the backend we've finished talking to it for the current session
237
+ def close
238
+ send("DONE")
239
+ @socket.close unless @socket.nil?
240
+ end
241
+
242
+ ############################################################################
243
+ # STATUS PORT METHODS
244
+
245
+ # Returns a string which contains a PNG image of the this recording. The time offset
246
+ # into the file defaults to two minutes, and the default image width is 120 pixels.
247
+ # This uses the separate status port, rather than talking over the backend control port
248
+ def preview_image(recording, options = {})
249
+ default_options = { :height => 120, :secs_in => 120 }
250
+ options = default_options.merge(options)
251
+
252
+ # Generate our query string for the MythTV request
253
+ query_string = "ChanId=#{recording.chanid}&StartTime=#{recording.myth_delimited_recstart}"
254
+
255
+ # Add in the optional parameters if they were specified
256
+ query_string += "&SecsIn=#{options[:secs_in]}" if options[:secs_in]
257
+ query_string += "&Height=#{options[:height]}" if options[:height]
258
+ query_string += "&Width=#{options[:width]}" if options[:width]
259
+
260
+ url = URI::HTTP.build( { :host => @host,
261
+ :port => @status_port,
262
+ :path => "/Myth/GetPreviewImage",
263
+ :query => query_string } )
264
+
265
+ # Make a GET request, and store the image data returned
266
+ image_data = Net::HTTP.get(url)
267
+
268
+ image_data
269
+ end
270
+
271
+ ############################################################################
272
+ # FILETRANSFER RELATED METHODS
273
+
274
+ # Yield into the given block with the data buffer of size TRANSFER_BLOCKSIZE
275
+ def stream(recording, options = {}, &block)
276
+
277
+ # Initialise a new connection of connection_type => :filetransfer
278
+ data_conn = Backend.new(:host => @host,
279
+ :port => @port,
280
+ :status_port => @status_port,
281
+ :connection_type => :filetransfer,
282
+ :filename => recording.path)
283
+
284
+ ft_port = data_conn.filetransfer_port
285
+ ft_size = data_conn.filetransfer_size
286
+
287
+ blocksize = options.has_key?(:transfer_blocksize) ? options[:transfer_blocksize] : TRANSFER_BLOCKSIZE
288
+
289
+ total_transfered = 0
290
+
291
+ begin
292
+ # While we still have data to fetch
293
+ while total_transfered < ft_size
294
+ # Make a request for the backend to send data
295
+ query_filetransfer_transfer_block(ft_port, blocksize)
296
+
297
+ # Collect the socket data in a string
298
+ buffer = ""
299
+
300
+ while buffer.length < blocksize
301
+ buffer += data_conn.socket.recv(blocksize)
302
+ # Special case for when the remainer to fetch is less than TRANSFER_BLOCKSIZE
303
+ break if total_transfered + buffer.length == ft_size
304
+ end
305
+
306
+ # Yield into the given block to allow the user to process as a stream
307
+ yield buffer
308
+
309
+ total_transfered += buffer.length
310
+
311
+ # If the user has only asked for a certain amount of data, stop when we hit this
312
+ break if options[:max_length] && total_transfered > options[:max_length]
313
+ end
314
+ ensure
315
+ # We need to close the data connection regardless of what is going on when we yield
316
+ data_conn.close
317
+ end
318
+
319
+ end
320
+
321
+ # Download the file to a given location, either with a default filename, or
322
+ # one specified by the caller
323
+ def download(recording, filename = nil)
324
+
325
+ # If no filename is given, we default to <title>_<recstartts>.<extension>
326
+ if filename.nil?
327
+ filename = recording.title + "_" +
328
+ recording.myth_nondelimited_recstart + File.extname(recording.filename)
329
+ end
330
+
331
+ File.open(filename, "wb") do |f|
332
+ stream(recording) { |data| f.write(data) }
333
+ end
334
+ end
335
+
336
+ # TODO: The LiveTV methods are still work-in-progress.
337
+ def start_livetv(channel = 1)
338
+ # If we have a free recorder...
339
+ if recorder_id = get_next_free_recorder
340
+ puts "Got a recorder ID of #{recorder_id}"
341
+ # If we can spawn live tv...
342
+ if chain_id = spawn_live_tv(recorder_id, channel)
343
+ puts "Got a chain ID of #{chain_id}"
344
+ # Send the two backend event messages
345
+ backend_message(["RECORDING_LIST_CHANGE", "empty"])
346
+ puts "Sent RECORDING_LIST_CHANGE"
347
+ backend_message(["LIVETV_CHAIN UPDATE #{chain_id}", "empty"])
348
+ puts "Sent LIVETV_CHAIN UPDATE"
349
+
350
+ # Find the filename from here...
351
+ query_recorder(recorder_id, "GET_CURRENT_RECORDING")
352
+ cur_rec = recv
353
+ puts "Current recording is:"
354
+ puts cur_rec.inspect
355
+ recording = Recording.new(cur_rec)
356
+ else
357
+ puts "spawn_live_tv returned with false or nil"
358
+ return false
359
+ end
360
+ else
361
+ puts "get_next_free_recorder returned with false or nil"
362
+ return false
363
+ end
364
+ end
365
+
366
+ # TODO: Finish this off. Check response?
367
+ def stop_livetv(recorder_id)
368
+ query_recorder(recorder_id, "STOP_LIVETV")
369
+ response = recv
370
+ end
371
+
372
+ private
373
+
374
+ # Private method for the generic QUERY_RECORDER command, which itself
375
+ # wraps a number of sub-commands.
376
+ def query_recorder(recorder_id, sub_command, options = [])
377
+ # place the recorder_id and sub_command strings on the front of options
378
+ # and join with the FIELD_SEPARATOR. This forms the QUERY_RECORDER command
379
+ cmd_string = options.unshift(recorder_id, sub_command)
380
+ send("QUERY_RECORDER #{options.join(FIELD_SEPARATOR)}")
381
+ end
382
+
383
+ # Wraps the BACKEND_MESSAGE command, which just sends events to the
384
+ # backend, with no responses provided. Only events expected are
385
+ # RECORDING_LIST_CHANGE, and LIVETV CHAIN_UPDATE
386
+ def backend_message(event_message = [])
387
+ event_message.unshift("BACKEND_MESSAGE")
388
+ send(message.join(FIELD_SEPARATOR))
389
+ end
390
+
391
+ # Send a message to the MythTV Backend
392
+ def send(message)
393
+ length = sprintf("%-8d", message.length)
394
+ @socket.write("#{length}#{message}")
395
+ end
396
+
397
+ # Fetch a reply from the MythTV Backend. Automatically splits around the
398
+ # FIELD_SEPARATOR
399
+ def recv
400
+ count = @socket.recv(8).to_i
401
+
402
+ # Where we accumulate the response
403
+ response = ""
404
+
405
+ # Keep fetching data until we have received the entire response
406
+ while (count > 0) do
407
+ buf = @socket.recv(TRANSFER_BLOCKSIZE)
408
+ response += buf
409
+ count -= buf.length
410
+ end
411
+
412
+ response.split(FIELD_SEPARATOR)
413
+ end
414
+
415
+ end # end Backend
416
+ end # end MythTV
@@ -0,0 +1,67 @@
1
+ module MythTV
2
+
3
+ class Recording
4
+ # Represents a recording that is held on the MythTV Backend server we are communicating with.
5
+ #
6
+ # The keys included here, and the order in which they are specified seem to change between protocol version bumps
7
+ # on the MythTV backend, so this array affects both the initialize() and to_s() methods.
8
+ #
9
+ # Found inside mythtv/libs/libmythtv/programinfo.cpp in the MythTV subversion repository
10
+ RECORDINGS_ELEMENTS = [ :title, :subtitle, :description, :category, :chanid, :chanstr, :chansign, :channame,
11
+ :pathname, :filesize_hi, :filesize_lo, :startts, :endts, :duplicate, :shareable, :findid,
12
+ :hostname, :sourceid, :cardid, :inputid, :recpriority, :recstatus, :recordid, :rectype,
13
+ :dupin, :dupmethod, :recstartts, :recendts, :repeat, :programflags, :recgroup, :chancommfree,
14
+ :chanOutputFilters, :seriesid, :programid, :lastmodified, :stars, :originalAirDate,
15
+ :hasAirDate, :playgroup, :recpriority2, :parentid, :storagegroup, :audioproperties,
16
+ :videoproperties, :subtitleType ]
17
+
18
+ # Warning, metaprogramming ahead: Create attr_accessors for each symbol defined in MythTVRecording::RECORDINGS_ELEMENTS
19
+ def initialize(recording_array)
20
+ class << self;self;end.class_eval { RECORDINGS_ELEMENTS.each { |field| attr_accessor field } }
21
+
22
+ RECORDINGS_ELEMENTS.each_with_index do |field, i|
23
+ send(field.to_s + '=', recording_array[i])
24
+ end
25
+ end
26
+
27
+ # A string representation of a Recording is used when we converse with the MythTV Backend about that recording
28
+ def to_s
29
+ RECORDINGS_ELEMENTS.collect { |field| self.send(field.to_s) }.join(MythTV::Backend::FIELD_SEPARATOR) + MythTV::Backend::FIELD_SEPARATOR
30
+ end
31
+
32
+ # Convenience methods to access the start and end times as Time objects, and duration as an Float
33
+ def start; Time.at(recstartts.to_i); end
34
+ def end; Time.at(recendts.to_i); end
35
+ def duration; self.end - self.start; end
36
+
37
+ # Cribbed from the Mythweb PHP code. Required for some method calls to the backend
38
+ def myth_delimited_recstart; myth_format_time(recstartts, :delimited); end
39
+
40
+ # Formats the start time for use in the copy process, as the latter half of the filename is a non-delimited time string
41
+ def myth_nondelimited_recstart; myth_format_time(recstartts, :nondelimited); end
42
+
43
+ # Convert the lo/hi long representation of the filesize into a string
44
+ def filesize
45
+ [filesize_lo.to_i, filesize_hi.to_i].pack("ll").unpack("Q").to_s
46
+ end
47
+
48
+ # Fetch the path section of the pathname
49
+ def path; URI.parse(pathname).path; end
50
+
51
+ # Strip the filename out from the path returned by the server
52
+ def filename; File.basename(URI.parse(pathname).path); end
53
+
54
+ private
55
+
56
+ def myth_format_time(timestamp, format = :nondelimited)
57
+ timestamp = timestamp.to_i if timestamp.class != Bignum
58
+ case format
59
+ when :nondelimited
60
+ Time.at(timestamp).strftime("%Y%m%d%H%M%S")
61
+ when :delimited
62
+ Time.at(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
63
+ end
64
+ end
65
+
66
+ end # end Recording
67
+ end # end MythTV
@@ -0,0 +1,31 @@
1
+ # The MIT License
2
+ #
3
+ # Copyright (c) 2008 Nick Ludlam
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ module MythTV;
24
+ VERSION = '0.1.0'
25
+ end
26
+
27
+ $:.unshift(File.dirname(__FILE__))
28
+
29
+ require 'mythtv/backend.rb'
30
+ require 'mythtv/recording.rb'
31
+
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestBackend < Test::Unit::TestCase
4
+ def setup
5
+ abort("\nERROR: You must set the environment variable MYTHTV_BACKEND to the name of your MythTV backend server\n\n") unless ENV['MYTHTV_BACKEND']
6
+ host = ENV['MYTHTV_BACKEND']
7
+ @backend = MythTV::Backend.new(:host => host)
8
+ end
9
+
10
+ def teardown
11
+ @backend.close
12
+ end
13
+
14
+ # Assuming the system is up for more than 0 seconds!
15
+ def test_connection
16
+ uptime = @backend.query_uptime
17
+ assert uptime > 0
18
+ end
19
+
20
+ # Assuming there is at least one recording on the test server
21
+ def test_get_recordings
22
+ recordings = @backend.query_recordings
23
+ assert recordings.length > 0
24
+ assert_kind_of MythTV::Recording, recordings[0]
25
+ end
26
+
27
+ # Assuming there is at least one scheduled recording on the test server
28
+ def test_get_scheduled
29
+ scheduled = @backend.query_scheduled
30
+ assert scheduled.length > 0
31
+ assert_kind_of MythTV::Recording, scheduled[0]
32
+ end
33
+
34
+ # Test the generation of a preview image
35
+ def test_make_preview_image
36
+ recordings = @backend.query_recordings
37
+
38
+ recording = recordings[0]
39
+ test_image = @backend.preview_image(recording, :secs_in => 1)
40
+ assert test_image.length > 0
41
+
42
+ # Define an array of the decimal values of the PNG magic number
43
+ png_sig = [137, 80, 78, 71, 13, 10, 26, 10]
44
+ test_image_sig = (0..7).collect { |i| test_image[i] }
45
+
46
+ assert_equal test_image_sig, png_sig
47
+ end
48
+
49
+ # Don't run this by default as it takes a while. Possibly limit to 100kB?
50
+ #def test_download
51
+ # recordings = @backend.query_recordings
52
+ #
53
+ # recording = recordings[-2]
54
+ # @backend.download(recording)
55
+ #end
56
+
57
+
58
+ end
@@ -0,0 +1,2 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/ruby-mythtv'
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-mythtv
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Ludlam
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-08 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Ruby implementation of the MythTV communication protocol
17
+ email: nick@recoil.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - History.txt
24
+ - License.txt
25
+ - README.txt
26
+ files:
27
+ - History.txt
28
+ - License.txt
29
+ - README.txt
30
+ - Rakefile
31
+ - lib/ruby-mythtv.rb
32
+ - lib/mythtv/backend.rb
33
+ - lib/mythtv/recording.rb
34
+ - test/test_backend.rb
35
+ - test/test_helper.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/nickludlam/ruby-mythtv/
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --main
41
+ - README.txt
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project: ruby-mythtv
59
+ rubygems_version: 1.1.1
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: Ruby implementation of the MythTV backend protocol
63
+ test_files:
64
+ - test/test_backend.rb
65
+ - test/test_helper.rb