/
MythBackend.php
278 lines (237 loc) · 9.84 KB
/
MythBackend.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
<?php
/**
*
* @license GPL
*
* @package MythTV
*
/**/
class MythBackend {
// MYTH_PROTO_VERSION is defined in libmyth in mythtv/libs/libmyth/mythcontext.h
// and should be the current MythTV protocol version.
static $protocol_version = '79';
static $protocol_token = 'BasaltGiant';
// The character string used by the backend to separate records
static $backend_separator = '[]:[]';
// NUMPROGRAMLINES is defined in mythtv/libs/libmythtv/programinfo.h and is
// the number of items in a ProgramInfo QStringList group used by
// ProgramInfo::ToSringList and ProgramInfo::FromStringList.
static $program_line_number = 49;
private $fp = null;
private $connected = false;
private $host = '127.0.0.1';
private $ip = '127.0.0.1';
private $port = null;
private $port_http = null;
static function find($host = null, $port = null) {
static $Backends = array();
// Looking for the master backend?
if (is_null($host)) {
$host = setting('MasterServerIP');
$port = setting('MasterServerPort');
if (!$host || !$port)
trigger_error("MasterServerIP or MasterServerPort not found! You may "
."need to check your mythweb.conf file or re-run mythtv-setup",
FATAL);
}
if (!isset($Backends[$host]))
$Backends[$host] = array();
if (!isset($Backends[$host][$port]))
$Backends[$host][$port] = new MythBackend($host, $port);
return $Backends[$host][$port];
}
function __construct($host, $port = null) {
$this->host = $host;
$this->ip = _or(setting('BackendServerIP', $this->host), $host);
// If the IP contains a ':' It's likely an IPv6 address so enclose it in '[]'
if (strpos($this->ip,":") > 0) {
$this->ip = "[" . $this->ip . "]";
}
$this->port = _or($port, _or(setting('BackendServerPort', $this->host), 6543));
$this->port_http = _or(setting('BackendStatusPort', $this->host), _or(setting('BackendStatusPort'), 6544));
}
function __destruct() {
$this->disconnect();
}
private function connect($listen_events) {
if ($this->fp)
return;
$this->fp = @fsockopen($this->ip, $this->port, $errno, $errstr, 25);
if (!$this->fp)
custom_error("Unable to connect to the master backend at {$this->ip}:{$this->port}".(($this->host == $this->ip)?'':" (hostname: {$this->host})").".\nIs it running?");
socket_set_timeout($this->fp, 30);
$this->checkProtocolVersion();
$this->announce($listen_events);
}
private function disconnect() {
if (!$this->fp)
return;
$this->sendCommand('DONE');
fclose($this->fp);
$this->fp = null;
}
private function checkProtocolVersion() {
// Allow overriding this check
if ($_SERVER['ignore_proto'] == true )
return true;
if ( time() - $_SESSION['backend'][$this->host]['proto_version']['last_check_time'] < 60*60*2
&& $_SESSION['backend'][$this->host]['proto_version']['last_check_version'] == MythBackend::$protocol_version )
return true;
$response = $this->sendCommand('MYTH_PROTO_VERSION '.MythBackend::$protocol_version.' '.MythBackend::$protocol_token);
$_SESSION['backend'][$this->host]['proto_version']['last_check_version'] = @$response[1];
if ($response[0] == 'ACCEPT') {
$_SESSION['backend'][$this->host]['proto_version']['last_check_time'] = time();
return true;
}
if ($response[0] == 'REJECT')
trigger_error("Incompatible protocol version (mythweb=" . MythBackend::$protocol_version . ", backend=" . @$response[1] . ")");
else
trigger_error("Unexpected response to MYTH_PROTO_VERSION '".MythBackend::$protocol_version."': ".print_r($response, true));
return false;
}
private function announce($listen_events) {
$response = $this->sendCommand('ANN Monitor '.hostname.($listen_events ? ' 2' : ' 0') );
if ($response == 'OK')
return true;
return false;
}
public function setTimezone() {
if (!is_string($_SESSION['backend']['timezone']['value']) || time() - $_SESSION['backend']['timezone']['last_check_time'] > 60*60*24) {
$response = $this->sendCommand('QUERY_TIME_ZONE');
$timezone = str_replace(' ', '_', $response[0]);
$_SESSION['backend']['timezone']['value'] = $timezone;
$_SESSION['backend']['timezone']['last_check_time'] = time();
}
if (!@date_default_timezone_set($_SESSION['backend']['timezone']['value'])) {
$attempted_value = $_SESSION['backend']['timezone']['value'];
unset($_SESSION['backend']['timezone']);
trigger_error('Failed to set php timezone to '.$attempted_value.(is_array($response) ? ' Response from backend was '.print_r($response, true) : ''));
}
}
public function sendCommand($command = null) {
$this->connect(false);
// The format should be <length + whitespace to 8 total bytes><data>
if (is_array($command))
$command = implode(MythBackend::$backend_separator, $command);
$command = strlen($command) . str_repeat(' ', 8 - strlen(strlen($command))) . $command;
fputs($this->fp, $command);
return $this->receiveData();
}
private function receiveDataReal($timeout, $listen_events) {
$this->connect($listen_events);
stream_set_timeout($this->fp, $timeout);
// Read the response header to find out how much data we'll be grabbing
$length = rtrim(fread($this->fp, 8));
// Read and return any data that was returned
$response = '';
while ($length > 0) {
$data = fread($this->fp, min(8192, $length));
if (strlen($data) < 1)
break; // EOF
$response .= $data;
$length -= strlen($data);
}
$response = explode(MythBackend::$backend_separator, $response);
if (count($response) == 1)
return $response[0];
if (count($response) == 0)
return false;
return $response;
}
public function receiveData($timeout = 30) {
return $this->receiveDataReal($timeout, false);
}
public function listenForEvent($event, $timeout = 120) {
if ($this->fp)
$this->disconnect();
$endtime = time() + $timeout;
do {
$response = $this->receiveDataReal($timeout, true);
} while ($response[1] != $event && $endtime < time());
if ($this->fp)
$this->disconnect();
if ($response[1] == $event)
return $response;
return false;
}
public function queryProgramRows($query = null, $offset = 1) {
$records = $this->sendCommand($query);
// Parse the records, starting at the offset point
$row = 0;
$col = 0;
$count = count($records);
for($i = $offset; $i < $count; $i++) {
$rows[$row][$col] = $records[$i];
// Every $NUMPROGRAMLINES fields (0 through ($NUMPROGRAMLINES-1)) means
// a new row. Please note that this changes between myth versions
if ($col == (MythBackend::$program_line_number - 1)) {
$col = 0;
$row++;
}
// Otherwise, just increment the column
else
$col++;
}
// Lastly, grab the offset data (if there is any)
for ($i=0; $i < $offset; $i++) {
$rows['offset'][$i] = $recs[$i];
}
// Return the data
return $rows;
}
/**
* Tell the backend to reschedule a particular record entry. If the change
* isn't specific to a single record entry (e.g. channel or record type
* priorities), then use 0. I don't think mythweb should need it, but if you
* need to indicate every record rule is affected, then use -1.
/**/
public function rescheduleRecording($recordid = -1) {
if ($recordid == 0) {
$this->sendCommand(array('RESCHEDULE_RECORDINGS ',
'CHECK 0 0 0 PHP',
'', '', '', '**any**'));
}
else {
if ($recordid == -1)
$recordid = 0;
$this->sendCommand(array('RESCHEDULE_RECORDINGS ',
'MATCH '.$recordid.' 0 0 - PHP'));
}
Cache::clear();
if ($this->listenForEvent('SCHEDULE_CHANGE'))
return true;
return false;
}
/**
* Request something from the backend's HTTP API and return it
* as JSON. This is just syntactic sugar for httpRequest
/**/
public function httpRequestAsJson($path, $args = array(), $opts = null) {
if (!$opts) {
$opts = array();
}
if (!$opts['http']) {
$opts['http'] = array();
}
if (!$opts['http']['method']) {
$opts['http']['method'] = "GET";
}
$opts['http']['header'] = "Accept: application/json\r\n";
return $this->httpRequest($path, $args, $opts);
}
/**
* Request something from the backend's HTTP API
/**/
public function httpRequest($path, $args = array(), $opts = null) {
$url = "http://{$this->ip}:{$this->port_http}/{$path}?";
foreach ($args as $key => $value) {
$url .= urlencode($key).'='.urlencode($value).'&';
}
if (!$opts) {
return @file_get_contents($url);
} else {
$context = stream_context_create($opts);
return @file_get_contents($url, false, $context);
}
}
}