tvdb_api_client 0.8.0__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.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.3
2
+ Name: tvdb_api_client
3
+ Version: 0.8.0
4
+ Summary: A python client for TVDB rest API
5
+ Keywords: tvdb,imdb,tv series
6
+ Author: Stephanos Kuma
7
+ Author-email: Stephanos Kuma <stephanos@kuma.ai>
8
+ License: BSD-3-Clause
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Intended Audience :: Developers
13
+ Requires-Dist: dj-settings~=8.0
14
+ Requires-Dist: pathurl~=0.8
15
+ Requires-Dist: pyutilkit~=0.11
16
+ Requires-Dist: requests>=2.34.2,<3.0
17
+ Requires-Python: >=3.10
18
+ Project-URL: homepage, https://tvdb-api-client.readthedocs.io/en/stable/
19
+ Project-URL: repository, https://github.com/spapanik/tvdb_api_client
20
+ Project-URL: documentation, https://tvdb-api-client.readthedocs.io/en/stable/
21
+ Description-Content-Type: text/markdown
22
+
23
+ # tvdb_api_client: an unofficial API for the TVDB
24
+
25
+ [![build][build_badge]][build_url]
26
+ [![lint][lint_badge]][lint_url]
27
+ [![tests][tests_badge]][tests_url]
28
+ [![license][licence_badge]][licence_url]
29
+ [![codecov][codecov_badge]][codecov_url]
30
+ [![readthedocs][readthedocs_badge]][readthedocs_url]
31
+ [![pypi][pypi_badge]][pypi_url]
32
+ [![downloads][pepy_badge]][pepy_url]
33
+ [![build automation: yam][yam_badge]][yam_url]
34
+ [![Lint: ruff][ruff_badge]][ruff_url]
35
+
36
+ `tvdb_api_client` is an unofficial API for the TVDB.
37
+
38
+ ## In a nutshell
39
+
40
+ ### Installation
41
+
42
+ [uv] is an extremely fast Python package installer.
43
+ You can use it to install `tvdb_api_client` and try it out:
44
+
45
+ ```console
46
+ $ uv pip install tvdb_api_client
47
+ ```
48
+
49
+ ### Usage
50
+
51
+ Initialise the client and fetch data:
52
+
53
+ ```python
54
+ from tvdb_api_client import TheTVDBClient
55
+
56
+ client = TheTVDBClient(api_key="your-api-key")
57
+
58
+ # Get a TV series by its TVDB id
59
+ series = client.get_series_by_id(81189) # Breaking Bad
60
+
61
+ # Get all episodes for a TV series
62
+ episodes = client.get_episodes_by_series(81189)
63
+ ```
64
+
65
+ Once the client has been initialised, you can use it to:
66
+
67
+ - get a TV series by its TVDB id
68
+ - get all episodes for a TV series by its TVDB id
69
+ - access raw API responses for custom processing
70
+
71
+ ## Links
72
+
73
+ - [Documentation]
74
+ - [Changelog]
75
+
76
+ [build_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/build.yml/badge.svg
77
+ [build_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/build.yml
78
+ [lint_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/lint.yml/badge.svg
79
+ [lint_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/lint.yml
80
+ [tests_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/tests.yml/badge.svg
81
+ [tests_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/tests.yml
82
+ [licence_badge]: https://img.shields.io/pypi/l/tvdb-api-client
83
+ [licence_url]: https://tvdb-api-client.readthedocs.io/en/stable/LICENSE/
84
+ [codecov_badge]: https://codecov.io/github/spapanik/tvdb-api-client/graph/badge.svg?token=Q20F84BW72
85
+ [codecov_url]: https://codecov.io/github/spapanik/tvdb-api-client
86
+ [readthedocs_badge]: https://readthedocs.org/projects/tvdb-api-client/badge/?version=latest
87
+ [readthedocs_url]: https://tvdb-api-client.readthedocs.io/en/latest/
88
+ [pypi_badge]: https://img.shields.io/pypi/v/tvdb-api-client
89
+ [pypi_url]: https://pypi.org/project/tvdb-api-client
90
+ [pepy_badge]: https://pepy.tech/badge/tvdb-api-client
91
+ [pepy_url]: https://pepy.tech/project/tvdb-api-client
92
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
93
+ [yam_url]: https://github.com/spapanik/yamk
94
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
95
+ [ruff_url]: https://github.com/charliermarsh/ruff
96
+ [uv]: https://github.com/astral-sh/uv
97
+ [Documentation]: https://tvdb-api-client.readthedocs.io/en/stable/
98
+ [Changelog]: https://tvdb-api-client.readthedocs.io/en/stable/CHANGELOG/
@@ -0,0 +1,76 @@
1
+ # tvdb_api_client: an unofficial API for the TVDB
2
+
3
+ [![build][build_badge]][build_url]
4
+ [![lint][lint_badge]][lint_url]
5
+ [![tests][tests_badge]][tests_url]
6
+ [![license][licence_badge]][licence_url]
7
+ [![codecov][codecov_badge]][codecov_url]
8
+ [![readthedocs][readthedocs_badge]][readthedocs_url]
9
+ [![pypi][pypi_badge]][pypi_url]
10
+ [![downloads][pepy_badge]][pepy_url]
11
+ [![build automation: yam][yam_badge]][yam_url]
12
+ [![Lint: ruff][ruff_badge]][ruff_url]
13
+
14
+ `tvdb_api_client` is an unofficial API for the TVDB.
15
+
16
+ ## In a nutshell
17
+
18
+ ### Installation
19
+
20
+ [uv] is an extremely fast Python package installer.
21
+ You can use it to install `tvdb_api_client` and try it out:
22
+
23
+ ```console
24
+ $ uv pip install tvdb_api_client
25
+ ```
26
+
27
+ ### Usage
28
+
29
+ Initialise the client and fetch data:
30
+
31
+ ```python
32
+ from tvdb_api_client import TheTVDBClient
33
+
34
+ client = TheTVDBClient(api_key="your-api-key")
35
+
36
+ # Get a TV series by its TVDB id
37
+ series = client.get_series_by_id(81189) # Breaking Bad
38
+
39
+ # Get all episodes for a TV series
40
+ episodes = client.get_episodes_by_series(81189)
41
+ ```
42
+
43
+ Once the client has been initialised, you can use it to:
44
+
45
+ - get a TV series by its TVDB id
46
+ - get all episodes for a TV series by its TVDB id
47
+ - access raw API responses for custom processing
48
+
49
+ ## Links
50
+
51
+ - [Documentation]
52
+ - [Changelog]
53
+
54
+ [build_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/build.yml/badge.svg
55
+ [build_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/build.yml
56
+ [lint_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/lint.yml/badge.svg
57
+ [lint_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/lint.yml
58
+ [tests_badge]: https://github.com/spapanik/tvdb_api_client/actions/workflows/tests.yml/badge.svg
59
+ [tests_url]: https://github.com/spapanik/tvdb_api_client/actions/workflows/tests.yml
60
+ [licence_badge]: https://img.shields.io/pypi/l/tvdb-api-client
61
+ [licence_url]: https://tvdb-api-client.readthedocs.io/en/stable/LICENSE/
62
+ [codecov_badge]: https://codecov.io/github/spapanik/tvdb-api-client/graph/badge.svg?token=Q20F84BW72
63
+ [codecov_url]: https://codecov.io/github/spapanik/tvdb-api-client
64
+ [readthedocs_badge]: https://readthedocs.org/projects/tvdb-api-client/badge/?version=latest
65
+ [readthedocs_url]: https://tvdb-api-client.readthedocs.io/en/latest/
66
+ [pypi_badge]: https://img.shields.io/pypi/v/tvdb-api-client
67
+ [pypi_url]: https://pypi.org/project/tvdb-api-client
68
+ [pepy_badge]: https://pepy.tech/badge/tvdb-api-client
69
+ [pepy_url]: https://pepy.tech/project/tvdb-api-client
70
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
71
+ [yam_url]: https://github.com/spapanik/yamk
72
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
73
+ [ruff_url]: https://github.com/charliermarsh/ruff
74
+ [uv]: https://github.com/astral-sh/uv
75
+ [Documentation]: https://tvdb-api-client.readthedocs.io/en/stable/
76
+ [Changelog]: https://tvdb-api-client.readthedocs.io/en/stable/CHANGELOG/
@@ -0,0 +1,172 @@
1
+ [build-system]
2
+ requires = [
3
+ "uv_build>=0.11.0,<0.12.0",
4
+ ]
5
+ build-backend = "uv_build"
6
+
7
+ [project]
8
+ name = "tvdb_api_client"
9
+ version = "0.8.0"
10
+
11
+ authors = [
12
+ { name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
13
+ ]
14
+ license = { text = "BSD-3-Clause" }
15
+
16
+ readme = "docs/README.md"
17
+ description = "A python client for TVDB rest API"
18
+ keywords = [
19
+ "tvdb",
20
+ "imdb",
21
+ "tv series",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Intended Audience :: Developers",
28
+ ]
29
+
30
+ requires-python = ">=3.10"
31
+ dependencies = [
32
+ "dj_settings~=8.0",
33
+ "pathurl~=0.8",
34
+ "pyutilkit~=0.11",
35
+ "requests>=2.34.2,<3.0",
36
+ ]
37
+
38
+ [project.urls]
39
+ homepage = "https://tvdb-api-client.readthedocs.io/en/stable/"
40
+ repository = "https://github.com/spapanik/tvdb_api_client"
41
+ documentation = "https://tvdb-api-client.readthedocs.io/en/stable/"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "ipdb~=0.13",
46
+ "ipython~=8.39",
47
+ "ptpython~=3.0",
48
+ { include-group = "lint" },
49
+ { include-group = "test" },
50
+ { include-group = "docs" },
51
+ ]
52
+ lint = [
53
+ "mypy~=2.1", # Keep mypy for CI/CD
54
+ "ruff~=0.15",
55
+ "ty~=0.0.42",
56
+ "types-requests~=2.33",
57
+ { include-group = "test_core" },
58
+ ]
59
+ test = [
60
+ "pytest-cov~=7.1",
61
+ { include-group = "test_core" },
62
+ ]
63
+ test_core = [
64
+ "pytest~=9.0",
65
+ ]
66
+ docs = [
67
+ "mkdocs~=1.6",
68
+ "mkdocs-material~=9.7",
69
+ "mkdocs-material-extensions~=1.3",
70
+ "pygments~=2.20",
71
+ "pymdown-extensions~=10.21",
72
+ ]
73
+
74
+ [tool.mypy]
75
+ # Keep mypy for CI/CD
76
+ check_untyped_defs = true
77
+ disallow_any_decorated = true
78
+ disallow_any_explicit = true
79
+ disallow_any_expr = false # many builtins are Any
80
+ disallow_any_generics = true
81
+ disallow_any_unimported = true
82
+ disallow_incomplete_defs = true
83
+ disallow_subclassing_any = true
84
+ disallow_untyped_calls = true
85
+ disallow_untyped_decorators = true
86
+ disallow_untyped_defs = true
87
+ extra_checks = true
88
+ ignore_missing_imports = true
89
+ no_implicit_reexport = true
90
+ show_column_numbers = true
91
+ show_error_codes = true
92
+ strict_equality = true
93
+ warn_redundant_casts = true
94
+ warn_return_any = true
95
+ warn_unused_configs = true
96
+ warn_unused_ignores = true
97
+ warn_unreachable = true
98
+
99
+ [[tool.mypy.overrides]]
100
+ # Keep mypy for CI/CD
101
+ module = "tests.*"
102
+ disallow_any_decorated = false # mock.MagicMock is Any
103
+
104
+ [tool.ruff]
105
+ src = [
106
+ "src",
107
+ ]
108
+ target-version = "py310"
109
+
110
+ [tool.ruff.lint]
111
+ select = [
112
+ "ALL",
113
+ ]
114
+ ignore = [
115
+ "C901", # Adding a limit to complexity is too arbitrary
116
+ "COM812", # Avoid conflicts with the formatter
117
+ "D10", # Not everything needs a docstring
118
+ "D203", # Prefer `no-blank-line-before-class` (D211)
119
+ "D213", # Prefer `multi-line-summary-first-line` (D212)
120
+ "PLR09", # Adding a limit to complexity is too arbitrary
121
+ ]
122
+
123
+ [tool.ruff.lint.per-file-ignores]
124
+ "tests/**" = [
125
+ "FBT001", # Test arguments are handled by pytest
126
+ "PLR2004", # Tests should contain magic number comparisons
127
+ "S101", # Pytest needs assert statements
128
+ ]
129
+
130
+ [tool.ruff.lint.flake8-tidy-imports]
131
+ ban-relative-imports = "all"
132
+
133
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
134
+ "mock".msg = "Use unittest.mock"
135
+ "pytz".msg = "Use zoneinfo"
136
+
137
+ [tool.ruff.lint.isort]
138
+ combine-as-imports = true
139
+ forced-separate = [
140
+ "tests",
141
+ ]
142
+ split-on-trailing-comma = false
143
+
144
+ [tool.pytest]
145
+ minversion = "9.0"
146
+ strict = true
147
+ addopts = ["-ra", "-v"]
148
+ testpaths = [
149
+ "tests",
150
+ ]
151
+
152
+ [tool.coverage.run]
153
+ branch = true
154
+ source = [
155
+ "src/",
156
+ ]
157
+ data_file = ".cov_cache/coverage.dat"
158
+ omit = [
159
+ "src/**/type_defs.py",
160
+ ]
161
+
162
+ [tool.coverage.report]
163
+ exclude_also = [
164
+ "if TYPE_CHECKING:",
165
+ "raise NotImplementedError",
166
+ "raise UnreachableCodeError",
167
+ ]
168
+ fail_under = 100
169
+ precision = 2
170
+ show_missing = true
171
+ skip_covered = true
172
+ skip_empty = true
@@ -0,0 +1,3 @@
1
+ from tvdb_api_client.client import TheTVDBClient
2
+
3
+ __all__ = ["TheTVDBClient"]
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("tvdb_api_client")
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from base64 import urlsafe_b64decode
5
+ from http import HTTPStatus
6
+ from typing import TYPE_CHECKING, cast
7
+
8
+ import requests
9
+ from dj_settings import get_setting
10
+ from pyutilkit.date_utils import now
11
+
12
+ from tvdb_api_client.constants import BASE_API_URL
13
+ from tvdb_api_client.models import Episode, Series
14
+
15
+ if TYPE_CHECKING:
16
+ from tvdb_api_client.lib.type_defs import (
17
+ AbstractCache,
18
+ CleanedEpisodeData,
19
+ FullEpisodeRawData,
20
+ SeriesRawData,
21
+ )
22
+
23
+
24
+ class _Cache(dict): # type: ignore[type-arg]
25
+ def set(self, key: str, value: object) -> None:
26
+ self[key] = value
27
+
28
+
29
+ class TheTVDBClient:
30
+ __slots__ = ("_auth_data", "_cache")
31
+
32
+ def __init__(
33
+ self,
34
+ api_key: str | None = None,
35
+ cache: AbstractCache | None = None,
36
+ pin: str | None = None,
37
+ ) -> None:
38
+ self._cache = cache or _Cache()
39
+ self._auth_data = self._get_auth_data(api_key, pin)
40
+
41
+ @staticmethod
42
+ def _get_auth_data(
43
+ api_key: str | None = None, pin: str | None = None
44
+ ) -> dict[str, str]:
45
+ filename = "the_tvdb.yaml"
46
+ sections = ["client"]
47
+
48
+ if api_key is None:
49
+ api_key = get_setting(
50
+ "api_key",
51
+ filename=filename,
52
+ sections=sections,
53
+ use_env="TVDB_API_KEY_V4",
54
+ )
55
+ if api_key is None:
56
+ msg = "API Key is required." # type: ignore[unreachable]
57
+ raise ValueError(msg)
58
+ output = {"apikey": api_key}
59
+
60
+ if pin is None:
61
+ pin = get_setting(
62
+ "pin", filename=filename, sections=sections, use_env="TVDB_PIN_V4"
63
+ )
64
+ if pin is not None:
65
+ output["pin"] = pin
66
+
67
+ return output
68
+
69
+ @staticmethod
70
+ def _get_expiry(token: str | None) -> int:
71
+ if token is None:
72
+ return 0
73
+
74
+ _, payload, *_ = token.split(".")
75
+ padding = "=" * (4 - len(payload) % 4)
76
+ data = json.loads(urlsafe_b64decode(payload + padding).decode())
77
+ return cast("int", data["exp"])
78
+
79
+ def _generate_token(self) -> str:
80
+ login_endpoint = "login"
81
+ url = BASE_API_URL.join(login_endpoint)
82
+ headers = {"Content-Type": "application/json", "accept": "application/json"}
83
+ response = requests.post(
84
+ url.string,
85
+ headers=headers,
86
+ data=json.dumps(self._auth_data),
87
+ timeout=(60, 120),
88
+ )
89
+ if response.status_code == HTTPStatus.UNAUTHORIZED:
90
+ msg = "Invalid credentials."
91
+ raise ConnectionRefusedError(msg)
92
+
93
+ if response.status_code != HTTPStatus.OK:
94
+ msg = "Unexpected Response."
95
+ raise ConnectionError(msg)
96
+
97
+ return cast("str", response.json()["data"]["token"])
98
+
99
+ def get(self, path: str) -> dict[str, object]:
100
+ url = BASE_API_URL.join(path)
101
+ cache_token_key = "tvdb_v4_token" # noqa: S105
102
+ token = cast("str | None", self._cache.get(cache_token_key))
103
+ if self._get_expiry(token) < now().timestamp() + 60:
104
+ token = self._generate_token()
105
+ self._cache.set(cache_token_key, token)
106
+
107
+ headers = {"accept": "application/json", "Authorization": f"Bearer {token}"}
108
+ response = requests.get(url.string, headers=headers, timeout=(60, 120))
109
+
110
+ if response.status_code == HTTPStatus.OK:
111
+ return cast("dict[str, object]", response.json())
112
+
113
+ if response.status_code in {HTTPStatus.BAD_REQUEST, HTTPStatus.NOT_FOUND}:
114
+ msg = "There are no data for this term."
115
+ raise LookupError(msg)
116
+
117
+ if response.status_code == HTTPStatus.UNAUTHORIZED:
118
+ msg = "Invalid credentials."
119
+ raise ConnectionRefusedError(msg)
120
+
121
+ msg = "Unexpected Response."
122
+ raise ConnectionError(msg)
123
+
124
+ def get_raw_series_by_id(
125
+ self, tvdb_id: int, *, refresh_cache: bool = False
126
+ ) -> SeriesRawData:
127
+ """Get the series info by its tvdb ib as returned by the TVDB."""
128
+ key = f"get_series_by_id::tvdb_id:{tvdb_id}"
129
+ data = cast("SeriesRawData | None", self._cache.get(key))
130
+ if data is None or refresh_cache:
131
+ path = f"series/{tvdb_id}"
132
+ data = cast("SeriesRawData", self.get(path)["data"])
133
+ self._cache.set(key, data)
134
+ return data
135
+
136
+ def get_series_by_id(self, tvdb_id: int, *, refresh_cache: bool = False) -> Series:
137
+ raw_data = self.get_raw_series_by_id(tvdb_id, refresh_cache=refresh_cache)
138
+ return Series.from_raw_data(raw_data)
139
+
140
+ def get_raw_episodes_by_series(
141
+ self,
142
+ tvdb_id: int,
143
+ season_type: str = "default",
144
+ page: int = 0,
145
+ *,
146
+ refresh_cache: bool = False,
147
+ ) -> CleanedEpisodeData:
148
+ """Get all the episodes for a TV series as returned by the TVDB."""
149
+ key = f"get_episodes_by_series::tvdb_id:{tvdb_id}:{page}"
150
+ data = cast("CleanedEpisodeData | None", self._cache.get(key))
151
+ if data is None or refresh_cache:
152
+ path = f"series/{tvdb_id}/episodes/{season_type}?page={page}"
153
+ all_data = cast("FullEpisodeRawData", self.get(path))
154
+ episodes = all_data["data"]["episodes"]
155
+ data = {
156
+ "episodes": episodes,
157
+ "has_next_page": all_data["links"]["next"] is not None,
158
+ }
159
+ self._cache.set(key, data)
160
+ # Redundant cast to satisfy mypy & ty compatibility
161
+ return cast("CleanedEpisodeData", data) # type: ignore[redundant-cast]
162
+
163
+ def get_episodes_by_series(
164
+ self, tvdb_id: int, season_type: str = "default", *, refresh_cache: bool = False
165
+ ) -> list[Episode]:
166
+ """Get all the episodes for a TV series."""
167
+ next_page = True
168
+ page = 0
169
+ data = []
170
+ while next_page:
171
+ raw_data = self.get_raw_episodes_by_series(
172
+ tvdb_id, season_type, refresh_cache=refresh_cache, page=page
173
+ )
174
+ data.extend(raw_data["episodes"])
175
+ next_page = raw_data["has_next_page"]
176
+ page += 1
177
+ return [Episode.from_raw_data(episode_info) for episode_info in data]
@@ -0,0 +1,5 @@
1
+ from pathurl import URL
2
+
3
+ BASE_API_URL = URL("https://api4.thetvdb.com/v4/")
4
+ DATE_FORMAT = "%Y-%m-%d"
5
+ DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol, TypedDict
4
+
5
+
6
+ class AbstractCache(Protocol):
7
+ def set(self, key: str, value: object) -> None: ...
8
+ def get(self, key: str) -> Any: ... # type: ignore[explicit-any] # noqa: ANN401
9
+
10
+
11
+ class AliasRawData(TypedDict):
12
+ language: str
13
+ name: str
14
+
15
+
16
+ class StatusRawData(TypedDict):
17
+ id: int
18
+ name: str
19
+ recordType: str
20
+ keepUpdated: bool
21
+
22
+
23
+ class SeriesRawData(TypedDict):
24
+ id: int
25
+ name: str
26
+ slug: str
27
+ image: str | None
28
+ nameTranslations: list[str]
29
+ overviewTranslations: list[str]
30
+ aliases: list[AliasRawData]
31
+ firstAired: str
32
+ lastAired: str
33
+ nextAired: str
34
+ score: float
35
+ status: StatusRawData
36
+ originalCountry: str
37
+ originalLanguage: str
38
+ defaultSeasonType: int
39
+ isOrderRandomized: bool
40
+ lastUpdated: str
41
+ averageRuntime: int
42
+ overview: str
43
+ episodes: EpisodeRawData | None
44
+ year: str
45
+
46
+
47
+ class EpisodeRawData(TypedDict):
48
+ id: int
49
+ seriesId: int
50
+ name: str
51
+ aired: str
52
+ runtime: int
53
+ nameTranslations: list[str]
54
+ overview: str
55
+ overviewTranslations: list[str]
56
+ image: str
57
+ imageType: int
58
+ isMovie: int
59
+ number: int
60
+ seasonNumber: int
61
+ lastUpdated: str
62
+ finaleType: str
63
+ seasons: int | None
64
+ absoluteNumber: int
65
+ year: str
66
+
67
+
68
+ class LinksRawData(TypedDict):
69
+ next: str | None
70
+ prev: str | None
71
+
72
+
73
+ class _FullRawData(TypedDict):
74
+ series: SeriesRawData
75
+ episodes: list[EpisodeRawData]
76
+
77
+
78
+ class CleanedEpisodeData(TypedDict):
79
+ episodes: list[EpisodeRawData]
80
+ has_next_page: bool
81
+
82
+
83
+ class FullEpisodeRawData(TypedDict):
84
+ data: _FullRawData
85
+ links: LinksRawData
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pathurl import URL
7
+
8
+ from tvdb_api_client.utils import get_tvdb_date, get_tvdb_datetime
9
+
10
+ if TYPE_CHECKING:
11
+ from datetime import date, datetime
12
+
13
+ from tvdb_api_client.lib.type_defs import (
14
+ AliasRawData,
15
+ EpisodeRawData,
16
+ SeriesRawData,
17
+ StatusRawData,
18
+ )
19
+
20
+
21
+ @dataclass
22
+ class Alias:
23
+ language: str
24
+ name: str
25
+
26
+ @classmethod
27
+ def from_raw_data(cls, raw_data: AliasRawData) -> Alias:
28
+ return cls(language=raw_data["language"], name=raw_data["name"])
29
+
30
+
31
+ @dataclass
32
+ class Status:
33
+ id: int
34
+ name: str
35
+ record_type: str
36
+ keep_updated: bool
37
+
38
+ @classmethod
39
+ def from_raw_data(cls, raw_data: StatusRawData) -> Status:
40
+ return cls(
41
+ id=raw_data["id"],
42
+ name=raw_data["name"],
43
+ record_type=raw_data["recordType"],
44
+ keep_updated=raw_data["keepUpdated"],
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class Series:
50
+ id: int
51
+ name: str
52
+ slug: str
53
+ image: URL | None
54
+ name_translations: list[str]
55
+ overview_translations: list[str]
56
+ aliases: list[Alias]
57
+ first_aired: date | None
58
+ last_aired: date | None
59
+ next_aired: date | None
60
+ score: float
61
+ status: Status
62
+ original_country: str
63
+ original_language: str
64
+ default_season_type: int
65
+ is_order_randomized: bool
66
+ last_updated: datetime | None
67
+ average_runtime: int
68
+ overview: str
69
+
70
+ @classmethod
71
+ def from_raw_data(cls, raw_data: SeriesRawData) -> Series:
72
+ image_url = raw_data["image"]
73
+ return cls(
74
+ id=raw_data["id"],
75
+ name=raw_data["name"],
76
+ slug=raw_data["slug"],
77
+ image=URL(image_url) if image_url else None,
78
+ name_translations=raw_data["nameTranslations"],
79
+ overview_translations=raw_data["overviewTranslations"],
80
+ aliases=[Alias.from_raw_data(alias) for alias in raw_data["aliases"]],
81
+ first_aired=get_tvdb_date(raw_data["firstAired"]),
82
+ last_aired=get_tvdb_date(raw_data["lastAired"]),
83
+ next_aired=get_tvdb_date(raw_data["nextAired"]),
84
+ score=raw_data["score"],
85
+ status=Status.from_raw_data(raw_data["status"]),
86
+ original_country=raw_data["originalCountry"],
87
+ original_language=raw_data["originalLanguage"],
88
+ default_season_type=raw_data["defaultSeasonType"],
89
+ is_order_randomized=raw_data["isOrderRandomized"],
90
+ last_updated=get_tvdb_datetime(raw_data["lastUpdated"]),
91
+ average_runtime=raw_data["averageRuntime"],
92
+ overview=raw_data["overview"],
93
+ )
94
+
95
+
96
+ @dataclass
97
+ class Episode:
98
+ id: int
99
+ series_id: int
100
+ name: str
101
+ aired: date | None
102
+ runtime: int
103
+ name_translations: list[str]
104
+ overview: str
105
+ overview_translations: list[str]
106
+ image: URL | None
107
+ image_type: int
108
+ is_movie: int
109
+ number: int
110
+ season_number: int
111
+ last_updated: datetime | None
112
+ finale_type: str
113
+
114
+ @classmethod
115
+ def from_raw_data(cls, raw_data: EpisodeRawData) -> Episode:
116
+ image_url = raw_data["image"]
117
+ return cls(
118
+ id=raw_data["id"],
119
+ series_id=raw_data["seriesId"],
120
+ name=raw_data["name"],
121
+ aired=get_tvdb_date(raw_data["aired"]),
122
+ runtime=raw_data["runtime"],
123
+ name_translations=raw_data["nameTranslations"],
124
+ overview=raw_data["overview"],
125
+ overview_translations=raw_data["overviewTranslations"],
126
+ image=URL(image_url) if image_url else None,
127
+ image_type=raw_data["imageType"],
128
+ is_movie=raw_data["isMovie"],
129
+ number=raw_data["number"],
130
+ season_number=raw_data["seasonNumber"],
131
+ last_updated=get_tvdb_datetime(raw_data["lastUpdated"]),
132
+ finale_type=raw_data["finaleType"],
133
+ )
File without changes
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime
4
+
5
+ from pyutilkit.date_utils import add_timezone
6
+
7
+ from tvdb_api_client.constants import DATE_FORMAT, DATETIME_FORMAT
8
+
9
+
10
+ def get_tvdb_date(date_string: str) -> date | None:
11
+ if not date_string:
12
+ return None
13
+
14
+ naive_datetime = datetime.strptime(date_string, DATE_FORMAT) # noqa: DTZ007
15
+ return add_timezone(naive_datetime).date()
16
+
17
+
18
+ def get_tvdb_datetime(datetime_string: str) -> datetime | None:
19
+ if not datetime_string:
20
+ return None
21
+
22
+ naive_datetime = datetime.strptime(datetime_string, DATETIME_FORMAT) # noqa: DTZ007
23
+ return add_timezone(naive_datetime)