1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Jalle19\StatusManager\Manager; |
4
|
|
|
|
5
|
|
|
use Jalle19\StatusManager\Database; |
6
|
|
|
use Jalle19\StatusManager\Database\Channel; |
7
|
|
|
use Jalle19\StatusManager\Database\ChannelQuery; |
8
|
|
|
use Jalle19\StatusManager\Database\Connection; |
9
|
|
|
use Jalle19\StatusManager\Database\ConnectionQuery; |
10
|
|
|
use Jalle19\StatusManager\Database\Input; |
11
|
|
|
use Jalle19\StatusManager\Database\InputQuery; |
12
|
|
|
use Jalle19\StatusManager\Database\InstanceQuery; |
13
|
|
|
use Jalle19\StatusManager\Database\Subscription; |
14
|
|
|
use Jalle19\StatusManager\Database\SubscriptionQuery; |
15
|
|
|
use Jalle19\StatusManager\Database\User; |
16
|
|
|
use Jalle19\StatusManager\Database\UserQuery; |
17
|
|
|
use Jalle19\StatusManager\Event\ConnectionSeenEvent; |
18
|
|
|
use Jalle19\StatusManager\Event\InputSeenEvent; |
19
|
|
|
use Jalle19\StatusManager\Event\InstanceSeenEvent; |
20
|
|
|
use Jalle19\StatusManager\Event\SubscriptionSeenEvent; |
21
|
|
|
use Jalle19\StatusManager\Event\SubscriptionStateChangeEvent; |
22
|
|
|
use Jalle19\StatusManager\Subscription\StateChange; |
23
|
|
|
use Jalle19\tvheadend\model\ConnectionStatus; |
24
|
|
|
use Jalle19\tvheadend\model\SubscriptionStatus; |
25
|
|
|
use Jalle19\tvheadend\Tvheadend; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Handles persisting of objects to the database |
29
|
|
|
* |
30
|
|
|
* @package Jalle19\StatusManager\Manager |
31
|
|
|
* @copyright Copyright © Sam Stenvall 2015- |
32
|
|
|
* @license https://www.gnu.org/licenses/gpl.html The GNU General Public License v2.0 |
33
|
|
|
*/ |
34
|
|
|
class PersistenceManager extends AbstractManager |
35
|
|
|
{ |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Removes stale subscriptions that haven't received a stop event |
39
|
|
|
*/ |
40
|
|
|
public function onMainLoopStarted() |
41
|
|
|
{ |
42
|
|
|
$numRemoved = SubscriptionQuery::create()->filterByStopped(null)->delete(); |
43
|
|
|
|
44
|
|
|
$this->getApplication()->getLogger()->info('Removed {numRemoved} stale subscriptions', [ |
45
|
|
|
'numRemoved' => $numRemoved, |
46
|
|
|
]); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param InstanceSeenEvent $event |
52
|
|
|
* |
53
|
|
|
* @throws \Propel\Runtime\Exception\PropelException |
54
|
|
|
*/ |
55
|
|
|
public function onInstanceSeen(InstanceSeenEvent $event) |
56
|
|
|
{ |
57
|
|
|
$instance = $event->getInstance(); |
58
|
|
|
|
59
|
|
|
if ($this->hasInstance($instance)) |
60
|
|
|
return; |
61
|
|
|
|
62
|
|
|
$instanceModel = new Database\Instance(); |
63
|
|
|
$instanceModel->setPrimaryKey($instance->getHostname()); |
64
|
|
|
$instanceModel->save(); |
65
|
|
|
|
66
|
|
|
$this->getApplication()->getLogger()->info('Stored new instance {instanceName}', [ |
67
|
|
|
'instanceName' => $instance->getHostname(), |
68
|
|
|
]); |
69
|
|
|
|
70
|
|
|
// Create a special user for eventual DVR subscriptions |
71
|
|
|
$user = new User(); |
72
|
|
|
$user->setInstance($instanceModel); |
73
|
|
|
$user->setName(User::NAME_DVR); |
74
|
|
|
$user->save(); |
75
|
|
|
|
76
|
|
|
$this->getApplication()->getLogger() |
77
|
|
|
->info('Stored new special user (instance: {instanceName}, user: {userName})', [ |
78
|
|
|
'instanceName' => $instance->getHostname(), |
79
|
|
|
'userName' => $user->getName(), |
80
|
|
|
]); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @param ConnectionSeenEvent $event |
86
|
|
|
*/ |
87
|
|
|
public function onConnectionSeen(ConnectionSeenEvent $event) |
88
|
|
|
{ |
89
|
|
|
$instanceName = $event->getInstance(); |
90
|
|
|
$connectionStatus = $event->getConnection(); |
91
|
|
|
|
92
|
|
|
if ($this->hasConnection($instanceName, $connectionStatus)) |
93
|
|
|
return; |
94
|
|
|
|
95
|
|
|
$user = null; |
96
|
|
|
|
97
|
|
|
// Find the user object when applicable |
98
|
|
|
if (!$connectionStatus->isAnonymous()) |
99
|
|
|
{ |
100
|
|
|
$this->onUserSeen($instanceName, $connectionStatus->user); |
101
|
|
|
|
102
|
|
|
$user = UserQuery::create()->filterByInstanceName($instanceName)->filterByName($connectionStatus->user) |
103
|
|
|
->findOne(); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
$connection = new Connection(); |
107
|
|
|
$connection->setInstanceName($instanceName)->setPeer($connectionStatus->peer) |
108
|
|
|
->setUser($user) |
109
|
|
|
->setStarted($connectionStatus->started)->setType($connectionStatus->type)->save(); |
110
|
|
|
|
111
|
|
|
$this->getApplication()->getLogger()->info('Stored new connection (instance: {instanceName}, peer: {peer})', [ |
112
|
|
|
'instanceName' => $instanceName, |
113
|
|
|
'peer' => $connectionStatus->peer, |
114
|
|
|
]); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @param InputSeenEvent $event |
120
|
|
|
*/ |
121
|
|
|
public function onInputSeen(InputSeenEvent $event) |
122
|
|
|
{ |
123
|
|
|
$instanceName = $event->getInstance(); |
124
|
|
|
$inputStatus = $event->getInput(); |
125
|
|
|
|
126
|
|
|
// Update the input and started fields for existing inputs |
127
|
|
|
if ($this->hasInput($inputStatus->uuid)) |
128
|
|
|
{ |
129
|
|
|
$input = InputQuery::create()->findPk($inputStatus->uuid); |
130
|
|
|
$input->setStarted(new \DateTime())->setWeight($inputStatus->weight); |
131
|
|
|
|
132
|
|
|
return; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
$input = new Input(); |
136
|
|
|
$input->setPrimaryKey($inputStatus->uuid); |
137
|
|
|
$input->setInstanceName($instanceName) |
138
|
|
|
->setStarted(new \DateTime()) |
139
|
|
|
->setInput($inputStatus->input) |
140
|
|
|
->setWeight($inputStatus->weight) |
141
|
|
|
->setNetwork(Input::parseNetwork($inputStatus)) |
142
|
|
|
->setMux(Input::parseMux($inputStatus))->save(); |
143
|
|
|
|
144
|
|
|
$this->getApplication()->getLogger() |
145
|
|
|
->info('Stored new input (instance: {instanceName}, network: {network}, mux: {mux}, weight: {weight})', |
146
|
|
|
[ |
147
|
|
|
'instanceName' => $instanceName, |
148
|
|
|
'network' => $input->getNetwork(), |
149
|
|
|
'mux' => $input->getMux(), |
150
|
|
|
'weight' => $input->getWeight(), |
151
|
|
|
]); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param SubscriptionSeenEvent $event |
157
|
|
|
* |
158
|
|
|
* @throws \Propel\Runtime\Exception\PropelException |
159
|
|
|
*/ |
160
|
|
|
public function onSubscriptionSeen(SubscriptionSeenEvent $event) |
161
|
|
|
{ |
162
|
|
|
$instanceName = $event->getInstance(); |
163
|
|
|
$status = $event->getSubscription(); |
164
|
|
|
|
165
|
|
|
// Ignore certain subscriptions |
166
|
|
|
if (in_array($status->getType(), [SubscriptionStatus::TYPE_EPGGRAB, SubscriptionStatus::TYPE_SERVICE_OR_MUX])) |
167
|
|
|
return; |
168
|
|
|
|
169
|
|
|
// Determine the username to store for the subscription |
170
|
|
|
$username = $status->username; |
171
|
|
|
|
172
|
|
|
switch ($status->getType()) |
173
|
|
|
{ |
174
|
|
|
case SubscriptionStatus::TYPE_RECORDING: |
175
|
|
|
$username = 'dvr'; |
176
|
|
|
break; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// Get the instance, user and channel |
180
|
|
|
$instance = InstanceQuery::create()->findPk($instanceName); |
181
|
|
|
$user = UserQuery::create()->filterByInstance($instance)->filterByName($username)->findOne(); |
182
|
|
|
|
183
|
|
|
// Ensure the channel exists |
184
|
|
|
$this->onChannelSeen($instanceName, $status->channel); |
185
|
|
|
$channel = ChannelQuery::create()->filterByInstance($instance)->filterByName($status->channel)->findOne(); |
186
|
|
|
|
187
|
|
|
if ($this->hasSubscription($instance, $user, $channel, $status)) |
188
|
|
|
return; |
189
|
|
|
|
190
|
|
|
// Try to determine which input is used by the subscription |
191
|
|
|
$input = InputQuery::create()->filterBySubscriptionStatus($instanceName, $status)->findOne(); |
192
|
|
|
|
193
|
|
|
if ($input === null) |
194
|
|
|
{ |
195
|
|
|
$this->getApplication()->getLogger() |
196
|
|
|
->warning('Got subscription that cannot be tied to an input ({instanceName}, user: {userName}, channel: {channelName})', |
197
|
|
|
[ |
198
|
|
|
'instanceName' => $instanceName, |
199
|
|
|
'userName' => $user !== null ? $user->getName() : 'N/A', |
200
|
|
|
'channelName' => $channel->getName(), |
201
|
|
|
]); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$subscription = new Subscription(); |
205
|
|
|
$subscription->setInstance($instance)->setInput($input)->setUser($user)->setChannel($channel) |
206
|
|
|
->setSubscriptionId($status->id)->setStarted($status->start)->setTitle($status->title) |
207
|
|
|
->setService($status->service); |
208
|
|
|
$subscription->save(); |
209
|
|
|
|
210
|
|
|
$this->getApplication()->getLogger() |
211
|
|
|
->info('Stored new subscription (instance: {instanceName}, user: {userName}, channel: {channelName})', |
212
|
|
|
[ |
213
|
|
|
'instanceName' => $instanceName, |
214
|
|
|
'userName' => $user !== null ? $user->getName() : 'N/A', |
215
|
|
|
'channelName' => $channel->getName(), |
216
|
|
|
]); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @param SubscriptionStateChangeEvent $event |
222
|
|
|
*/ |
223
|
|
|
public function onSubscriptionStateChange(SubscriptionStateChangeEvent $event) |
224
|
|
|
{ |
225
|
|
|
$instanceName = $event->getInstance(); |
226
|
|
|
$stateChange = $event->getStateChange(); |
227
|
|
|
|
228
|
|
|
// We only need to persist subscription stops |
229
|
|
|
if ($stateChange->getState() === StateChange::STATE_SUBSCRIPTION_STARTED) |
230
|
|
|
return; |
231
|
|
|
|
232
|
|
|
// Find the latest matching subscription |
233
|
|
|
$subscription = SubscriptionQuery::create()->filterByInstanceName($instanceName) |
234
|
|
|
->filterBySubscriptionId($stateChange->getSubscriptionId()) |
235
|
|
|
->addDescendingOrderByColumn('started')->findOne(); |
236
|
|
|
|
237
|
|
|
// EPG grab subscriptions are not stored so we don't want to log these with a high level |
238
|
|
|
if ($subscription === null) |
239
|
|
|
{ |
240
|
|
|
$this->getApplication()->getLogger() |
241
|
|
|
->debug('Got subscription stop without a matching start (instance: {instanceName}, subscription: {subscriptionId})', |
242
|
|
|
[ |
243
|
|
|
'instanceName' => $instanceName, |
244
|
|
|
'subscriptionId' => $stateChange->getSubscriptionId(), |
245
|
|
|
]); |
246
|
|
|
|
247
|
|
|
return; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$subscription->setStopped(new \DateTime()); |
251
|
|
|
$subscription->save(); |
252
|
|
|
|
253
|
|
|
$user = $subscription->getUser(); |
254
|
|
|
$channel = $subscription->getChannel(); |
255
|
|
|
|
256
|
|
|
$this->getApplication()->getLogger() |
257
|
|
|
->info('Stored subscription stop (instance: {instanceName}, user: {userName}, channel: {channelName})', |
258
|
|
|
[ |
259
|
|
|
'instanceName' => $instanceName, |
260
|
|
|
'userName' => $user !== null ? $user->getName() : 'N/A', |
261
|
|
|
'channelName' => $channel->getName(), |
262
|
|
|
]); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param string $instanceName |
268
|
|
|
* @param string $userName |
269
|
|
|
* |
270
|
|
|
* @throws \Propel\Runtime\Exception\PropelException |
271
|
|
|
*/ |
272
|
|
View Code Duplication |
private function onUserSeen($instanceName, $userName) |
|
|
|
|
273
|
|
|
{ |
274
|
|
|
if ($this->hasUser($instanceName, $userName)) |
275
|
|
|
return; |
276
|
|
|
|
277
|
|
|
$user = new User(); |
278
|
|
|
$user->setInstanceName($instanceName)->setName($userName); |
279
|
|
|
$user->save(); |
280
|
|
|
|
281
|
|
|
$this->getApplication()->getLogger()->info('Stored new user (instance: {instanceName}, username: {userName})', [ |
282
|
|
|
'instanceName' => $instanceName, |
283
|
|
|
'userName' => $userName, |
284
|
|
|
]); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* @param string $instanceName |
290
|
|
|
* @param string $channelName |
291
|
|
|
* |
292
|
|
|
* @throws \Propel\Runtime\Exception\PropelException |
293
|
|
|
*/ |
294
|
|
View Code Duplication |
private function onChannelSeen($instanceName, $channelName) |
|
|
|
|
295
|
|
|
{ |
296
|
|
|
if ($this->hasChannel($instanceName, $channelName)) |
297
|
|
|
return; |
298
|
|
|
|
299
|
|
|
$channel = new Channel(); |
300
|
|
|
$channel->setInstanceName($instanceName)->setName($channelName); |
301
|
|
|
$channel->save(); |
302
|
|
|
|
303
|
|
|
$this->getApplication()->getLogger() |
304
|
|
|
->info('Stored new channel (instance: {instanceName}, name: {channelName})', [ |
305
|
|
|
'instanceName' => $instanceName, |
306
|
|
|
'channelName' => $channelName, |
307
|
|
|
]); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* @param Tvheadend $instance |
313
|
|
|
* |
314
|
|
|
* @return bool whether the instance exists in the database |
315
|
|
|
*/ |
316
|
|
|
private function hasInstance(Tvheadend $instance) |
317
|
|
|
{ |
318
|
|
|
return InstanceQuery::create()->findPk($instance->getHostname()) !== null; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* @param $instanceName |
324
|
|
|
* @param ConnectionStatus $connectionStatus |
325
|
|
|
* |
326
|
|
|
* @return bool whether the connection exists in the database |
327
|
|
|
*/ |
328
|
|
|
private function hasConnection($instanceName, ConnectionStatus $connectionStatus) |
329
|
|
|
{ |
330
|
|
|
return ConnectionQuery::create()->filterByInstanceName($instanceName)->filterByPeer($connectionStatus->peer) |
331
|
|
|
->filterByStarted($connectionStatus->started)->findOne() !== null; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* @param string $uuid |
337
|
|
|
* |
338
|
|
|
* @return bool |
339
|
|
|
*/ |
340
|
|
|
private function hasInput($uuid) |
341
|
|
|
{ |
342
|
|
|
return InputQuery::create()->findPk($uuid) !== null; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* @param string $instanceName |
348
|
|
|
* @param string $userName |
349
|
|
|
* |
350
|
|
|
* @return bool |
351
|
|
|
*/ |
352
|
|
|
private function hasUser($instanceName, $userName) |
353
|
|
|
{ |
354
|
|
|
return UserQuery::create()->filterByInstanceName($instanceName)->filterByName($userName)->findOne() !== null; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* @param string $instanceName |
360
|
|
|
* @param string $channelName |
361
|
|
|
* |
362
|
|
|
* @return bool |
363
|
|
|
*/ |
364
|
|
|
private function hasChannel($instanceName, $channelName) |
365
|
|
|
{ |
366
|
|
|
return ChannelQuery::create()->filterByInstanceName($instanceName)->filterByName($channelName) |
367
|
|
|
->findOne() !== null; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* @param Database\Instance $instance |
373
|
|
|
* @param User|null $user |
374
|
|
|
* @param Channel $channel |
375
|
|
|
* @param SubscriptionStatus $subscription |
376
|
|
|
* |
377
|
|
|
* @return bool |
378
|
|
|
* @throws \Propel\Runtime\Exception\PropelException |
379
|
|
|
*/ |
380
|
|
|
private function hasSubscription( |
381
|
|
|
Database\Instance $instance, |
382
|
|
|
$user, |
383
|
|
|
Channel $channel, |
384
|
|
|
SubscriptionStatus $subscription |
385
|
|
|
) { |
386
|
|
|
// Not all subscriptions are tied to a user |
387
|
|
|
$userId = $user !== null ? $user->getId() : null; |
388
|
|
|
|
389
|
|
|
return SubscriptionQuery::create()->filterByInstance($instance)->filterByUserId($userId) |
390
|
|
|
->filterByChannel($channel) |
391
|
|
|
->filterBySubscriptionId($subscription->id)->filterByStarted($subscription->start) |
392
|
|
|
->findOne() !== null; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
} |
396
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.