Class: MythTV::Backend
- Inherits:
-
Object
- Object
- MythTV::Backend
- 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
-
#connection_type ⇒ Object
readonly
Returns the value of attribute connection_type.
-
#filetransfer_port ⇒ Object
readonly
Returns the value of attribute filetransfer_port.
-
#filetransfer_size ⇒ Object
readonly
Returns the value of attribute filetransfer_size.
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
-
#socket ⇒ Object
readonly
Returns the value of attribute socket.
-
#status_port ⇒ Object
readonly
Returns the value of attribute status_port.
Instance Method Summary collapse
-
#announce_filetransfer(filename = nil) ⇒ Object
Announce ourselves as a FileTransfer connection.
-
#announce_playback ⇒ Object
Announce ourselves as a Playback connection.
-
#check_proto ⇒ Object
Tell the backend we speak a specific version of the protocol.
-
#close ⇒ Object
Tell the backend we’ve finished talking to it for the current session.
-
#download(recording, filename = nil) ⇒ Object
Download the file to a given location, either with a default filename, or one specified by the caller.
-
#get_next_free_recorder ⇒ Object
This method will return the next free recorder that the backend has available to it TODO: Fix up the checking of response.
-
#initialize(options = {}) ⇒ Backend
constructor
Open the socket, make a protocol check, and announce we’d like an interactive session with the backend server.
-
#preview_image(recording, options = {}) ⇒ Object
Returns a string which contains a PNG image of the this recording.
-
#query_filetransfer_transfer_block(sock_num, size) ⇒ Object
This is used when transfering files from the backend.
-
#query_load ⇒ Object
Simple method to query the load of the backend server.
-
#query_memstats ⇒ Object
Wrap the QUERY_MEMSTATS backend command.
-
#query_recordings(options = {}) ⇒ Object
List all recordings stored on the backend.
-
#query_scheduled ⇒ Object
This method returns an array of recording objects which describe which programmes are to be recorded as far as the current EPG data extends.
-
#query_uptime ⇒ Object
Wrap the QUERY_UPTIME backend command.
-
#spawn_live_tv(recorder_id, start_channel = 1) ⇒ Object
This will trigger the backend to start recording Live TV from a certain channel.
-
#start_livetv(channel = 1) ⇒ Object
TODO: The LiveTV methods are still work-in-progress.
-
#stop_livetv(recorder_id) ⇒ Object
TODO: Finish this off.
-
#stream(recording, options = {}, &block) ⇒ Object
Yield into the given block with the data buffer of size TRANSFER_BLOCKSIZE.
Constructor Details
#initialize(options = {}) ⇒ Backend
Open the socket, make a protocol check, and announce we’d like an interactive session with the backend server
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( = {}) = { :port => 6543, :status_port => 6544, :connection_type => :playback } = .merge() raise ArgumentError, "You must specify a :host key and value to initialize()" unless .has_key?(:host) @host = [:host] @port = [:port] @status_port = [:status_port] @socket = TCPSocket.new(@host, @port) check_proto if [:connection_type] == :playback announce_playback() elsif [:connection_type] == :filetransfer announce_filetransfer([:filename]) else raise ArgumentError, "Unknown connection type '#{[:connection_type]}'" end end |
Instance Attribute Details
#connection_type ⇒ Object (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_port ⇒ Object (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_size ⇒ Object (readonly)
Returns the value of attribute filetransfer_size.
34 35 36 |
# File 'lib/mythtv/backend.rb', line 34 def filetransfer_size @filetransfer_size end |
#host ⇒ Object (readonly)
Returns the value of attribute host.
34 35 36 |
# File 'lib/mythtv/backend.rb', line 34 def host @host end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
34 35 36 |
# File 'lib/mythtv/backend.rb', line 34 def port @port end |
#socket ⇒ Object (readonly)
Returns the value of attribute socket.
34 35 36 |
# File 'lib/mythtv/backend.rb', line 34 def socket @socket end |
#status_port ⇒ Object (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
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_playback ⇒ Object
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_proto ⇒ Object
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 |
#close ⇒ Object
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_recorder ⇒ Object
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, = {}) = { :height => 120, :secs_in => 120 } = .merge() # 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=#{[:secs_in]}" if [:secs_in] query_string += "&Height=#{[:height]}" if [:height] query_string += "&Width=#{[:width]}" if [: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_load ⇒ Object
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_memstats ⇒ Object
Wrap the QUERY_MEMSTATS backend command. Returns a hash with keys for :used_memory, :free_memory, :total_swap and :free_swap
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( = {}) = { :filter => { :storagegroup => /Default/ } } = .merge() 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) [: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_scheduled ⇒ Object
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_uptime ⇒ Object
Wrap the QUERY_UPTIME backend command. Return a single integer
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 (["RECORDING_LIST_CHANGE", "empty"]) puts "Sent RECORDING_LIST_CHANGE" (["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, = {}, &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 = .has_key?(:transfer_blocksize) ? [: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 [:max_length] && total_transfered > [: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 |