lghorizon 0.9.0.dev3__py3-none-any.whl → 0.9.0.dev4__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.
@@ -1,270 +1,539 @@
1
- """LG Horizon API client."""
1
+ """Python client for LGHorizon."""
2
+ # pylint: disable=broad-exception-caught
3
+ # pylint: disable=line-too-long
2
4
 
3
5
  import logging
4
- from typing import Any, Dict, cast
5
-
6
- from .lghorizon_device import LGHorizonDevice
7
- from .lghorizon_models import LGHorizonChannel
8
- from .lghorizon_models import LGHorizonAuth
9
- from .lghorizon_models import LGHorizonCustomer
10
- from .lghorizon_mqtt_client import LGHorizonMqttClient
11
- from .lghorizon_models import LGHorizonServicesConfig
12
- from .lghorizon_models import LGHorizonEntitlements
13
- from .lghorizon_models import LGHorizonProfile
14
- from .lghorizon_models import LGHorizonMessageType
15
- from .lghorizon_message_factory import LGHorizonMessageFactory
16
- from .lghorizon_models import LGHorizonStatusMessage, LGHorizonUIStatusMessage
17
- from .lghorizon_models import LGHorizonRunningState
18
- from .lghorizon_models import LGHorizonRecordingList, LGHorizonRecordingQuota
19
- from .lghorizon_recording_factory import LGHorizonRecordingFactory
20
- from .lghorizon_device_state_processor import LGHorizonDeviceStateProcessor
21
-
22
-
23
- _LOGGER = logging.getLogger(__name__)
6
+ import json
7
+ import re
8
+
9
+ from typing import Any, Callable, Dict, List
10
+ import backoff
11
+
12
+ from requests import Session, exceptions as request_exceptions
13
+
14
+ from .exceptions import (
15
+ LGHorizonApiUnauthorizedError,
16
+ LGHorizonApiConnectionError,
17
+ LGHorizonApiLockedError,
18
+ )
19
+
20
+ from .models import (
21
+ LGHorizonAuth,
22
+ LGHorizonBox,
23
+ LGHorizonMqttClient,
24
+ LGHorizonCustomer,
25
+ LGHorizonChannel,
26
+ LGHorizonReplayEvent,
27
+ LGHorizonRecordingSingle,
28
+ LGHorizonVod,
29
+ LGHorizonApp,
30
+ LGHorizonBaseRecording,
31
+ LGHorizonRecordingListSeasonShow,
32
+ LGHorizonRecordingEpisode,
33
+ LGHorizonRecordingShow,
34
+ )
35
+
36
+ from .const import (
37
+ COUNTRY_SETTINGS,
38
+ BOX_PLAY_STATE_BUFFER,
39
+ BOX_PLAY_STATE_CHANNEL,
40
+ BOX_PLAY_STATE_DVR,
41
+ BOX_PLAY_STATE_REPLAY,
42
+ BOX_PLAY_STATE_VOD,
43
+ RECORDING_TYPE_SINGLE,
44
+ RECORDING_TYPE_SEASON,
45
+ RECORDING_TYPE_SHOW,
46
+ )
47
+
48
+
49
+ _logger = logging.getLogger(__name__)
50
+ _supported_platforms = ["EOS", "EOS2", "HORIZON", "APOLLO"]
24
51
 
25
52
 
26
53
  class LGHorizonApi:
27
- """LG Horizon API client."""
28
-
29
- _mqtt_client: LGHorizonMqttClient
30
- auth: LGHorizonAuth
31
- _service_config: LGHorizonServicesConfig
32
- _customer: LGHorizonCustomer
33
- _channels: Dict[str, LGHorizonChannel]
34
- _entitlements: LGHorizonEntitlements
35
- _profile_id: str
36
- _initialized: bool = False
37
- _devices: Dict[str, LGHorizonDevice] = {}
38
- _message_factory: LGHorizonMessageFactory = LGHorizonMessageFactory()
39
- _device_state_processor: LGHorizonDeviceStateProcessor | None
40
- _recording_factory: LGHorizonRecordingFactory = LGHorizonRecordingFactory()
41
-
42
- def __init__(self, auth: LGHorizonAuth, profile_id: str = "") -> None:
43
- """Initialize LG Horizon API client."""
44
- self.auth = auth
45
- self._profile_id = profile_id
54
+ """Main class for handling connections with LGHorizon Settop boxes."""
55
+
56
+ _auth: LGHorizonAuth = None
57
+ _session: Session = None
58
+ settop_boxes: Dict[str, LGHorizonBox] = None
59
+ customer: LGHorizonCustomer = None
60
+ _mqtt_client: LGHorizonMqttClient = None
61
+ _channels: Dict[str, LGHorizonChannel] = None
62
+ _country_settings = None
63
+ _country_code: str = None
64
+ recording_capacity: int = None
65
+ _entitlements: List[str] = None
66
+ _identifier: str = None
67
+ _config: str = None
68
+ _refresh_callback: Callable = None
69
+ _profile_id: str = None
70
+
71
+ def __init__(
72
+ self,
73
+ username: str,
74
+ password: str,
75
+ country_code: str = "nl",
76
+ identifier: str = None,
77
+ refresh_token=None,
78
+ profile_id=None,
79
+ ) -> None:
80
+ """Create LGHorizon API."""
81
+ self.username = username
82
+ self.password = password
83
+ self.refresh_token = refresh_token
84
+ self._session = Session()
85
+ self._country_settings = COUNTRY_SETTINGS[country_code]
86
+ self._country_code = country_code
87
+ self._auth = LGHorizonAuth()
88
+ self.settop_boxes = {}
46
89
  self._channels = {}
47
- self._device_state_processor = None
48
-
49
- async def initialize(self) -> None:
50
- """Initialize the API client."""
51
- self._service_config = await self.auth.get_service_config()
52
- self._customer = await self._get_customer_info()
53
- if self._profile_id == "":
54
- self._profile_id = list(self._customer.profiles.keys())[0]
55
- await self._refresh_entitlements()
56
- await self._refresh_channels()
57
- self._mqtt_client = await self._create_mqtt_client()
58
- await self._mqtt_client.connect()
59
- await self._register_devices()
60
- self._device_state_processor = LGHorizonDeviceStateProcessor(
61
- self.auth, self._channels, self._customer, self._profile_id
90
+ self._entitlements = []
91
+ self._identifier = identifier
92
+ self._profile_id = profile_id
93
+
94
+ def _authorize(self) -> None:
95
+ ctry_code = self._country_code[0:2]
96
+ if ctry_code in ("gb", "ch", "be"):
97
+ self._authorize_with_refresh_token()
98
+ else:
99
+ self._authorize_default()
100
+
101
+ def _authorize_default(self) -> None:
102
+ _logger.debug("Authorizing")
103
+ auth_url = f"{self._country_settings['api_url']}/auth-service/v1/authorization"
104
+ auth_headers = {"x-device-code": "web"}
105
+ auth_payload = {"password": self.password, "username": self.username}
106
+ try:
107
+ auth_response = self._session.post(
108
+ auth_url, headers=auth_headers, json=auth_payload
109
+ )
110
+ except Exception as ex:
111
+ raise LGHorizonApiConnectionError("Unknown connection failure") from ex
112
+
113
+ if not auth_response.ok:
114
+ error_json = auth_response.json()
115
+ error = error_json["error"]
116
+ if error and error["statusCode"] == 97401:
117
+ raise LGHorizonApiUnauthorizedError("Invalid credentials")
118
+ elif error and error["statusCode"] == 97117:
119
+ raise LGHorizonApiLockedError("Account locked")
120
+ elif error:
121
+ raise LGHorizonApiConnectionError(error["message"])
122
+ else:
123
+ raise LGHorizonApiConnectionError("Unknown connection error")
124
+
125
+ self._auth.fill(auth_response.json())
126
+ _logger.debug("Authorization succeeded")
127
+
128
+ def _authorize_with_refresh_token(self) -> None:
129
+ """Handle authorizzationg using request token."""
130
+ _logger.debug("Authorizing via refresh")
131
+ refresh_url = (
132
+ f"{self._country_settings['api_url']}/auth-service/v1/authorization/refresh"
62
133
  )
63
- self._initialized = True
134
+ headers = {"content-type": "application/json", "charset": "utf-8"}
135
+ payload = '{"refreshToken":"' + self.refresh_token + '"}'
136
+
137
+ try:
138
+ auth_response = self._session.post(
139
+ refresh_url, headers=headers, data=payload
140
+ )
141
+ except Exception as ex:
142
+ raise LGHorizonApiConnectionError("Unknown connection failure") from ex
143
+
144
+ if not auth_response.ok:
145
+ _logger.debug("response %s", auth_response)
146
+ error_json = auth_response.json()
147
+ error = None
148
+ if "error" in error_json:
149
+ error = error_json["error"]
150
+ if error and error["statusCode"] == 97401:
151
+ raise LGHorizonApiUnauthorizedError("Invalid credentials")
152
+ elif error:
153
+ raise LGHorizonApiConnectionError(error["message"])
154
+ else:
155
+ raise LGHorizonApiConnectionError("Unknown connection error")
156
+
157
+ self._auth.fill(auth_response.json())
158
+ self.refresh_token = self._auth.refresh_token
159
+ self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
160
+
161
+ if self._refresh_callback:
162
+ self._refresh_callback()
163
+
164
+ _logger.debug("Authorization succeeded")
165
+
166
+ def set_callback(self, refresh_callback: Callable) -> None:
167
+ """Set the refresh callback."""
168
+ self._refresh_callback = refresh_callback
169
+
170
+ def _authorize_telenet(self):
171
+ """Authorize telenet users."""
172
+ try:
173
+ login_session = Session()
174
+ # Step 1 - Get Authorization data
175
+ _logger.debug("Step 1 - Get Authorization data")
176
+ auth_url = (
177
+ f"{self._country_settings['api_url']}/auth-service/v1/sso/authorization"
178
+ )
179
+ auth_response = login_session.get(auth_url)
180
+ if not auth_response.ok:
181
+ raise LGHorizonApiConnectionError("Can't connect to authorization URL")
182
+ auth_response_json = auth_response.json()
183
+ authorization_uri = auth_response_json["authorizationUri"]
184
+ authorization_validity_token = auth_response_json["validityToken"]
64
185
 
65
- async def get_devices(self) -> Dict[str, LGHorizonDevice]:
66
- """Get devices."""
67
- if not self._initialized:
68
- raise RuntimeError("LGHorizonApi not initialized")
186
+ # Step 2 - Get Authorization cookie
187
+ _logger.debug("Step 2 - Get Authorization cookie")
69
188
 
70
- return self._devices
189
+ auth_cookie_response = login_session.get(authorization_uri)
190
+ if not auth_cookie_response.ok:
191
+ raise LGHorizonApiConnectionError("Can't connect to authorization URL")
71
192
 
72
- async def get_profiles(self) -> Dict[str, LGHorizonProfile]:
73
- """Get profile IDs."""
74
- if not self._initialized:
75
- raise RuntimeError("LGHorizonApi not initialized")
193
+ _logger.debug("Step 3 - Login")
76
194
 
77
- return self._customer.profiles
195
+ username_fieldname = self._country_settings["oauth_username_fieldname"]
196
+ pasword_fieldname = self._country_settings["oauth_password_fieldname"]
78
197
 
79
- async def get_profile_channels(
80
- self, profile_id: str
81
- ) -> Dict[str, LGHorizonChannel]:
82
- """Returns channels to display baed on profile."""
83
- # Attempt to retrieve the profile by the given profile_id
84
- profile = self._customer.profiles.get(profile_id)
85
-
86
- # If the specified profile is not found, and there are other profiles available,
87
- # default to the first profile in the customer's list.
88
- if not profile and self._customer.profiles:
89
- _LOGGER.debug(
90
- "Profile with ID '%s' not found. Defaulting to first available profile.",
91
- profile_id,
92
- )
93
- profile = list(self._customer.profiles.values())[0]
94
-
95
- # If a profile is found and it has favorite channels, filter the main channels list.
96
- if profile and profile.favorite_channels:
97
- _LOGGER.debug("Returning favorite channels for profile '%s'.", profile.name)
98
- # Use a set for faster lookup of favorite channel IDs
99
- profile_channel_ids = set(profile.favorite_channels)
100
- return {
101
- channel.id: channel
102
- for channel in self._channels.values()
103
- if channel.id in profile_channel_ids
198
+ payload = {
199
+ username_fieldname: self.username,
200
+ pasword_fieldname: self.password,
201
+ "rememberme": "true",
104
202
  }
105
203
 
106
- # If no profile is found (even after defaulting) or the profile has no favorite channels,
107
- # return all available channels.
108
- _LOGGER.debug("No specific profile channels found, returning all channels.")
109
- return self._channels
110
-
111
- async def _register_devices(self) -> None:
112
- """Register devices."""
113
- _LOGGER.debug("Registering devices...")
114
- self._devices = {}
115
- channels = await self.get_profile_channels(self._profile_id)
116
- for raw_box in self._customer.assigned_devices:
117
- _LOGGER.debug("Creating box for device: %s", raw_box)
118
- if self._device_state_processor is None:
119
- self._device_state_processor = LGHorizonDeviceStateProcessor(
120
- self.auth, self._channels, self._customer, self._profile_id
121
- )
122
- device = LGHorizonDevice(
123
- raw_box,
124
- self._mqtt_client,
125
- self._device_state_processor,
126
- self.auth,
127
- channels,
204
+ login_response = login_session.post(
205
+ self._country_settings["oauth_url"], payload, allow_redirects=False
128
206
  )
129
- self._devices[device.device_id] = device
130
-
131
- async def disconnect(self) -> None:
132
- """Disconnect the client."""
133
- if self._mqtt_client:
134
- await self._mqtt_client.disconnect()
135
- self._initialized = False
136
-
137
- async def _create_mqtt_client(self) -> LGHorizonMqttClient:
138
- mqtt_client = await LGHorizonMqttClient.create(
139
- self.auth,
207
+ if not login_response.ok:
208
+ raise LGHorizonApiConnectionError("Can't connect to authorization URL")
209
+ redirect_url = login_response.headers[
210
+ self._country_settings["oauth_redirect_header"]
211
+ ]
212
+
213
+ if self._identifier is not None:
214
+ redirect_url += f"&dtv_identifier={self._identifier}"
215
+ redirect_response = login_session.get(redirect_url, allow_redirects=False)
216
+ success_url = redirect_response.headers[
217
+ self._country_settings["oauth_redirect_header"]
218
+ ]
219
+ code_matches = re.findall(r"code=(.*)&", success_url)
220
+
221
+ authorization_code = code_matches[0]
222
+
223
+ new_payload = {
224
+ "authorizationGrant": {
225
+ "authorizationCode": authorization_code,
226
+ "validityToken": authorization_validity_token,
227
+ }
228
+ }
229
+ headers = {
230
+ "content-type": "application/json",
231
+ }
232
+ post_result = login_session.post(
233
+ auth_url, json.dumps(new_payload), headers=headers
234
+ )
235
+ self._auth.fill(post_result.json())
236
+ self._session.cookies["ACCESSTOKEN"] = self._auth.access_token
237
+ except Exception:
238
+ pass
239
+
240
+ def _obtain_mqtt_token(self):
241
+ _logger.debug("Obtain mqtt token...")
242
+ mqtt_auth_url = self._config["authorizationService"]["URL"]
243
+ mqtt_response = self._do_api_call(f"{mqtt_auth_url}/v1/mqtt/token")
244
+ self._auth.mqttToken = mqtt_response["token"]
245
+ _logger.debug("MQTT token: %s", self._auth.mqttToken)
246
+
247
+ @backoff.on_exception(
248
+ backoff.expo,
249
+ BaseException,
250
+ jitter=None,
251
+ max_tries=3,
252
+ logger=_logger,
253
+ giveup=lambda e: isinstance(
254
+ e, (LGHorizonApiLockedError, LGHorizonApiUnauthorizedError)
255
+ ),
256
+ )
257
+ def connect(self) -> None:
258
+ """Start connection process."""
259
+ self._config = self._get_config(self._country_code)
260
+ _logger.debug("Connect to API")
261
+ self._authorize()
262
+ self._obtain_mqtt_token()
263
+ self._mqtt_client = LGHorizonMqttClient(
264
+ self._auth,
265
+ self._config["mqttBroker"]["URL"],
140
266
  self._on_mqtt_connected,
141
267
  self._on_mqtt_message,
142
268
  )
143
- return mqtt_client
144
-
145
- async def _on_mqtt_connected(self):
146
- """MQTT connected callback."""
147
- await self._mqtt_client.subscribe(self.auth.household_id)
148
- # await self._mqtt_client.subscribe(self.auth.household_id + "/#")
149
- await self._mqtt_client.subscribe(
150
- self.auth.household_id + "/" + self._mqtt_client.client_id
151
- )
152
- await self._mqtt_client.subscribe(self.auth.household_id + "/+/status")
153
- await self._mqtt_client.subscribe(
154
- self.auth.household_id + "/+/networkRecordings"
155
- )
156
- await self._mqtt_client.subscribe(
157
- self.auth.household_id + "/+/networkRecordings/capacity"
158
- )
159
- await self._mqtt_client.subscribe(self.auth.household_id + "/+/localRecordings")
160
- await self._mqtt_client.subscribe(
161
- self.auth.household_id + "/+/localRecordings/capacity"
162
- )
163
- await self._mqtt_client.subscribe(self.auth.household_id + "/watchlistService")
164
- await self._mqtt_client.subscribe(self.auth.household_id + "/purchaseService")
165
- await self._mqtt_client.subscribe(
166
- self.auth.household_id + "/personalizationService"
167
- )
168
- await self._mqtt_client.subscribe(self.auth.household_id + "/recordingStatus")
169
- await self._mqtt_client.subscribe(
170
- self.auth.household_id + "/recordingStatus/lastUserAction"
171
- )
172
269
 
173
- async def _on_mqtt_message(self, mqtt_message: dict, mqtt_topic: str):
174
- """MQTT message callback."""
175
- message = await self._message_factory.create_message(mqtt_topic, mqtt_message)
176
- match message.message_type:
177
- case LGHorizonMessageType.STATUS:
178
- message.__class__ = LGHorizonStatusMessage
179
- status_message = cast(LGHorizonStatusMessage, message)
180
- device = self._devices[status_message.source]
181
- await device.handle_status_message(status_message)
182
- case LGHorizonMessageType.UI_STATUS:
183
- message.__class__ = LGHorizonUIStatusMessage
184
- ui_status_message = cast(LGHorizonUIStatusMessage, message)
185
- device = self._devices[ui_status_message.source]
186
- if (
187
- not device.device_state.state
188
- == LGHorizonRunningState.ONLINE_RUNNING
189
- ):
190
- return
191
- await device.handle_ui_status_message(ui_status_message)
192
-
193
- async def _get_customer_info(self) -> Any:
194
- service_url = await self._service_config.get_service_url(
195
- "personalizationService"
196
- )
197
- result = await self.auth.request(
198
- service_url,
199
- f"/v1/customer/{self.auth.household_id}?with=profiles%2Cdevices",
200
- )
201
- return LGHorizonCustomer(result)
202
-
203
- async def _refresh_entitlements(self) -> Any:
204
- """Retrieve entitlements."""
205
- _LOGGER.debug("Retrieving entitlements...")
206
- service_url = await self._service_config.get_service_url("purchaseService")
207
- result = await self.auth.request(
208
- service_url,
209
- f"/v2/customers/{self.auth.household_id}/entitlements?enableDaypass=true",
270
+ self._register_customer_and_boxes()
271
+ self._mqtt_client.connect()
272
+
273
+ def disconnect(self):
274
+ """Disconnect."""
275
+ _logger.debug("Disconnect from API")
276
+ if not self._mqtt_client or not self._mqtt_client.is_connected:
277
+ return
278
+ self._mqtt_client.disconnect()
279
+
280
+ def _on_mqtt_connected(self) -> None:
281
+ _logger.debug("Connected to MQTT server. Registering all boxes...")
282
+ box: LGHorizonBox
283
+ for box in self.settop_boxes.values():
284
+ box.register_mqtt()
285
+
286
+ def _on_mqtt_message(self, message: str, topic: str) -> None:
287
+ if "action" in message and message["action"] == "OPS.getProfilesUpdate":
288
+ self._update_customer()
289
+ elif "source" in message:
290
+ device_id = message["source"]
291
+ if not isinstance(device_id, str):
292
+ _logger.debug("ignoring message - not a string")
293
+ return
294
+ if device_id not in self.settop_boxes:
295
+ return
296
+ try:
297
+ if "deviceType" in message and message["deviceType"] == "STB":
298
+ self.settop_boxes[device_id].update_state(message)
299
+ if "status" in message:
300
+ self._handle_box_update(device_id, message)
301
+
302
+ except Exception:
303
+ _logger.exception("Could not handle status message")
304
+ _logger.warning("Full message: %s", str(message))
305
+ self.settop_boxes[device_id].playing_info.reset()
306
+ self.settop_boxes[device_id].playing_info.set_paused(False)
307
+ elif "CPE.capacity" in message:
308
+ splitted_topic = topic.split("/")
309
+ if len(splitted_topic) != 4:
310
+ return
311
+ device_id = splitted_topic[1]
312
+ if device_id not in self.settop_boxes:
313
+ return
314
+ self.settop_boxes[device_id].update_recording_capacity(message)
315
+
316
+ def _handle_box_update(self, device_id: str, raw_message: Any) -> None:
317
+ status_payload = raw_message["status"]
318
+ if "uiStatus" not in status_payload:
319
+ return
320
+ ui_status = status_payload["uiStatus"]
321
+ if ui_status == "mainUI":
322
+ player_state = status_payload["playerState"]
323
+ if "sourceType" not in player_state or "source" not in player_state:
324
+ return
325
+ source_type = player_state["sourceType"]
326
+ state_source = player_state["source"]
327
+ self.settop_boxes[device_id].playing_info.set_paused(
328
+ player_state["speed"] == 0
329
+ )
330
+ if (
331
+ source_type
332
+ in (
333
+ BOX_PLAY_STATE_CHANNEL,
334
+ BOX_PLAY_STATE_BUFFER,
335
+ BOX_PLAY_STATE_REPLAY,
336
+ )
337
+ and "eventId" in state_source
338
+ ):
339
+ event_id = state_source["eventId"]
340
+ raw_replay_event = self._do_api_call(
341
+ f"{self._config['linearService']['URL']}/v2/replayEvent/{event_id}?returnLinearContent=true&language={self._country_settings['language']}"
342
+ )
343
+ replay_event = LGHorizonReplayEvent(raw_replay_event)
344
+ channel = self._channels[replay_event.channel_id]
345
+ self.settop_boxes[device_id].update_with_replay_event(
346
+ source_type, replay_event, channel
347
+ )
348
+ elif source_type == BOX_PLAY_STATE_DVR:
349
+ recording_id = state_source["recordingId"]
350
+ session_start_time = state_source["sessionStartTime"]
351
+ session_end_time = state_source["sessionEndTime"]
352
+ last_speed_change_time = player_state["lastSpeedChangeTime"]
353
+ relative_position = player_state["relativePosition"]
354
+ raw_recording = self._do_api_call(
355
+ f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/details/single/{recording_id}?profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&language={self._country_settings['language']}"
356
+ )
357
+ recording = LGHorizonRecordingSingle(raw_recording)
358
+ channel = self._channels[recording.channel_id]
359
+ self.settop_boxes[device_id].update_with_recording(
360
+ source_type,
361
+ recording,
362
+ channel,
363
+ session_start_time,
364
+ session_end_time,
365
+ last_speed_change_time,
366
+ relative_position,
367
+ )
368
+ elif source_type == BOX_PLAY_STATE_VOD:
369
+ title_id = state_source["titleId"]
370
+ last_speed_change_time = player_state["lastSpeedChangeTime"]
371
+ relative_position = player_state["relativePosition"]
372
+ raw_vod = self._do_api_call(
373
+ f"{self._config['vodService']['URL']}/v2/detailscreen/{title_id}?language={self._country_settings['language']}&profileId=4504e28d-c1cb-4284-810b-f5eaab06f034&cityId={self.customer.city_id}"
374
+ )
375
+ vod = LGHorizonVod(raw_vod)
376
+ self.settop_boxes[device_id].update_with_vod(
377
+ source_type, vod, last_speed_change_time, relative_position
378
+ )
379
+ elif ui_status == "apps":
380
+ app = LGHorizonApp(status_payload["appsState"])
381
+ self.settop_boxes[device_id].update_with_app("app", app)
382
+
383
+ @backoff.on_exception(
384
+ backoff.expo, LGHorizonApiConnectionError, max_tries=3, logger=_logger
385
+ )
386
+ def _do_api_call(self, url: str) -> str:
387
+ _logger.info("Executing API call to %s", url)
388
+ try:
389
+ api_response = self._session.get(url)
390
+ api_response.raise_for_status()
391
+ json_response = api_response.json()
392
+ except request_exceptions.HTTPError as http_ex:
393
+ self._authorize()
394
+ raise LGHorizonApiConnectionError(
395
+ f"Unable to call {url}. Error:{str(http_ex)}"
396
+ ) from http_ex
397
+ _logger.debug("Result API call: %s", json_response)
398
+ return json_response
399
+
400
+ def _register_customer_and_boxes(self):
401
+ self._update_customer()
402
+ self._get_channels()
403
+ if len(self.customer.settop_boxes) == 0:
404
+ _logger.warning("No boxes found.")
405
+ return
406
+ _logger.info("Registering boxes")
407
+ for device in self.customer.settop_boxes:
408
+ platform_type = device["platformType"]
409
+ if platform_type not in _supported_platforms:
410
+ continue
411
+ if (
412
+ "platform_types" in self._country_settings
413
+ and platform_type in self._country_settings["platform_types"]
414
+ ):
415
+ platform_type = self._country_settings["platform_types"][platform_type]
416
+ else:
417
+ platform_type = None
418
+ box = LGHorizonBox(
419
+ device, platform_type, self._mqtt_client, self._auth, self._channels
420
+ )
421
+ self.settop_boxes[box.device_id] = box
422
+ _logger.info("Box %s registered...", box.device_id)
423
+
424
+ def _update_customer(self):
425
+ _logger.info("Get customer data")
426
+ personalisation_result = self._do_api_call(
427
+ f"{self._config['personalizationService']['URL']}/v1/customer/{self._auth.household_id}?with=profiles%2Cdevices"
210
428
  )
211
- self._entitlements = LGHorizonEntitlements(result)
212
-
213
- async def _refresh_channels(self):
214
- """Retrieve channels."""
215
- _LOGGER.debug("Retrieving channels...")
216
- service_url = await self._service_config.get_service_url("linearService")
217
- lang = await self._customer.get_profile_lang(self._profile_id)
218
- channels_json = await self.auth.request(
219
- service_url,
220
- f"/v2/channels?cityId={self._customer.city_id}&language={lang}&productClass=Orion-DASH",
429
+ _logger.debug("Personalisation result: %s ", personalisation_result)
430
+ self.customer = LGHorizonCustomer(personalisation_result)
431
+
432
+ def _get_channels(self):
433
+ self._update_entitlements()
434
+ _logger.info("Retrieving channels...")
435
+ channels_result = self._do_api_call(
436
+ f"{self._config['linearService']['URL']}/v2/channels?cityId={self.customer.city_id}&language={self._country_settings['language']}&productClass=Orion-DASH"
221
437
  )
222
- for channel_json in channels_json:
223
- channel = LGHorizonChannel(channel_json)
438
+ for channel in channels_result:
439
+ if "isRadio" in channel and channel["isRadio"]:
440
+ continue
224
441
  common_entitlements = list(
225
- set(self._entitlements.entitlement_ids) & set(channel.linear_products)
442
+ set(self._entitlements) & set(channel["linearProducts"])
226
443
  )
227
-
228
444
  if len(common_entitlements) == 0:
229
445
  continue
446
+ channel_id = channel["id"]
447
+ self._channels[channel_id] = LGHorizonChannel(channel)
448
+ _logger.info("%s retrieved.", len(self._channels))
230
449
 
231
- self._channels[channel.id] = channel
232
-
233
- async def get_all_recordings(self) -> LGHorizonRecordingList:
234
- """Retrieve all recordings."""
235
- _LOGGER.debug("Retrieving recordings...")
236
- service_url = await self._service_config.get_service_url("recordingService")
237
- lang = await self._customer.get_profile_lang(self._profile_id)
238
- recordings_json = await self.auth.request(
239
- service_url,
240
- f"/customers/{self.auth.household_id}/recordings?isAdult=false&offset=0&limit=100&sort=time&sortOrder=desc&profileId={self._profile_id}&language={lang}",
450
+ def get_display_channels(self):
451
+ """Returns channels to display baed on profile."""
452
+ all_channels = self._channels.values()
453
+ if not self._profile_id or self._profile_id not in self.customer.profiles:
454
+ return all_channels
455
+ profile_channel_ids = self.customer.profiles[self._profile_id].favorite_channels
456
+ if len(profile_channel_ids) == 0:
457
+ return all_channels
458
+
459
+ return [
460
+ channel for channel in all_channels if channel.id in profile_channel_ids
461
+ ]
462
+
463
+ def _get_replay_event(self, listing_id) -> Any:
464
+ """Get listing."""
465
+ _logger.info("Retrieving replay event details...")
466
+ response = self._do_api_call(
467
+ f"{self._config['linearService']['URL']}/v2/replayEvent/{listing_id}?returnLinearContent=true&language={self._country_settings['language']}"
468
+ )
469
+ _logger.info("Replay event details retrieved")
470
+ return response
471
+
472
+ def get_recording_capacity(self) -> int:
473
+ """Returns remaining recording capacity"""
474
+ ctry_code = self._country_code[0:2]
475
+ if ctry_code == "gb":
476
+ _logger.debug("GB: not supported")
477
+ return None
478
+ try:
479
+ _logger.info("Retrieving recordingcapacity...")
480
+ quota_content = self._do_api_call(
481
+ f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/quota"
482
+ )
483
+ if "quota" not in quota_content and "occupied" not in quota_content:
484
+ _logger.error("Unable to fetch recording capacity...")
485
+ return None
486
+ capacity = (quota_content["occupied"] / quota_content["quota"]) * 100
487
+ self.recording_capacity = round(capacity)
488
+ _logger.debug("Remaining recordingcapacity %s %%", self.recording_capacity)
489
+ return self.recording_capacity
490
+ except Exception:
491
+ _logger.error("Unable to fetch recording capacity...")
492
+ return None
493
+
494
+ def get_recordings(self) -> List[LGHorizonBaseRecording]:
495
+ """Returns recordings."""
496
+ _logger.info("Retrieving recordings...")
497
+ recording_content = self._do_api_call(
498
+ f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/recordings?sort=time&sortOrder=desc&language={self._country_settings['language']}"
241
499
  )
242
- recordings = await self._recording_factory.create_recordings(recordings_json)
500
+ recordings = []
501
+ for recording_data_item in recording_content["data"]:
502
+ recording_type = recording_data_item["type"]
503
+ if recording_type == RECORDING_TYPE_SINGLE:
504
+ recordings.append(LGHorizonRecordingSingle(recording_data_item))
505
+ elif recording_type in (RECORDING_TYPE_SEASON, RECORDING_TYPE_SHOW):
506
+ recordings.append(LGHorizonRecordingListSeasonShow(recording_data_item))
507
+ _logger.info("%s recordings retrieved...", len(recordings))
243
508
  return recordings
244
509
 
245
- async def get_show_recordings(
246
- self, show_id: str, channel_id: str
247
- ) -> LGHorizonRecordingList:
248
- """Retrieve all recordings."""
249
- _LOGGER.debug("Retrieving recordings fro show...")
250
- service_url = await self._service_config.get_service_url("recordingService")
251
- lang = await self._customer.get_profile_lang(self._profile_id)
252
- episodes_json = await self.auth.request(
253
- service_url,
254
- f"/customers/8436830_nl/episodes/shows/{show_id}?source=recording&isAdult=false&offset=0&limit=100&profileId={self._profile_id}&language={lang}&channelId={channel_id}&sort=time&sortOrder=asc",
510
+ def get_recording_show(self, show_id: str) -> list[LGHorizonRecordingSingle]:
511
+ """Returns show recording"""
512
+ _logger.info("Retrieving show recordings...")
513
+ show_recording_content = self._do_api_call(
514
+ f"{self._config['recordingService']['URL']}/customers/{self._auth.household_id}/episodes/shows/{show_id}?source=recording&language=nl&sort=time&sortOrder=asc"
255
515
  )
256
- recordings = await self._recording_factory.create_episodes(episodes_json)
516
+ recordings = []
517
+ for item in show_recording_content["data"]:
518
+ if item["source"] == "show":
519
+ recordings.append(LGHorizonRecordingShow(item))
520
+ else:
521
+ recordings.append(LGHorizonRecordingEpisode(item))
522
+ _logger.info("%s showrecordings retrieved...", len(recordings))
257
523
  return recordings
258
524
 
259
- async def get_recording_quota(self) -> LGHorizonRecordingQuota:
260
- """Refresh recording quota."""
261
- _LOGGER.debug("Refreshing recording quota...")
262
- service_url = await self._service_config.get_service_url("recordingService")
263
- quota_json = await self.auth.request(
264
- service_url,
265
- f"/customers/{self.auth.household_id}/quota",
525
+ def _update_entitlements(self) -> None:
526
+ _logger.info("Retrieving entitlements...")
527
+ entitlements_json = self._do_api_call(
528
+ f"{self._config['purchaseService']['URL']}/v2/customers/{self._auth.household_id}/entitlements?enableDaypass=true"
266
529
  )
267
- return LGHorizonRecordingQuota(quota_json)
268
-
269
-
270
- __all__ = ["LGHorizonApi", "LGHorizonAuth"]
530
+ self._entitlements.clear()
531
+ for entitlement in entitlements_json["entitlements"]:
532
+ self._entitlements.append(entitlement["id"])
533
+
534
+ def _get_config(self, country_code: str):
535
+ base_country_code = country_code[0:2]
536
+ config_url = f"{self._country_settings['api_url']}/{base_country_code}/en/config-service/conf/web/backoffice.json"
537
+ result = self._do_api_call(config_url)
538
+ _logger.debug(result)
539
+ return result