nickludlam-ruby-mythtv 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,10 @@
1
+ == 0.1.1 (2008-07-27)
2
+
3
+ * Updated documentation
4
+ * Added initial multiple protocol version support via protocol.rb
5
+ * Implemented MythTV::Backend#get_program_guide() to wrap "/Myth/GetProgramGuide" from the Mythbackend Status port
6
+ * Created a Utils class to hold miscellaneous methods. The utils.rb file also holds the exception classes
7
+
1
8
  == 0.1.0 (2008-06-08)
2
9
 
3
10
  * Initial release onto GitHub/Rubyforge
data/README.txt CHANGED
@@ -28,13 +28,21 @@ and can be cloned from
28
28
  require 'ruby-mythtv'
29
29
 
30
30
  # Connect to the server
31
- backend = MythTV::Backend.new(:host => 'mythtv.localdomain')
31
+ mythbackend, mythdb = MythTV.connect(:host => 'mythtv.localdomain')
32
32
 
33
33
  # Get an array of recordings
34
- recordings = backend.query_recordings
34
+ recordings = mythbackend.query_recordings
35
+
36
+ # Download a recording
37
+ mythbackend.download(recordings[0])
38
+
39
+ # Stream a recording into a block
40
+ mythbackend.stream(recordings[0], :transfer_blocksize => 65535) do |chunk|
41
+ ..do something with the 64k chunk..
42
+ end
35
43
 
36
44
  # 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)
45
+ preview_thumbnail = mythbackend.preview_image(recordings[0], :secs_in => 60)
38
46
  File.open('preview_thumbnail.png', 'w') { |f| f.write(preview_thumbnail) }
39
47
 
40
48
  == Author
data/Rakefile CHANGED
@@ -5,23 +5,23 @@ require 'rake/testtask'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = %q{ruby-mythtv}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.2"
9
9
 
10
10
  s.specification_version = 2 if s.respond_to? :specification_version=
11
11
 
12
12
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
13
13
  s.authors = ["Nick Ludlam"]
14
- s.date = %q{2008-06-08}
14
+ s.date = %q{2008-07-27}
15
15
  s.description = %q{Ruby implementation of the MythTV communication protocol}
16
16
  s.email = %q{nick@recoil.org}
17
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"]
18
+ s.files = ["History.txt", "License.txt", "README.txt", "Rakefile", "lib/ruby-mythtv.rb", "lib/mythtv/backend.rb", "lib/mythtv/channel.rb", "lib/mythtv/database.rb", "lib/mythtv/program.rb", "lib/mythtv/protocol.rb", "lib/mythtv/recording.rb", "lib/mythtv/recording_schedule.rb", "lib/mythtv/utils.rb", "test/test_backend.rb", "test/test_db.rb", "test/test_helper.rb"]
19
19
  s.has_rdoc = true
20
20
  s.homepage = %q{http://github.com/nickludlam/ruby-mythtv/}
21
21
  s.rdoc_options = ["--main", "README.txt"]
22
22
  s.require_paths = ["lib"]
23
23
  s.rubyforge_project = %q{ruby-mythtv}
24
- s.rubygems_version = %q{0.1.0}
24
+ s.rubygems_version = %q{0.1.2}
25
25
  s.summary = %q{Ruby implementation of the MythTV backend protocol}
26
26
  s.test_files = ["test/test_backend.rb", "test/test_helper.rb"]
27
27
  end
@@ -36,10 +36,17 @@ end
36
36
 
37
37
  desc "Run basic unit tests"
38
38
  Rake::TestTask.new("test_units") do |t|
39
- t.pattern = 'test/test_*.rb'
39
+ t.pattern = ENV["TESTFILES"] || ['test/test_backend.rb', 'test/test_db.rb']
40
40
  t.verbose = true
41
41
  t.warning = true
42
42
  end
43
43
 
44
+ task :test => :test_units
45
+
46
+ Rake::TestTask.new('test_db') do |t|
47
+ t.pattern = ['test/test_db.rb']
48
+ t.verbose = true
49
+ end
50
+
44
51
  desc "Run unit tests as default"
45
- task :default => :test_units
52
+ task :default => :test_units
data/Todo.txt ADDED
@@ -0,0 +1,5 @@
1
+ - Implement the Recorder class, and associated functions (see existing MythTV Python module)
2
+ - Look at how we obtain the channel icon by streaming a backend file (see existing MythTV Perl module)
3
+ - Support Ruby 1.9
4
+ - Support seeking with the MythTV::Backend#stream() method
5
+ - Work out method to remove and add database columns for Channel/Program/RecordingSchedule, as we do for the PROTOCOL mapping system. It seems the database can change without the Protocol version being bumped, so they are independent.
data/lib/ruby-mythtv.rb CHANGED
@@ -20,12 +20,33 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- module MythTV;
24
- VERSION = '0.1.0'
23
+ module MythTV
24
+ VERSION = '0.1.2'
25
+
26
+ def self.connect(options)
27
+ backend = connect_backend(options)
28
+ database = connect_database(options)
29
+
30
+ [backend, database]
31
+ end
32
+
33
+ def self.connect_backend(options)
34
+ Backend.new(options)
35
+ end
36
+
37
+ def self.connect_database(options)
38
+ Database.new(options)
39
+ end
40
+
25
41
  end
26
42
 
27
43
  $:.unshift(File.dirname(__FILE__))
28
44
 
29
- require 'mythtv/backend.rb'
45
+ require 'mythtv/channel.rb'
46
+ require 'mythtv/program.rb'
47
+ require 'mythtv/protocol.rb'
30
48
  require 'mythtv/recording.rb'
31
-
49
+ require 'mythtv/recording_schedule.rb'
50
+ require 'mythtv/utils.rb'
51
+ require 'mythtv/database.rb'
52
+ require 'mythtv/backend.rb'
data/test/test_backend.rb CHANGED
@@ -2,9 +2,8 @@ require File.dirname(__FILE__) + '/test_helper.rb'
2
2
 
3
3
  class TestBackend < Test::Unit::TestCase
4
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)
5
+ abort("\n\tERROR: You must set the environment variable MYTHTV_BACKEND to the name of your MythTV backend server\n\n") unless ENV['MYTHTV_BACKEND']
6
+ @backend = MythTV.connect_backend(:host => ENV['MYTHTV_BACKEND'])
8
7
  end
9
8
 
10
9
  def teardown
@@ -46,6 +45,12 @@ class TestBackend < Test::Unit::TestCase
46
45
  assert_equal test_image_sig, png_sig
47
46
  end
48
47
 
48
+ # def test_process_guide_xml
49
+ # guide_data = @backend.get_program_guide
50
+ #
51
+ # channels = MythTV::Backend.process_guide_xml(guide_data)
52
+ # end
53
+
49
54
  # Don't run this by default as it takes a while. Possibly limit to 100kB?
50
55
  #def test_download
51
56
  # recordings = @backend.query_recordings
data/test/test_db.rb ADDED
@@ -0,0 +1,109 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestDatabase < Test::Unit::TestCase
4
+ def setup
5
+ abort("\n\tmyERROR: You must set the environment variable MYTHTV_DB to the name of your MythTV database server\n\n") unless ENV['MYTHTV_DB']
6
+ @db = MythTV.connect_database(:host => ENV['MYTHTV_DB'],
7
+ :database_user => 'mythtv',
8
+ :database_password => '4c6UUCJp',
9
+ :log_level => Logger::WARN)
10
+ end
11
+
12
+ def teardown
13
+ @db.close
14
+ end
15
+
16
+ # Check the DBSchemaVer key in the settings table as our first check
17
+ # It should always be present
18
+ def test_get_setting
19
+ schema_version = @db.get_setting('DBSchemaVer')
20
+ assert schema_version.to_i > 0
21
+ end
22
+
23
+ # Check the DBSchemaVer, once queried, is in the setting cache
24
+ def test_get_setting_cache
25
+ schema_version = @db.get_setting('DBSchemaVer')
26
+ assert @db.setting_cache['DBSchemaVer_'].to_i > 0
27
+ end
28
+
29
+ def test_list_channels
30
+ channels = @db.list_channels
31
+
32
+ assert_kind_of Array, channels
33
+ assert channels.length > 0
34
+ assert_kind_of MythTV::Channel, channels[0]
35
+ assert channels[0].chanid > 0
36
+ end
37
+
38
+ # Test we can pull back a single channel when
39
+ # specifying a :chanid
40
+ def test_list_single_chanid
41
+ first_channel_list = @db.list_channels
42
+ wanted_chanid = first_channel_list[0].chanid
43
+
44
+ second_channel_list = @db.list_channels(:chanid => wanted_chanid)
45
+ assert_equal 1, second_channel_list.length
46
+
47
+ channel = second_channel_list[0]
48
+ assert_kind_of MythTV::Channel, channel
49
+ assert_equal channel.chanid, wanted_chanid
50
+ end
51
+
52
+ def test_list_multiple_chanids
53
+ first_channel_list = @db.list_channels
54
+ first_five = first_channel_list.slice(0..4)
55
+ wanted_chanids = first_five.map { |x| x.chanid }
56
+
57
+ second_channel_list = @db.list_channels(:chanid => wanted_chanids)
58
+ assert_equal 5, second_channel_list.length
59
+ second_channel_list
60
+ end
61
+
62
+ def test_list_programs
63
+ programs = @db.list_programs(:limit => 10)
64
+
65
+ assert_equal 10, programs.length
66
+ end
67
+
68
+ def test_list_programs_with_search
69
+ programs = @db.list_programs(:conditions => ['title LIKE ?', "%"],
70
+ :limit => 5)
71
+ assert programs.length > 0
72
+ end
73
+
74
+ def test_list_programs_with_starttime_range
75
+ # Programs in the next two hours
76
+ programs = @db.list_programs(:conditions => ['starttime BETWEEN ? AND ?', Time.now, Time.now + 7200],
77
+ :limit => 1)
78
+
79
+ assert_equal 1, programs.length
80
+ end
81
+
82
+ def test_program_links_to_channel
83
+ programs = @db.list_programs(:conditions => ['title LIKE ?', "%"], :limit => 1)
84
+ program_channel = programs[0].channel
85
+ assert_kind_of MythTV::Channel, program_channel
86
+ end
87
+
88
+ def test_new_schedule
89
+ # Get list of schedules for later reference
90
+ num_schedules = @db.list_recording_schedules
91
+ programs = @db.list_programs(:conditions => ['starttime BETWEEN ? AND ?', Time.now + 3600, Time.now + 7200],
92
+ :limit => 1)
93
+
94
+ # Convert our first program selected into a recording schedule
95
+ new_schedule = MythTV::RecordingSchedule.new(programs[0], @db)
96
+ new_schedule.save
97
+
98
+ # Get new list
99
+ new_num_schedules = @db.list_recording_schedules
100
+ # Assert that we now have one more schedule
101
+ assert_equal num_schedules.length + 1, new_num_schedules.length
102
+
103
+ assert new_schedule.recordid > 0
104
+
105
+ assert new_schedule.destroy()
106
+ end
107
+
108
+ end
109
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nickludlam-ruby-mythtv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Ludlam
@@ -9,10 +9,18 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-06-08 00:00:00 -07:00
12
+ date: 2008-09-24 00:00:00 -07:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mysql
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
16
24
  description: Ruby implementation of the MythTV communication protocol
17
25
  email: nick@recoil.org
18
26
  executables: []
@@ -29,10 +37,19 @@ files:
29
37
  - README.txt
30
38
  - Rakefile
31
39
  - lib/ruby-mythtv.rb
32
- - lib/mythtv/backend.rb
33
- - lib/mythtv/recording.rb
40
+ - mythtv/backend.rb
41
+ - mythtv/channel.rb
42
+ - mythtv/database.rb
43
+ - mythtv/program.rb
44
+ - mythtv/protocol.rb
45
+ - mythtv/recording.rb
46
+ - mythtv/recording_schedule.rb
47
+ - mythtv/utils.rb
34
48
  - test/test_backend.rb
49
+ - test/test_db.rb
35
50
  - test/test_helper.rb
51
+ - test_stream.rb
52
+ - Todo.txt
36
53
  has_rdoc: true
37
54
  homepage: http://github.com/nickludlam/ruby-mythtv/
38
55
  post_install_message:
@@ -56,10 +73,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
73
  requirements: []
57
74
 
58
75
  rubyforge_project: ruby-mythtv
59
- rubygems_version: 1.0.1
76
+ rubygems_version: 1.2.0
60
77
  signing_key:
61
78
  specification_version: 2
62
79
  summary: Ruby implementation of the MythTV backend protocol
63
80
  test_files:
64
- - test/test_backend.rb
65
81
  - test/test_helper.rb
82
+ - test/test_backend.rb
83
+ - test/test_db.rb
@@ -1,416 +0,0 @@
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
@@ -1,67 +0,0 @@
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