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.
@@ -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