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.
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/PKG-INFO +8 -4
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/README.md +5 -2
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/TODO.md +15 -13
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/__init__.py +4 -3
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/catalogue.py +4 -5
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/__main__.py +33 -15
- anime_sama_api-0.2.2.post3/anime_sama_api/cli/config.py +119 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/config.toml +2 -1
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/downloader.py +42 -46
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/episode_extra_info.py +36 -5
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/internal_player.py +4 -4
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/play_menu.py +1 -1
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/utils.py +2 -2
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/episode.py +3 -3
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/langs.py +5 -7
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/season.py +8 -8
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/top_level.py +27 -12
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/utils.py +8 -2
- anime_sama_api-0.2.2.post3/debug_vidmoly.py +42 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/exemples/new_episodes_rss.py +9 -4
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/pyproject.toml +4 -3
- anime_sama_api-0.2.2.post3/tests/data/catalogue_data.py +6 -0
- anime_sama_api-0.2.2.post3/tests/data/season_data.py +32 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_catalogue.py +2 -2
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_download.py +1 -1
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_top_level.py +1 -1
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/uv.lock +33 -22
- anime_sama_api-0.2.2.post1/anime_sama_api/cli/config.py +0 -109
- anime_sama_api-0.2.2.post1/tests/data/catalogue_data.py +0 -6
- anime_sama_api-0.2.2.post1/tests/data/season_data.py +0 -32
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.github/workflows/ci.yaml +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.github/workflows/release.yaml +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.gitignore +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/.python-version +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/LICENSE +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/error_handeling.py +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/exemples/run_cli_(for_debugging).py +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/__init__.py +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/data/episode_data.py +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_cli_utils.py +0 -0
- {anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/tests/test_seasons.py +0 -0
- {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.
|
|
4
|
-
Summary: Api for anime-sama
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- [
|
|
2
|
-
- [
|
|
3
|
-
- [
|
|
4
|
-
- [
|
|
5
|
-
- [
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
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
|
-
- [
|
|
12
|
+
- [x] Make a package
|
|
13
13
|
- [ ] Implement all NotImplemented
|
|
14
|
-
- [
|
|
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
|
-
- [
|
|
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,
|
|
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 .
|
|
7
|
+
from .langs import Lang, flags
|
|
8
8
|
from .season import Season
|
|
9
|
-
from .
|
|
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
|
|
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(
|
|
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
|
-
|
|
33
|
-
season = select_one(seasons)
|
|
39
|
+
seasons_list = await catalogue.seasons()
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
episodes = await season.episodes()
|
|
41
|
+
selected_seasons = select_range(seasons_list, msg="Choose season(s)")
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 = "
|
|
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.
|
|
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
|
-
|
|
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 .
|
|
29
|
-
from .
|
|
30
|
-
from
|
|
31
|
-
from .
|
|
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] =
|
|
73
|
-
players_config: 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
|
-
|
|
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
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
195
|
-
players_config: 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
|
-
|
|
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
|
-
|
|
214
|
+
video_format,
|
|
219
215
|
format_sort,
|
|
220
216
|
)
|
{anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/episode_extra_info.py
RENAMED
|
@@ -7,9 +7,9 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from .
|
|
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
|
-
|
|
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.
|
|
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())
|
{anime_sama_api-0.2.2.post1 → anime_sama_api-0.2.2.post3}/anime_sama_api/cli/internal_player.py
RENAMED
|
@@ -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
|
|
9
|
-
from .
|
|
10
|
-
from
|
|
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]:
|
|
@@ -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
|
|
8
|
+
from .langs import Lang, LangId, flags, id2lang, lang2ids
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|