xtream-api-client 0.1.0__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.
- xtream_api_client/__init__.py +38 -0
- xtream_api_client/client.py +249 -0
- xtream_api_client/exceptions.py +40 -0
- xtream_api_client/models.py +123 -0
- xtream_api_client-0.1.0.dist-info/METADATA +187 -0
- xtream_api_client-0.1.0.dist-info/RECORD +9 -0
- xtream_api_client-0.1.0.dist-info/WHEEL +5 -0
- xtream_api_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- xtream_api_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Xtream API Client - a small library for Xtream-style IPTV API endpoints.
|
|
2
|
+
|
|
3
|
+
This package provides a Python interface for interacting with servers that
|
|
4
|
+
implement the Xtream Codes-style API:
|
|
5
|
+
|
|
6
|
+
* configuring and normalizing a server base URL
|
|
7
|
+
* authenticating with a username/password pair supplied by the caller
|
|
8
|
+
* retrieving account information
|
|
9
|
+
* retrieving live, VOD, and series categories
|
|
10
|
+
* retrieving basic stream lists
|
|
11
|
+
* exporting results as JSON
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import XtreamClient
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
XtreamAPIError,
|
|
17
|
+
XtreamAuthenticationError,
|
|
18
|
+
XtreamConnectionError,
|
|
19
|
+
XtreamHTTPError,
|
|
20
|
+
XtreamTimeoutError,
|
|
21
|
+
XtreamURLValidationError,
|
|
22
|
+
)
|
|
23
|
+
from .models import AccountInfo, Category, Channel
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"XtreamClient",
|
|
27
|
+
"XtreamAPIError",
|
|
28
|
+
"XtreamAuthenticationError",
|
|
29
|
+
"XtreamConnectionError",
|
|
30
|
+
"XtreamHTTPError",
|
|
31
|
+
"XtreamTimeoutError",
|
|
32
|
+
"XtreamURLValidationError",
|
|
33
|
+
"AccountInfo",
|
|
34
|
+
"Category",
|
|
35
|
+
"Channel",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""HTTP client for Xtream-style API endpoints.
|
|
2
|
+
|
|
3
|
+
The client uses only the Python standard library (:mod:`urllib.request`)
|
|
4
|
+
so the package has no runtime dependencies. All responses are parsed into
|
|
5
|
+
Python dictionaries or lists; helper methods convert the most common
|
|
6
|
+
responses into the dataclasses from :mod:`xtream_api_client.models`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
XtreamAPIError,
|
|
20
|
+
XtreamAuthenticationError,
|
|
21
|
+
XtreamConnectionError,
|
|
22
|
+
XtreamHTTPError,
|
|
23
|
+
XtreamTimeoutError,
|
|
24
|
+
XtreamURLValidationError,
|
|
25
|
+
)
|
|
26
|
+
from .models import AccountInfo, Category, Channel
|
|
27
|
+
|
|
28
|
+
__all__ = ["XtreamClient", "normalize_server_url"]
|
|
29
|
+
|
|
30
|
+
# Actions supported by the player_api endpoint of Xtream-style servers.
|
|
31
|
+
_DEFAULT_ACTION_KEY = "action"
|
|
32
|
+
|
|
33
|
+
_USER_AGENT = "xtream-api-client/0.1.0"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_server_url(server_url: str) -> str:
|
|
37
|
+
"""Validate and normalize a server base URL.
|
|
38
|
+
|
|
39
|
+
The function accepts URLs with or without a scheme, strips trailing
|
|
40
|
+
slashes and whitespace, and requires a hostname.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
server_url: The base URL of an Xtream-compatible server, e.g.
|
|
44
|
+
``"http://example.com:8080"`` or ``"example.com:8080"``.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The normalized URL without a trailing slash. If no scheme was
|
|
48
|
+
given, ``http://`` is assumed.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
XtreamURLValidationError: If the URL is empty, has no hostname,
|
|
52
|
+
or uses a scheme other than ``http``/``https``.
|
|
53
|
+
"""
|
|
54
|
+
if not server_url or not server_url.strip():
|
|
55
|
+
raise XtreamURLValidationError("Server URL must not be empty.")
|
|
56
|
+
|
|
57
|
+
url = server_url.strip()
|
|
58
|
+
if "://" not in url:
|
|
59
|
+
url = "http://" + url
|
|
60
|
+
|
|
61
|
+
parsed = urllib.parse.urlparse(url)
|
|
62
|
+
if parsed.scheme not in ("http", "https"):
|
|
63
|
+
raise XtreamURLValidationError(
|
|
64
|
+
f"Unsupported URL scheme {parsed.scheme!r}; expected http or https."
|
|
65
|
+
)
|
|
66
|
+
if not parsed.netloc:
|
|
67
|
+
raise XtreamURLValidationError(f"URL {server_url!r} has no hostname.")
|
|
68
|
+
|
|
69
|
+
return url.rstrip("/")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class XtreamClient:
|
|
73
|
+
"""A small client for Xtream-style IPTV API endpoints.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> client = XtreamClient("http://example.com:8080", "user", "pass")
|
|
77
|
+
>>> info = client.get_account_info() # doctest: +SKIP
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
server_url: Base URL of the server (with or without scheme).
|
|
81
|
+
username: Username supplied by the user or their provider.
|
|
82
|
+
password: Password supplied by the user or their provider.
|
|
83
|
+
timeout: Request timeout in seconds (default 10.0).
|
|
84
|
+
verify_input: When True (default), URLs are normalized up front.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
server_url: str,
|
|
90
|
+
username: str,
|
|
91
|
+
password: str,
|
|
92
|
+
timeout: float = 10.0,
|
|
93
|
+
verify_input: bool = True,
|
|
94
|
+
) -> None:
|
|
95
|
+
if verify_input:
|
|
96
|
+
self.server_url = normalize_server_url(server_url)
|
|
97
|
+
else:
|
|
98
|
+
self.server_url = (server_url or "").rstrip("/")
|
|
99
|
+
if not username or not password:
|
|
100
|
+
raise XtreamAuthenticationError("Username and password are required.")
|
|
101
|
+
self.username = username
|
|
102
|
+
self.password = password
|
|
103
|
+
self.timeout = float(timeout)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Low-level request machinery
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _build_url(self, action: Optional[str] = None, extra_params: Optional[Dict[str, Any]] = None) -> str:
|
|
110
|
+
"""Construct a player_api URL for the given action."""
|
|
111
|
+
params: Dict[str, Any] = {
|
|
112
|
+
"username": self.username,
|
|
113
|
+
"password": self.password,
|
|
114
|
+
}
|
|
115
|
+
if action is not None:
|
|
116
|
+
params[_DEFAULT_ACTION_KEY] = action
|
|
117
|
+
if extra_params:
|
|
118
|
+
params.update(extra_params)
|
|
119
|
+
query = urllib.parse.urlencode(params)
|
|
120
|
+
return f"{self.server_url}/player_api.php?{query}"
|
|
121
|
+
|
|
122
|
+
def _request_json(self, url: str) -> Union[Dict[str, Any], List[Any]]:
|
|
123
|
+
"""Perform a GET request and parse the JSON body.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
XtreamTimeoutError: On timeout.
|
|
127
|
+
XtreamConnectionError: When the server cannot be reached.
|
|
128
|
+
XtreamHTTPError: On non-200 HTTP responses.
|
|
129
|
+
XtreamAPIError: When the body is not valid JSON.
|
|
130
|
+
"""
|
|
131
|
+
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
|
132
|
+
try:
|
|
133
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
|
134
|
+
body = response.read()
|
|
135
|
+
except urllib.error.HTTPError as exc:
|
|
136
|
+
raise XtreamHTTPError(exc.code, url) from exc
|
|
137
|
+
except urllib.error.URLError as exc:
|
|
138
|
+
reason = getattr(exc, "reason", exc)
|
|
139
|
+
if isinstance(reason, TimeoutError) or "timed out" in str(reason).lower():
|
|
140
|
+
raise XtreamTimeoutError(f"Request to {url} timed out.") from exc
|
|
141
|
+
raise XtreamConnectionError(f"Cannot reach {url}: {reason}") from exc
|
|
142
|
+
except TimeoutError as exc:
|
|
143
|
+
raise XtreamTimeoutError(f"Request to {url} timed out.") from exc
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
data = json.loads(body.decode("utf-8", errors="replace"))
|
|
147
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
148
|
+
raise XtreamAPIError(f"Server returned non-JSON content from {url}.") from exc
|
|
149
|
+
if not isinstance(data, (dict, list)):
|
|
150
|
+
raise XtreamAPIError(f"Unexpected JSON shape from {url}.")
|
|
151
|
+
return data
|
|
152
|
+
|
|
153
|
+
def _call(self, action: Optional[str] = None, extra_params: Optional[Dict[str, Any]] = None) -> Any:
|
|
154
|
+
"""Call the player_api endpoint and apply basic auth checks."""
|
|
155
|
+
url = self._build_url(action=action, extra_params=extra_params)
|
|
156
|
+
data = self._request_json(url)
|
|
157
|
+
if isinstance(data, dict):
|
|
158
|
+
auth = data.get("user_info", {})
|
|
159
|
+
if isinstance(auth, dict) and auth.get("auth") in (0, False, "0"):
|
|
160
|
+
raise XtreamAuthenticationError(
|
|
161
|
+
"Server rejected the supplied credentials."
|
|
162
|
+
)
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Public API
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def get_account_info(self) -> Dict[str, Any]:
|
|
170
|
+
"""Return the raw account/user information dictionary.
|
|
171
|
+
|
|
172
|
+
Makes an unauthenticated-action call to ``player_api.php`` and
|
|
173
|
+
returns the parsed JSON, which typically contains ``user_info``
|
|
174
|
+
and ``server_info`` keys.
|
|
175
|
+
"""
|
|
176
|
+
return self._call()
|
|
177
|
+
|
|
178
|
+
def get_account_info_parsed(self) -> AccountInfo:
|
|
179
|
+
"""Return parsed :class:`~xtream_api_client.models.AccountInfo`."""
|
|
180
|
+
return AccountInfo.from_dict(self._call())
|
|
181
|
+
|
|
182
|
+
def get_live_categories(self) -> List[Dict[str, Any]]:
|
|
183
|
+
"""Return live TV categories as a list of dictionaries."""
|
|
184
|
+
return self._call("get_live_categories") # type: ignore[return-value]
|
|
185
|
+
|
|
186
|
+
def get_vod_categories(self) -> List[Dict[str, Any]]:
|
|
187
|
+
"""Return VOD (movie) categories as a list of dictionaries."""
|
|
188
|
+
return self._call("get_vod_categories") # type: ignore[return-value]
|
|
189
|
+
|
|
190
|
+
def get_series_categories(self) -> List[Dict[str, Any]]:
|
|
191
|
+
"""Return series categories as a list of dictionaries."""
|
|
192
|
+
return self._call("get_series_categories") # type: ignore[return-value]
|
|
193
|
+
|
|
194
|
+
def get_live_streams(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
195
|
+
"""Return the live channel list, optionally filtered by category."""
|
|
196
|
+
params = {"category_id": category_id} if category_id else None
|
|
197
|
+
return self._call("get_live_streams", extra_params=params) # type: ignore[return-value]
|
|
198
|
+
|
|
199
|
+
def get_vod_streams(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
200
|
+
"""Return the VOD stream list, optionally filtered by category."""
|
|
201
|
+
params = {"category_id": category_id} if category_id else None
|
|
202
|
+
return self._call("get_vod_streams", extra_params=params) # type: ignore[return-value]
|
|
203
|
+
|
|
204
|
+
def get_series(self, category_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
205
|
+
"""Return the series list, optionally filtered by category."""
|
|
206
|
+
params = {"category_id": category_id} if category_id else None
|
|
207
|
+
return self._call("get_series", extra_params=params) # type: ignore[return-value]
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Model-based helpers
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def list_live_categories(self) -> List[Category]:
|
|
214
|
+
"""Return live categories as :class:`Category` objects."""
|
|
215
|
+
return [Category.from_dict(c, "live") for c in self.get_live_categories()]
|
|
216
|
+
|
|
217
|
+
def list_vod_categories(self) -> List[Category]:
|
|
218
|
+
"""Return VOD categories as :class:`Category` objects."""
|
|
219
|
+
return [Category.from_dict(c, "vod") for c in self.get_vod_categories()]
|
|
220
|
+
|
|
221
|
+
def list_series_categories(self) -> List[Category]:
|
|
222
|
+
"""Return series categories as :class:`Category` objects."""
|
|
223
|
+
return [Category.from_dict(c, "series") for c in self.get_series_categories()]
|
|
224
|
+
|
|
225
|
+
def list_live_streams(self, category_id: Optional[str] = None) -> List[Channel]:
|
|
226
|
+
"""Return live streams as :class:`Channel` objects."""
|
|
227
|
+
return [Channel.from_dict(s) for s in self.get_live_streams(category_id)]
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# Export helpers
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def export_json(self, data: Any, path: Optional[str] = None, indent: int = 2) -> Union[str, None]:
|
|
234
|
+
"""Serialize results to a JSON string and optionally write to a file.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
data: Any JSON-serializable result from the client methods.
|
|
238
|
+
path: When given, the JSON is also written to this file path.
|
|
239
|
+
indent: Indentation level for the output (default 2).
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The JSON string, or ``None`` when ``path`` is given.
|
|
243
|
+
"""
|
|
244
|
+
text = json.dumps(data, indent=indent, ensure_ascii=False, default=str)
|
|
245
|
+
if path:
|
|
246
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
247
|
+
fh.write(text)
|
|
248
|
+
return None
|
|
249
|
+
return text
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Exception classes for the xtream_api_client package."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class XtreamAPIError(Exception):
|
|
5
|
+
"""Base exception for all errors raised by this library.
|
|
6
|
+
|
|
7
|
+
Callers can catch this single type to handle any failure produced
|
|
8
|
+
by :class:`~xtream_api_client.XtreamClient`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class XtreamURLValidationError(XtreamAPIError):
|
|
13
|
+
"""Raised when the server URL is missing, malformed, or unsupported."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class XtreamAuthenticationError(XtreamAPIError):
|
|
17
|
+
"""Raised when the server rejects the supplied username/password."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class XtreamTimeoutError(XtreamAPIError):
|
|
21
|
+
"""Raised when a request exceeds the configured timeout."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class XtreamConnectionError(XtreamAPIError):
|
|
25
|
+
"""Raised when the server cannot be reached (DNS, refused, etc.)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class XtreamHTTPError(XtreamAPIError):
|
|
29
|
+
"""Raised when the server returns an unexpected HTTP status code.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
status_code: The HTTP status code returned by the server.
|
|
33
|
+
url: The request URL that produced the error.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, status_code: int, url: str, message: str = "") -> None:
|
|
37
|
+
self.status_code = status_code
|
|
38
|
+
self.url = url
|
|
39
|
+
detail = message or f"HTTP {status_code} from {url}"
|
|
40
|
+
super().__init__(detail)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Lightweight data models returned by the client.
|
|
2
|
+
|
|
3
|
+
The raw API responses are plain dictionaries, and :meth:`dict` access is
|
|
4
|
+
always available. These models add attribute-style access and light
|
|
5
|
+
normalization for the most commonly used fields.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AccountInfo:
|
|
16
|
+
"""Parsed account information returned by the ``player_api`` login check.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
username: The account username.
|
|
20
|
+
status: Server-reported account status (for example ``"Active"``).
|
|
21
|
+
exp_date: Optional expiration date as a Unix timestamp string.
|
|
22
|
+
is_trial: Whether the server reports this as a trial account.
|
|
23
|
+
active_connections: Currently active connections, when reported.
|
|
24
|
+
max_connections: Maximum allowed connections, when reported.
|
|
25
|
+
raw: The complete, unparsed response dictionary.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
username: str
|
|
29
|
+
status: Optional[str] = None
|
|
30
|
+
exp_date: Optional[str] = None
|
|
31
|
+
is_trial: Optional[bool] = None
|
|
32
|
+
active_connections: Optional[int] = None
|
|
33
|
+
max_connections: Optional[int] = None
|
|
34
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AccountInfo":
|
|
38
|
+
"""Build an :class:`AccountInfo` from a raw ``user_info`` dictionary."""
|
|
39
|
+
user = data.get("user_info", data) if isinstance(data, dict) else {}
|
|
40
|
+
return cls(
|
|
41
|
+
username=str(user.get("username", "")),
|
|
42
|
+
status=user.get("status"),
|
|
43
|
+
exp_date=user.get("exp_date"),
|
|
44
|
+
is_trial=_to_bool(user.get("is_trial")),
|
|
45
|
+
active_connections=_to_int(user.get("active_cons")),
|
|
46
|
+
max_connections=_to_int(user.get("max_connections")),
|
|
47
|
+
raw=dict(data),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Category:
|
|
53
|
+
"""A single category entry (live, VOD, or series).
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
category_id: The server-side category identifier.
|
|
57
|
+
name: Human-readable category name.
|
|
58
|
+
category_type: One of ``"live"``, ``"vod"``, or ``"series"``.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
category_id: str
|
|
62
|
+
name: str
|
|
63
|
+
category_type: str = "live"
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: Dict[str, Any], category_type: str = "live") -> "Category":
|
|
67
|
+
"""Build a :class:`Category` from a raw category dictionary."""
|
|
68
|
+
return cls(
|
|
69
|
+
category_id=str(data.get("category_id", "")),
|
|
70
|
+
name=str(data.get("category_name", "")),
|
|
71
|
+
category_type=category_type,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class Channel:
|
|
77
|
+
"""A single live channel / VOD item from a stream list.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
stream_id: The server-side stream identifier.
|
|
81
|
+
name: Human-readable stream name.
|
|
82
|
+
category_id: Category this stream belongs to.
|
|
83
|
+
stream_type: Stream type reported by the server, when present.
|
|
84
|
+
raw: The complete, unparsed entry dictionary.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
stream_id: str
|
|
88
|
+
name: str
|
|
89
|
+
category_id: Optional[str] = None
|
|
90
|
+
stream_type: Optional[str] = None
|
|
91
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Channel":
|
|
95
|
+
"""Build a :class:`Channel` from a raw stream-list entry."""
|
|
96
|
+
return cls(
|
|
97
|
+
stream_id=str(data.get("stream_id", data.get("series_id", ""))),
|
|
98
|
+
name=str(data.get("name", "")),
|
|
99
|
+
category_id=data.get("category_id"),
|
|
100
|
+
stream_type=data.get("stream_type"),
|
|
101
|
+
raw=dict(data),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _to_bool(value: Any) -> Optional[bool]:
|
|
106
|
+
"""Coerce common server representations of booleans."""
|
|
107
|
+
if value is None:
|
|
108
|
+
return None
|
|
109
|
+
if isinstance(value, bool):
|
|
110
|
+
return value
|
|
111
|
+
if isinstance(value, str):
|
|
112
|
+
return value.strip().lower() in ("true", "1", "yes")
|
|
113
|
+
return bool(value)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _to_int(value: Any) -> Optional[int]:
|
|
117
|
+
"""Coerce common server representations of integers, returning None on failure."""
|
|
118
|
+
if value is None or value == "":
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
return int(value)
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return None
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtream-api-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small Python client for Xtream-style IPTV API endpoints using only the standard library.
|
|
5
|
+
Author: XtreamTech
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://xtreamtech.net
|
|
8
|
+
Project-URL: Documentation, https://xtreamtech.net
|
|
9
|
+
Keywords: xtream,iptv,api,client,player_api
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Internet
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# xtream-api-client
|
|
29
|
+
|
|
30
|
+
A small Python client for working with Xtream-style IPTV API endpoints (`player_api.php`). It is built entirely on the Python standard library — no third-party runtime dependencies — and returns plain dictionaries, lists, and optional lightweight dataclass models.
|
|
31
|
+
|
|
32
|
+
The library handles URL validation/normalization, query construction, JSON parsing, and error mapping so you can work with an Xtream-compatible server from a few lines of Python.
|
|
33
|
+
|
|
34
|
+
## What the package does
|
|
35
|
+
|
|
36
|
+
- Configures and validates an Xtream-compatible server base URL
|
|
37
|
+
- Authenticates with a username/password pair that you supply
|
|
38
|
+
- Retrieves account information
|
|
39
|
+
- Retrieves live, VOD, and series categories
|
|
40
|
+
- Retrieves basic live, VOD, and series stream lists
|
|
41
|
+
- Maps HTTP, connection, timeout, and authentication failures to dedicated exception classes
|
|
42
|
+
- Exports any result as JSON (to a string or a file)
|
|
43
|
+
|
|
44
|
+
This client only calls documented read-style endpoints (`player_api.php` actions). It does not implement stream playback, does not bypass authentication, and does not attempt to circumvent any provider restriction.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install xtream-api-client
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.9 or newer.
|
|
53
|
+
|
|
54
|
+
Or from source:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone <your-repo-url>
|
|
58
|
+
cd xtreamtech
|
|
59
|
+
py -m pip install -e .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick-start example
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from xtream_api_client import XtreamClient
|
|
66
|
+
|
|
67
|
+
client = XtreamClient(
|
|
68
|
+
server_url="http://example.com:8080",
|
|
69
|
+
username="your-username",
|
|
70
|
+
password="your-password",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
account = client.get_account_info()
|
|
74
|
+
print(account["user_info"]["status"])
|
|
75
|
+
|
|
76
|
+
for category in client.get_live_categories():
|
|
77
|
+
print(category["category_id"], category["category_name"])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
`XtreamClient` accepts:
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Default | Description |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `server_url` | `str` | required | Base URL, with or without a scheme (`http://` is assumed). Trailing slashes and whitespace are stripped. |
|
|
87
|
+
| `username` | `str` | required | Your account username. |
|
|
88
|
+
| `password` | `str` | required | Your account password. |
|
|
89
|
+
| `timeout` | `float` | `10.0` | Per-request timeout in seconds. |
|
|
90
|
+
| `verify_input` | `bool` | `True` | Validate/normalize the URL on construction. |
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
Credentials are sent as query parameters on every `player_api.php` request, matching the Xtream API convention. If the server reports `user_info.auth` as falsy, an `XtreamAuthenticationError` is raised.
|
|
95
|
+
|
|
96
|
+
There are no hard-coded credentials or servers — supply your own.
|
|
97
|
+
|
|
98
|
+
## Retrieving account information
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
info = client.get_account_info() # raw dict with user_info / server_info
|
|
102
|
+
parsed = client.get_account_info_parsed() # AccountInfo dataclass
|
|
103
|
+
print(parsed.username, parsed.status, parsed.max_connections)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Retrieving categories and streams
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
client.get_live_categories() # list[dict]
|
|
110
|
+
client.get_vod_categories() # list[dict]
|
|
111
|
+
client.get_series_categories() # list[dict]
|
|
112
|
+
|
|
113
|
+
client.get_live_streams(category_id="1") # optional category filter
|
|
114
|
+
client.get_vod_streams(category_id="2")
|
|
115
|
+
client.get_series(category_id="3")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Model-based equivalents:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
cats = client.list_live_categories() # list[Category]
|
|
122
|
+
streams = client.list_live_streams() # list[Channel]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Error handling
|
|
126
|
+
|
|
127
|
+
All exceptions derive from `XtreamAPIError`:
|
|
128
|
+
|
|
129
|
+
| Exception | Raised when |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `XtreamURLValidationError` | The server URL is empty, malformed, or uses an unsupported scheme. |
|
|
132
|
+
| `XtreamAuthenticationError` | Credentials are missing or the server rejects them. |
|
|
133
|
+
| `XtreamTimeoutError` | A request exceeds the configured timeout. |
|
|
134
|
+
| `XtreamConnectionError` | The server cannot be reached (DNS failure, connection refused, etc.). |
|
|
135
|
+
| `XtreamHTTPError` | The server returns a non-200 status (`status_code` and `url` attributes). |
|
|
136
|
+
| `XtreamAPIError` | Base class; also raised for non-JSON or unexpected responses. |
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from xtream_api_client.exceptions import XtreamAPIError, XtreamTimeoutError
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
categories = client.get_live_categories()
|
|
143
|
+
except XtreamTimeoutError:
|
|
144
|
+
...
|
|
145
|
+
except XtreamAPIError as exc:
|
|
146
|
+
...
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Timeout configuration
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
client = XtreamClient(url, user, password, timeout=5.0) # seconds
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The timeout applies per request to both connection establishment and reading.
|
|
156
|
+
|
|
157
|
+
## Exporting results as JSON
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
text = client.export_json(client.get_live_categories()) # returns str
|
|
161
|
+
client.export_json(data, path="categories.json") # writes a file
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Testing
|
|
165
|
+
|
|
166
|
+
Tests use mocked HTTP responses only — no real servers are contacted.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
py -m pip install -e ".[test]"
|
|
170
|
+
py -m pytest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Limitations
|
|
174
|
+
|
|
175
|
+
- Read-style `player_api.php` actions only; no episode/stream URL building or playback.
|
|
176
|
+
- Error detection relies on the server returning well-formed JSON; behavior varies between server implementations and versions.
|
|
177
|
+
- Credentials appear in query strings, matching the API convention; avoid logging full request URLs.
|
|
178
|
+
- Synchronous `urllib` based I/O only; no async support or connection pooling.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT — see [LICENSE](LICENSE).
|
|
183
|
+
|
|
184
|
+
## Additional Xtream/IPTV technical resources
|
|
185
|
+
|
|
186
|
+
- Project documentation and resources: [https://xtreamtech.net](https://xtreamtech.net)
|
|
187
|
+
- Python `urllib.request` documentation: https://docs.python.org/3/library/urllib.request.html
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
xtream_api_client/__init__.py,sha256=3heqP4VP9kaUH4GSxN75diCn6fprcZUaB60ZWIbvBPo,991
|
|
2
|
+
xtream_api_client/client.py,sha256=oH4CBm74YXX3qdZpNeH4pztUbokuTJGoy0x0ixUyaGE,10419
|
|
3
|
+
xtream_api_client/exceptions.py,sha256=uvc4mTwFit1mYa5eO3veJFA4kD0A9sjHUeOy86eUqWo,1253
|
|
4
|
+
xtream_api_client/models.py,sha256=SmrUgj9ZRhqHdqc3l9qzeG9q2KCyJavL5nkNXCjZ3Es,4112
|
|
5
|
+
xtream_api_client-0.1.0.dist-info/licenses/LICENSE,sha256=0FFfT2Q1WBS3zQTM74B2X8ns2mYEBe1NnqD_-I-XZOo,1067
|
|
6
|
+
xtream_api_client-0.1.0.dist-info/METADATA,sha256=ClmGQcJPRfCsSUphTAFNNhNaRL521-5F5AiIKpOw23s,6565
|
|
7
|
+
xtream_api_client-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
xtream_api_client-0.1.0.dist-info/top_level.txt,sha256=XXZLto4CLmQ9ZmBTo94u1jyIyE0GSC7FPtAhX4-CUSY,18
|
|
9
|
+
xtream_api_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XtreamTech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xtream_api_client
|