anime-sama-api 0.2.2.post1__tar.gz → 0.2.2.post3__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.post1 → anime_sama_api-0.2.2.post3}/PKG-INFO +8 -4
  2. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/README.md +5 -2
  3. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/TODO.md +15 -13
  4. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/__init__.py +4 -3
  5. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/catalogue.py +4 -5
  6. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/__main__.py +33 -15
  7. anime_sama_api-0.2.2.post3/anime_sama_api/cli/config.py +119 -0
  8. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/config.toml +2 -1
  9. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/downloader.py +42 -46
  10. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/episode_extra_info.py +36 -5
  11. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/internal_player.py +4 -4
  12. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/play_menu.py +1 -1
  13. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/utils.py +2 -2
  14. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/episode.py +3 -3
  15. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/langs.py +5 -7
  16. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/season.py +8 -8
  17. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/top_level.py +27 -12
  18. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/utils.py +8 -2
  19. anime_sama_api-0.2.2.post3/debug_vidmoly.py +42 -0
  20. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/exemples/new_episodes_rss.py +9 -4
  21. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/pyproject.toml +4 -3
  22. anime_sama_api-0.2.2.post3/tests/data/catalogue_data.py +6 -0
  23. anime_sama_api-0.2.2.post3/tests/data/season_data.py +32 -0
  24. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_catalogue.py +2 -2
  25. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_download.py +1 -1
  26. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_top_level.py +1 -1
  27. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/uv.lock +33 -22
  28. anime_sama_api-0.2.2.post1/anime_sama_api/cli/config.py +0 -109
  29. anime_sama_api-0.2.2.post1/tests/data/catalogue_data.py +0 -6
  30. anime_sama_api-0.2.2.post1/tests/data/season_data.py +0 -32
  31. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.github/workflows/ci.yaml +0 -0
  32. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.github/workflows/release.yaml +0 -0
  33. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.gitignore +0 -0
  34. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.python-version +0 -0
  35. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/LICENSE +0 -0
  36. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/error_handeling.py +0 -0
  37. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/exemples/run_cli_(for_debugging).py +0 -0
  38. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/__init__.py +0 -0
  39. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/data/episode_data.py +0 -0
  40. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_cli_utils.py +0 -0
  41. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_seasons.py +0 -0
  42. {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_utils.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anime-sama_api
3
- Version: 0.2.2.post1
4
- Summary: Api for anime-sama.eu
3
+ Version: 0.2.2.post3
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>
7
7
  License-Expression: GPL-3.0-or-later
@@ -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'
@@ -29,7 +30,7 @@ Description-Content-Type: text/markdown
29
30
  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.
30
31
 
31
32
  # Anime-Sama API
32
- An API for anime-sama.eu, also provides a CLI to download videos.
33
+ An API for anime-sama, also provides a CLI to download videos.
33
34
 
34
35
  I have implemented all the features I care about. This project is now in maintenance mode.
35
36
 
@@ -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
@@ -1,7 +1,7 @@
1
1
  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.
2
2
 
3
3
  # Anime-Sama API
4
- An API for anime-sama.eu, also provides a CLI to download videos.
4
+ An API for anime-sama, also provides a CLI to download videos.
5
5
 
6
6
  I have implemented all the features I care about. This project is now in maintenance mode.
7
7
 
@@ -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
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
@@ -38,6 +38,7 @@ __all__ = [
38
38
  "download",
39
39
  "multi_download",
40
40
  "main",
41
+ "find_site_url",
41
42
  ]
42
43
 
43
44
  """__locals = locals()
@@ -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
@@ -56,7 +55,7 @@ class Catalogue:
56
55
 
57
56
  response = await self.client.get(self.url)
58
57
 
59
- if not response.is_success:
58
+ if response.is_error:
60
59
  self._page = ""
61
60
  else:
62
61
  self._page = response.text
@@ -1,16 +1,17 @@
1
1
  import asyncio
2
2
  import logging
3
+ from urllib.error import URLError
3
4
 
5
+ from httpx import AsyncClient
4
6
  from rich import get_console
5
7
  from rich.logging import RichHandler
6
8
  from rich.status import Status
7
9
 
8
- from . import downloader, internal_player
9
- from .config import config
10
- from .episode_extra_info import convert_with_extra_info
11
- from .utils import safe_input, select_one, select_range
12
-
13
- from ..top_level import AnimeSama
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
14
15
 
15
16
  console = get_console()
16
17
  console._highlight = False
@@ -24,21 +25,38 @@ def spinner(text: str) -> Status:
24
25
  async def async_main() -> None:
25
26
  query = safe_input("Anime name: \033[0;34m", str)
26
27
 
28
+ client = AsyncClient()
29
+ url = config.url or await find_site_url(client, config.provider_url)
30
+
31
+ if url is None:
32
+ raise URLError("Failed to get AnimeSama url")
33
+
27
34
  with spinner(f"Searching for [blue]{query}"):
28
- catalogues = await AnimeSama(config.url).search(query)
35
+ catalogues = await AnimeSama(url, client).search(query)
29
36
  catalogue = select_one(catalogues)
30
37
 
31
38
  with spinner(f"Getting season list for [blue]{catalogue.name}"):
32
- seasons = await catalogue.seasons()
33
- season = select_one(seasons)
39
+ seasons_list = await catalogue.seasons()
34
40
 
35
- with spinner(f"Getting episode list for [blue]{season.name}"):
36
- episodes = await season.episodes()
41
+ selected_seasons = select_range(seasons_list, msg="Choose season(s)")
37
42
 
38
- console.print(f"\n[cyan bold underline]{season.serie_name} - {season.name}")
39
- selected_episodes = select_range(
40
- episodes, msg="Choose episode(s)", print_choices=True
41
- )
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
+ )
42
60
 
43
61
  if config.download:
44
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()
@@ -21,7 +21,8 @@ format_sort = ""
21
21
  internal_player_command = "mpv"
22
22
 
23
23
  # url of anime-sama (You shouldn't touch that)
24
- url = "https://anime-sama.eu/"
24
+ url = ""
25
+ provider_url = "https://anime-sama.pw/"
25
26
 
26
27
  [concurrent_downloads]
27
28
  # how many fragment of a video to download at once
@@ -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:
@@ -87,7 +95,14 @@ def _get_mal_listing(serie: Catalogue) -> None | Any:
87
95
  continue
88
96
 
89
97
  return anime
98
+
90
99
  if name in title or title in name:
100
+ if len(anime_genres) == 0:
101
+ if len(serie.genres) != 0:
102
+ continue
103
+ else:
104
+ return anime
105
+
91
106
  # Because this condition is not a guarantee, we do an additionnal screenning base on corresponding genres
92
107
  not_corresponding_genres = [
93
108
  genre
@@ -103,3 +118,19 @@ def _get_mal_listing(serie: Catalogue) -> None | Any:
103
118
  return anime
104
119
 
105
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