lghorizon 0.9.0.dev4__py3-none-any.whl → 0.9.1__py3-none-any.whl
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.
- lghorizon/__init__.py +58 -25
- lghorizon/const.py +23 -81
- lghorizon/exceptions.py +3 -3
- lghorizon/helpers.py +1 -1
- lghorizon/lghorizon_api.py +263 -502
- lghorizon/lghorizon_device.py +409 -0
- lghorizon/lghorizon_device_state_processor.py +365 -0
- lghorizon/lghorizon_message_factory.py +38 -0
- lghorizon/lghorizon_models.py +1512 -0
- lghorizon/lghorizon_mqtt_client.py +335 -0
- lghorizon/lghorizon_recording_factory.py +55 -0
- lghorizon-0.9.1.dist-info/METADATA +189 -0
- lghorizon-0.9.1.dist-info/RECORD +17 -0
- lghorizon/models.py +0 -768
- lghorizon-0.9.0.dev4.dist-info/METADATA +0 -41
- lghorizon-0.9.0.dev4.dist-info/RECORD +0 -12
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/WHEEL +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {lghorizon-0.9.0.dev4.dist-info → lghorizon-0.9.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""LG Horizon device (set-top box) model."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import json
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from datetime import datetime as dt, timezone
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from typing import cast, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from .lghorizon_models import LGHorizonDeviceState, LGHorizonRunningState
|
|
13
|
+
from .lghorizon_models import LGHorizonStatusMessage, LGHorizonUIStatusMessage
|
|
14
|
+
from .lghorizon_models import (
|
|
15
|
+
LGHorizonSourceType,
|
|
16
|
+
LGHorizonLinearSource,
|
|
17
|
+
LGHorizonVODSource,
|
|
18
|
+
LGHorizonReplaySource,
|
|
19
|
+
LGHorizonNDVRSource,
|
|
20
|
+
LGHorizonReviewBufferSource,
|
|
21
|
+
LGHorizonRecordingSource,
|
|
22
|
+
)
|
|
23
|
+
from .lghorizon_models import LGHorizonAuth
|
|
24
|
+
from .lghorizon_models import LGHorizonReplayEvent, LGHorizonVOD, LGHorizonVODType
|
|
25
|
+
|
|
26
|
+
from .lghorizon_models import LGHorizonRecordingSingle
|
|
27
|
+
from .lghorizon_models import LGHorizonChannel
|
|
28
|
+
from .lghorizon_models import (
|
|
29
|
+
LGHorizonUIStateType,
|
|
30
|
+
LGHorizonAppsState,
|
|
31
|
+
LGHorizonPlayerState,
|
|
32
|
+
)
|
|
33
|
+
from .lghorizon_models import LGHorizonCustomer
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LGHorizonDeviceStateProcessor:
|
|
37
|
+
"""Process incoming device state messages"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
auth: LGHorizonAuth,
|
|
42
|
+
channels: Dict[str, LGHorizonChannel],
|
|
43
|
+
customer: LGHorizonCustomer,
|
|
44
|
+
profile_id: str,
|
|
45
|
+
):
|
|
46
|
+
self._auth = auth
|
|
47
|
+
self._channels = channels
|
|
48
|
+
self._customer = customer
|
|
49
|
+
self._profile_id = profile_id
|
|
50
|
+
|
|
51
|
+
async def process_state(
|
|
52
|
+
self, device_state: LGHorizonDeviceState, status_message: LGHorizonStatusMessage
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Process the device state based on the status message."""
|
|
55
|
+
await device_state.reset()
|
|
56
|
+
device_state.state = status_message.running_state
|
|
57
|
+
|
|
58
|
+
async def process_ui_state(
|
|
59
|
+
self,
|
|
60
|
+
device_state: LGHorizonDeviceState,
|
|
61
|
+
ui_status_message: LGHorizonUIStatusMessage,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Process the device state based on the UI status message."""
|
|
64
|
+
await device_state.reset()
|
|
65
|
+
if (
|
|
66
|
+
ui_status_message.ui_state is None
|
|
67
|
+
or device_state.state == LGHorizonRunningState.ONLINE_STANDBY
|
|
68
|
+
):
|
|
69
|
+
await device_state.reset()
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if ui_status_message.ui_state is None:
|
|
73
|
+
return
|
|
74
|
+
match ui_status_message.ui_state.ui_status:
|
|
75
|
+
case LGHorizonUIStateType.MAINUI:
|
|
76
|
+
if ui_status_message.ui_state.player_state is None:
|
|
77
|
+
return
|
|
78
|
+
await self._process_main_ui_state(
|
|
79
|
+
device_state, ui_status_message.ui_state.player_state
|
|
80
|
+
)
|
|
81
|
+
case LGHorizonUIStateType.APPS:
|
|
82
|
+
if ui_status_message.ui_state.apps_state is None:
|
|
83
|
+
return
|
|
84
|
+
await self._process_apps_state(
|
|
85
|
+
device_state, ui_status_message.ui_state.apps_state
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if ui_status_message.ui_state.ui_status == LGHorizonUIStateType.APPS:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if ui_status_message.ui_state.player_state is None:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
async def _process_main_ui_state(
|
|
95
|
+
self,
|
|
96
|
+
device_state: LGHorizonDeviceState,
|
|
97
|
+
player_state: LGHorizonPlayerState,
|
|
98
|
+
) -> None:
|
|
99
|
+
if player_state is None:
|
|
100
|
+
return
|
|
101
|
+
await device_state.reset()
|
|
102
|
+
device_state.source_type = player_state.source_type
|
|
103
|
+
device_state.ui_state_type = LGHorizonUIStateType.MAINUI
|
|
104
|
+
device_state.speed = player_state.speed
|
|
105
|
+
|
|
106
|
+
match player_state.source_type:
|
|
107
|
+
case LGHorizonSourceType.LINEAR:
|
|
108
|
+
await self._process_linear_state(device_state, player_state)
|
|
109
|
+
case LGHorizonSourceType.REVIEWBUFFER:
|
|
110
|
+
await self._process_reviewbuffer_state(device_state, player_state)
|
|
111
|
+
case LGHorizonSourceType.REPLAY:
|
|
112
|
+
await self._process_replay_state(device_state, player_state)
|
|
113
|
+
case LGHorizonSourceType.VOD:
|
|
114
|
+
await self._process_vod_state(device_state, player_state)
|
|
115
|
+
case LGHorizonSourceType.NDVR:
|
|
116
|
+
await self._process_ndvr_state(device_state, player_state)
|
|
117
|
+
|
|
118
|
+
async def _process_apps_state(
|
|
119
|
+
self,
|
|
120
|
+
device_state: LGHorizonDeviceState,
|
|
121
|
+
apps_state: LGHorizonAppsState,
|
|
122
|
+
) -> None:
|
|
123
|
+
device_state.id = apps_state.id
|
|
124
|
+
device_state.show_title = apps_state.app_name
|
|
125
|
+
device_state.image = apps_state.logo_path
|
|
126
|
+
device_state.ui_state_type = LGHorizonUIStateType.APPS
|
|
127
|
+
|
|
128
|
+
async def _process_linear_state(
|
|
129
|
+
self,
|
|
130
|
+
device_state: LGHorizonDeviceState,
|
|
131
|
+
player_state: LGHorizonPlayerState,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Process the device state based on the UI status message."""
|
|
134
|
+
if player_state.source is None:
|
|
135
|
+
return
|
|
136
|
+
player_state.source.__class__ = LGHorizonLinearSource
|
|
137
|
+
source = cast(LGHorizonLinearSource, player_state.source)
|
|
138
|
+
service_config = await self._auth.get_service_config()
|
|
139
|
+
service_url = await service_config.get_service_url("linearService")
|
|
140
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
141
|
+
service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
|
|
142
|
+
|
|
143
|
+
event_json = await self._auth.request(
|
|
144
|
+
service_url,
|
|
145
|
+
service_path,
|
|
146
|
+
)
|
|
147
|
+
replay_event = LGHorizonReplayEvent(event_json)
|
|
148
|
+
device_state.id = replay_event.event_id
|
|
149
|
+
channel = self._channels[replay_event.channel_id]
|
|
150
|
+
device_state.source_type = source.source_type
|
|
151
|
+
device_state.channel_id = channel.id
|
|
152
|
+
device_state.channel_name = channel.title
|
|
153
|
+
device_state.episode_title = replay_event.episode_name
|
|
154
|
+
device_state.season_number = replay_event.season_number
|
|
155
|
+
device_state.episode_number = replay_event.episode_number
|
|
156
|
+
device_state.show_title = replay_event.title
|
|
157
|
+
now_in_ms = int(time.time() * 1000)
|
|
158
|
+
|
|
159
|
+
device_state.last_position_update = int(time.time() * 1000)
|
|
160
|
+
device_state.start_time = replay_event.start_time
|
|
161
|
+
device_state.end_time = replay_event.end_time
|
|
162
|
+
device_state.duration = replay_event.end_time - replay_event.start_time
|
|
163
|
+
device_state.position = now_in_ms - int(replay_event.start_time * 1000)
|
|
164
|
+
|
|
165
|
+
# Add random number to url to force refresh
|
|
166
|
+
join_param = "?"
|
|
167
|
+
if join_param in channel.stream_image:
|
|
168
|
+
join_param = "&"
|
|
169
|
+
image_url = (
|
|
170
|
+
f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}"
|
|
171
|
+
)
|
|
172
|
+
device_state.image = image_url
|
|
173
|
+
|
|
174
|
+
async def _process_reviewbuffer_state(
|
|
175
|
+
self,
|
|
176
|
+
device_state: LGHorizonDeviceState,
|
|
177
|
+
player_state: LGHorizonPlayerState,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Process the device state based on the UI status message."""
|
|
180
|
+
if player_state.source is None:
|
|
181
|
+
return
|
|
182
|
+
player_state.source.__class__ = LGHorizonReviewBufferSource
|
|
183
|
+
source = cast(LGHorizonReviewBufferSource, player_state.source)
|
|
184
|
+
service_config = await self._auth.get_service_config()
|
|
185
|
+
service_url = await service_config.get_service_url("linearService")
|
|
186
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
187
|
+
service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
|
|
188
|
+
|
|
189
|
+
event_json = await self._auth.request(
|
|
190
|
+
service_url,
|
|
191
|
+
service_path,
|
|
192
|
+
)
|
|
193
|
+
replay_event = LGHorizonReplayEvent(event_json)
|
|
194
|
+
device_state.id = replay_event.event_id
|
|
195
|
+
channel = self._channels[replay_event.channel_id]
|
|
196
|
+
device_state.source_type = source.source_type
|
|
197
|
+
device_state.channel_id = channel.id
|
|
198
|
+
device_state.channel_name = channel.title
|
|
199
|
+
device_state.episode_title = replay_event.episode_name
|
|
200
|
+
device_state.season_number = replay_event.season_number
|
|
201
|
+
device_state.episode_number = replay_event.episode_number
|
|
202
|
+
device_state.show_title = replay_event.title
|
|
203
|
+
device_state.last_position_update = player_state.last_speed_change_time
|
|
204
|
+
device_state.position = player_state.relative_position
|
|
205
|
+
device_state.start_time = replay_event.start_time
|
|
206
|
+
device_state.end_time = replay_event.end_time
|
|
207
|
+
device_state.duration = replay_event.end_time - replay_event.start_time
|
|
208
|
+
# Add random number to url to force refresh
|
|
209
|
+
join_param = "?"
|
|
210
|
+
if join_param in channel.stream_image:
|
|
211
|
+
join_param = "&"
|
|
212
|
+
image_url = (
|
|
213
|
+
f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}"
|
|
214
|
+
)
|
|
215
|
+
device_state.image = image_url
|
|
216
|
+
|
|
217
|
+
async def _process_replay_state(
|
|
218
|
+
self,
|
|
219
|
+
device_state: LGHorizonDeviceState,
|
|
220
|
+
player_state: LGHorizonPlayerState,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Process the device state based on the UI status message."""
|
|
223
|
+
if player_state.source is None:
|
|
224
|
+
return
|
|
225
|
+
player_state.source.__class__ = LGHorizonReplaySource
|
|
226
|
+
source = cast(LGHorizonReplaySource, player_state.source)
|
|
227
|
+
service_config = await self._auth.get_service_config()
|
|
228
|
+
service_url = await service_config.get_service_url("linearService")
|
|
229
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
230
|
+
service_path = f"/v2/replayEvent/{source.event_id}?returnLinearContent=true&language={lang}"
|
|
231
|
+
|
|
232
|
+
event_json = await self._auth.request(
|
|
233
|
+
service_url,
|
|
234
|
+
service_path,
|
|
235
|
+
)
|
|
236
|
+
replay_event = LGHorizonReplayEvent(event_json)
|
|
237
|
+
device_state.id = replay_event.event_id
|
|
238
|
+
# Iets met buffer doen
|
|
239
|
+
channel = self._channels[replay_event.channel_id]
|
|
240
|
+
padding = channel.replay_pre_padding + channel.replay_post_padding
|
|
241
|
+
device_state.source_type = source.source_type
|
|
242
|
+
device_state.channel_id = channel.id
|
|
243
|
+
device_state.episode_title = replay_event.episode_name
|
|
244
|
+
device_state.season_number = replay_event.season_number
|
|
245
|
+
device_state.episode_number = replay_event.episode_number
|
|
246
|
+
device_state.show_title = replay_event.title
|
|
247
|
+
device_state.last_position_update = int(time.time() * 1000)
|
|
248
|
+
device_state.start_time = replay_event.start_time
|
|
249
|
+
device_state.end_time = replay_event.end_time
|
|
250
|
+
device_state.duration = (
|
|
251
|
+
replay_event.end_time - replay_event.start_time + padding
|
|
252
|
+
)
|
|
253
|
+
device_state.position = (
|
|
254
|
+
player_state.relative_position + channel.replay_pre_padding
|
|
255
|
+
)
|
|
256
|
+
# Add random number to url to force refresh
|
|
257
|
+
device_state.image = await self._get_intent_image_url(replay_event.event_id)
|
|
258
|
+
|
|
259
|
+
async def _process_vod_state(
|
|
260
|
+
self,
|
|
261
|
+
device_state: LGHorizonDeviceState,
|
|
262
|
+
player_state: LGHorizonPlayerState,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Process the device state based on the UI status message."""
|
|
265
|
+
if player_state.source is None:
|
|
266
|
+
return
|
|
267
|
+
player_state.source.__class__ = LGHorizonVODSource
|
|
268
|
+
source = cast(LGHorizonVODSource, player_state.source)
|
|
269
|
+
service_config = await self._auth.get_service_config()
|
|
270
|
+
service_url = await service_config.get_service_url("vodService")
|
|
271
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
272
|
+
service_path = f"/v2/detailscreen/{source.title_id}?language={lang}&profileId={self._profile_id}&cityId={self._customer.city_id}"
|
|
273
|
+
|
|
274
|
+
vod_json = await self._auth.request(
|
|
275
|
+
service_url,
|
|
276
|
+
service_path,
|
|
277
|
+
)
|
|
278
|
+
vod = LGHorizonVOD(vod_json)
|
|
279
|
+
device_state.id = vod.id
|
|
280
|
+
if vod.vod_type == LGHorizonVODType.EPISODE:
|
|
281
|
+
device_state.show_title = vod.series_title
|
|
282
|
+
device_state.episode_title = vod.title
|
|
283
|
+
device_state.season_number = vod.season
|
|
284
|
+
device_state.episode_number = vod.episode
|
|
285
|
+
else:
|
|
286
|
+
device_state.show_title = vod.title
|
|
287
|
+
|
|
288
|
+
device_state.duration = vod.duration
|
|
289
|
+
device_state.last_position_update = int(time.time() * 1000)
|
|
290
|
+
device_state.position = player_state.relative_position
|
|
291
|
+
|
|
292
|
+
device_state.image = await self._get_intent_image_url(vod.id)
|
|
293
|
+
|
|
294
|
+
async def _process_ndvr_state(
|
|
295
|
+
self, device_state: LGHorizonDeviceState, player_state: LGHorizonPlayerState
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Process the device state based on the UI status message."""
|
|
298
|
+
if player_state.source is None:
|
|
299
|
+
return
|
|
300
|
+
player_state.source.__class__ = LGHorizonNDVRSource
|
|
301
|
+
source = cast(LGHorizonNDVRSource, player_state.source)
|
|
302
|
+
service_config = await self._auth.get_service_config()
|
|
303
|
+
service_url = await service_config.get_service_url("recordingService")
|
|
304
|
+
lang = await self._customer.get_profile_lang(self._profile_id)
|
|
305
|
+
service_path = f"/customers/{self._customer.customer_id}/details/single/{source.recording_id}?profileId={self._profile_id}&language={lang}"
|
|
306
|
+
recording_json = await self._auth.request(
|
|
307
|
+
service_url,
|
|
308
|
+
service_path,
|
|
309
|
+
)
|
|
310
|
+
recording = LGHorizonRecordingSingle(recording_json)
|
|
311
|
+
device_state.id = recording.id
|
|
312
|
+
device_state.channel_id = recording.channel_id
|
|
313
|
+
if recording.channel_id:
|
|
314
|
+
channel = self._channels[recording.channel_id]
|
|
315
|
+
device_state.channel_name = channel.title
|
|
316
|
+
|
|
317
|
+
device_state.episode_title = recording.episode_title
|
|
318
|
+
device_state.season_number = recording.season_number
|
|
319
|
+
device_state.episode_number = recording.episode_number
|
|
320
|
+
device_state.last_position_update = player_state.last_speed_change_time
|
|
321
|
+
device_state.position = player_state.relative_position
|
|
322
|
+
if recording.start_time:
|
|
323
|
+
device_state.start_time = int(
|
|
324
|
+
dt.fromisoformat(
|
|
325
|
+
recording.start_time.replace("Z", "+00:00")
|
|
326
|
+
).timestamp()
|
|
327
|
+
)
|
|
328
|
+
if recording.end_time:
|
|
329
|
+
device_state.end_time = int(
|
|
330
|
+
dt.fromisoformat(recording.end_time.replace("Z", "+00:00")).timestamp()
|
|
331
|
+
)
|
|
332
|
+
if recording.start_time and recording.end_time:
|
|
333
|
+
device_state.duration = device_state.end_time - device_state.start_time
|
|
334
|
+
if recording.source == LGHorizonRecordingSource.SHOW:
|
|
335
|
+
device_state.show_title = recording.title
|
|
336
|
+
else:
|
|
337
|
+
device_state.show_title = recording.show_title
|
|
338
|
+
|
|
339
|
+
device_state.image = await self._get_intent_image_url(recording.id)
|
|
340
|
+
|
|
341
|
+
async def _get_intent_image_url(self, intent_id: str) -> Optional[str]:
|
|
342
|
+
"""Get intent image url."""
|
|
343
|
+
service_config = await self._auth.get_service_config()
|
|
344
|
+
intents_url = await service_config.get_service_url("imageService")
|
|
345
|
+
intents_path = "/intent"
|
|
346
|
+
body_json = [
|
|
347
|
+
{
|
|
348
|
+
"id": intent_id,
|
|
349
|
+
"intents": ["detailedBackground", "posterTile"],
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
intents_body = urllib.parse.quote(
|
|
353
|
+
json.dumps(body_json, separators=(",", ":"), indent=None), safe="~"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Construct the full path with the URL-encoded JSON as a query parameter
|
|
357
|
+
full_intents_path = f"{intents_path}?jsonBody={intents_body}"
|
|
358
|
+
intents_result = await self._auth.request(intents_url, full_intents_path)
|
|
359
|
+
if (
|
|
360
|
+
"intents" in intents_result[0]
|
|
361
|
+
and len(intents_result[0]["intents"]) > 0
|
|
362
|
+
and intents_result[0]["intents"][0]["url"]
|
|
363
|
+
):
|
|
364
|
+
return intents_result[0]["intents"][0]["url"]
|
|
365
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"LG Horizon Message Factory."
|
|
2
|
+
|
|
3
|
+
from .lghorizon_models import (
|
|
4
|
+
LGHorizonMessage,
|
|
5
|
+
LGHorizonStatusMessage,
|
|
6
|
+
LGHorizonUnknownMessage,
|
|
7
|
+
LGHorizonUIStatusMessage,
|
|
8
|
+
LGHorizonMessageType, # Import LGHorizonMessageType from here
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LGHorizonMessageFactory:
|
|
13
|
+
"""Handle incoming MQTT messages for LG Horizon devices."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initialize the LG Horizon Message Factory."""
|
|
17
|
+
|
|
18
|
+
async def create_message(self, topic: str, payload: dict) -> LGHorizonMessage:
|
|
19
|
+
"""Create an LG Horizon message based on the topic and payload."""
|
|
20
|
+
message_type = await self._get_message_type(topic, payload)
|
|
21
|
+
match message_type:
|
|
22
|
+
case LGHorizonMessageType.STATUS:
|
|
23
|
+
return LGHorizonStatusMessage(payload, topic)
|
|
24
|
+
case LGHorizonMessageType.UI_STATUS:
|
|
25
|
+
return LGHorizonUIStatusMessage(payload, topic)
|
|
26
|
+
case LGHorizonMessageType.UNKNOWN:
|
|
27
|
+
return LGHorizonUnknownMessage(payload, topic)
|
|
28
|
+
|
|
29
|
+
async def _get_message_type(
|
|
30
|
+
self, topic: str, payload: dict
|
|
31
|
+
) -> LGHorizonMessageType:
|
|
32
|
+
"""Determine the message type based on topic and payload."""
|
|
33
|
+
if "status" in topic:
|
|
34
|
+
return LGHorizonMessageType.STATUS
|
|
35
|
+
if "type" in payload:
|
|
36
|
+
if payload["type"] == "CPE.uiStatus":
|
|
37
|
+
return LGHorizonMessageType.UI_STATUS
|
|
38
|
+
return LGHorizonMessageType.UNKNOWN
|