anime-sama-api 0.2.2.post4__tar.gz → 0.3.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.
Files changed (49) hide show
  1. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/.github/workflows/ci.yaml +2 -2
  2. anime_sama_api-0.3.0/.python-version +1 -0
  3. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/PKG-INFO +7 -9
  4. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/README.md +1 -1
  5. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/__init__.py +9 -9
  6. anime_sama_api-0.3.0/anime_sama_api/catalogue.py +252 -0
  7. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/__main__.py +7 -6
  8. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/config.py +1 -6
  9. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/config.toml +3 -3
  10. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/downloader.py +41 -34
  11. anime_sama_api-0.2.2.post4/anime_sama_api/cli/error_handeling.py → anime_sama_api-0.3.0/anime_sama_api/cli/error_handling.py +3 -5
  12. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/utils.py +2 -2
  13. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/episode.py +4 -6
  14. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/langs.py +2 -2
  15. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/season.py +48 -21
  16. anime_sama_api-0.3.0/anime_sama_api/top_level.py +366 -0
  17. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/utils.py +8 -4
  18. {anime_sama_api-0.2.2.post4/exemples → anime_sama_api-0.3.0/examples}/new_episodes_rss.py +4 -4
  19. {anime_sama_api-0.2.2.post4/exemples → anime_sama_api-0.3.0/examples}/run_cli_(for_debugging).py +0 -1
  20. anime_sama_api-0.3.0/examples/scan_catalogue.py +26 -0
  21. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/pyproject.toml +14 -12
  22. anime_sama_api-0.3.0/tests/data/catalogue_data.py +5 -0
  23. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/data/episode_data.py +669 -573
  24. anime_sama_api-0.3.0/tests/data/season_data.py +32 -0
  25. anime_sama_api-0.3.0/tests/data/update_episode_data.py +11 -0
  26. anime_sama_api-0.3.0/tests/test_catalogue.py +83 -0
  27. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/test_cli_utils.py +15 -15
  28. anime_sama_api-0.3.0/tests/test_download.py +25 -0
  29. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/test_top_level.py +3 -2
  30. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/test_utils.py +1 -1
  31. anime_sama_api-0.3.0/uv.lock +746 -0
  32. anime_sama_api-0.2.2.post4/.python-version +0 -1
  33. anime_sama_api-0.2.2.post4/anime_sama_api/catalogue.py +0 -154
  34. anime_sama_api-0.2.2.post4/anime_sama_api/cli/episode_extra_info.py +0 -136
  35. anime_sama_api-0.2.2.post4/anime_sama_api/top_level.py +0 -250
  36. anime_sama_api-0.2.2.post4/debug_vidmoly.py +0 -42
  37. anime_sama_api-0.2.2.post4/tests/data/catalogue_data.py +0 -6
  38. anime_sama_api-0.2.2.post4/tests/data/season_data.py +0 -32
  39. anime_sama_api-0.2.2.post4/tests/test_catalogue.py +0 -35
  40. anime_sama_api-0.2.2.post4/tests/test_download.py +0 -27
  41. anime_sama_api-0.2.2.post4/uv.lock +0 -616
  42. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/.github/workflows/release.yaml +0 -0
  43. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/.gitignore +0 -0
  44. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/LICENSE +0 -0
  45. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/TODO.md +0 -0
  46. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/internal_player.py +0 -0
  47. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/anime_sama_api/cli/play_menu.py +0 -0
  48. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/__init__.py +0 -0
  49. {anime_sama_api-0.2.2.post4 → anime_sama_api-0.3.0}/tests/test_seasons.py +0 -0
@@ -11,11 +11,11 @@ jobs:
11
11
  matrix:
12
12
  os: [ubuntu-latest, windows-latest]
13
13
  python-version:
14
- - "3.10"
15
14
  - "3.11"
16
15
  - "3.12"
17
16
  - "3.13"
18
17
  - "3.14"
18
+ - "3.15"
19
19
 
20
20
  name: Test
21
21
  runs-on: ${{ matrix.os }}
@@ -26,7 +26,7 @@ jobs:
26
26
  - name: Install uv
27
27
  uses: astral-sh/setup-uv@v5
28
28
  with:
29
- version: "0.6.3"
29
+ version: "0.13.3"
30
30
  python-version: ${{ matrix.python-version }}
31
31
  enable-cache: true
32
32
  cache-dependency-glob: "uv.lock"
@@ -0,0 +1 @@
1
+ 3.11
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: anime-sama_api
3
- Version: 0.2.2.post4
3
+ Version: 0.3.0
4
4
  Summary: Api for anime-sama
5
5
  Project-URL: Repository, https://github.com/Sky-NiniKo/anime-sama_api/
6
6
  Author-email: NiniKo <pcninik@gmail.com>
@@ -12,19 +12,17 @@ Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: End Users/Desktop
13
13
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Topic :: Multimedia :: Video :: Capture
20
19
  Classifier: Topic :: System :: Archiving
21
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
22
21
  Requires-Dist: httpx>=0.28.1
23
22
  Provides-Extra: cli
24
- Requires-Dist: platformdirs>=4.5.1; extra == 'cli'
25
- Requires-Dist: rich>=14.2.0; extra == 'cli'
26
- Requires-Dist: tomli>=2.2.1; (python_full_version < '3.11') and extra == 'cli'
27
- Requires-Dist: yt-dlp>=2025.8.22; extra == 'cli'
23
+ Requires-Dist: platformdirs>=4.13.5; extra == 'cli'
24
+ Requires-Dist: rich>=15.0.0; extra == 'cli'
25
+ Requires-Dist: yt-dlp>=2026.8.19; extra == 'cli'
28
26
  Description-Content-Type: text/markdown
29
27
 
30
28
  This project is archive. I do not have the time to work on it anymore. I may make some small fixes if a breaking issue arises.
@@ -36,7 +34,7 @@ I have implemented all the features I care about. This project is now in mainten
36
34
 
37
35
  # Installation
38
36
  Requirements:
39
- - Python 3.10 or higher
37
+ - Python 3.11 or higher
40
38
 
41
39
  You can simply install it with (note that you can use tools like pipx to isolate the installation):
42
40
  ```bash
@@ -7,7 +7,7 @@ I have implemented all the features I care about. This project is now in mainten
7
7
 
8
8
  # Installation
9
9
  Requirements:
10
- - Python 3.10 or higher
10
+ - Python 3.11 or higher
11
11
 
12
12
  You can simply install it with (note that you can use tools like pipx to isolate the installation):
13
13
  ```bash
@@ -19,26 +19,26 @@ except ImportError:
19
19
 
20
20
  sys.exit(1)
21
21
 
22
- download = multi_download = main # type: ignore
22
+ download = multi_download = main
23
23
 
24
24
 
25
25
  # __package__ = "anime-sama_api"
26
26
  __all__ = [
27
27
  "AnimeSama",
28
28
  "Catalogue",
29
- "Season",
30
- "Players",
31
- "Languages",
32
29
  "Episode",
33
30
  "Lang",
34
31
  "LangId",
35
- "lang2ids",
36
- "id2lang",
37
- "flags",
32
+ "Languages",
33
+ "Players",
34
+ "Season",
38
35
  "download",
39
- "multi_download",
40
- "main",
41
36
  "find_site_url",
37
+ "flags",
38
+ "id2lang",
39
+ "lang2ids",
40
+ "main",
41
+ "multi_download",
42
42
  ]
43
43
 
44
44
  """__locals = locals()
@@ -0,0 +1,252 @@
1
+ import logging
2
+ import re
3
+ from collections.abc import Sequence
4
+ from datetime import date
5
+ from html import unescape
6
+ from typing import Literal, get_args
7
+
8
+ from httpx import AsyncClient
9
+
10
+ from .langs import Lang, flags
11
+ from .season import Season
12
+ from .utils import remove_some_js_comments
13
+
14
+ Type = Literal["Anime", "Film", "Scans", "Webtoon", "Autres"]
15
+ State = Literal["À venir", "En cours", "Terminé", "Annulé", "En pause"]
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Catalogue:
21
+ def __init__(
22
+ self,
23
+ url: str,
24
+ name: str = "",
25
+ alternative_names: Sequence[str] | None = None,
26
+ genres: Sequence[str] | None = None,
27
+ types: set[Type] | None = None,
28
+ languages: set[Lang] | None = None,
29
+ image_url: str = "",
30
+ client: AsyncClient | None = None,
31
+ ) -> None:
32
+ if alternative_names is None:
33
+ alternative_names = []
34
+ if genres is None:
35
+ genres = []
36
+ if types is None:
37
+ types = set()
38
+ if languages is None:
39
+ languages = set()
40
+
41
+ self.url = url + "/" if url[-1] != "/" else url
42
+ self.site_url = "/".join(url.split("/")[:3]) + "/"
43
+ self.client = client or AsyncClient()
44
+
45
+ self.name = name or url.split("/")[-2]
46
+
47
+ self._page: str | None = None
48
+ self.alternative_names = alternative_names
49
+ self.genres = genres
50
+ self.types = types
51
+ self.languages = languages
52
+ self.image_url = image_url
53
+
54
+ async def page(self) -> str:
55
+ if self._page is not None:
56
+ return self._page
57
+
58
+ response = await self.client.get(self.url)
59
+
60
+ if response.is_error:
61
+ self._page = ""
62
+ else:
63
+ self._page = response.text
64
+
65
+ return self._page
66
+
67
+ async def seasons(self) -> list[Season]:
68
+ page_without_comments = remove_some_js_comments(string=await self.page())
69
+
70
+ seasons = re.findall(
71
+ r'panneauAnime\("(.+?)", *"(.+?)(?:vostfr|vf)"\);', page_without_comments
72
+ )
73
+
74
+ seasons = [
75
+ Season(
76
+ url=self.url + link,
77
+ name=name,
78
+ series_name=self.name,
79
+ client=self.client,
80
+ )
81
+ for name, link in seasons
82
+ ]
83
+
84
+ return seasons
85
+
86
+ async def _info_values(self) -> dict[str, list[str]]:
87
+ values: dict[str, list[str]] = {}
88
+ page = await self.page()
89
+ info_blocks = re.findall(
90
+ r"""<dl\b[^>]*class=["'][^"']*\boe-infos\b[^"']*["'][^>]*>(.*?)</dl\s*>""",
91
+ page,
92
+ re.IGNORECASE | re.DOTALL,
93
+ )
94
+ for block in info_blocks:
95
+ for label, value in re.findall(
96
+ r"<dt\b[^>]*>(.*?)</dt\s*>\s*<dd\b[^>]*>(.*?)</dd\s*>",
97
+ block,
98
+ re.IGNORECASE | re.DOTALL,
99
+ ):
100
+ clean_label = self._clean_html(label).casefold()
101
+ values.setdefault(clean_label, []).append(self._clean_html(value))
102
+ return values
103
+
104
+ # TODO: move to utils
105
+ @staticmethod
106
+ def _clean_html(value: str) -> str:
107
+ return re.sub(r"\s+", " ", unescape(re.sub(r"<[^>]*>", " ", value))).strip()
108
+
109
+ async def news(self) -> str:
110
+ search = re.findall(r"Actualité.+?>(.+?)<", await self.page())
111
+
112
+ if not search:
113
+ return ""
114
+
115
+ return search[0].lstrip()
116
+
117
+ async def state(self) -> str:
118
+ info = await self._info_values()
119
+ states = info.get("statut", [])
120
+ if states:
121
+ if states[0] in get_args(State):
122
+ return states[0]
123
+ logger.warning(
124
+ f"Error while parsing '{states[0]}'. \nPlease report this to the developer with URL: {self.url}",
125
+ )
126
+ return ""
127
+
128
+ async def broadcasting_date(self) -> date | None:
129
+ raise NotImplementedError
130
+
131
+ info = await self._info_values()
132
+ diffusions = info.get("diffusion", [])
133
+
134
+ if not diffusions:
135
+ return
136
+
137
+ import time
138
+
139
+ return date(
140
+ *(time.strptime(diffusions[0], "%d %b. %Y")[0:3])
141
+ ) # issue with local: %b should be interpreted with french local
142
+
143
+ async def year(self) -> int | None:
144
+ info = await self._info_values()
145
+ for label in ("diffusion", "année", "année de sortie", "début"):
146
+ for value in info.get(label, []):
147
+ match = re.search(r"\b(?:19|20)\d{2}\b", value)
148
+ if match:
149
+ return int(match.group(0))
150
+
151
+ async def episode_count(self) -> int | None:
152
+ info = await self._info_values()
153
+ for value in info.get("épisodes", []):
154
+ match = re.search(r"\d+", value)
155
+ if match:
156
+ return int(match.group(0))
157
+
158
+ # Currently not provided by Anime Sama. We consider this as deprecated: TODO add a warning
159
+ async def chapter_count(self) -> int | None:
160
+ info = await self._info_values()
161
+ for value in info.get("chapitres", []):
162
+ match = re.search(r"\d+", value)
163
+ if match:
164
+ return int(match.group(0))
165
+
166
+ async def creator(self) -> str:
167
+ info = await self._info_values()
168
+ creators = info.get("auteur", [])
169
+
170
+ if not creators:
171
+ return ""
172
+
173
+ return creators[0]
174
+
175
+ async def studio(self) -> str:
176
+ info = await self._info_values()
177
+ studios = info.get("studio", [])
178
+
179
+ if not studios:
180
+ return ""
181
+
182
+ return studios[0]
183
+
184
+ async def correspondence(self) -> str:
185
+ page = await self.page()
186
+ match = re.search(
187
+ r"Correspondance\s*:.*?</strong>\s*(.*?)</span>",
188
+ page,
189
+ re.IGNORECASE | re.DOTALL,
190
+ )
191
+
192
+ if not match:
193
+ return ""
194
+
195
+ return self._clean_html(match.group(1))
196
+
197
+ async def synopsis(self) -> str:
198
+ page = await self.page()
199
+ synopsis = re.search(
200
+ r"""<p\b[^>]*id=["'][^"']*\bsynopsisText\b[^"']*["'][^>]*>(.*?)</p\s*>""",
201
+ page,
202
+ re.IGNORECASE | re.DOTALL,
203
+ )
204
+
205
+ if not synopsis:
206
+ return ""
207
+
208
+ return self._clean_html(synopsis.group(1))
209
+
210
+ async def is_mature(self) -> bool:
211
+ """Return True if the catalogue contain a warning about adult content"""
212
+ return (
213
+ re.search(
214
+ r'<div class=".*?yellow.*?">[\W\w]+?public averti', await self.page()
215
+ )
216
+ is not None
217
+ )
218
+
219
+ @property
220
+ def is_anime(self) -> bool:
221
+ return "Anime" in self.types
222
+
223
+ @property
224
+ def is_manga(self) -> bool:
225
+ return "Scans" in self.types
226
+
227
+ @property
228
+ def is_film(self) -> bool:
229
+ return "Film" in self.types
230
+
231
+ @property
232
+ def is_other(self) -> bool:
233
+ return "Autres" in self.types
234
+
235
+ @property
236
+ def fancy_name(self) -> str:
237
+ names = [""] + list(self.alternative_names) if self.alternative_names else []
238
+ return f"{self.name}[bright_black]{' - '.join(names)} {' '.join(flags[lang] for lang in self.languages if lang != 'VOSTFR')}"
239
+
240
+ def __repr__(self) -> str:
241
+ return f"Catalogue({self.url!r}, {self.name!r})"
242
+
243
+ def __str__(self) -> str:
244
+ return self.fancy_name
245
+
246
+ def __eq__(self, value: object) -> bool:
247
+ if not isinstance(value, Catalogue):
248
+ return False
249
+ return self.url == value.url
250
+
251
+ def __hash__(self) -> int:
252
+ return hash(self.url + self.name + "".join(self.alternative_names))
@@ -9,8 +9,8 @@ from rich.status import Status
9
9
 
10
10
  from anime_sama_api.cli import downloader, internal_player
11
11
  from anime_sama_api.cli.config import config
12
- from anime_sama_api.cli.episode_extra_info import convert_with_extra_info
13
12
  from anime_sama_api.cli.utils import safe_input, select_one, select_range
13
+ from anime_sama_api.season import EpisodeDataUnavailableError
14
14
  from anime_sama_api.top_level import AnimeSama, find_site_url
15
15
 
16
16
  console = get_console()
@@ -53,17 +53,14 @@ async def async_main() -> None:
53
53
  with spinner(f"Getting episode list for [blue]{season.name}"):
54
54
  episodes = await season.episodes()
55
55
 
56
- console.print(f"\n[cyan bold underline]{season.serie_name} - {season.name}")
56
+ console.print(f"\n[cyan bold underline]{season.series_name} - {season.name}")
57
57
  selected_episodes = select_range(
58
58
  episodes, msg="Choose episode(s)", print_choices=True
59
59
  )
60
60
 
61
61
  if config.download:
62
62
  downloader.multi_download(
63
- [
64
- convert_with_extra_info(episode, catalogue)
65
- for episode in selected_episodes
66
- ],
63
+ selected_episodes,
67
64
  config.download_path,
68
65
  config.episode_path,
69
66
  config.concurrent_downloads,
@@ -72,6 +69,7 @@ async def async_main() -> None:
72
69
  config.max_retry_time,
73
70
  config.format,
74
71
  config.format_sort,
72
+ [await catalogue.year()] * len(selected_episodes),
75
73
  )
76
74
  else:
77
75
  command = internal_player.play_episode(
@@ -86,6 +84,9 @@ def main() -> int:
86
84
  asyncio.run(async_main())
87
85
  except (KeyboardInterrupt, asyncio.exceptions.CancelledError, EOFError):
88
86
  console.print("\n[red]Exiting...")
87
+ except EpisodeDataUnavailableError as error:
88
+ console.print(f"\n[red]{error}")
89
+ return 1
89
90
 
90
91
  return 0
91
92
 
@@ -1,6 +1,6 @@
1
1
  # TODO: Allway hard to read but I can't find a better way to do it
2
2
  import logging
3
- import sys
3
+ import tomllib
4
4
  from dataclasses import dataclass, field
5
5
  from pathlib import Path
6
6
 
@@ -9,11 +9,6 @@ from platformdirs import user_config_dir, user_downloads_dir
9
9
 
10
10
  from anime_sama_api.langs import Lang, lang2ids
11
11
 
12
- if sys.version_info >= (3, 11):
13
- import tomllib
14
- else:
15
- import tomli as tomllib
16
-
17
12
  logger = logging.getLogger(__name__)
18
13
 
19
14
 
@@ -4,8 +4,8 @@ prefer_languages = ["VOSTFR"]
4
4
 
5
5
  # Where to put the downloads
6
6
  download_path = "~/Downloads/Anime-Sama"
7
- # How to organize the downloads (you can use {serie} {season} {episode} {release_year_parentheses})
8
- episode_path = "{serie}/{season}/{episode}"
7
+ # How to organize the downloads (you can use {series} {season} {episode} {release_year_parentheses})
8
+ episode_path = "{series}/{season}/{episode}"
9
9
 
10
10
  # If it downloads or plays the selected episodes
11
11
  download = true
@@ -13,7 +13,7 @@ show_players = false
13
13
  max_retry_time = 1024
14
14
 
15
15
  # See https://github.com/yt-dlp/yt-dlp#format-selection
16
- # For exemple, if you want the lowest size, put "+size,+br,+res,+fps" in format_sort
16
+ # For example, if you want the lowest size, put "+size,+br,+res,+fps" in format_sort
17
17
  format = "all"
18
18
  format_sort = ""
19
19
 
@@ -24,10 +24,10 @@ from rich.table import Column
24
24
  from yt_dlp import YoutubeDL
25
25
  from yt_dlp.utils import DownloadError
26
26
 
27
- from anime_sama_api.langs import Lang
28
27
  from anime_sama_api.cli.config import PlayersConfig, config
29
- from anime_sama_api.cli.episode_extra_info import EpisodeWithExtraInfo
30
- from anime_sama_api.cli.error_handeling import YDL_log_filter, reaction_to
28
+ from anime_sama_api.cli.error_handling import YDL_log_filter, reaction_to
29
+ from anime_sama_api.episode import Episode
30
+ from anime_sama_api.langs import Lang
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
  logger.addFilter(YDL_log_filter)
@@ -64,7 +64,7 @@ progress = Group(total_progress, download_progress)
64
64
 
65
65
 
66
66
  def download(
67
- episode: EpisodeWithExtraInfo,
67
+ episode: Episode,
68
68
  path: Path,
69
69
  episode_path: str = "{episode}",
70
70
  prefer_languages: list[Lang] | None = None,
@@ -73,28 +73,29 @@ def download(
73
73
  max_retry_time: int = 1024,
74
74
  video_format: str = "",
75
75
  format_sort: str = "",
76
+ ep_year: int | None = None,
76
77
  ) -> None:
77
78
  if prefer_languages is None:
78
79
  prefer_languages = ["VOSTFR"]
79
80
  if players_config is None:
80
81
  players_config = PlayersConfig([], [])
81
82
 
82
- if not any(episode.warpped.languages.values()):
83
+ if not any(episode.languages.values()):
83
84
  logger.error("No player available")
84
85
  return
85
86
 
86
87
  me = download_progress.add_task(
87
- "download", episode_name=episode.warpped.name, site="", total=None
88
+ "download", episode_name=episode.name, site="", total=None
88
89
  )
89
90
  task = next(t for t in download_progress.tasks if t.id == me)
90
91
 
91
92
  full_path = (
92
93
  path
93
94
  / episode_path.format(
94
- serie=episode.warpped.serie_name,
95
- season=episode.warpped.season_name,
96
- episode=episode.warpped.name,
97
- release_year_parentheses=episode.release_year_parentheses(),
95
+ series=episode.series_name,
96
+ season=episode.season_name,
97
+ episode=episode.name,
98
+ release_year_parentheses="" if ep_year is None else f" {ep_year}",
98
99
  )
99
100
  ).expanduser()
100
101
 
@@ -115,11 +116,11 @@ def download(
115
116
  "format_sort": format_sort.split(","),
116
117
  }
117
118
 
118
- for player in episode.warpped.consume_player(
119
+ for player in episode.consume_player(
119
120
  prefer_languages, players_config.prefers, players_config.bans
120
121
  ):
121
122
  retry_time = 1
122
- sucess = False
123
+ success = False
123
124
  download_progress.update(me, site=urlparse(player).hostname)
124
125
 
125
126
  while True:
@@ -128,7 +129,7 @@ def download(
128
129
  error_code = cast(int, ydl.download([player]))
129
130
 
130
131
  if not error_code:
131
- sucess = True
132
+ success = True
132
133
  else:
133
134
  logger.fatal(
134
135
  "The download encountered an error code %s. Please report this to the developer with URL: %s",
@@ -149,7 +150,7 @@ def download(
149
150
 
150
151
  logger.warning(
151
152
  "%s interrupted (%s). Retrying in %ss.",
152
- episode.warpped.name,
153
+ episode.name,
153
154
  exception.msg,
154
155
  retry_time,
155
156
  )
@@ -158,7 +159,7 @@ def download(
158
159
  retry_time *= 2
159
160
 
160
161
  case "crash":
161
- raise exception
162
+ raise # Reraise the same exception
162
163
 
163
164
  case "":
164
165
  logger.fatal(
@@ -167,7 +168,7 @@ def download(
167
168
  )
168
169
  break
169
170
 
170
- if sucess:
171
+ if success:
171
172
  break
172
173
 
173
174
  download_progress.update(me, visible=False)
@@ -176,7 +177,7 @@ def download(
176
177
 
177
178
 
178
179
  def multi_download(
179
- episodes: list[EpisodeWithExtraInfo],
180
+ episodes: list[Episode],
180
181
  path: Path,
181
182
  episode_path: str = "{episode}",
182
183
  concurrent_downloads: dict[str, int] | None = None,
@@ -185,6 +186,7 @@ def multi_download(
185
186
  max_retry_time: int = 1024,
186
187
  video_format: str = "",
187
188
  format_sort: str = "",
189
+ eps_years: list[int | None] | None = None,
188
190
  ) -> None:
189
191
  if concurrent_downloads is None:
190
192
  concurrent_downloads = {}
@@ -193,24 +195,29 @@ def multi_download(
193
195
  if players_config is None:
194
196
  players_config = PlayersConfig([], [])
195
197
 
198
+ # Ensure eps_years len
199
+ if eps_years is None:
200
+ eps_years = []
201
+ eps_years += [None] * (len(episodes) - len(eps_years))
202
+ eps_years = eps_years[: len(episodes)]
203
+
196
204
  """
197
205
  Not sure if you can use this function multiple times
198
206
  """
199
207
  total_progress.add_task("Downloaded", total=len(episodes))
200
- with Live(progress, console=console):
201
- with ThreadPoolExecutor(
202
- max_workers=concurrent_downloads.get("video", 1)
203
- ) as executor:
204
- for episode in episodes:
205
- executor.submit(
206
- download,
207
- episode,
208
- path,
209
- episode_path,
210
- prefer_languages,
211
- players_config,
212
- concurrent_downloads.get("fragment", 1),
213
- max_retry_time,
214
- video_format,
215
- format_sort,
216
- )
208
+ with Live(progress, console=console), ThreadPoolExecutor(
209
+ max_workers=concurrent_downloads.get("video", 1)
210
+ ) as executor:
211
+ for episode, ep_year in zip(episodes, eps_years):
212
+ executor.submit(
213
+ download,
214
+ episode,
215
+ path,
216
+ episode_path,
217
+ prefer_languages,
218
+ players_config,
219
+ concurrent_downloads.get("fragment", 1),
220
+ max_retry_time,
221
+ video_format,
222
+ format_sort,
223
+ )
@@ -53,15 +53,13 @@ def YDL_log_filter(record: LogRecord) -> bool:
53
53
 
54
54
  match record.levelname:
55
55
  case "WARNING":
56
- if any(
57
- msg in record.msg
56
+ return all(
57
+ msg not in record.msg
58
58
  for msg in (
59
59
  "Falling back on generic information extractor",
60
60
  "Live HLS streams are not supported by the native downloader.",
61
61
  )
62
- ):
63
- return False
64
- return True
62
+ )
65
63
  case "ERROR":
66
64
  return not is_error_handle(record.msg)
67
65
  case _:
@@ -1,7 +1,7 @@
1
1
  import re
2
2
  import sys
3
3
  from collections.abc import Callable
4
- from typing import Any, TypeVar, cast
4
+ from typing import Any, TypeVar
5
5
 
6
6
  from rich import print as print_func
7
7
 
@@ -82,7 +82,7 @@ def select_range(
82
82
  ints[0] = 1
83
83
  if ints[1] is None:
84
84
  ints[1] = len(choices)
85
- ints_set.update(range(cast(int, ints[0]), cast(int, ints[1]) + 1))
85
+ ints_set.update(range(ints[0], ints[1] + 1))
86
86
  else:
87
87
  raise ValueError
88
88