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,1512 @@
|
|
|
1
|
+
"""LG Horizon Model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
12
|
+
|
|
13
|
+
import backoff
|
|
14
|
+
from aiohttp import ClientResponseError, ClientSession
|
|
15
|
+
|
|
16
|
+
from .const import (
|
|
17
|
+
COUNTRY_SETTINGS,
|
|
18
|
+
)
|
|
19
|
+
from .exceptions import LGHorizonApiConnectionError, LGHorizonApiUnauthorizedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_LOGGER = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LGHorizonRunningState(Enum):
|
|
26
|
+
"""Running state of horizon box."""
|
|
27
|
+
|
|
28
|
+
UNKNOWN = "UNKNOWN"
|
|
29
|
+
ONLINE_RUNNING = "ONLINE_RUNNING"
|
|
30
|
+
ONLINE_STANDBY = "ONLINE_STANDBY"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LGHorizonMessageType(Enum):
|
|
34
|
+
"""Enumeration of LG Horizon message types."""
|
|
35
|
+
|
|
36
|
+
UNKNOWN = 0
|
|
37
|
+
STATUS = 1
|
|
38
|
+
UI_STATUS = 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LGHorizonRecordingSource(Enum):
|
|
42
|
+
"""LGHorizon recording."""
|
|
43
|
+
|
|
44
|
+
SHOW = "show"
|
|
45
|
+
UNKNOWN = "unknown"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LGHorizonRecordingState(Enum):
|
|
49
|
+
"""Enumeration of LG Horizon recording states."""
|
|
50
|
+
|
|
51
|
+
RECORDED = "recorded"
|
|
52
|
+
UNKNOWN = "unknown"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LGHorizonRecordingType(Enum): # type: ignore[no-redef]
|
|
56
|
+
"""Enumeration of LG Horizon recording states."""
|
|
57
|
+
|
|
58
|
+
SINGLE = "single"
|
|
59
|
+
SEASON = "season"
|
|
60
|
+
SHOW = "show"
|
|
61
|
+
UNKNOWN = "unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LGHorizonUIStateType(Enum):
|
|
65
|
+
"""Enumeration of LG Horizon UI State types."""
|
|
66
|
+
|
|
67
|
+
MAINUI = "mainUI"
|
|
68
|
+
APPS = "apps"
|
|
69
|
+
UNKNOWN = "unknown"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LGHorizonMessage(ABC):
|
|
73
|
+
"""Abstract base class for LG Horizon messages."""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def topic(self) -> str:
|
|
77
|
+
"""Return the topic of the message."""
|
|
78
|
+
return self._topic
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def payload(self) -> dict:
|
|
82
|
+
"""Return the payload of the message."""
|
|
83
|
+
return self._payload
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def message_type(self) -> LGHorizonMessageType | None:
|
|
88
|
+
"""Return the message type."""
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def __init__(self, topic: str, payload: dict) -> None:
|
|
92
|
+
"""Abstract base class for LG Horizon messages."""
|
|
93
|
+
self._topic = topic
|
|
94
|
+
"""Initialize the abstract base class for LG Horizon messages.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
topic: The MQTT topic of the message.
|
|
98
|
+
payload: The dictionary payload of the message.
|
|
99
|
+
"""
|
|
100
|
+
self._payload = payload
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
"""Return a string representation of the message."""
|
|
104
|
+
return f"LGHorizonStatusMessage(topic='{self._topic}', payload={json.dumps(self._payload, indent=2)})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class LGHorizonStatusMessage(LGHorizonMessage):
|
|
108
|
+
"""Represents an LG Horizon status message received via MQTT."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, payload: dict, topic: str) -> None:
|
|
111
|
+
"""Initialize an LG Horizon status message."""
|
|
112
|
+
super().__init__(topic, payload)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def message_type(self) -> LGHorizonMessageType:
|
|
116
|
+
"""Return the message type from the payload, if available."""
|
|
117
|
+
return LGHorizonMessageType.STATUS
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def source(self) -> str:
|
|
121
|
+
"""Return the device ID from the payload, if available."""
|
|
122
|
+
return self._payload.get("source", "unknown")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def running_state(self) -> LGHorizonRunningState:
|
|
126
|
+
"""Return the device ID from the payload, if available."""
|
|
127
|
+
return LGHorizonRunningState[self._payload.get("state", "unknown").upper()]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LGHorizonSourceType(Enum):
|
|
131
|
+
"""Enumeration of LG Horizon source types."""
|
|
132
|
+
|
|
133
|
+
LINEAR = "linear"
|
|
134
|
+
REVIEWBUFFER = "reviewBuffer"
|
|
135
|
+
NDVR = "nDVR"
|
|
136
|
+
REPLAY = "replay"
|
|
137
|
+
VOD = "VOD"
|
|
138
|
+
UNKNOWN = "unknown"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class LGHorizonSource(ABC):
|
|
142
|
+
"""Abstract base class for LG Horizon sources."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, raw_json: dict) -> None:
|
|
145
|
+
"""Initialize the LG Horizon source."""
|
|
146
|
+
self._raw_json = raw_json
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def source_type(self) -> LGHorizonSourceType: # type: ignore[no-redef]
|
|
151
|
+
"""Return the message type."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LGHorizonLinearSource(LGHorizonSource):
|
|
155
|
+
"""Represent the Linear Source of an LG Horizon device."""
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def channel_id(self) -> str:
|
|
159
|
+
"""Return the source type."""
|
|
160
|
+
return self._raw_json.get("channelId", "")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def event_id(self) -> str:
|
|
164
|
+
"""Return the event ID."""
|
|
165
|
+
return self._raw_json.get("eventId", "")
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
169
|
+
return LGHorizonSourceType.LINEAR
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LGHorizonReviewBufferSource(LGHorizonSource):
|
|
173
|
+
"""Represent the ReviewBuffer Source of an LG Horizon device."""
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def channel_id(self) -> str:
|
|
177
|
+
"""Return the source type."""
|
|
178
|
+
return self._raw_json.get("channelId", "")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def event_id(self) -> str:
|
|
182
|
+
"""Return the event ID."""
|
|
183
|
+
return self._raw_json.get("eventId", "")
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
187
|
+
return LGHorizonSourceType.REVIEWBUFFER
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class LGHorizonNDVRSource(LGHorizonSource):
|
|
191
|
+
"""Represent the Network Digital Video Recorder (NDVR) Source of an LG Horizon device."""
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def recording_id(self) -> str:
|
|
195
|
+
"""Return the recording ID."""
|
|
196
|
+
return self._raw_json.get("recordingId", "")
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def channel_id(self) -> str:
|
|
200
|
+
"""Return the channel ID."""
|
|
201
|
+
return self._raw_json.get("channelId", "")
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
205
|
+
return LGHorizonSourceType.NDVR
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class LGHorizonVODSource(LGHorizonSource):
|
|
209
|
+
"""Represent the VOD Source of an LG Horizon device."""
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def title_id(self) -> str:
|
|
213
|
+
"""Return the title ID."""
|
|
214
|
+
return self._raw_json.get("titleId", "")
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def start_intro_time(self) -> int:
|
|
218
|
+
"""Return the start intro time."""
|
|
219
|
+
return self._raw_json.get("startIntroTime", 0)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def end_intro_time(self) -> int:
|
|
223
|
+
"""Return the end intro time."""
|
|
224
|
+
return self._raw_json.get("endIntroTime", 0)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
228
|
+
return LGHorizonSourceType.VOD
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class LGHorizonReplaySource(LGHorizonSource):
|
|
232
|
+
"""Represent the Replay Source of an LG Horizon device."""
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def event_id(self) -> str:
|
|
236
|
+
"""Return the title ID."""
|
|
237
|
+
return self._raw_json.get("eventId", "")
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
241
|
+
"""Return the source type."""
|
|
242
|
+
return LGHorizonSourceType.REPLAY
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class LGHorizonUnknownSource(LGHorizonSource):
|
|
246
|
+
"""Represent an unknown source type of an LG Horizon device."""
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
250
|
+
return LGHorizonSourceType.UNKNOWN
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class LGHorizonPlayerState:
|
|
254
|
+
"""Represent the Player State of an LG Horizon device."""
|
|
255
|
+
|
|
256
|
+
def __init__(self, raw_json: dict) -> None:
|
|
257
|
+
"""Initialize the Player State."""
|
|
258
|
+
self._raw_json = raw_json
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
262
|
+
"""Return the source type."""
|
|
263
|
+
return LGHorizonSourceType[self._raw_json.get("sourceType", "unknown").upper()]
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def speed(self) -> int:
|
|
267
|
+
"""Return the Player State dictionary."""
|
|
268
|
+
return self._raw_json.get("speed", 0)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def last_speed_change_time(
|
|
272
|
+
self,
|
|
273
|
+
) -> int:
|
|
274
|
+
"""Return the last speed change time."""
|
|
275
|
+
return self._raw_json.get("lastSpeedChangeTime", 0.0)
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def relative_position(
|
|
279
|
+
self,
|
|
280
|
+
) -> int:
|
|
281
|
+
"""Return the last speed change time."""
|
|
282
|
+
return self._raw_json.get("relativePosition", 0.0)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def source(self) -> LGHorizonSource | None: # Added None to the return type
|
|
286
|
+
"""Return the last speed change time."""
|
|
287
|
+
if "source" in self._raw_json:
|
|
288
|
+
match self.source_type:
|
|
289
|
+
case LGHorizonSourceType.LINEAR:
|
|
290
|
+
return LGHorizonLinearSource(self._raw_json["source"])
|
|
291
|
+
case LGHorizonSourceType.VOD:
|
|
292
|
+
return LGHorizonVODSource(self._raw_json["source"])
|
|
293
|
+
case LGHorizonSourceType.REPLAY:
|
|
294
|
+
return LGHorizonReplaySource(self._raw_json["source"])
|
|
295
|
+
case LGHorizonSourceType.NDVR:
|
|
296
|
+
return LGHorizonNDVRSource(self._raw_json["source"])
|
|
297
|
+
case LGHorizonSourceType.REVIEWBUFFER:
|
|
298
|
+
return LGHorizonReviewBufferSource(self._raw_json["source"])
|
|
299
|
+
|
|
300
|
+
return LGHorizonUnknownSource(self._raw_json["source"])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class LGHorizonAppsState:
|
|
304
|
+
"""Represent the Apps State of an LG Horizon device."""
|
|
305
|
+
|
|
306
|
+
def __init__(self, raw_json: dict) -> None:
|
|
307
|
+
"""Initialize the Apps state."""
|
|
308
|
+
self._raw_json = raw_json
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def id(self) -> str:
|
|
312
|
+
"""Return the id."""
|
|
313
|
+
return self._raw_json.get("id", "")
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def app_name(self) -> str:
|
|
317
|
+
"""Return the app name."""
|
|
318
|
+
return self._raw_json.get("appName", "")
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def logo_path(self) -> str:
|
|
322
|
+
"""Return the logo path."""
|
|
323
|
+
return self._raw_json.get("logoPath", "")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class LGHorizonUIState:
|
|
327
|
+
"""Represent the UI State of an LG Horizon device."""
|
|
328
|
+
|
|
329
|
+
_player_state: LGHorizonPlayerState | None = None
|
|
330
|
+
_apps_state: LGHorizonAppsState | None = None
|
|
331
|
+
|
|
332
|
+
def __init__(self, raw_json: dict) -> None:
|
|
333
|
+
"""Initialize the State."""
|
|
334
|
+
self._raw_json = raw_json
|
|
335
|
+
"""Initialize the UI State.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
raw_json: The raw JSON dictionary containing UI state information.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def ui_status(self) -> LGHorizonUIStateType:
|
|
343
|
+
"""Return the UI status dictionary."""
|
|
344
|
+
return LGHorizonUIStateType[self._raw_json.get("uiStatus", "unknown").upper()]
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def player_state(
|
|
348
|
+
self,
|
|
349
|
+
) -> LGHorizonPlayerState | None: # Added None to the return type
|
|
350
|
+
"""Return the UI status dictionary."""
|
|
351
|
+
# Check if _player_state is None and if "playerState" key exists in raw_json
|
|
352
|
+
if self._player_state is None and "playerState" in self._raw_json:
|
|
353
|
+
self._player_state = LGHorizonPlayerState(
|
|
354
|
+
self._raw_json["playerState"]
|
|
355
|
+
) # Access directly as existence is checked
|
|
356
|
+
return self._player_state
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def apps_state(
|
|
360
|
+
self,
|
|
361
|
+
) -> LGHorizonAppsState | None: # Added None to the return type
|
|
362
|
+
"""Return the UI status dictionary."""
|
|
363
|
+
# Check if _player_state is None and if "playerState" key exists in raw_json
|
|
364
|
+
if self._apps_state is None and "appsState" in self._raw_json:
|
|
365
|
+
self._apps_state = LGHorizonAppsState(
|
|
366
|
+
self._raw_json["appsState"]
|
|
367
|
+
) # Access directly as existence is checked
|
|
368
|
+
return self._apps_state
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class LGHorizonUIStatusMessage(LGHorizonMessage):
|
|
372
|
+
"""Represents an LG Horizon UI status message received via MQTT."""
|
|
373
|
+
|
|
374
|
+
_status: LGHorizonUIState | None = None
|
|
375
|
+
|
|
376
|
+
def __init__(self, payload: dict, topic: str) -> None:
|
|
377
|
+
"""Initialize an LG Horizon UI status message."""
|
|
378
|
+
super().__init__(topic, payload)
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def message_type(self) -> LGHorizonMessageType:
|
|
382
|
+
"""Return the message type from the payload, if available."""
|
|
383
|
+
return LGHorizonMessageType.UI_STATUS
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def source(self) -> str:
|
|
387
|
+
"""Return the device ID from the payload, if available."""
|
|
388
|
+
return self._payload.get("source", "unknown")
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def message_timestamp(self) -> int:
|
|
392
|
+
"""Return the device ID from the payload, if available."""
|
|
393
|
+
return self._payload.get("messageTimeStamp", 0)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def ui_state(self) -> LGHorizonUIState | None:
|
|
397
|
+
"""Return the device ID from the payload, if available."""
|
|
398
|
+
if not self._status and "status" in self._payload:
|
|
399
|
+
self._status = LGHorizonUIState(self._payload["status"])
|
|
400
|
+
return self._status
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class LGHorizonUnknownMessage(LGHorizonMessage):
|
|
404
|
+
"""Represents an unknown LG Horizon message received via MQTT."""
|
|
405
|
+
|
|
406
|
+
def __init__(self, payload: dict, topic: str) -> None:
|
|
407
|
+
"""Initialize an LG Horizon unknown message."""
|
|
408
|
+
super().__init__(topic, payload)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def message_type(self) -> LGHorizonMessageType:
|
|
412
|
+
"""Return the message type from the payload, if available."""
|
|
413
|
+
return LGHorizonMessageType.UNKNOWN
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class LGHorizonProfileOptions:
|
|
417
|
+
"""LGHorizon profile options."""
|
|
418
|
+
|
|
419
|
+
def __init__(self, options_payload: dict):
|
|
420
|
+
"""Initialize a profile options."""
|
|
421
|
+
self._options_payload = options_payload
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def lang(self) -> str:
|
|
425
|
+
"""Return the language."""
|
|
426
|
+
return self._options_payload["lang"]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class LGHorizonProfile:
|
|
430
|
+
"""LGHorizon profile."""
|
|
431
|
+
|
|
432
|
+
_options: LGHorizonProfileOptions
|
|
433
|
+
_profile_payload: dict
|
|
434
|
+
|
|
435
|
+
def __init__(self, profile_payload: dict):
|
|
436
|
+
"""Initialize a profile."""
|
|
437
|
+
self._profile_payload = profile_payload
|
|
438
|
+
self._options = LGHorizonProfileOptions(self._profile_payload["options"])
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def id(self) -> str:
|
|
442
|
+
"""Return the profile id."""
|
|
443
|
+
return self._profile_payload["profileId"]
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def name(self) -> str:
|
|
447
|
+
"""Return the profile name."""
|
|
448
|
+
return self._profile_payload["name"]
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def favorite_channels(self) -> list[str]:
|
|
452
|
+
"""Return the favorite channels."""
|
|
453
|
+
return self._profile_payload.get("favoriteChannels", [])
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def options(self) -> LGHorizonProfileOptions:
|
|
457
|
+
"""Return the profile options."""
|
|
458
|
+
return self._options
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class LGHorizonAuth:
|
|
462
|
+
"""Class to make authenticated requests."""
|
|
463
|
+
|
|
464
|
+
_websession: ClientSession
|
|
465
|
+
_refresh_token: str
|
|
466
|
+
_access_token: Optional[str]
|
|
467
|
+
_username: str
|
|
468
|
+
_password: str
|
|
469
|
+
_household_id: str
|
|
470
|
+
_token_expiry: Optional[int]
|
|
471
|
+
_country_code: str
|
|
472
|
+
_host: str
|
|
473
|
+
_use_refresh_token: bool
|
|
474
|
+
_token_refresh_callback: Callable[str, None] | None # pyright: ignore[reportInvalidTypeForm]
|
|
475
|
+
|
|
476
|
+
def __init__(
|
|
477
|
+
self,
|
|
478
|
+
websession: ClientSession,
|
|
479
|
+
country_code: str,
|
|
480
|
+
refresh_token: str = "",
|
|
481
|
+
username: str = "",
|
|
482
|
+
password: str = "",
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Initialize the auth with refresh token."""
|
|
485
|
+
self._websession = websession
|
|
486
|
+
self._refresh_token = refresh_token
|
|
487
|
+
self._access_token = None
|
|
488
|
+
self._username = username
|
|
489
|
+
self._password = password
|
|
490
|
+
self._household_id = ""
|
|
491
|
+
self._token_expiry = None
|
|
492
|
+
self._country_code = country_code
|
|
493
|
+
self._host = COUNTRY_SETTINGS[country_code]["api_url"]
|
|
494
|
+
self._use_refresh_token = COUNTRY_SETTINGS[country_code]["use_refreshtoken"]
|
|
495
|
+
self._service_config = None
|
|
496
|
+
self._token_refresh_callback = None
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def websession(self) -> ClientSession:
|
|
500
|
+
"""Return the aiohttp client session."""
|
|
501
|
+
return self._websession
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def refresh_token(self) -> str:
|
|
505
|
+
"""Return the refresh token."""
|
|
506
|
+
return self._refresh_token
|
|
507
|
+
|
|
508
|
+
@refresh_token.setter
|
|
509
|
+
def refresh_token(self, value: str) -> None:
|
|
510
|
+
"""Set the refresh token."""
|
|
511
|
+
self._refresh_token = value
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def access_token(self) -> Optional[str]:
|
|
515
|
+
"""Return the access token."""
|
|
516
|
+
return self._access_token
|
|
517
|
+
|
|
518
|
+
@access_token.setter
|
|
519
|
+
def access_token(self, value: Optional[str]) -> None:
|
|
520
|
+
"""Set the access token."""
|
|
521
|
+
self._access_token = value
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def username(self) -> str:
|
|
525
|
+
"""Return the username."""
|
|
526
|
+
return self._username
|
|
527
|
+
|
|
528
|
+
@username.setter
|
|
529
|
+
def username(self, value: str) -> None:
|
|
530
|
+
"""Set the username."""
|
|
531
|
+
self._username = value
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def password(self) -> str:
|
|
535
|
+
"""Return the password."""
|
|
536
|
+
return self._password
|
|
537
|
+
|
|
538
|
+
@password.setter
|
|
539
|
+
def password(self, value: str) -> None:
|
|
540
|
+
"""Set the password."""
|
|
541
|
+
self._password = value
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def household_id(self) -> str:
|
|
545
|
+
"""Return the household ID."""
|
|
546
|
+
return self._household_id
|
|
547
|
+
|
|
548
|
+
@household_id.setter
|
|
549
|
+
def household_id(self, value: str) -> None:
|
|
550
|
+
"""Set the household ID."""
|
|
551
|
+
self._household_id = value
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def token_expiry(self) -> Optional[int]:
|
|
555
|
+
"""Return the token expiry timestamp."""
|
|
556
|
+
return self._token_expiry
|
|
557
|
+
|
|
558
|
+
@token_expiry.setter
|
|
559
|
+
def token_expiry(self, value: Optional[int]) -> None:
|
|
560
|
+
"""Set the token expiry timestamp."""
|
|
561
|
+
self._token_expiry = value
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def country_code(self) -> str:
|
|
565
|
+
"""Return the country code."""
|
|
566
|
+
return self._country_code
|
|
567
|
+
|
|
568
|
+
async def is_token_expiring(self) -> bool:
|
|
569
|
+
"""Check if the token is expiring within one day."""
|
|
570
|
+
if not self.access_token or not self.token_expiry:
|
|
571
|
+
return True
|
|
572
|
+
current_unix_time = int(time.time())
|
|
573
|
+
return current_unix_time >= (self.token_expiry - 86400)
|
|
574
|
+
|
|
575
|
+
async def fetch_access_token(self) -> None:
|
|
576
|
+
"""Fetch the access token."""
|
|
577
|
+
_LOGGER.debug("Fetching access token")
|
|
578
|
+
headers = dict()
|
|
579
|
+
headers["content-type"] = "application/json"
|
|
580
|
+
headers["charset"] = "utf-8"
|
|
581
|
+
|
|
582
|
+
if not self._use_refresh_token and self.access_token is None:
|
|
583
|
+
payload = {"password": self.password, "username": self.username}
|
|
584
|
+
headers["x-device-code"] = "web"
|
|
585
|
+
auth_url_path = "/auth-service/v1/authorization"
|
|
586
|
+
else:
|
|
587
|
+
payload = {"refreshToken": self.refresh_token}
|
|
588
|
+
auth_url_path = "/auth-service/v1/authorization/refresh"
|
|
589
|
+
try: # Use properties and backing fields
|
|
590
|
+
auth_response = await self.websession.post(
|
|
591
|
+
f"{self._host}{auth_url_path}",
|
|
592
|
+
json=payload,
|
|
593
|
+
headers=headers,
|
|
594
|
+
)
|
|
595
|
+
except Exception as ex:
|
|
596
|
+
raise LGHorizonApiConnectionError from ex
|
|
597
|
+
auth_json = await auth_response.json()
|
|
598
|
+
if not auth_response.ok:
|
|
599
|
+
error = None
|
|
600
|
+
if "error" in auth_json:
|
|
601
|
+
error = auth_json["error"]
|
|
602
|
+
if error and error["statusCode"] == 97401:
|
|
603
|
+
raise LGHorizonApiUnauthorizedError("Invalid credentials")
|
|
604
|
+
elif error:
|
|
605
|
+
raise LGHorizonApiConnectionError(error["message"])
|
|
606
|
+
else:
|
|
607
|
+
raise LGHorizonApiConnectionError("Unknown connection error")
|
|
608
|
+
|
|
609
|
+
self.household_id = auth_json["householdId"]
|
|
610
|
+
self.access_token = auth_json["accessToken"]
|
|
611
|
+
self.refresh_token = auth_json["refreshToken"]
|
|
612
|
+
if self._token_refresh_callback:
|
|
613
|
+
self._token_refresh_callback(self.refresh_token)
|
|
614
|
+
self.username = auth_json["username"]
|
|
615
|
+
self.token_expiry = auth_json["refreshTokenExpiry"]
|
|
616
|
+
|
|
617
|
+
@backoff.on_exception(backoff.expo, LGHorizonApiConnectionError, max_tries=3)
|
|
618
|
+
async def request(self, host: str, path: str, params=None, **kwargs) -> Any:
|
|
619
|
+
"""Make a request."""
|
|
620
|
+
if headers := kwargs.pop("headers", {}):
|
|
621
|
+
headers = dict(headers)
|
|
622
|
+
request_url = f"{host}{path}"
|
|
623
|
+
if await self.is_token_expiring(): # Use property
|
|
624
|
+
_LOGGER.debug("Access token is expiring, fetching a new one")
|
|
625
|
+
await self.fetch_access_token()
|
|
626
|
+
try:
|
|
627
|
+
web_response = await self.websession.request(
|
|
628
|
+
"GET", request_url, **kwargs, headers=headers, params=params
|
|
629
|
+
)
|
|
630
|
+
web_response.raise_for_status()
|
|
631
|
+
json_response = await web_response.json()
|
|
632
|
+
_LOGGER.debug(
|
|
633
|
+
"Response from %s:\n %s",
|
|
634
|
+
request_url,
|
|
635
|
+
json.dumps(json_response, indent=2),
|
|
636
|
+
)
|
|
637
|
+
return json_response
|
|
638
|
+
except ClientResponseError as cre:
|
|
639
|
+
_LOGGER.error("Error response from %s: %s", request_url, str(cre))
|
|
640
|
+
if cre.status == 401:
|
|
641
|
+
await self.fetch_access_token()
|
|
642
|
+
raise LGHorizonApiConnectionError(
|
|
643
|
+
f"Unable to call {request_url}. Error:{str(cre)}"
|
|
644
|
+
) from cre
|
|
645
|
+
|
|
646
|
+
except Exception as ex:
|
|
647
|
+
_LOGGER.error("Error calling %s: %s", request_url, str(ex))
|
|
648
|
+
raise LGHorizonApiConnectionError(
|
|
649
|
+
f"Unable to call {request_url}. Error:{str(ex)}"
|
|
650
|
+
) from ex
|
|
651
|
+
|
|
652
|
+
async def get_mqtt_token(self) -> Any:
|
|
653
|
+
"""Get the MQTT token."""
|
|
654
|
+
_LOGGER.debug("Fetching MQTT token")
|
|
655
|
+
config = await self.get_service_config()
|
|
656
|
+
service_url = await config.get_service_url("authorizationService")
|
|
657
|
+
result = await self.request(
|
|
658
|
+
service_url,
|
|
659
|
+
"/v1/mqtt/token",
|
|
660
|
+
)
|
|
661
|
+
return result["token"]
|
|
662
|
+
|
|
663
|
+
async def get_service_config(self):
|
|
664
|
+
"""Get the service configuration."""
|
|
665
|
+
_LOGGER.debug("Fetching service configuration")
|
|
666
|
+
if self._service_config is None: # Use property and backing field
|
|
667
|
+
base_country_code = self.country_code[0:2]
|
|
668
|
+
result = await self.request(
|
|
669
|
+
self._host,
|
|
670
|
+
f"/{base_country_code}/en/config-service/conf/web/backoffice.json",
|
|
671
|
+
)
|
|
672
|
+
self._service_config = LGHorizonServicesConfig(result)
|
|
673
|
+
|
|
674
|
+
return self._service_config
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class LGHorizonChannel:
|
|
678
|
+
"""Class to represent a channel."""
|
|
679
|
+
|
|
680
|
+
def __init__(self, channel_json):
|
|
681
|
+
"""Initialize a channel."""
|
|
682
|
+
self.channel_json = channel_json
|
|
683
|
+
|
|
684
|
+
@property
|
|
685
|
+
def id(self) -> str:
|
|
686
|
+
"""Returns the id."""
|
|
687
|
+
return self.channel_json["id"]
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def channel_number(self) -> str:
|
|
691
|
+
"""Returns the channel number."""
|
|
692
|
+
return self.channel_json["logicalChannelNumber"]
|
|
693
|
+
|
|
694
|
+
@property
|
|
695
|
+
def replay_pre_padding(self) -> int:
|
|
696
|
+
"""Returns the channel number."""
|
|
697
|
+
return self.channel_json.get("replayPrePadding", 0)
|
|
698
|
+
|
|
699
|
+
@property
|
|
700
|
+
def replay_post_padding(self) -> int:
|
|
701
|
+
"""Returns the channel number."""
|
|
702
|
+
return self.channel_json.get("replayPostPadding", 0)
|
|
703
|
+
|
|
704
|
+
@property
|
|
705
|
+
def is_radio(self) -> bool:
|
|
706
|
+
"""Returns if the channel is a radio channel."""
|
|
707
|
+
return self.channel_json.get("isRadio", False)
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
def title(self) -> str:
|
|
711
|
+
"""Returns the title."""
|
|
712
|
+
return self.channel_json["name"]
|
|
713
|
+
|
|
714
|
+
@property
|
|
715
|
+
def logo_image(self) -> str:
|
|
716
|
+
"""Returns the logo image."""
|
|
717
|
+
if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
|
|
718
|
+
return self.channel_json["logo"]["focused"]
|
|
719
|
+
return ""
|
|
720
|
+
|
|
721
|
+
@property
|
|
722
|
+
def linear_products(self) -> list[str]:
|
|
723
|
+
"""Returns the linear products."""
|
|
724
|
+
return self.channel_json.get("linearProducts", [])
|
|
725
|
+
|
|
726
|
+
@property
|
|
727
|
+
def stream_image(self) -> str:
|
|
728
|
+
"""Returns the stream image."""
|
|
729
|
+
image_stream = self.channel_json["imageStream"]
|
|
730
|
+
if "full" in image_stream:
|
|
731
|
+
return image_stream["full"]
|
|
732
|
+
if "small" in image_stream:
|
|
733
|
+
return image_stream["small"]
|
|
734
|
+
if "logo" in self.channel_json and "focused" in self.channel_json["logo"]:
|
|
735
|
+
return self.channel_json["logo"]["focused"]
|
|
736
|
+
return ""
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class LGHorizonServicesConfig:
|
|
740
|
+
"""Handle LG Horizon configuration and service URLs."""
|
|
741
|
+
|
|
742
|
+
def __init__(self, config_data: dict[str, Any]) -> None:
|
|
743
|
+
"""Initialize LG Horizon config.
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
config_data: Configuration dictionary with service endpoints
|
|
747
|
+
"""
|
|
748
|
+
self._config = config_data
|
|
749
|
+
|
|
750
|
+
async def get_service_url(self, service_name: str) -> str:
|
|
751
|
+
"""Get the URL for a specific service.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
service_name: Name of the service (e.g., 'authService', 'recordingService')
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
URL for the service
|
|
758
|
+
|
|
759
|
+
Raises:
|
|
760
|
+
ValueError: If the service or its URL is not found
|
|
761
|
+
"""
|
|
762
|
+
if service_name in self._config and "URL" in self._config[service_name]:
|
|
763
|
+
return self._config[service_name]["URL"]
|
|
764
|
+
raise ValueError(f"Service URL for '{service_name}' not found in configuration")
|
|
765
|
+
|
|
766
|
+
async def get_all_services(self) -> dict[str, str]:
|
|
767
|
+
"""Get all available services and their URLs.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
Dictionary mapping service names to URLs
|
|
771
|
+
"""
|
|
772
|
+
return {
|
|
773
|
+
name: url
|
|
774
|
+
for name, service in self._config.items()
|
|
775
|
+
if isinstance(service, dict) and (url := service.get("URL"))
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async def __getattr__(self, name: str) -> Optional[str]:
|
|
779
|
+
"""Access service URLs as attributes.
|
|
780
|
+
|
|
781
|
+
Example: config.authService returns the auth service URL
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
name: Service name
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
URL for the service or None if not found
|
|
788
|
+
"""
|
|
789
|
+
if name.startswith("_"):
|
|
790
|
+
raise AttributeError(
|
|
791
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
792
|
+
)
|
|
793
|
+
return await self.get_service_url(name)
|
|
794
|
+
|
|
795
|
+
def __repr__(self) -> str:
|
|
796
|
+
"""Return string representation."""
|
|
797
|
+
services = list(self._config.keys())
|
|
798
|
+
return f"LGHorizonConfig({len(services)} services)"
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
class LGHorizonCustomer:
|
|
802
|
+
"""LGHorizon customer."""
|
|
803
|
+
|
|
804
|
+
_profiles: Dict[str, LGHorizonProfile] = {}
|
|
805
|
+
|
|
806
|
+
def __init__(self, json_payload: dict):
|
|
807
|
+
"""Initialize a customer."""
|
|
808
|
+
self._json_payload = json_payload
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def customer_id(self) -> str:
|
|
812
|
+
"""Return the customer id."""
|
|
813
|
+
return self._json_payload["customerId"]
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def hashed_customer_id(self) -> str:
|
|
817
|
+
"""Return the hashed customer id."""
|
|
818
|
+
return self._json_payload["hashedCustomerId"]
|
|
819
|
+
|
|
820
|
+
@property
|
|
821
|
+
def country_id(self) -> str:
|
|
822
|
+
"""Return the country id."""
|
|
823
|
+
return self._json_payload["countryId"]
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def city_id(self) -> int:
|
|
827
|
+
"""Return the city id."""
|
|
828
|
+
return self._json_payload["cityId"]
|
|
829
|
+
|
|
830
|
+
@property
|
|
831
|
+
def assigned_devices(self) -> list[str]:
|
|
832
|
+
"""Return the assigned set-top boxes."""
|
|
833
|
+
return self._json_payload.get("assignedDevices", [])
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def profiles(self) -> Dict[str, LGHorizonProfile]:
|
|
837
|
+
"""Return the profiles."""
|
|
838
|
+
if not self._profiles or self._profiles == {}:
|
|
839
|
+
self._profiles = {
|
|
840
|
+
p["profileId"]: LGHorizonProfile(p)
|
|
841
|
+
for p in self._json_payload.get("profiles", [])
|
|
842
|
+
}
|
|
843
|
+
return self._profiles
|
|
844
|
+
|
|
845
|
+
async def get_profile_lang(self, profile_id: str) -> str:
|
|
846
|
+
"""Return the profile language."""
|
|
847
|
+
if profile_id not in self.profiles:
|
|
848
|
+
return "nl"
|
|
849
|
+
return self.profiles[profile_id].options.lang
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class LGHorizonDeviceState:
|
|
853
|
+
"""Represent current state of a box."""
|
|
854
|
+
|
|
855
|
+
_id: Optional[str]
|
|
856
|
+
_channel_id: Optional[str]
|
|
857
|
+
_channel_name: Optional[str]
|
|
858
|
+
_show_title: Optional[str]
|
|
859
|
+
_episode_title: Optional[str]
|
|
860
|
+
_season_number: Optional[int]
|
|
861
|
+
_episode_number: Optional[int]
|
|
862
|
+
_image: Optional[str]
|
|
863
|
+
_source_type: LGHorizonSourceType
|
|
864
|
+
_paused: bool
|
|
865
|
+
_duration: Optional[float]
|
|
866
|
+
_position: Optional[float]
|
|
867
|
+
_last_position_update: Optional[datetime]
|
|
868
|
+
_state: LGHorizonRunningState
|
|
869
|
+
_speed: Optional[int]
|
|
870
|
+
_start_time: Optional[int]
|
|
871
|
+
_end_time: Optional[int]
|
|
872
|
+
|
|
873
|
+
def __init__(self) -> None:
|
|
874
|
+
"""Initialize the playing info."""
|
|
875
|
+
self._channel_id = None
|
|
876
|
+
self._show_title = None
|
|
877
|
+
self._episode_title = None
|
|
878
|
+
self._season_number = None
|
|
879
|
+
self._episode_number = None
|
|
880
|
+
self._image = None
|
|
881
|
+
self._source_type = LGHorizonSourceType.UNKNOWN
|
|
882
|
+
self._ui_state_type = LGHorizonUIStateType.UNKNOWN
|
|
883
|
+
self._paused = False
|
|
884
|
+
self._duration = None
|
|
885
|
+
self._position = None
|
|
886
|
+
self._last_position_update = None
|
|
887
|
+
self._state = LGHorizonRunningState.UNKNOWN
|
|
888
|
+
self._speed = None
|
|
889
|
+
self._channel_name = None
|
|
890
|
+
self._id = None
|
|
891
|
+
self._start_time = None
|
|
892
|
+
self._end_time = None
|
|
893
|
+
|
|
894
|
+
@property
|
|
895
|
+
def state(self) -> LGHorizonRunningState:
|
|
896
|
+
"""Return the channel ID."""
|
|
897
|
+
return self._state
|
|
898
|
+
|
|
899
|
+
@state.setter
|
|
900
|
+
def state(self, value: LGHorizonRunningState) -> None:
|
|
901
|
+
"""Set the channel ID."""
|
|
902
|
+
self._state = value
|
|
903
|
+
|
|
904
|
+
@property
|
|
905
|
+
def channel_id(self) -> Optional[str]:
|
|
906
|
+
"""Return the channel ID."""
|
|
907
|
+
return self._channel_id
|
|
908
|
+
|
|
909
|
+
@channel_id.setter
|
|
910
|
+
def channel_id(self, value: Optional[str]) -> None:
|
|
911
|
+
"""Set the channel ID."""
|
|
912
|
+
self._channel_id = value
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def id(self) -> Optional[str]:
|
|
916
|
+
"""Return the channel ID."""
|
|
917
|
+
return self._id
|
|
918
|
+
|
|
919
|
+
@id.setter
|
|
920
|
+
def id(self, value: Optional[str]) -> None:
|
|
921
|
+
"""Set the channel ID."""
|
|
922
|
+
self._id = value
|
|
923
|
+
|
|
924
|
+
@property
|
|
925
|
+
def channel_name(self) -> Optional[str]:
|
|
926
|
+
"""Return the channel ID."""
|
|
927
|
+
return self._channel_name
|
|
928
|
+
|
|
929
|
+
@channel_name.setter
|
|
930
|
+
def channel_name(self, value: Optional[str]) -> None:
|
|
931
|
+
"""Set the channel ID."""
|
|
932
|
+
self._channel_name = value
|
|
933
|
+
|
|
934
|
+
@property
|
|
935
|
+
def show_title(self) -> Optional[str]:
|
|
936
|
+
"""Return the title."""
|
|
937
|
+
return self._show_title
|
|
938
|
+
|
|
939
|
+
@show_title.setter
|
|
940
|
+
def show_title(self, value: Optional[str]) -> None:
|
|
941
|
+
"""Set the title."""
|
|
942
|
+
self._show_title = value
|
|
943
|
+
|
|
944
|
+
@property
|
|
945
|
+
def app_name(self) -> Optional[str]:
|
|
946
|
+
"""Return the title."""
|
|
947
|
+
return self._app_name
|
|
948
|
+
|
|
949
|
+
@app_name.setter
|
|
950
|
+
def app_name(self, value: Optional[str]) -> None:
|
|
951
|
+
"""Set the title."""
|
|
952
|
+
self._app_name = value
|
|
953
|
+
|
|
954
|
+
@property
|
|
955
|
+
def episode_title(self) -> Optional[str]:
|
|
956
|
+
"""Return the title."""
|
|
957
|
+
return self._episode_title
|
|
958
|
+
|
|
959
|
+
@episode_title.setter
|
|
960
|
+
def episode_title(self, value: Optional[str]) -> None:
|
|
961
|
+
"""Set the title."""
|
|
962
|
+
self._episode_title = value
|
|
963
|
+
|
|
964
|
+
@property
|
|
965
|
+
def episode_number(self) -> Optional[int]:
|
|
966
|
+
"""Return the title."""
|
|
967
|
+
return self._episode_number
|
|
968
|
+
|
|
969
|
+
@episode_number.setter
|
|
970
|
+
def episode_number(self, value: Optional[int]) -> None:
|
|
971
|
+
"""Set the title."""
|
|
972
|
+
self._episode_number = value
|
|
973
|
+
|
|
974
|
+
@property
|
|
975
|
+
def season_number(self) -> Optional[int]:
|
|
976
|
+
"""Return the title."""
|
|
977
|
+
return self._season_number
|
|
978
|
+
|
|
979
|
+
@season_number.setter
|
|
980
|
+
def season_number(self, value: Optional[int]) -> None:
|
|
981
|
+
"""Set the title."""
|
|
982
|
+
self._season_number = value
|
|
983
|
+
|
|
984
|
+
@property
|
|
985
|
+
def start_time(self) -> Optional[int]:
|
|
986
|
+
"""Return the title."""
|
|
987
|
+
return self._start_time
|
|
988
|
+
|
|
989
|
+
@start_time.setter
|
|
990
|
+
def start_time(self, value: Optional[int]) -> None:
|
|
991
|
+
"""Set the title."""
|
|
992
|
+
self._start_time = value
|
|
993
|
+
|
|
994
|
+
@property
|
|
995
|
+
def end_time(self) -> Optional[int]:
|
|
996
|
+
"""Return the title."""
|
|
997
|
+
return self._end_time
|
|
998
|
+
|
|
999
|
+
@end_time.setter
|
|
1000
|
+
def end_time(self, value: Optional[int]) -> None:
|
|
1001
|
+
"""Set the title."""
|
|
1002
|
+
self._end_time = value
|
|
1003
|
+
|
|
1004
|
+
@property
|
|
1005
|
+
def image(self) -> Optional[str]:
|
|
1006
|
+
"""Return the image URL."""
|
|
1007
|
+
return self._image
|
|
1008
|
+
|
|
1009
|
+
@image.setter
|
|
1010
|
+
def image(self, value: Optional[str]) -> None:
|
|
1011
|
+
"""Set the image URL."""
|
|
1012
|
+
self._image = value
|
|
1013
|
+
|
|
1014
|
+
@property
|
|
1015
|
+
def source_type(self) -> LGHorizonSourceType:
|
|
1016
|
+
"""Return the source type."""
|
|
1017
|
+
return self._source_type
|
|
1018
|
+
|
|
1019
|
+
@source_type.setter
|
|
1020
|
+
def source_type(self, value: LGHorizonSourceType) -> None:
|
|
1021
|
+
"""Set the source type."""
|
|
1022
|
+
self._source_type = value
|
|
1023
|
+
|
|
1024
|
+
@property
|
|
1025
|
+
def ui_state_type(self) -> LGHorizonUIStateType:
|
|
1026
|
+
"""Return the source type."""
|
|
1027
|
+
return self._ui_state_type
|
|
1028
|
+
|
|
1029
|
+
@ui_state_type.setter
|
|
1030
|
+
def ui_state_type(self, value: LGHorizonUIStateType) -> None:
|
|
1031
|
+
"""Set the source type."""
|
|
1032
|
+
self._ui_state_type = value
|
|
1033
|
+
|
|
1034
|
+
@property
|
|
1035
|
+
def paused(self) -> bool:
|
|
1036
|
+
"""Return if the media is paused."""
|
|
1037
|
+
if self.speed is None:
|
|
1038
|
+
return False
|
|
1039
|
+
return self.speed == 0
|
|
1040
|
+
|
|
1041
|
+
@property
|
|
1042
|
+
def duration(self) -> Optional[float]:
|
|
1043
|
+
"""Return the duration of the media."""
|
|
1044
|
+
return self._duration
|
|
1045
|
+
|
|
1046
|
+
@duration.setter
|
|
1047
|
+
def duration(self, value: Optional[float]) -> None:
|
|
1048
|
+
"""Set the duration of the media."""
|
|
1049
|
+
self._duration = value
|
|
1050
|
+
|
|
1051
|
+
@property
|
|
1052
|
+
def position(self) -> Optional[float]:
|
|
1053
|
+
"""Return the current position in the media."""
|
|
1054
|
+
return self._position
|
|
1055
|
+
|
|
1056
|
+
@position.setter
|
|
1057
|
+
def position(self, value: Optional[float]) -> None:
|
|
1058
|
+
"""Set the current position in the media."""
|
|
1059
|
+
self._position = value
|
|
1060
|
+
|
|
1061
|
+
@property
|
|
1062
|
+
def last_position_update(self) -> Optional[int]:
|
|
1063
|
+
"""Return the last time the position was updated."""
|
|
1064
|
+
return self._last_position_update
|
|
1065
|
+
|
|
1066
|
+
@last_position_update.setter
|
|
1067
|
+
def last_position_update(self, value: Optional[int]) -> None:
|
|
1068
|
+
"""Set the last position update time."""
|
|
1069
|
+
self._last_position_update = value
|
|
1070
|
+
|
|
1071
|
+
async def reset_progress(self) -> None:
|
|
1072
|
+
"""Reset the progress-related attributes."""
|
|
1073
|
+
self.last_position_update = None
|
|
1074
|
+
self.duration = None
|
|
1075
|
+
self.position = None
|
|
1076
|
+
|
|
1077
|
+
@property
|
|
1078
|
+
def speed(self) -> Optional[int]:
|
|
1079
|
+
"""Return the speed."""
|
|
1080
|
+
return self._speed
|
|
1081
|
+
|
|
1082
|
+
@speed.setter
|
|
1083
|
+
def speed(self, value: int | None) -> None:
|
|
1084
|
+
"""Set the channel ID."""
|
|
1085
|
+
self._speed = value
|
|
1086
|
+
|
|
1087
|
+
async def reset(self) -> None:
|
|
1088
|
+
"""Reset all playing information."""
|
|
1089
|
+
self.channel_id = None
|
|
1090
|
+
self.episode_number = None
|
|
1091
|
+
self.season_number = None
|
|
1092
|
+
self.episode_title = None
|
|
1093
|
+
self.show_title = None
|
|
1094
|
+
self.app_name = None
|
|
1095
|
+
self.image = None
|
|
1096
|
+
self.source_type = LGHorizonSourceType.UNKNOWN
|
|
1097
|
+
self.speed = None
|
|
1098
|
+
self.channel_name = None
|
|
1099
|
+
self.id = None
|
|
1100
|
+
self.start_time = None
|
|
1101
|
+
self.end_time = None
|
|
1102
|
+
await self.reset_progress()
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class LGHorizonEntitlements:
|
|
1106
|
+
"""Class to represent entitlements."""
|
|
1107
|
+
|
|
1108
|
+
def __init__(self, entitlements_json):
|
|
1109
|
+
"""Initialize entitlements."""
|
|
1110
|
+
self.entitlements_json = entitlements_json
|
|
1111
|
+
|
|
1112
|
+
@property
|
|
1113
|
+
def entitlements(self):
|
|
1114
|
+
"""Returns the entitlements."""
|
|
1115
|
+
return self.entitlements_json.get("entitlements", [])
|
|
1116
|
+
|
|
1117
|
+
@property
|
|
1118
|
+
def entitlement_ids(self) -> list[str]:
|
|
1119
|
+
"""Returns a list of entitlement IDs."""
|
|
1120
|
+
return [e["id"] for e in self.entitlements if "id" in e]
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
class LGHorizonReplayEvent:
|
|
1124
|
+
"""LGhorizon replay event."""
|
|
1125
|
+
|
|
1126
|
+
def __init__(self, raw_json: dict):
|
|
1127
|
+
"""Initialize an LG Horizon replay event."""
|
|
1128
|
+
self._raw_json = raw_json
|
|
1129
|
+
|
|
1130
|
+
@property
|
|
1131
|
+
def episode_number(self) -> Optional[int]:
|
|
1132
|
+
"""Return the episode number."""
|
|
1133
|
+
return self._raw_json.get("episodeNumber")
|
|
1134
|
+
|
|
1135
|
+
@property
|
|
1136
|
+
def channel_id(self) -> str:
|
|
1137
|
+
"""Return the channel ID."""
|
|
1138
|
+
return self._raw_json["channelId"]
|
|
1139
|
+
|
|
1140
|
+
@property
|
|
1141
|
+
def event_id(self) -> str:
|
|
1142
|
+
"""Return the event ID."""
|
|
1143
|
+
return self._raw_json["eventId"]
|
|
1144
|
+
|
|
1145
|
+
@property
|
|
1146
|
+
def season_number(self) -> Optional[int]:
|
|
1147
|
+
"""Return the season number."""
|
|
1148
|
+
return self._raw_json.get("seasonNumber")
|
|
1149
|
+
|
|
1150
|
+
@property
|
|
1151
|
+
def start_time(self) -> Optional[int]:
|
|
1152
|
+
"""Return the season number."""
|
|
1153
|
+
return self._raw_json.get("startTime", None)
|
|
1154
|
+
|
|
1155
|
+
@property
|
|
1156
|
+
def end_time(self) -> Optional[int]:
|
|
1157
|
+
"""Return the season number."""
|
|
1158
|
+
return self._raw_json.get("endTime", None)
|
|
1159
|
+
|
|
1160
|
+
@property
|
|
1161
|
+
def title(self) -> str:
|
|
1162
|
+
"""Return the title of the event."""
|
|
1163
|
+
return self._raw_json["title"]
|
|
1164
|
+
|
|
1165
|
+
@property
|
|
1166
|
+
def episode_name(self) -> Optional[str]:
|
|
1167
|
+
"""Return the episode name."""
|
|
1168
|
+
return self._raw_json.get("episodeName", None)
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def full_episode_title(self) -> Optional[str]:
|
|
1172
|
+
"""Return the full episode title."""
|
|
1173
|
+
|
|
1174
|
+
if not self.season_number and not self.episode_number:
|
|
1175
|
+
return None
|
|
1176
|
+
full_title = f"""S{self.season_number:02d}E{self.episode_number:02d}"""
|
|
1177
|
+
if self.episode_name:
|
|
1178
|
+
full_title += f": {self.episode_name}"
|
|
1179
|
+
return full_title
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
class LGHorizonVODType(Enum):
|
|
1183
|
+
"""Enumeration of LG Horizon VOD types."""
|
|
1184
|
+
|
|
1185
|
+
ASSET = "ASSET"
|
|
1186
|
+
EPISODE = "EPISODE"
|
|
1187
|
+
UNKNOWN = "UNKNOWN"
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
class LGHorizonVOD:
|
|
1191
|
+
"""LGHorizon video on demand."""
|
|
1192
|
+
|
|
1193
|
+
def __init__(self, vod_json) -> None:
|
|
1194
|
+
"""Initialize an LG Horizon VOD object.
|
|
1195
|
+
|
|
1196
|
+
Args:
|
|
1197
|
+
vod_json: The raw JSON dictionary containing VOD information.
|
|
1198
|
+
"""
|
|
1199
|
+
self._vod_json = vod_json
|
|
1200
|
+
|
|
1201
|
+
@property
|
|
1202
|
+
def vod_type(self) -> LGHorizonVODType:
|
|
1203
|
+
"""Return the ID of the VOD."""
|
|
1204
|
+
return LGHorizonVODType[self._vod_json.get("type", "unknown").upper()]
|
|
1205
|
+
|
|
1206
|
+
@property
|
|
1207
|
+
def id(self) -> str:
|
|
1208
|
+
"""Return the ID of the VOD."""
|
|
1209
|
+
return self._vod_json["id"]
|
|
1210
|
+
|
|
1211
|
+
@property
|
|
1212
|
+
def season(self) -> Optional[int]:
|
|
1213
|
+
"""Return the season number of the recording."""
|
|
1214
|
+
return self._vod_json.get("season", None)
|
|
1215
|
+
|
|
1216
|
+
@property
|
|
1217
|
+
def episode(self) -> Optional[int]:
|
|
1218
|
+
"""Return the episode number of the recording."""
|
|
1219
|
+
return self._vod_json.get("episode", None)
|
|
1220
|
+
|
|
1221
|
+
@property
|
|
1222
|
+
def title(self) -> str:
|
|
1223
|
+
"""Return the ID of the VOD."""
|
|
1224
|
+
return self._vod_json["title"]
|
|
1225
|
+
|
|
1226
|
+
@property
|
|
1227
|
+
def series_title(self) -> Optional[str]:
|
|
1228
|
+
"""Return the series title of the VOD."""
|
|
1229
|
+
return self._vod_json.get("seriesTitle", None)
|
|
1230
|
+
|
|
1231
|
+
@property
|
|
1232
|
+
def duration(self) -> float:
|
|
1233
|
+
"""Return the duration of the VOD."""
|
|
1234
|
+
return self._vod_json["duration"]
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
class LGHOrizonRelevantEpisode:
|
|
1238
|
+
"""Represents a relevant episode within a recording season or show."""
|
|
1239
|
+
|
|
1240
|
+
def __init__(self, episode_json: dict) -> None:
|
|
1241
|
+
"""Abstract base class for LG Horizon recordings."""
|
|
1242
|
+
self._episode_json = episode_json
|
|
1243
|
+
|
|
1244
|
+
@property
|
|
1245
|
+
def recording_state(self) -> LGHorizonRecordingState:
|
|
1246
|
+
"""Return the recording state."""
|
|
1247
|
+
return LGHorizonRecordingState[
|
|
1248
|
+
self._episode_json.get("recordingState", "unknown").upper()
|
|
1249
|
+
]
|
|
1250
|
+
|
|
1251
|
+
@property
|
|
1252
|
+
def season_number(self) -> Optional[int]:
|
|
1253
|
+
"""Return the season number of the recording."""
|
|
1254
|
+
return self._episode_json.get("seasonNumber", None)
|
|
1255
|
+
|
|
1256
|
+
@property
|
|
1257
|
+
def episode_number(self) -> Optional[int]:
|
|
1258
|
+
"""Return the episode number of the recording."""
|
|
1259
|
+
return self._episode_json.get("episodeNumber", None)
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
class LGHorizonRecording(ABC):
|
|
1263
|
+
"""Abstract base class for LG Horizon recordings."""
|
|
1264
|
+
|
|
1265
|
+
@property
|
|
1266
|
+
def recording_payload(self) -> dict:
|
|
1267
|
+
"""Return the payload of the message."""
|
|
1268
|
+
return self._recording_payload
|
|
1269
|
+
|
|
1270
|
+
@property
|
|
1271
|
+
def recording_state(self) -> LGHorizonRecordingState:
|
|
1272
|
+
"""Return the recording state."""
|
|
1273
|
+
return LGHorizonRecordingState[
|
|
1274
|
+
self._recording_payload.get("recordingState", "unknown").upper()
|
|
1275
|
+
]
|
|
1276
|
+
|
|
1277
|
+
@property
|
|
1278
|
+
def source(self) -> LGHorizonRecordingSource:
|
|
1279
|
+
"""Return the recording source."""
|
|
1280
|
+
return LGHorizonRecordingSource[
|
|
1281
|
+
self._recording_payload.get("source", "unknown").upper()
|
|
1282
|
+
]
|
|
1283
|
+
|
|
1284
|
+
@property
|
|
1285
|
+
def type(self) -> LGHorizonRecordingType:
|
|
1286
|
+
"""Return the recording source."""
|
|
1287
|
+
return LGHorizonRecordingType[
|
|
1288
|
+
self._recording_payload.get("type", "unknown").upper()
|
|
1289
|
+
]
|
|
1290
|
+
|
|
1291
|
+
@property
|
|
1292
|
+
def id(self) -> str:
|
|
1293
|
+
"""Return the ID of the recording."""
|
|
1294
|
+
return self._recording_payload["id"]
|
|
1295
|
+
|
|
1296
|
+
@property
|
|
1297
|
+
def title(self) -> str:
|
|
1298
|
+
"""Return the title of the recording."""
|
|
1299
|
+
return self._recording_payload.get("title", "unknown")
|
|
1300
|
+
|
|
1301
|
+
@property
|
|
1302
|
+
def channel_id(self) -> str:
|
|
1303
|
+
"""Return the channel ID of the recording."""
|
|
1304
|
+
return self._recording_payload["channelId"]
|
|
1305
|
+
|
|
1306
|
+
@property
|
|
1307
|
+
def poster_url(self) -> Optional[str]:
|
|
1308
|
+
"""Return the title of the recording."""
|
|
1309
|
+
poster = self._recording_payload.get("poster")
|
|
1310
|
+
if poster:
|
|
1311
|
+
return poster.get("url")
|
|
1312
|
+
return None
|
|
1313
|
+
|
|
1314
|
+
def __init__(self, recording_payload: dict) -> None:
|
|
1315
|
+
"""Abstract base class for LG Horizon recordings.
|
|
1316
|
+
Args:
|
|
1317
|
+
recording_payload: The raw JSON dictionary containing recording information.
|
|
1318
|
+
"""
|
|
1319
|
+
self._recording_payload = recording_payload
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
class LGHorizonRecordingSingle(LGHorizonRecording):
|
|
1323
|
+
"""LGHorizon recording."""
|
|
1324
|
+
|
|
1325
|
+
@property
|
|
1326
|
+
def episode_title(self) -> Optional[str]:
|
|
1327
|
+
"""Return the episode title of the recording."""
|
|
1328
|
+
return self._recording_payload.get("episodeTitle", None)
|
|
1329
|
+
|
|
1330
|
+
@property
|
|
1331
|
+
def episode_id(self) -> Optional[str]:
|
|
1332
|
+
"""Return the episode title of the recording."""
|
|
1333
|
+
return self._recording_payload.get("episodeId", None)
|
|
1334
|
+
|
|
1335
|
+
@property
|
|
1336
|
+
def season_number(self) -> Optional[int]:
|
|
1337
|
+
"""Return the season number of the recording."""
|
|
1338
|
+
return self._recording_payload.get("seasonNumber", None)
|
|
1339
|
+
|
|
1340
|
+
@property
|
|
1341
|
+
def episode_number(self) -> Optional[int]:
|
|
1342
|
+
"""Return the episode number of the recording."""
|
|
1343
|
+
return self._recording_payload.get("episodeNumber", None)
|
|
1344
|
+
|
|
1345
|
+
@property
|
|
1346
|
+
def show_id(self) -> Optional[str]:
|
|
1347
|
+
"""Return the show ID of the recording."""
|
|
1348
|
+
return self._recording_payload.get("showId", None)
|
|
1349
|
+
|
|
1350
|
+
@property
|
|
1351
|
+
def show_title(self) -> Optional[str]:
|
|
1352
|
+
"""Return the show ID of the recording."""
|
|
1353
|
+
return self._recording_payload.get("showTitle", None)
|
|
1354
|
+
|
|
1355
|
+
@property
|
|
1356
|
+
def season_id(self) -> Optional[str]:
|
|
1357
|
+
"""Return the season ID of the recording."""
|
|
1358
|
+
return self._recording_payload.get("seasonId", None)
|
|
1359
|
+
|
|
1360
|
+
@property
|
|
1361
|
+
def channel_id(self) -> Optional[str]:
|
|
1362
|
+
"""Return the channel ID of the recording."""
|
|
1363
|
+
return self._recording_payload.get("channelId", None)
|
|
1364
|
+
|
|
1365
|
+
@property
|
|
1366
|
+
def duration(self) -> Optional[int]:
|
|
1367
|
+
"""Return the title."""
|
|
1368
|
+
return self.recording_payload.get("duration", None)
|
|
1369
|
+
|
|
1370
|
+
@property
|
|
1371
|
+
def start_time(self) -> Optional[int]:
|
|
1372
|
+
"""Return the title."""
|
|
1373
|
+
return self.recording_payload.get("startTime", None)
|
|
1374
|
+
|
|
1375
|
+
@property
|
|
1376
|
+
def end_time(self) -> Optional[int]:
|
|
1377
|
+
"""Return the title."""
|
|
1378
|
+
return self.recording_payload.get("endTime", None)
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
class LGHorizonRecordingSeason(LGHorizonRecording):
|
|
1382
|
+
"""Represents an LG Horizon recording season."""
|
|
1383
|
+
|
|
1384
|
+
_most_relevant_epsode: Optional[LGHOrizonRelevantEpisode]
|
|
1385
|
+
|
|
1386
|
+
def __init__(self, payload: dict) -> None:
|
|
1387
|
+
"""Abstract base class for LG Horizon recordings."""
|
|
1388
|
+
super().__init__(payload)
|
|
1389
|
+
episode_payload = payload.get("mostRelevantEpisode")
|
|
1390
|
+
if episode_payload:
|
|
1391
|
+
self._most_relevant_epsode = LGHOrizonRelevantEpisode(episode_payload)
|
|
1392
|
+
|
|
1393
|
+
@property
|
|
1394
|
+
def no_of_episodes(self) -> int:
|
|
1395
|
+
"""Return the number of episodes in the season."""
|
|
1396
|
+
return self._recording_payload.get("noOfEpisodes", 0)
|
|
1397
|
+
|
|
1398
|
+
@property
|
|
1399
|
+
def season_title(self) -> str:
|
|
1400
|
+
"""Return the season title of the recording."""
|
|
1401
|
+
return self._recording_payload.get("seasonTitle", "")
|
|
1402
|
+
|
|
1403
|
+
@property
|
|
1404
|
+
def show_id(self) -> str:
|
|
1405
|
+
"""Return the season title of the recording."""
|
|
1406
|
+
return self._recording_payload.get("showId", "")
|
|
1407
|
+
|
|
1408
|
+
@property
|
|
1409
|
+
def most_relevant_episode(self) -> Optional[LGHOrizonRelevantEpisode]:
|
|
1410
|
+
"""Return the most relevant episode of the season."""
|
|
1411
|
+
return self._most_relevant_epsode
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
class LGHorizonRecordingShow(LGHorizonRecording):
|
|
1415
|
+
"""Represents an LG Horizon recording show."""
|
|
1416
|
+
|
|
1417
|
+
_most_relevant_epsode: Optional[LGHOrizonRelevantEpisode]
|
|
1418
|
+
|
|
1419
|
+
def __init__(self, payload: dict) -> None:
|
|
1420
|
+
"""Abstract base class for LG Horizon recordings."""
|
|
1421
|
+
super().__init__(payload)
|
|
1422
|
+
episode_payload = payload.get("mostRelevantEpisode")
|
|
1423
|
+
if episode_payload:
|
|
1424
|
+
self._most_relevant_epsode = LGHOrizonRelevantEpisode(episode_payload)
|
|
1425
|
+
|
|
1426
|
+
@property
|
|
1427
|
+
def no_of_episodes(self) -> int:
|
|
1428
|
+
"""Return the number of episodes in the season."""
|
|
1429
|
+
return self._recording_payload.get("noOfEpisodes", 0)
|
|
1430
|
+
|
|
1431
|
+
@property
|
|
1432
|
+
def most_relevant_episode(self) -> Optional[LGHOrizonRelevantEpisode]:
|
|
1433
|
+
"""Return the most relevant episode of the season."""
|
|
1434
|
+
return self._most_relevant_epsode
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
class LGHorizonRecordingList:
|
|
1438
|
+
"""Represents a list of LG Horizon recordings."""
|
|
1439
|
+
|
|
1440
|
+
@property
|
|
1441
|
+
def total(self) -> int:
|
|
1442
|
+
"""Return the total number of recordings."""
|
|
1443
|
+
return len(self._recordings)
|
|
1444
|
+
|
|
1445
|
+
def __init__(self, recordings: List[LGHorizonRecording]) -> None:
|
|
1446
|
+
"""Initialize an LG Horizon recording list.
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
recordings: A list of LGHorizonRecording objects.
|
|
1450
|
+
"""
|
|
1451
|
+
self._recordings = recordings
|
|
1452
|
+
|
|
1453
|
+
@property
|
|
1454
|
+
def recordings(self) -> List[LGHorizonRecording]:
|
|
1455
|
+
"""Return the total number of recordings."""
|
|
1456
|
+
return self._recordings
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
class LGHorizonShowRecordingList(LGHorizonRecordingList):
|
|
1460
|
+
"""LGHorizon recording."""
|
|
1461
|
+
|
|
1462
|
+
def __init__(
|
|
1463
|
+
self,
|
|
1464
|
+
show_title: Optional[str],
|
|
1465
|
+
show_image,
|
|
1466
|
+
recordings: List[LGHorizonRecording],
|
|
1467
|
+
) -> None:
|
|
1468
|
+
"""Initialize an LG Horizon show recording list.
|
|
1469
|
+
|
|
1470
|
+
Args:
|
|
1471
|
+
show_title: The title of the show.
|
|
1472
|
+
show_image: The image URL for the show.
|
|
1473
|
+
recordings: A list of LGHorizonRecording objects belonging to the show.
|
|
1474
|
+
"""
|
|
1475
|
+
super().__init__(recordings)
|
|
1476
|
+
self._show_title = show_title
|
|
1477
|
+
self._show_image = show_image
|
|
1478
|
+
|
|
1479
|
+
@property
|
|
1480
|
+
def show_title(self) -> str:
|
|
1481
|
+
"""Title of the show."""
|
|
1482
|
+
return self._show_title
|
|
1483
|
+
|
|
1484
|
+
@property
|
|
1485
|
+
def show_image(self) -> Optional[str]:
|
|
1486
|
+
"""Image of the show."""
|
|
1487
|
+
return self._show_image
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
class LGHorizonRecordingQuota:
|
|
1491
|
+
"""LGHorizon recording quota."""
|
|
1492
|
+
|
|
1493
|
+
def __init__(self, quota_json: dict) -> None:
|
|
1494
|
+
"""Initialize the recording quota."""
|
|
1495
|
+
self._quota_json = quota_json
|
|
1496
|
+
|
|
1497
|
+
@property
|
|
1498
|
+
def quota(self) -> int:
|
|
1499
|
+
"""Return the total space in MB."""
|
|
1500
|
+
return self._quota_json.get("quota", 0)
|
|
1501
|
+
|
|
1502
|
+
@property
|
|
1503
|
+
def occupied(self) -> int:
|
|
1504
|
+
"""Return the used space in MB."""
|
|
1505
|
+
return self._quota_json.get("occupied", 0)
|
|
1506
|
+
|
|
1507
|
+
@property
|
|
1508
|
+
def percentage_used(self) -> float:
|
|
1509
|
+
"""Return the percentage of space used."""
|
|
1510
|
+
if self.quota == 0:
|
|
1511
|
+
return 0.0
|
|
1512
|
+
return (self.occupied / self.quota) * 100
|