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 +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
|