anime-sama-api 0.2.2.post2__tar.gz → 0.2.2.post4__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 (42) hide show
  1. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/PKG-INFO +6 -2
  2. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/README.md +4 -1
  3. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/TODO.md +15 -13
  4. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/__init__.py +3 -3
  5. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/catalogue.py +3 -4
  6. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/__main__.py +24 -14
  7. anime_sama_api-0.2.2.post4/anime_sama_api/cli/config.py +119 -0
  8. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/downloader.py +42 -46
  9. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/episode_extra_info.py +29 -5
  10. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/internal_player.py +4 -4
  11. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/play_menu.py +1 -1
  12. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/utils.py +2 -2
  13. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/episode.py +3 -3
  14. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/langs.py +1 -2
  15. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/season.py +6 -6
  16. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/top_level.py +17 -8
  17. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/utils.py +8 -2
  18. anime_sama_api-0.2.2.post4/debug_vidmoly.py +42 -0
  19. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/pyproject.toml +3 -2
  20. anime_sama_api-0.2.2.post4/tests/data/catalogue_data.py +6 -0
  21. anime_sama_api-0.2.2.post4/tests/data/season_data.py +32 -0
  22. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_download.py +1 -1
  23. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_top_level.py +1 -1
  24. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/uv.lock +33 -22
  25. anime_sama_api-0.2.2.post2/anime_sama_api/cli/config.py +0 -110
  26. anime_sama_api-0.2.2.post2/tests/data/catalogue_data.py +0 -6
  27. anime_sama_api-0.2.2.post2/tests/data/season_data.py +0 -32
  28. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/.github/workflows/ci.yaml +0 -0
  29. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/.github/workflows/release.yaml +0 -0
  30. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/.gitignore +0 -0
  31. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/.python-version +0 -0
  32. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/LICENSE +0 -0
  33. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/config.toml +0 -0
  34. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/anime_sama_api/cli/error_handeling.py +0 -0
  35. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/exemples/new_episodes_rss.py +0 -0
  36. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/exemples/run_cli_(for_debugging).py +0 -0
  37. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/__init__.py +0 -0
  38. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/data/episode_data.py +0 -0
  39. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_catalogue.py +0 -0
  40. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_cli_utils.py +0 -0
  41. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_seasons.py +0 -0
  42. {anime_sama_api-0.2.2.post2 → anime_sama_api-0.2.2.post4}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anime-sama_api
3
- Version: 0.2.2.post2
3
+ Version: 0.2.2.post4
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>
@@ -21,6 +21,7 @@ Classifier: Topic :: System :: Archiving
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: httpx>=0.28.1
23
23
  Provides-Extra: cli
24
+ Requires-Dist: platformdirs>=4.5.1; extra == 'cli'
24
25
  Requires-Dist: rich>=14.2.0; extra == 'cli'
25
26
  Requires-Dist: tomli>=2.2.1; (python_full_version < '3.11') and extra == 'cli'
26
27
  Requires-Dist: yt-dlp>=2025.8.22; extra == 'cli'
@@ -47,7 +48,10 @@ anime-sama
47
48
  ```
48
49
 
49
50
  ## Configuration
50
- You can customize the config at `~/.config/anime-sama_cli/config.toml` for macOS/Linux and at `%USER%/AppData/Local/anime-sama_cli/config.toml` for Windows.
51
+ You can customize the config at :
52
+ - Linux: `~/.config/anime-sama_cli/config.toml`
53
+ - Windows: `%USER%/AppData/Local/anime-sama_cli/config.toml`
54
+ - macOS: `~/Library/Application Support/anime-sama_api/config.toml`
51
55
 
52
56
  # For developers
53
57
  ## Requirements
@@ -19,7 +19,10 @@ anime-sama
19
19
  ```
20
20
 
21
21
  ## Configuration
22
- You can customize the config at `~/.config/anime-sama_cli/config.toml` for macOS/Linux and at `%USER%/AppData/Local/anime-sama_cli/config.toml` for Windows.
22
+ You can customize the config at :
23
+ - Linux: `~/.config/anime-sama_cli/config.toml`
24
+ - Windows: `%USER%/AppData/Local/anime-sama_cli/config.toml`
25
+ - macOS: `~/Library/Application Support/anime-sama_api/config.toml`
23
26
 
24
27
  # For developers
25
28
  ## Requirements
@@ -1,19 +1,19 @@
1
- - [X] Search
2
- - [X] Download
3
- - [X] Play
4
- - [X] Name Episode
5
- - [X] All language
6
- - [X] Order Episode
7
- - [X] Separate backend
8
- - [X] Fallback on other players
9
- - [X] Retry players
10
- - [X] Config
1
+ - [x] Search
2
+ - [x] Download
3
+ - [x] Play
4
+ - [x] Name Episode
5
+ - [x] All language
6
+ - [x] Order Episode
7
+ - [x] Separate backend
8
+ - [x] Fallback on other players
9
+ - [x] Retry players
10
+ - [x] Config
11
11
  - [ ] Tests (ie: search that doesn't exist)
12
- - [X] Make a package
12
+ - [x] Make a package
13
13
  - [ ] Implement all NotImplemented
14
- - [ ] Auto detect download folder and config folder, see https://stackoverflow.com/questions/35851281/python-finding-the-users-downloads-folder and appdirs (pypi)
14
+ - [x] Auto detect download folder and config folder, see https://stackoverflow.com/questions/35851281/python-finding-the-users-downloads-folder and appdirs (pypi)
15
15
  - [ ] Add new {} for episode_path
16
- - [ ] Select a range of seasons to download
16
+ - [x] Select a range of seasons to download
17
17
  - [ ] Take args in cli
18
18
  - [ ] Cache players link for offline use
19
19
  - [ ] Nix?
@@ -26,3 +26,5 @@
26
26
  - [ ] MAL sync
27
27
  - [ ] Play from download
28
28
  - [ ] Discord presence
29
+
30
+ - [ ] Seven deadly sins S1 EP 26 "Episode 26 interrupted (ERROR: unable to download video data: HTTP Error 416: Requested Range Not Satisfiable). Retrying in 256s."
@@ -1,8 +1,8 @@
1
- from .top_level import AnimeSama, find_site_url
2
1
  from .catalogue import Catalogue
3
- from .season import Season
4
2
  from .episode import Episode, Languages, Players
5
- from .langs import Lang, LangId, lang2ids, id2lang, flags
3
+ from .langs import Lang, LangId, flags, id2lang, lang2ids
4
+ from .season import Season
5
+ from .top_level import AnimeSama, find_site_url
6
6
 
7
7
  try:
8
8
  from .cli.__main__ import main
@@ -1,13 +1,12 @@
1
- from collections.abc import Sequence
2
1
  import re
2
+ from collections.abc import Sequence
3
3
  from typing import Any, Literal, cast
4
4
 
5
5
  from httpx import AsyncClient
6
6
 
7
- from .utils import remove_some_js_comments
7
+ from .langs import Lang, flags
8
8
  from .season import Season
9
- from .langs import flags, Lang
10
-
9
+ from .utils import remove_some_js_comments
11
10
 
12
11
  # Oversight from anime-sama that we should handle
13
12
  # 'Animes' instead of 'Anime' seen in Cyberpunk: Edgerunners and Valkyrie Apocalypse
@@ -7,12 +7,11 @@ from rich import get_console
7
7
  from rich.logging import RichHandler
8
8
  from rich.status import Status
9
9
 
10
- from . import downloader, internal_player
11
- from .config import config
12
- from .episode_extra_info import convert_with_extra_info
13
- from .utils import safe_input, select_one, select_range
14
-
15
- from ..top_level import AnimeSama, find_site_url
10
+ from anime_sama_api.cli import downloader, internal_player
11
+ from anime_sama_api.cli.config import config
12
+ from anime_sama_api.cli.episode_extra_info import convert_with_extra_info
13
+ from anime_sama_api.cli.utils import safe_input, select_one, select_range
14
+ from anime_sama_api.top_level import AnimeSama, find_site_url
16
15
 
17
16
  console = get_console()
18
17
  console._highlight = False
@@ -37,16 +36,27 @@ async def async_main() -> None:
37
36
  catalogue = select_one(catalogues)
38
37
 
39
38
  with spinner(f"Getting season list for [blue]{catalogue.name}"):
40
- seasons = await catalogue.seasons()
41
- season = select_one(seasons)
39
+ seasons_list = await catalogue.seasons()
42
40
 
43
- with spinner(f"Getting episode list for [blue]{season.name}"):
44
- episodes = await season.episodes()
41
+ selected_seasons = select_range(seasons_list, msg="Choose season(s)")
45
42
 
46
- console.print(f"\n[cyan bold underline]{season.serie_name} - {season.name}")
47
- selected_episodes = select_range(
48
- episodes, msg="Choose episode(s)", print_choices=True
49
- )
43
+ if len(selected_seasons) > 1:
44
+ # Multi-season automatic download
45
+ selected_episodes = []
46
+ for s in selected_seasons:
47
+ with spinner(f"Getting episode list for [blue]{s.name}"):
48
+ episodes = await s.episodes()
49
+ selected_episodes.extend(episodes)
50
+ else:
51
+ # Single season behavior (manual episode selection)
52
+ season = selected_seasons[0]
53
+ with spinner(f"Getting episode list for [blue]{season.name}"):
54
+ episodes = await season.episodes()
55
+
56
+ console.print(f"\n[cyan bold underline]{season.serie_name} - {season.name}")
57
+ selected_episodes = select_range(
58
+ episodes, msg="Choose episode(s)", print_choices=True
59
+ )
50
60
 
51
61
  if config.download:
52
62
  downloader.multi_download(
@@ -0,0 +1,119 @@
1
+ # TODO: Allway hard to read but I can't find a better way to do it
2
+ import logging
3
+ import sys
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ # import platformdirs for config_dir and download_dir
8
+ from platformdirs import user_config_dir, user_downloads_dir
9
+
10
+ from anime_sama_api.langs import Lang, lang2ids
11
+
12
+ if sys.version_info >= (3, 11):
13
+ import tomllib
14
+ else:
15
+ import tomli as tomllib
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class PlayersConfig:
22
+ prefers: list[str] = field(default_factory=list)
23
+ bans: list[str] = field(default_factory=list)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Config:
28
+ prefer_languages: list[Lang]
29
+ download_path: Path
30
+ episode_path: str
31
+ download: bool
32
+ show_players: bool
33
+ max_retry_time: int
34
+ format: str
35
+ format_sort: str
36
+ internal_player_command: list[str]
37
+ url: str
38
+ provider_url: str
39
+ players_config: PlayersConfig
40
+ concurrent_downloads: dict[str, int]
41
+
42
+
43
+ def load_config() -> Config:
44
+ # 1. Load default config from the package
45
+ default_config_file = Path(__file__).parent / "config.toml"
46
+ if not default_config_file.exists():
47
+ raise FileNotFoundError(
48
+ f"The default config.toml could not be found at {default_config_file}"
49
+ )
50
+
51
+ with open(default_config_file, "rb") as f:
52
+ config_dict = tomllib.load(f)
53
+
54
+ # 2. Determine user config path
55
+ # Order of priority:
56
+ # - current directory (./config.toml)
57
+ # - system config directory (e.g. ~/AppData/Local/anime-sama_api/config.toml)
58
+ local_config = Path("config.toml")
59
+ system_config_dir = Path(user_config_dir("anime-sama_api", appauthor=False))
60
+ system_config_file = system_config_dir / "config.toml"
61
+
62
+ user_config_data = {}
63
+ if local_config.exists():
64
+ with open(local_config, "rb") as f:
65
+ user_config_data = tomllib.load(f)
66
+ elif system_config_file.exists():
67
+ with open(system_config_file, "rb") as f:
68
+ user_config_data = tomllib.load(f)
69
+ else:
70
+ # Recreate default config if not found anywhere
71
+ from shutil import copy
72
+
73
+ system_config_dir.mkdir(parents=True, exist_ok=True)
74
+ copy(default_config_file, system_config_file)
75
+ logger.info("Default config created at %s", system_config_file)
76
+
77
+ # 3. Merge configs (user values override defaults)
78
+ config_dict.update(user_config_data)
79
+
80
+ # 4. Backward compatibility and data cleaning
81
+ # Languages conversion
82
+ new_langs: list[Lang] = []
83
+ for lang in config_dict.get("prefer_languages", []):
84
+ if lang == "VO":
85
+ lang = "VOSTFR"
86
+ if lang in lang2ids:
87
+ new_langs.append(lang)
88
+ else:
89
+ logger.warning("'%s' is not a valid language, ignoring it.", lang)
90
+ config_dict["prefer_languages"] = new_langs
91
+
92
+ # Path detection and expansion
93
+ raw_path = config_dict.get("download_path")
94
+ if not raw_path:
95
+ # Default to system Downloads / Anime-Sama if not specified
96
+ config_dict["download_path"] = Path(user_downloads_dir()) / "Anime-Sama"
97
+ else:
98
+ config_dict["download_path"] = Path(raw_path).expanduser()
99
+
100
+ # Internal player command string to list
101
+ player_cmd = config_dict.get("internal_player_command")
102
+ if isinstance(player_cmd, str):
103
+ config_dict["internal_player_command"] = player_cmd.split()
104
+ elif player_cmd is None:
105
+ config_dict["internal_player_command"] = []
106
+
107
+ # Players config mapping
108
+ players_data = config_dict.pop("players_hostname", {})
109
+ if "players" in config_dict: # Old key removal
110
+ del config_dict["players"]
111
+ config_dict["players_config"] = PlayersConfig(**players_data)
112
+
113
+ # Ensure all required keys are present for Config dataclass
114
+ # (Checking against keys in Config.__annotations__ if necessary)
115
+ return Config(**config_dict)
116
+
117
+
118
+ # Exported config instance
119
+ config = load_config()
@@ -1,35 +1,33 @@
1
+ import logging
1
2
  import random
2
3
  import time
3
- import logging
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
  from pathlib import Path
6
6
  from typing import cast
7
7
  from urllib.parse import urlparse
8
8
 
9
- import httpx
10
- from yt_dlp import YoutubeDL
11
- from yt_dlp.utils import DownloadError
12
9
  from rich import get_console
13
- from rich.live import Live
14
10
  from rich.console import Group
15
- from rich.table import Column
11
+ from rich.live import Live
16
12
  from rich.progress import (
17
13
  BarColumn,
14
+ MofNCompleteColumn,
18
15
  Progress,
16
+ ProgressColumn,
17
+ TaskID,
19
18
  TextColumn,
20
19
  TimeRemainingColumn,
21
- TransferSpeedColumn,
22
- MofNCompleteColumn,
23
- TaskID,
24
20
  TotalFileSizeColumn,
25
- ProgressColumn,
21
+ TransferSpeedColumn,
26
22
  )
23
+ from rich.table import Column
24
+ from yt_dlp import YoutubeDL
25
+ from yt_dlp.utils import DownloadError
27
26
 
28
- from .episode_extra_info import EpisodeWithExtraInfo
29
- from .error_handeling import YDL_log_filter, reaction_to
30
- from ..langs import Lang
31
- from .config import PlayersConfig, config
32
-
27
+ from anime_sama_api.langs import Lang
28
+ 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
33
31
 
34
32
  logger = logging.getLogger(__name__)
35
33
  logger.addFilter(YDL_log_filter)
@@ -69,13 +67,18 @@ def download(
69
67
  episode: EpisodeWithExtraInfo,
70
68
  path: Path,
71
69
  episode_path: str = "{episode}",
72
- prefer_languages: list[Lang] = ["VOSTFR"],
73
- players_config: PlayersConfig = PlayersConfig([], []),
70
+ prefer_languages: list[Lang] | None = None,
71
+ players_config: PlayersConfig | None = None,
74
72
  concurrent_fragment_downloads: int = 3,
75
73
  max_retry_time: int = 1024,
76
- format: str = "",
74
+ video_format: str = "",
77
75
  format_sort: str = "",
78
76
  ) -> None:
77
+ if prefer_languages is None:
78
+ prefer_languages = ["VOSTFR"]
79
+ if players_config is None:
80
+ players_config = PlayersConfig([], [])
81
+
79
82
  if not any(episode.warpped.languages.values()):
80
83
  logger.error("No player available")
81
84
  return
@@ -83,7 +86,7 @@ def download(
83
86
  me = download_progress.add_task(
84
87
  "download", episode_name=episode.warpped.name, site="", total=None
85
88
  )
86
- task = download_progress.tasks[me]
89
+ task = next(t for t in download_progress.tasks if t.id == me)
87
90
 
88
91
  full_path = (
89
92
  path
@@ -108,7 +111,7 @@ def download(
108
111
  "concurrent_fragment_downloads": concurrent_fragment_downloads,
109
112
  "progress_hooks": [hook],
110
113
  "logger": logger,
111
- "format": format,
114
+ "format": video_format,
112
115
  "format_sort": format_sort.split(","),
113
116
  }
114
117
 
@@ -120,17 +123,6 @@ def download(
120
123
  download_progress.update(me, site=urlparse(player).hostname)
121
124
 
122
125
  while True:
123
- # Check if the video is not accessible through vidmoly
124
- try:
125
- if (
126
- player.startswith("https://vidmoly.")
127
- and "Please wait" # Note "Please wait" appear in all the player page
128
- not in httpx.get(player, headers={"User-Agent": ""}).text
129
- ):
130
- break
131
- except httpx.ConnectError:
132
- break
133
-
134
126
  try:
135
127
  with YoutubeDL(option) as ydl: # type: ignore
136
128
  error_code = cast(int, ydl.download([player]))
@@ -139,20 +131,14 @@ def download(
139
131
  sucess = True
140
132
  else:
141
133
  logger.fatal(
142
- f"The download encountered an error code {error_code}. Please report this to the developer with URL: {player}",
134
+ "The download encountered an error code %s. Please report this to the developer with URL: %s",
135
+ error_code,
136
+ player,
143
137
  )
144
138
 
145
139
  break
146
140
 
147
141
  except DownloadError as exception:
148
- # yt-dlp thinks vidmoly is unsupported but it just need wait
149
- if (
150
- player.startswith("https://vidmoly.")
151
- and exception.msg is not None
152
- and "Unsupported URL: https://vidmoly.net/" in exception.msg
153
- ):
154
- exception.msg = "Waiting for vidmoly"
155
-
156
142
  match reaction_to(exception.msg):
157
143
  case "continue":
158
144
  break
@@ -162,7 +148,10 @@ def download(
162
148
  break
163
149
 
164
150
  logger.warning(
165
- f"{episode.warpped.name} interrupted. Retrying in {retry_time}s."
151
+ "%s interrupted (%s). Retrying in %ss.",
152
+ episode.warpped.name,
153
+ exception.msg,
154
+ retry_time,
166
155
  )
167
156
  # random is used to spread the resume time and so mitigate deadlock when multiple downloads resume at the same time
168
157
  time.sleep(retry_time * random.uniform(0.8, 1.2))
@@ -190,13 +179,20 @@ def multi_download(
190
179
  episodes: list[EpisodeWithExtraInfo],
191
180
  path: Path,
192
181
  episode_path: str = "{episode}",
193
- concurrent_downloads: dict[str, int] = {},
194
- prefer_languages: list[Lang] = ["VOSTFR"],
195
- players_config: PlayersConfig = PlayersConfig([], []),
182
+ concurrent_downloads: dict[str, int] | None = None,
183
+ prefer_languages: list[Lang] | None = None,
184
+ players_config: PlayersConfig | None = None,
196
185
  max_retry_time: int = 1024,
197
- format: str = "",
186
+ video_format: str = "",
198
187
  format_sort: str = "",
199
188
  ) -> None:
189
+ if concurrent_downloads is None:
190
+ concurrent_downloads = {}
191
+ if prefer_languages is None:
192
+ prefer_languages = ["VOSTFR"]
193
+ if players_config is None:
194
+ players_config = PlayersConfig([], [])
195
+
200
196
  """
201
197
  Not sure if you can use this function multiple times
202
198
  """
@@ -215,6 +211,6 @@ def multi_download(
215
211
  players_config,
216
212
  concurrent_downloads.get("fragment", 1),
217
213
  max_retry_time,
218
- format,
214
+ video_format,
219
215
  format_sort,
220
216
  )
@@ -7,9 +7,9 @@ from typing import Any
7
7
 
8
8
  import httpx
9
9
 
10
- from ..episode import Episode
11
- from ..catalogue import Catalogue
12
- from .utils import normalize
10
+ from anime_sama_api.catalogue import Catalogue
11
+ from anime_sama_api.cli.utils import normalize
12
+ from anime_sama_api.episode import Episode
13
13
 
14
14
 
15
15
  @dataclass(frozen=True)
@@ -68,12 +68,20 @@ def _get_mal_listing(serie: Catalogue) -> None | Any:
68
68
  for name in [serie.name] + list(serie.alternative_names):
69
69
  i = 0
70
70
  while True:
71
- response = httpx.get(f"https://api.jikan.moe/v4/anime?q={name}&limit=5")
71
+ try:
72
+ response = httpx.get(
73
+ f"https://api.jikan.moe/v4/anime?q={name}&limit=5", timeout=10
74
+ )
75
+ except httpx.HTTPError:
76
+ return None
77
+
72
78
  i += 1
73
79
  if response.status_code != 429 or i > 9:
74
80
  break
75
81
 
76
- response.raise_for_status()
82
+ if response.status_code != 200:
83
+ return None
84
+
77
85
  animes = response.json().get("data", [])
78
86
 
79
87
  for anime in animes:
@@ -110,3 +118,19 @@ def _get_mal_listing(serie: Catalogue) -> None | Any:
110
118
  return anime
111
119
 
112
120
  return None
121
+
122
+
123
+ if __name__ == "__main__":
124
+ import asyncio
125
+ from anime_sama_api import find_site_url, AnimeSama
126
+
127
+ async def main():
128
+ url = await find_site_url()
129
+
130
+ if url is None:
131
+ raise
132
+
133
+ async for catalogue in AnimeSama(url).catalogues_iter():
134
+ print(catalogue.name, get_serie_release_date(catalogue))
135
+
136
+ asyncio.run(main())
@@ -1,13 +1,13 @@
1
1
  import os
2
- import sys
3
2
  import subprocess
3
+ import sys
4
4
  from pathlib import Path
5
5
 
6
6
  from rich import print
7
7
 
8
- from ..langs import Lang
9
- from .config import config
10
- from ..episode import Episode
8
+ from anime_sama_api.cli.config import config
9
+ from anime_sama_api.episode import Episode
10
+ from anime_sama_api.langs import Lang
11
11
 
12
12
 
13
13
  def open_silent_process(command: list[str]) -> subprocess.Popen[bytes]:
@@ -1,4 +1,4 @@
1
- from ..episode import Episode
1
+ from anime_sama_api.episode import Episode
2
2
 
3
3
 
4
4
  class EpisodesManager:
@@ -67,8 +67,8 @@ def select_range(
67
67
  return [choices[0]]
68
68
 
69
69
  def transform(string: str) -> list[T]:
70
- # Return all choices if the user enter '*' symbol
71
- if string == "*":
70
+ # Return all choices if the user enter '*' symbol or 'all'
71
+ if string in ("*", "all"):
72
72
  return choices
73
73
  # Else, detect the string and transform it into a list of choices
74
74
  ints_set: set[int] = set()
@@ -1,11 +1,11 @@
1
- from collections.abc import Generator, Sequence
2
- import re
3
1
  import logging
2
+ import re
3
+ from collections.abc import Generator, Sequence
4
4
  from dataclasses import dataclass
5
5
  from typing import Any
6
6
  from urllib.parse import urlparse
7
7
 
8
- from .langs import flags, Lang, LangId, id2lang, lang2ids
8
+ from .langs import Lang, LangId, flags, id2lang, lang2ids
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -1,6 +1,5 @@
1
1
  from typing import Literal
2
2
 
3
-
4
3
  Lang = Literal["VASTFR", "VCN", "VF", "VJSTFR", "VKR", "VQC", "VOSTFR"]
5
4
  LangId = Literal["va", "vcn", "vf", "vf1", "vf2", "vj", "vkr", "vqc", "vostfr"]
6
5
  FlagId = Literal["cn", "qc", "en", "pal", "kr", "fr", "jp"]
@@ -44,8 +43,8 @@ for language, language_ids in lang2ids.items():
44
43
 
45
44
 
46
45
  if __name__ == "__main__":
47
- import re
48
46
  import asyncio
47
+ import re
49
48
  from pprint import pprint
50
49
 
51
50
  import httpx
@@ -1,15 +1,15 @@
1
+ import asyncio
2
+ import re
1
3
  from ast import literal_eval
2
4
  from dataclasses import dataclass, replace
3
5
  from functools import reduce
4
- import re
5
- import asyncio
6
6
  from typing import Any, cast, get_args
7
7
 
8
8
  from httpx import AsyncClient
9
9
 
10
- from .langs import LangId, lang2ids, flagid2lang
11
- from .episode import Episode, Players, Languages
12
- from .utils import remove_some_js_comments, zip_varlen, split_and_strip
10
+ from .episode import Episode, Languages, Players
11
+ from .langs import LangId, flagid2lang, lang2ids
12
+ from .utils import remove_some_js_comments, split_and_strip, zip_varlen
13
13
 
14
14
 
15
15
  @dataclass
@@ -94,7 +94,7 @@ class Season:
94
94
  ) -> list[str]:
95
95
  functions = re.findall(
96
96
  r"resetListe\(\); *[\n\r]+\t*(.*?)}",
97
- page.html,
97
+ remove_some_js_comments(page.html),
98
98
  re.DOTALL,
99
99
  )[-1]
100
100
  functions_list = split_and_strip(functions, (";", "\n"))[:-1]
@@ -1,19 +1,18 @@
1
1
  import asyncio
2
- from collections.abc import AsyncIterator, Generator
3
- from html import unescape
4
- from dataclasses import dataclass
5
2
  import logging
6
3
  import re
4
+ from collections.abc import AsyncIterator, Generator
5
+ from dataclasses import dataclass
6
+ from html import unescape
7
7
  from typing import Any, cast
8
8
 
9
9
  from httpx import AsyncClient
10
10
 
11
+ from .catalogue import Catalogue, Category
11
12
  from .episode import Episode
12
- from .season import Season
13
13
  from .langs import Lang, flags
14
+ from .season import Season
14
15
  from .utils import filter_literal, is_Literal
15
- from .catalogue import Catalogue, Category
16
-
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
@@ -28,10 +27,20 @@ async def find_site_url(
28
27
  if response.is_error:
29
28
  return None
30
29
 
31
- match = re.search(r"href=\"(.+?)\">Accéder à Anime-Sama", response.text)
30
+ # * Sometimes need to check for the great word "anime-sama" in lowercase or uppercase but if add re.IGNORECASE it will work
31
+ match = re.search(
32
+ r"href=\"(.+?)\">Accéder à Anime-Sama", response.text, re.IGNORECASE
33
+ )
34
+
35
+ # * Ajouter un suive de redirection d'url au match au cas ou le site n'est pas a jour et redirige vers une autre url, puis garder l'url finale
32
36
 
33
37
  if match:
34
- return match.group(1) + "/"
38
+ redirected = await client.get(match.group(1), follow_redirects=False)
39
+ return (
40
+ redirected.headers["location"] + "/"
41
+ if redirected.has_redirect_location
42
+ else match.group(1)
43
+ )
35
44
 
36
45
 
37
46
  @dataclass(frozen=True)
@@ -1,7 +1,7 @@
1
1
  import re
2
- from typing import Any, TypeVar, cast, get_args
3
- from itertools import zip_longest
4
2
  from collections.abc import Callable, Generator, Iterable, Sequence
3
+ from itertools import zip_longest
4
+ from typing import Any, TypeVar, cast, get_args
5
5
 
6
6
  T = TypeVar("T")
7
7
 
@@ -43,6 +43,9 @@ def is_Literal(
43
43
  callback_when_false(value)
44
44
  return False
45
45
 
46
+ # def is_Literal(value: Any, Lit: Any) -> bool:
47
+ # return value in get_args(Lit)
48
+
46
49
 
47
50
  def filter_literal(
48
51
  iterable: Iterable[Any],
@@ -50,3 +53,6 @@ def filter_literal(
50
53
  callback_when_false: Callable[[Any], None] = lambda _: None,
51
54
  ) -> Generator[T]:
52
55
  return (value for value in iterable if is_Literal(value, Lit, callback_when_false))
56
+
57
+ # def filter_literal(iterable: Iterable[Any], Lit: T) -> Generator[T, None, None]:
58
+ # return (value for value in iterable if is_Literal(value, Lit))
@@ -0,0 +1,42 @@
1
+ """
2
+ Script de débogage pour comprendre ce que VidMoly retourne
3
+ """
4
+
5
+ import httpx
6
+ import sys
7
+
8
+ if len(sys.argv) < 2:
9
+ print("Usage: python debug_vidmoly.py <vidmoly_url>")
10
+ sys.exit(1)
11
+
12
+ url = sys.argv[1]
13
+
14
+ print(f"Fetching: {url}")
15
+ print("=" * 80)
16
+
17
+ try:
18
+ response = httpx.get(
19
+ url, headers={"User-Agent": ""}, timeout=10.0, follow_redirects=True
20
+ )
21
+ print(f"Status Code: {response.status_code}")
22
+ print(f"Final URL: {response.url}")
23
+ print("=" * 80)
24
+ print("Response Text (first 2000 chars):")
25
+ print(response.text[:2000])
26
+ print("=" * 80)
27
+
28
+ # Check specific criteria
29
+ print("\nChecking criteria:")
30
+ print(f" 'Please wait' found: {'Please wait' in response.text}")
31
+ print(
32
+ f" 'please wait' found (lowercase): {'please wait' in response.text.lower()}"
33
+ )
34
+ print(f" Response length: {len(response.text)} chars")
35
+
36
+ # Save full response to file for inspection
37
+ with open("vidmoly_response.html", "w", encoding="utf-8") as f:
38
+ f.write(response.text)
39
+ print("\nFull response saved to: vidmoly_response.html")
40
+
41
+ except Exception as e:
42
+ print(f"Error: {type(e).__name__}: {e}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "anime-sama_api"
7
- version = "0.2.2-2"
7
+ version = "0.2.2-4"
8
8
  description = "Api for anime-sama"
9
9
  authors = [
10
10
  {name = "NiniKo", email = "pcninik@gmail.com"}
@@ -39,6 +39,7 @@ Repository = "https://github.com/Sky-NiniKo/anime-sama_api/"
39
39
 
40
40
  [project.optional-dependencies]
41
41
  cli = [
42
+ "platformdirs>=4.5.1",
42
43
  "rich>=14.2.0",
43
44
  "tomli>=2.2.1 ; python_full_version < '3.11'",
44
45
  "yt-dlp>=2025.8.22",
@@ -46,7 +47,7 @@ cli = [
46
47
 
47
48
  [dependency-groups]
48
49
  dev = [
49
- "ruff>=0.14.11",
50
+ "ruff>=0.14.13",
50
51
  "mypy>=1.19.1",
51
52
  "pytest>=9.0.2",
52
53
  "pytest-asyncio>=1.3.0",
@@ -0,0 +1,6 @@
1
+ from anime_sama_api.catalogue import Catalogue
2
+
3
+
4
+ one_piece = Catalogue("https://anime-sama.tv/catalogue/one-piece/")
5
+ mha = Catalogue("https://anime-sama.tv/catalogue/my-hero-academia/")
6
+ gumball = Catalogue("https://anime-sama.tv/catalogue/le-monde-incroyable-de-gumball/")
@@ -0,0 +1,32 @@
1
+ from anime_sama_api.season import Season
2
+
3
+ one_piece = (
4
+ [
5
+ Season(f"https://anime-sama.tv/catalogue/one-piece/saison{i}/")
6
+ for i in range(1, 12)
7
+ ]
8
+ + [
9
+ Season("https://anime-sama.tv/catalogue/one-piece/film/"),
10
+ Season("https://anime-sama.tv/catalogue/one-piece/oav/"),
11
+ Season("https://anime-sama.tv/catalogue/one-piece/saison1hs/"),
12
+ Season("https://anime-sama.tv/catalogue/one-piece/kai/"),
13
+ ]
14
+ + [
15
+ Season(f"https://anime-sama.tv/catalogue/one-piece/kai{i}/")
16
+ for i in range(2, 12)
17
+ ]
18
+ )
19
+ gumball = [
20
+ Season(f"https://anime-sama.tv/catalogue/le-monde-incroyable-de-gumball/saison{i}/")
21
+ for i in range(1, 7)
22
+ ] + [
23
+ Season("https://anime-sama.tv/catalogue/le-monde-incroyable-de-gumball/saison1hs/")
24
+ ]
25
+
26
+ mha = [
27
+ Season(f"https://anime-sama.tv/catalogue/my-hero-academia/saison{i}/")
28
+ for i in range(1, 9)
29
+ ] + [
30
+ Season("https://anime-sama.tv/catalogue/my-hero-academia/film/"),
31
+ Season("https://anime-sama.tv/catalogue/my-hero-academia/oav/"),
32
+ ]
@@ -16,7 +16,7 @@ def test_download():
16
16
  vf=Players(),
17
17
  vostfr=Players(
18
18
  [
19
- "https://s22.anime-sama.si/s2/",
19
+ "https://s22.anime-sama.tv/s2/",
20
20
  ]
21
21
  ),
22
22
  ),
@@ -4,7 +4,7 @@ from anime_sama_api.top_level import AnimeSama
4
4
  from .data import catalogue_data
5
5
 
6
6
  pytest_plugins = ("pytest_asyncio",)
7
- anime_sama = AnimeSama(site_url="https://anime-sama.si/")
7
+ anime_sama = AnimeSama(site_url="https://anime-sama.tv/")
8
8
 
9
9
 
10
10
  @pytest.mark.asyncio(loop_scope="session")
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "anime-sama-api"
7
- version = "0.2.2.post1"
7
+ version = "0.2.2.post4"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "httpx" },
@@ -12,6 +12,7 @@ dependencies = [
12
12
 
13
13
  [package.optional-dependencies]
14
14
  cli = [
15
+ { name = "platformdirs" },
15
16
  { name = "rich" },
16
17
  { name = "tomli", marker = "python_full_version < '3.11'" },
17
18
  { name = "yt-dlp" },
@@ -30,6 +31,7 @@ dev = [
30
31
  [package.metadata]
31
32
  requires-dist = [
32
33
  { name = "httpx", specifier = ">=0.28.1" },
34
+ { name = "platformdirs", marker = "extra == 'cli'", specifier = ">=4.5.1" },
33
35
  { name = "rich", marker = "extra == 'cli'", specifier = ">=14.2.0" },
34
36
  { name = "tomli", marker = "python_full_version < '3.11' and extra == 'cli'", specifier = ">=2.2.1" },
35
37
  { name = "yt-dlp", marker = "extra == 'cli'", specifier = ">=2025.8.22" },
@@ -42,7 +44,7 @@ dev = [
42
44
  { name = "pytest", specifier = ">=9.0.2" },
43
45
  { name = "pytest-asyncio", specifier = ">=1.3.0" },
44
46
  { name = "pytest-cov", specifier = ">=7.0.0" },
45
- { name = "ruff", specifier = ">=0.14.11" },
47
+ { name = "ruff", specifier = ">=0.14.13" },
46
48
  { name = "yt-dlp-types", specifier = ">=0.0.18" },
47
49
  ]
48
50
 
@@ -423,6 +425,15 @@ wheels = [
423
425
  { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
424
426
  ]
425
427
 
428
+ [[package]]
429
+ name = "platformdirs"
430
+ version = "4.5.1"
431
+ source = { registry = "https://pypi.org/simple" }
432
+ sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
433
+ wheels = [
434
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
435
+ ]
436
+
426
437
  [[package]]
427
438
  name = "pluggy"
428
439
  version = "1.5.0"
@@ -502,28 +513,28 @@ wheels = [
502
513
 
503
514
  [[package]]
504
515
  name = "ruff"
505
- version = "0.14.11"
516
+ version = "0.14.13"
506
517
  source = { registry = "https://pypi.org/simple" }
507
- sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
518
+ sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" }
508
519
  wheels = [
509
- { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
510
- { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
511
- { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
512
- { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
513
- { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
514
- { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
515
- { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
516
- { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
517
- { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
518
- { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
519
- { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
520
- { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
521
- { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
522
- { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
523
- { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
524
- { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
525
- { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
526
- { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
520
+ { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" },
521
+ { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" },
522
+ { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" },
523
+ { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" },
524
+ { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" },
525
+ { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" },
526
+ { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" },
527
+ { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" },
528
+ { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" },
529
+ { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" },
530
+ { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" },
531
+ { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" },
532
+ { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" },
533
+ { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" },
534
+ { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" },
535
+ { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" },
536
+ { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" },
537
+ { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
527
538
  ]
528
539
 
529
540
  [[package]]
@@ -1,110 +0,0 @@
1
- # TODO: Code hard to read / violent error, need refactor
2
-
3
- import logging
4
- from pathlib import Path
5
- from dataclasses import dataclass
6
- import os
7
- import sys
8
-
9
- from ..langs import Lang, lang2ids
10
-
11
- if sys.version_info >= (3, 11):
12
- import tomllib
13
- else:
14
- import tomli as tomllib
15
-
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- @dataclass(frozen=True)
21
- class PlayersConfig:
22
- prefers: list[str]
23
- bans: list[str]
24
-
25
-
26
- @dataclass(frozen=True)
27
- class Config:
28
- prefer_languages: list[Lang]
29
- download_path: Path
30
- episode_path: str
31
- download: bool
32
- show_players: bool
33
- max_retry_time: int
34
- format: str
35
- format_sort: str
36
- internal_player_command: list[str]
37
- url: str
38
- provider_url: str
39
- players_config: PlayersConfig
40
- concurrent_downloads: dict[str, int]
41
-
42
-
43
- # Load default config
44
- exemple_config = Path(__file__).parent / "config.toml"
45
- if not exemple_config.exists():
46
- raise FileNotFoundError(
47
- "There is an issues with the installation of the package.\nThe exemple config cannot be found."
48
- )
49
- with open(exemple_config, "rb") as file:
50
- default_config = tomllib.load(file)
51
-
52
-
53
- # Load user config
54
- possible_path_str = ["."]
55
- possible_path_str.append(
56
- "~/AppData/Local/anime-sama_api" if os.name == "nt" else "~/.config/anime-sama_cli"
57
- )
58
- possible_path = [Path(path).expanduser() for path in possible_path_str]
59
- del possible_path_str
60
-
61
- user_config = {}
62
- for path in possible_path:
63
- config_file = path / "config.toml"
64
- if config_file.is_file():
65
- with open(config_file, "rb") as config_file_reader:
66
- user_config = tomllib.load(config_file_reader)
67
- break
68
- else:
69
- from shutil import copy
70
-
71
- possible_path[1].mkdir(parents=True, exist_ok=True)
72
- copy(exemple_config, possible_path[1])
73
- logger.info("Default config created at %s", possible_path[1])
74
- del possible_path
75
-
76
- # Update the default values by values set by the user
77
- config_dict = default_config | user_config
78
-
79
- # Check if value respect the type
80
- for index, lang in enumerate(config_dict["prefer_languages"]):
81
- # Backward compatibility
82
- if lang == "VO":
83
- config_dict["prefer_languages"][index] = "VOSTFR"
84
- lang = "VOSTFR"
85
-
86
- assert lang in lang2ids, (
87
- f"{lang} is not a valid languages for prefer_languages\nOnly the following are acceptable: {list(lang2ids.keys())}"
88
- )
89
-
90
- # Convert type
91
- config_dict["download_path"] = (
92
- Path(config_dict["download_path"])
93
- if config_dict.get("download_path") is not None
94
- else ""
95
- )
96
- config_dict["internal_player_command"] = (
97
- config_dict["internal_player_command"].split()
98
- if config_dict.get("internal_player_command") is not None
99
- else ""
100
- )
101
- config_dict["players_config"] = (
102
- PlayersConfig(**config_dict["players_hostname"])
103
- if config_dict.get("players_hostname") is not None
104
- else PlayersConfig([], [])
105
- )
106
- del config_dict["players_hostname"]
107
- if config_dict.get("players"): # Backward compatibility
108
- del config_dict["players"]
109
- config = Config(**config_dict)
110
- del config_dict
@@ -1,6 +0,0 @@
1
- from anime_sama_api.catalogue import Catalogue
2
-
3
-
4
- one_piece = Catalogue("https://anime-sama.si/catalogue/one-piece/")
5
- mha = Catalogue("https://anime-sama.si/catalogue/my-hero-academia/")
6
- gumball = Catalogue("https://anime-sama.si/catalogue/le-monde-incroyable-de-gumball/")
@@ -1,32 +0,0 @@
1
- from anime_sama_api.season import Season
2
-
3
- one_piece = (
4
- [
5
- Season(f"https://anime-sama.si/catalogue/one-piece/saison{i}/")
6
- for i in range(1, 12)
7
- ]
8
- + [
9
- Season("https://anime-sama.si/catalogue/one-piece/film/"),
10
- Season("https://anime-sama.si/catalogue/one-piece/oav/"),
11
- Season("https://anime-sama.si/catalogue/one-piece/saison1hs/"),
12
- Season("https://anime-sama.si/catalogue/one-piece/kai/"),
13
- ]
14
- + [
15
- Season(f"https://anime-sama.si/catalogue/one-piece/kai{i}/")
16
- for i in range(2, 12)
17
- ]
18
- )
19
- gumball = [
20
- Season(f"https://anime-sama.si/catalogue/le-monde-incroyable-de-gumball/saison{i}/")
21
- for i in range(1, 7)
22
- ] + [
23
- Season("https://anime-sama.si/catalogue/le-monde-incroyable-de-gumball/saison1hs/")
24
- ]
25
-
26
- mha = [
27
- Season(f"https://anime-sama.si/catalogue/my-hero-academia/saison{i}/")
28
- for i in range(1, 9)
29
- ] + [
30
- Season("https://anime-sama.si/catalogue/my-hero-academia/film/"),
31
- Season("https://anime-sama.si/catalogue/my-hero-academia/oav/"),
32
- ]