lghorizon 0.6.13__tar.gz → 0.7.1__tar.gz

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.
Files changed (29) hide show
  1. {lghorizon-0.6.13 → lghorizon-0.7.1}/.github/workflows/build-on-pr.yml +1 -1
  2. {lghorizon-0.6.13 → lghorizon-0.7.1}/.github/workflows/publish-to-pypi.yml +1 -1
  3. {lghorizon-0.6.13 → lghorizon-0.7.1}/PKG-INFO +1 -1
  4. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/const.py +0 -2
  5. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/lghorizon_api.py +40 -106
  6. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/PKG-INFO +1 -1
  7. {lghorizon-0.6.13 → lghorizon-0.7.1}/test.py +6 -0
  8. {lghorizon-0.6.13 → lghorizon-0.7.1}/.coverage +0 -0
  9. {lghorizon-0.6.13 → lghorizon-0.7.1}/.flake8 +0 -0
  10. {lghorizon-0.6.13 → lghorizon-0.7.1}/.gitignore +0 -0
  11. {lghorizon-0.6.13 → lghorizon-0.7.1}/LICENSE +0 -0
  12. {lghorizon-0.6.13 → lghorizon-0.7.1}/README.md +0 -0
  13. {lghorizon-0.6.13 → lghorizon-0.7.1}/instructions.txt +0 -0
  14. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/__init__.py +0 -0
  15. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/exceptions.py +0 -0
  16. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/helpers.py +0 -0
  17. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/models.py +0 -0
  18. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon/py.typed +0 -0
  19. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/SOURCES.txt +0 -0
  20. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/dependency_links.txt +0 -0
  21. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/not-zip-safe +0 -0
  22. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/requires.txt +0 -0
  23. {lghorizon-0.6.13 → lghorizon-0.7.1}/lghorizon.egg-info/top_level.txt +0 -0
  24. {lghorizon-0.6.13 → lghorizon-0.7.1}/lib64 +0 -0
  25. {lghorizon-0.6.13 → lghorizon-0.7.1}/pyvenv.cfg +0 -0
  26. {lghorizon-0.6.13 → lghorizon-0.7.1}/renovate.json +0 -0
  27. {lghorizon-0.6.13 → lghorizon-0.7.1}/secrets_stub.json +0 -0
  28. {lghorizon-0.6.13 → lghorizon-0.7.1}/setup.cfg +0 -0
  29. {lghorizon-0.6.13 → lghorizon-0.7.1}/setup.py +0 -0
@@ -7,7 +7,7 @@ on:
7
7
  jobs:
8
8
  build:
9
9
  name: Build Python 🐍 distribution 📦
10
- runs-on: ubuntu-22.04
10
+ runs-on: ubuntu-24.04
11
11
  steps:
12
12
  - uses: actions/checkout@master
13
13
  - name: Set up Python 3.10
@@ -6,7 +6,7 @@ on:
6
6
  jobs:
7
7
  build-n-publish:
8
8
  name: Publish Python 🐍 distribution 📦 to Pypi
9
- runs-on: ubuntu-22.04
9
+ runs-on: ubuntu-24.04
10
10
  steps:
11
11
  - uses: actions/checkout@master
12
12
  - name: Set up Python 3.10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lghorizon
3
- Version: 0.6.13
3
+ Version: 0.7.1
4
4
  Summary: Python client for Liberty Global Horizon settop boxes
5
5
  Home-page: https://github.com/sholofly/LGHorizon-python
6
6
  Author: Rudolf Offereins
@@ -119,9 +119,7 @@ COUNTRY_SETTINGS = {
119
119
  },
120
120
  "gb": {
121
121
  "api_url": "https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com",
122
- "oauth_url": "https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true",
123
122
  "channels": [],
124
- "oesp_url": "https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web",
125
123
  "language": "en",
126
124
  },
127
125
  "ie": {
@@ -63,10 +63,12 @@ class LGHorizonApi:
63
63
  password: str,
64
64
  country_code: str = "nl",
65
65
  identifier: str = None,
66
+ refresh_token=None,
66
67
  ) -> None:
67
68
  """Create LGHorizon API."""
68
69
  self.username = username
69
70
  self.password = password
71
+ self.refresh_token = refresh_token
70
72
  self._session = Session()
71
73
  self._country_settings = COUNTRY_SETTINGS[country_code]
72
74
  self._country_code = country_code
@@ -113,92 +115,38 @@ class LGHorizonApi:
113
115
  self._auth.fill(auth_response.json())
114
116
  _logger.debug("Authorization succeeded")
115
117
 
116
- def authorize_gb(self):
117
- try:
118
- login_session = Session()
119
- ####################################
120
- _logger.debug("Step 1 - Get Authorization data")
121
- auth_url = f"{self._country_settings['oesp_url']}/authorization"
122
- auth_response = login_session.get(auth_url)
123
- if not auth_response.ok:
124
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
125
- auth_response_json = auth_response.json()
126
- auth_session = auth_response_json["session"]
127
- auth_state = auth_session["state"]
128
- authorizationUri = auth_session["authorizationUri"]
129
- authValidityToken = auth_session["validityToken"]
130
- ####################################
131
- _logger.debug("Step 2 - Get Authorization cookie")
132
-
133
- auth_cookie_response = login_session.get(authorizationUri)
134
- if not auth_cookie_response.ok:
135
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
136
- ####################################
137
- _logger.debug("Step 3 - Login")
138
- payload = {"username": self.username, "credential": self.password}
139
- headers = {"accept": "application/json; charset=UTF-8, */*"}
118
+ def authorize_gb(self) -> None:
119
+ _logger.debug("Authorizing via refresh")
120
+ refresh_url = (
121
+ f"{self._country_settings['api_url']}/auth-service/v1/authorization/refresh"
122
+ )
123
+ headers = {"content-type": "application/json", "charset": "utf-8"}
124
+ payload = '{"refreshToken":"' + self.refresh_token + '"}'
140
125
 
141
- login_response = login_session.post(
142
- self._country_settings["oauth_url"],
143
- json.dumps(payload),
144
- headers=headers,
145
- allow_redirects=False,
126
+ try:
127
+ auth_response = self._session.post(
128
+ refresh_url, headers=headers, data=payload
146
129
  )
147
- if not login_response.ok:
148
- raise LGHorizonApiConnectionError("Can't connect to authorization URL")
149
-
150
- if not "x-redirect-location" in login_response.headers:
151
- raise LGHorizonApiConnectionError("No redirect location in headers.")
130
+ except Exception as ex:
131
+ raise LGHorizonApiConnectionError("Unknown connection failure") from ex
152
132
 
153
- redirect_url = login_response.headers["x-redirect-location"]
154
- ####################################
155
- _logger.debug("Step 4 - Follow redirect")
156
- redirect_response = login_session.get(redirect_url, allow_redirects=False)
157
- if not "Location" in redirect_response.headers:
158
- raise LGHorizonApiConnectionError("No success url in redirect.")
159
- ####################################
160
- _logger.debug("Step 5 - Extract auth code")
161
- success_url = redirect_response.headers["Location"]
162
- codeMatches = re.findall(r"code=(.*)&", success_url)
163
- if len(codeMatches) == 0:
164
- raise LGHorizonApiConnectionError("No code in redirect headers")
165
- authorizationCode = codeMatches[0]
166
- stateMatches = re.findall(r"state=(.*)", success_url)
167
- if len(codeMatches) == 0:
168
- raise LGHorizonApiConnectionError("No state in redirect headers")
169
- authorizationState = stateMatches[0]
170
- _logger.debug(
171
- f"Auth code: {authorizationCode}, Auth state: {authorizationState}"
172
- )
173
- ####################################
174
- _logger.debug("Step 6 - Post auth data with valid code")
175
- authorization_payload = {
176
- "authorizationGrant": {
177
- "authorizationCode": authorizationCode,
178
- "validityToken": authValidityToken,
179
- "state": authorizationState,
180
- }
181
- }
182
- headers = {
183
- "content-type": "application/json",
184
- }
185
- # VM requires the client to pass the response from /authorization verbatim to /session?token=true
186
- post_authorization_result = login_session.post(
187
- self._country_settings["oesp_url"] + "/authorization",
188
- json.dumps(authorization_payload),
189
- headers=headers,
190
- )
191
- post_session_result = login_session.post(
192
- self._country_settings["oesp_url"] + "/session?token=true",
193
- json.dumps(post_authorization_result.json()),
194
- headers=headers,
195
- )
133
+ if not auth_response.ok:
134
+ _logger.debug("response %s", auth_response)
135
+ error_json = auth_response.json()
136
+ error = None
137
+ if "error" in error_json:
138
+ error = error_json["error"]
139
+ if error and error["statusCode"] == 97401:
140
+ raise LGHorizonApiUnauthorizedError("Invalid credentials")
141
+ elif error:
142
+ raise LGHorizonApiConnectionError(error["message"])
143
+ else:
144
+ raise LGHorizonApiConnectionError("Unknown connection error")
196
145
 
197
- self._auth.fill(post_session_result.json())
198
- self._session.cookies["ACCESSTOKEN"] = self._auth.accessToken
199
- ####################################
200
- except Exception as ex:
201
- pass
146
+ self._auth.fill(auth_response.json())
147
+ self.refresh_token = self._auth.refreshToken
148
+ self._session.cookies["ACCESSTOKEN"] = self._auth.accessToken
149
+ _logger.debug("Authorization succeeded")
202
150
 
203
151
  def authorize_telenet(self):
204
152
  try:
@@ -276,32 +224,14 @@ class LGHorizonApi:
276
224
  self._auth.mqttToken = mqtt_response["token"]
277
225
  _logger.debug(f"MQTT token: {self._auth.mqttToken}")
278
226
 
279
- def _obtain_mqtt_token_gb(self):
280
- _logger.debug("Obtain Virgin GB mqtt token...")
281
- self._session.headers["x-oesp-token"] = self._auth.accessToken
282
- self._session.headers["x-oesp-username"] = self._auth.username
283
-
284
- mqtt_response = self._do_api_call(
285
- f"{self._country_settings['oesp_url']}/tokens/jwt"
286
- )
287
- self._auth.mqttToken = mqtt_response["token"]
288
- _logger.debug(f"MQTT token: {self._auth.mqttToken}")
289
-
290
227
  @backoff.on_exception(
291
- backoff.expo,
292
- BaseException,
293
- jitter=None,
294
- max_tries=3,
295
- logger=_logger,
228
+ backoff.expo, BaseException, jitter=None, max_time=600, logger=_logger
296
229
  )
297
230
  def connect(self) -> None:
298
231
  self._config = self._get_config(self._country_code)
299
232
  _logger.debug("Connect to API")
300
233
  self._authorize()
301
- if self._country_code == "gb":
302
- self._obtain_mqtt_token_gb()
303
- else:
304
- self._obtain_mqtt_token()
234
+ self._obtain_mqtt_token()
305
235
  self._mqttClient = LGHorizonMqttClient(
306
236
  self._auth,
307
237
  self._config["mqttBroker"]["URL"],
@@ -362,10 +292,14 @@ class LGHorizonApi:
362
292
  self.settop_boxes[deviceId].playing_info.set_paused(
363
293
  playerState["speed"] == 0
364
294
  )
365
- if source_type in (
366
- BOX_PLAY_STATE_CHANNEL,
367
- BOX_PLAY_STATE_BUFFER,
368
- BOX_PLAY_STATE_REPLAY,
295
+ if (
296
+ source_type
297
+ in (
298
+ BOX_PLAY_STATE_CHANNEL,
299
+ BOX_PLAY_STATE_BUFFER,
300
+ BOX_PLAY_STATE_REPLAY,
301
+ )
302
+ and "eventId" in state_source
369
303
  ):
370
304
  eventId = state_source["eventId"]
371
305
  raw_replay_event = self._do_api_call(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lghorizon
3
- Version: 0.6.13
3
+ Version: 0.7.1
4
4
  Summary: Python client for Liberty Global Horizon settop boxes
5
5
  Home-page: https://github.com/sholofly/LGHorizon-python
6
6
  Author: Rudolf Offereins
@@ -52,11 +52,17 @@ if __name__ == "__main__":
52
52
  try:
53
53
  secrets_file_path = "secrets.json"
54
54
  secrets = read_secrets(secrets_file_path)
55
+
56
+ refresh_token = None
57
+ if "refresh_token" in secrets:
58
+ refresh_token = secrets["refresh_token"]
59
+
55
60
  api = LGHorizonApi(
56
61
  secrets["username"],
57
62
  secrets["password"],
58
63
  secrets["country"],
59
64
  # identifier="DTV3907048",
65
+ refresh_token = refresh_token,
60
66
  )
61
67
  api.connect()
62
68
  event_loop()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes