Class: MythTV::Backend

Inherits:
Object
  • Object
show all
Includes:
Socket::Constants
Defined in:
lib/mythtv/backend.rb

Constant Summary collapse

MYTHTV_PROTO_VERSION =

Our current protocol implementation. TODO: Consider how we support multiple protocol versions within a single gem. In theory this is just a case of limiting the number of attr_accessors that are class_eval’d onto MythTV::Recording, and bumping the number below

40
FIELD_SEPARATOR =

The currently defined field separator in responses

"[]:[]"
TRANSFER_BLOCKSIZE =

The payload size we request from the backend when performing a filetransfer

65535

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Backend

Open the socket, make a protocol check, and announce we’d like an interactive session with the backend server

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/mythtv/backend.rb', line 44

def initialize(options = {})
  default_options = { :port => 6543, :status_port => 6544, :connection_type => :playback }
  options = default_options.merge(options)
  
  raise ArgumentError, "You must specify a :host key and value to initialize()" unless options.has_key?(:host)
  
  @host = options[:host]
  @port = options[:port]
  @status_port = options[:status_port]

  @socket = TCPSocket.new(@host, @port)
  
  check_proto
  
  if options[:connection_type] == :playback
    announce_playback()
  elsif options[:connection_type] == :filetransfer
    announce_filetransfer(options[:filename])
  else
    raise ArgumentError, "Unknown connection type '#{options[:connection_type]}'"
  end
end

Instance Attribute Details

#connection_typeObject (readonly)

Returns the value of attribute connection_type.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def connection_type
  @connection_type
end

#filetransfer_portObject (readonly)

Returns the value of attribute filetransfer_port.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def filetransfer_port
  @filetransfer_port
end

#filetransfer_sizeObject (readonly)

Returns the value of attribute filetransfer_size.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def filetransfer_size
  @filetransfer_size
end

#hostObject (readonly)

Returns the value of attribute host.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def host
  @host
end

#portObject (readonly)

Returns the value of attribute port.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def port
  @port
end

#socketObject (readonly)

Returns the value of attribute socket.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def socket
  @socket
end

#status_portObject (readonly)

Returns the value of attribute status_port.



34
35
36
# File 'lib/mythtv/backend.rb', line 34

def status_port
  @status_port
end

Instance Method Details

#announce_filetransfer(filename = nil) ⇒ Object

Announce ourselves as a FileTransfer connection. www.mythtv.org/wiki/index.php/Myth_Protocol_Command_ANN for details

Raises:



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/mythtv/backend.rb', line 102

def announce_filetransfer(filename = nil)
  raise ArgumentError, "you must specify a filename" if filename.nil?
  
  client_hostname = Socket.gethostname
  
  filename = "/" + filename if filename[0] != "/"  # Ensure leading slash
  
  send("ANN FileTransfer #{client_hostname}#{FIELD_SEPARATOR}#{filename}")
  response = recv
  
  # Should get back something like:
  #   OK[]:[]<socket number>[]:[]<file size high 32 bits>[]:[]<file size low 32 bits>
  unless response[0] == "OK"
    close
    raise CommunicationError, response.join(": ")
  else
    @filetransfer_port = response[1]
    @filetransfer_size = [response[3].to_i, response[2].to_i].pack("ll").unpack("Q")[0]
    @connection_type = :filetransfer   # Not currently used, but may be in later versions
  end
end

#announce_playbackObject

Announce ourselves as a Playback connection. www.mythtv.org/wiki/index.php/Myth_Protocol_Command_ANN for details



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/mythtv/backend.rb', line 83

def announce_playback
  client_hostname = Socket.gethostname
  
  # We don't want to receive broadcast events for this connection
  want_events = "0"
  
  send("ANN Playback #{client_hostname} #{want_events}")
  response = recv

  unless response[0] == "OK"
    close
    raise CommunicationError, response.join(": ")
  else
    @connection_type = :playback  # Not currently used, but may be in later versions
  end
end

#check_protoObject

Tell the backend we speak a specific version of the protocol. Raise an error if the backend does not accept that version.



72
73
74
75
76
77
78
79
# File 'lib/mythtv/backend.rb', line 72

def check_proto
  send("MYTH_PROTO_VERSION #{MYTHTV_PROTO_VERSION}")
  response = recv
  unless response[0] == "ACCEPT" && response[1] == MYTHTV_PROTO_VERSION.to_s
    close
    raise ProcolError, response.join(": ")
  end
end

#closeObject

Tell the backend we’ve finished talking to it for the current session



237
238
239
240
# File 'lib/mythtv/backend.rb', line 237

def close
  send("DONE")
  @socket.close unless @socket.nil?
end

#download(recording, filename = nil) ⇒ Object

Download the file to a given location, either with a default filename, or one specified by the caller



323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/mythtv/backend.rb', line 323

def download(recording, filename = nil)
  
  # If no filename is given, we default to <title>_<recstartts>.<extension>
  if filename.nil?
    filename = recording.title + "_" +
               recording.myth_nondelimited_recstart + File.extname(recording.filename) 
  end

  File.open(filename, "wb") do |f|
    stream(recording) { |data| f.write(data) }
  end
end

#get_next_free_recorderObject

This method will return the next free recorder that the backend has available to it TODO: Fix up the checking of response. Does it return an IP or address in element 1?



165
166
167
168
169
170
171
# File 'lib/mythtv/backend.rb', line 165

def get_next_free_recorder
  send("GET_NEXT_FREE_RECORDER#{FIELD_SEPARATOR}-1")
  response = recv

  # If we have a recorder free, return the recorder id, otherwise false
  response[0] == "-1" ? false : response[0].to_i
end

#preview_image(recording, options = {}) ⇒ Object

Returns a string which contains a PNG image of the this recording. The time offset into the file defaults to two minutes, and the default image width is 120 pixels. This uses the separate status port, rather than talking over the backend control port



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/mythtv/backend.rb', line 248

def preview_image(recording, options = {})
  default_options = { :height => 120, :secs_in => 120 }
  options = default_options.merge(options)

  # Generate our query string for the MythTV request
  query_string = "ChanId=#{recording.chanid}&StartTime=#{recording.myth_delimited_recstart}"

  # Add in the optional parameters if they were specified
  query_string += "&SecsIn=#{options[:secs_in]}" if options[:secs_in]
  query_string += "&Height=#{options[:height]}" if options[:height]
  query_string += "&Width=#{options[:width]}" if options[:width]

  url = URI::HTTP.build( { :host  => @host,
                           :port  => @status_port,
                           :path  => "/Myth/GetPreviewImage",
                           :query => query_string } )
  
  # Make a GET request, and store the image data returned
  image_data = Net::HTTP.get(url)

  image_data
end

#query_filetransfer_transfer_block(sock_num, size) ⇒ Object

This is used when transfering files from the backend. It requests that the next block of data be sent to the socket, ready for us to recieve



231
232
233
234
# File 'lib/mythtv/backend.rb', line 231

def query_filetransfer_transfer_block(sock_num, size)
  query = "QUERY_FILETRANSFER #{sock_num}#{FIELD_SEPARATOR}REQUEST_BLOCK#{FIELD_SEPARATOR}#{size}"
  send(query)
end

#query_loadObject

Simple method to query the load of the backend server. Returns a hash with keys for :one_minute, :five_minute and :fifteen_minute



126
127
128
129
130
# File 'lib/mythtv/backend.rb', line 126

def query_load
  send("QUERY_LOAD")
  response = recv
  { :one_minute => response[0].to_f, :five_minute => response[1].to_f, :fifteen_minute => response[2].to_f }
end

#query_memstatsObject

Wrap the QUERY_MEMSTATS backend command. Returns a hash with keys for :used_memory, :free_memory, :total_swap and :free_swap

Raises:



208
209
210
211
212
213
214
215
216
# File 'lib/mythtv/backend.rb', line 208

def query_memstats
  send("QUERY_MEMSTATS")
  response = recv
  
  # We expect to get back 4 elements only for this method
  raise CommunicationError, "Unexpected response from QUERY_MEMSTATS: #{response.join(":")}" if response.length != 4

  { :used_memory => response[0].to_i, :free_memory => response[1].to_i, :total_swap => response[2].to_i, :free_swap => response[3].to_i }
end

#query_recordings(options = {}) ⇒ Object

List all recordings stored on the backend. You can filter via the storagegroup property, and this defaults to /Default/, to list the recordings, rather than any which are from LiveTV sessions.

Returns an array of MythTV::Recording objects



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/mythtv/backend.rb', line 137

def query_recordings(options = {})
  default_options = { :filter => { :storagegroup => /Default/ } }
  options = default_options.merge(options)
  
  send("QUERY_RECORDINGS Play")
  response = recv

  recording_count = response.shift.to_i
  recordings = []

  while recording_count > 0
    recording_array = response.slice!(0, Recording::RECORDINGS_ELEMENTS.length)

    recording = Recording.new(recording_array)

    options[:filter].each_pair do |k, v|
      recordings.push(recording) if recording.send(k) =~ v
    end
    
    recording_count -= 1
  end

  recordings = recordings.sort_by { |r| r.startts }
  recordings.reverse!
end

#query_scheduledObject

This method returns an array of recording objects which describe which programmes are to be recorded as far as the current EPG data extends



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/mythtv/backend.rb', line 189

def query_scheduled
  send("QUERY_GETALLSCHEDULED")
  response = recv
  
  recording_count = response.shift.to_i
  recordings = []

  while recording_count > 0
    recording_array = response.slice!(0, Recording::RECORDINGS_ELEMENTS.length)
    recordings << Recording.new(recording_array)
    recording_count -= 1
  end

  recordings = recordings.sort_by { |r| r.startts }
  recordings.reverse!
end

#query_uptimeObject

Wrap the QUERY_UPTIME backend command. Return a single integer

Raises:



219
220
221
222
223
224
225
226
227
# File 'lib/mythtv/backend.rb', line 219

def query_uptime
  send("QUERY_UPTIME")
  response = recv

  # We expect to get back 1 element only for this method
  raise CommunicationError, "Unexpected response from QUERY_UPTIME: #{response.join(":")}" if response.length != 1

  response[0].to_i
end

#spawn_live_tv(recorder_id, start_channel = 1) ⇒ Object

This will trigger the backend to start recording Live TV from a certain channel. TODO: This is currently buggy, so avoid until it’s fixed in a later release



175
176
177
178
179
180
181
182
183
184
185
# File 'lib/mythtv/backend.rb', line 175

def spawn_live_tv(recorder_id, start_channel = 1)
  client_hostname = Socket.gethostname
  spawn_time = Time.now.strftime("%y-%m-%dT%H:%M:%S")
  chain_id = "livetv-#{client_hostname}-#{spawn_time}"
  
  query_recorder(recorder_id, "SPAWN_LIVETV", [chain_id, 0, "#{start_channel}"])
  response = recv
  
  # If we have an "OK" back, then return the chain_id, otherwise return false
  response[0] == "OK" ? chain_id : false
end

#start_livetv(channel = 1) ⇒ Object

TODO: The LiveTV methods are still work-in-progress.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/mythtv/backend.rb', line 337

def start_livetv(channel = 1)
  # If we have a free recorder...
  if recorder_id = get_next_free_recorder
    puts "Got a recorder ID of #{recorder_id}"
    # If we can spawn live tv...
    if chain_id = spawn_live_tv(recorder_id, channel)
      puts "Got a chain ID of #{chain_id}"
      # Send the two backend event messages
      backend_message(["RECORDING_LIST_CHANGE", "empty"])
      puts "Sent RECORDING_LIST_CHANGE"
      backend_message(["LIVETV_CHAIN UPDATE #{chain_id}", "empty"])
      puts "Sent LIVETV_CHAIN UPDATE"
      
      # Find the filename from here...
      query_recorder(recorder_id, "GET_CURRENT_RECORDING")
      cur_rec = recv
      puts "Current recording is:"
      puts cur_rec.inspect
      recording = Recording.new(cur_rec)
    else
      puts "spawn_live_tv returned with false or nil"
      return false
    end
  else
    puts "get_next_free_recorder returned with false or nil"
    return false
  end
end

#stop_livetv(recorder_id) ⇒ Object

TODO: Finish this off. Check response?



367
368
369
370
# File 'lib/mythtv/backend.rb', line 367

def stop_livetv(recorder_id)
  query_recorder(recorder_id, "STOP_LIVETV")
  response = recv
end

#stream(recording, options = {}, &block) ⇒ Object

Yield into the given block with the data buffer of size TRANSFER_BLOCKSIZE



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/mythtv/backend.rb', line 275

def stream(recording, options = {}, &block)

  # Initialise a new connection of connection_type => :filetransfer
  data_conn = Backend.new(:host => @host,
                          :port => @port,
                          :status_port => @status_port,
                          :connection_type => :filetransfer,
                          :filename => recording.path)

  ft_port = data_conn.filetransfer_port
  ft_size = data_conn.filetransfer_size

  blocksize = options.has_key?(:transfer_blocksize) ? options[:transfer_blocksize] : TRANSFER_BLOCKSIZE

  total_transfered = 0

  begin
    # While we still have data to fetch
    while total_transfered < ft_size
      # Make a request for the backend to send data
      query_filetransfer_transfer_block(ft_port, blocksize)

      # Collect the socket data in a string
      buffer = ""
   
      while buffer.length < blocksize
        buffer += data_conn.socket.recv(blocksize)
        # Special case for when the remainer to fetch is less than TRANSFER_BLOCKSIZE
        break if total_transfered + buffer.length == ft_size
      end

      # Yield into the given block to allow the user to process as a stream
      yield buffer
   
      total_transfered += buffer.length
   
      # If the user has only asked for a certain amount of data, stop when we hit this
      break if options[:max_length] && total_transfered > options[:max_length]
    end
  ensure
    # We need to close the data connection regardless of what is going on when we yield
    data_conn.close
  end
  
end