nickludlam-ruby-mythtv 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +7 -0
- data/README.txt +11 -3
- data/Rakefile +13 -6
- data/Todo.txt +5 -0
- data/lib/ruby-mythtv.rb +25 -4
- data/test/test_backend.rb +8 -3
- data/test/test_db.rb +109 -0
- metadata +26 -8
- data/lib/mythtv/backend.rb +0 -416
- data/lib/mythtv/recording.rb +0 -67
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
|
-
|
31
|
+
mythbackend, mythdb = MythTV.connect(:host => 'mythtv.localdomain')
|
32
32
|
|
33
33
|
# Get an array of recordings
|
34
|
-
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 =
|
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.
|
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-
|
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.
|
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/
|
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.
|
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/
|
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("\
|
6
|
-
|
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.
|
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-
|
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
|
-
-
|
33
|
-
-
|
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
|
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
|
data/lib/mythtv/backend.rb
DELETED
@@ -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
|
data/lib/mythtv/recording.rb
DELETED
@@ -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
|