anime-sama-cli-api 0.4.0__py3-none-any.whl
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/__init__.py +52 -0
- anime_sama_api/catalogue.py +252 -0
- anime_sama_api/cli/__main__.py +80 -0
- anime_sama_api/cli/config.py +119 -0
- anime_sama_api/cli/config.toml +35 -0
- anime_sama_api/cli/downloader.py +323 -0
- anime_sama_api/cli/error_handling.py +66 -0
- anime_sama_api/cli/internal_player.py +56 -0
- anime_sama_api/cli/play_menu.py +61 -0
- anime_sama_api/cli/utils.py +142 -0
- anime_sama_api/episode.py +207 -0
- anime_sama_api/langs.py +76 -0
- anime_sama_api/link_resolver.py +187 -0
- anime_sama_api/season.py +226 -0
- anime_sama_api/top_level.py +376 -0
- anime_sama_api/utils.py +54 -0
- anime_sama_cli_api-0.4.0.dist-info/METADATA +103 -0
- anime_sama_cli_api-0.4.0.dist-info/RECORD +21 -0
- anime_sama_cli_api-0.4.0.dist-info/WHEEL +4 -0
- anime_sama_cli_api-0.4.0.dist-info/entry_points.txt +2 -0
- anime_sama_cli_api-0.4.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from .catalogue import Catalogue
|
|
2
|
+
from .episode import Episode, Languages, Players
|
|
3
|
+
from .langs import Lang, LangId, flags, id2lang, lang2ids
|
|
4
|
+
from .season import Season
|
|
5
|
+
from .top_level import AnimeSama, find_site_url
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from .cli.downloader import download, multi_download
|
|
9
|
+
except ImportError:
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
print(
|
|
14
|
+
"This anime-sama_api function could not run because the required "
|
|
15
|
+
"dependencies were not installed.\nMake sure you've installed "
|
|
16
|
+
"everything with: pip install 'anime-sama_api[cli]'"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
download = multi_download = main
|
|
22
|
+
else:
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
from .cli.__main__ import main as cli_main
|
|
26
|
+
|
|
27
|
+
return cli_main()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# __package__ = "anime-sama_api"
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AnimeSama",
|
|
33
|
+
"Catalogue",
|
|
34
|
+
"Episode",
|
|
35
|
+
"Lang",
|
|
36
|
+
"LangId",
|
|
37
|
+
"Languages",
|
|
38
|
+
"Players",
|
|
39
|
+
"Season",
|
|
40
|
+
"download",
|
|
41
|
+
"find_site_url",
|
|
42
|
+
"flags",
|
|
43
|
+
"id2lang",
|
|
44
|
+
"lang2ids",
|
|
45
|
+
"main",
|
|
46
|
+
"multi_download",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
"""__locals = locals()
|
|
50
|
+
for __name in __all__:
|
|
51
|
+
if not __name.startswith("__"):
|
|
52
|
+
setattr(__locals[__name], "__module__", "anime-sama_api") # noqa"""
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from datetime import date
|
|
5
|
+
from html import unescape
|
|
6
|
+
from typing import Literal, get_args
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from .langs import Lang, flags
|
|
11
|
+
from .season import Season
|
|
12
|
+
from .utils import remove_some_js_comments
|
|
13
|
+
|
|
14
|
+
Type = Literal["Anime", "Film", "Scans", "Webtoon", "Autres"]
|
|
15
|
+
State = Literal["À venir", "En cours", "Terminé", "Annulé", "En pause"]
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Catalogue:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
url: str,
|
|
24
|
+
name: str = "",
|
|
25
|
+
alternative_names: Sequence[str] | None = None,
|
|
26
|
+
genres: Sequence[str] | None = None,
|
|
27
|
+
types: set[Type] | None = None,
|
|
28
|
+
languages: set[Lang] | None = None,
|
|
29
|
+
image_url: str = "",
|
|
30
|
+
client: AsyncClient | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
if alternative_names is None:
|
|
33
|
+
alternative_names = []
|
|
34
|
+
if genres is None:
|
|
35
|
+
genres = []
|
|
36
|
+
if types is None:
|
|
37
|
+
types = set()
|
|
38
|
+
if languages is None:
|
|
39
|
+
languages = set()
|
|
40
|
+
|
|
41
|
+
self.url = url + "/" if url[-1] != "/" else url
|
|
42
|
+
self.site_url = "/".join(url.split("/")[:3]) + "/"
|
|
43
|
+
self.client = client or AsyncClient()
|
|
44
|
+
|
|
45
|
+
self.name = name or url.split("/")[-2]
|
|
46
|
+
|
|
47
|
+
self._page: str | None = None
|
|
48
|
+
self.alternative_names = alternative_names
|
|
49
|
+
self.genres = genres
|
|
50
|
+
self.types = types
|
|
51
|
+
self.languages = languages
|
|
52
|
+
self.image_url = image_url
|
|
53
|
+
|
|
54
|
+
async def page(self) -> str:
|
|
55
|
+
if self._page is not None:
|
|
56
|
+
return self._page
|
|
57
|
+
|
|
58
|
+
response = await self.client.get(self.url)
|
|
59
|
+
|
|
60
|
+
if response.is_error:
|
|
61
|
+
self._page = ""
|
|
62
|
+
else:
|
|
63
|
+
self._page = response.text
|
|
64
|
+
|
|
65
|
+
return self._page
|
|
66
|
+
|
|
67
|
+
async def seasons(self) -> list[Season]:
|
|
68
|
+
page_without_comments = remove_some_js_comments(string=await self.page())
|
|
69
|
+
|
|
70
|
+
seasons = re.findall(
|
|
71
|
+
r'panneauAnime\("(.+?)", *"(.+?)(?:vostfr|vf)"\);?', page_without_comments
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
seasons = [
|
|
75
|
+
Season(
|
|
76
|
+
url=self.url + link,
|
|
77
|
+
name=name,
|
|
78
|
+
series_name=self.name,
|
|
79
|
+
client=self.client,
|
|
80
|
+
)
|
|
81
|
+
for name, link in seasons
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
return seasons
|
|
85
|
+
|
|
86
|
+
async def _info_values(self) -> dict[str, list[str]]:
|
|
87
|
+
values: dict[str, list[str]] = {}
|
|
88
|
+
page = await self.page()
|
|
89
|
+
info_blocks = re.findall(
|
|
90
|
+
r"""<dl\b[^>]*class=["'][^"']*\boe-infos\b[^"']*["'][^>]*>(.*?)</dl\s*>""",
|
|
91
|
+
page,
|
|
92
|
+
re.IGNORECASE | re.DOTALL,
|
|
93
|
+
)
|
|
94
|
+
for block in info_blocks:
|
|
95
|
+
for label, value in re.findall(
|
|
96
|
+
r"<dt\b[^>]*>(.*?)</dt\s*>\s*<dd\b[^>]*>(.*?)</dd\s*>",
|
|
97
|
+
block,
|
|
98
|
+
re.IGNORECASE | re.DOTALL,
|
|
99
|
+
):
|
|
100
|
+
clean_label = self._clean_html(label).casefold()
|
|
101
|
+
values.setdefault(clean_label, []).append(self._clean_html(value))
|
|
102
|
+
return values
|
|
103
|
+
|
|
104
|
+
# TODO: move to utils
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _clean_html(value: str) -> str:
|
|
107
|
+
return re.sub(r"\s+", " ", unescape(re.sub(r"<[^>]*>", " ", value))).strip()
|
|
108
|
+
|
|
109
|
+
async def news(self) -> str:
|
|
110
|
+
search = re.findall(r"Actualité.+?>(.+?)<", await self.page())
|
|
111
|
+
|
|
112
|
+
if not search:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
return search[0].lstrip()
|
|
116
|
+
|
|
117
|
+
async def state(self) -> str:
|
|
118
|
+
info = await self._info_values()
|
|
119
|
+
states = info.get("statut", [])
|
|
120
|
+
if states:
|
|
121
|
+
if states[0] in get_args(State):
|
|
122
|
+
return states[0]
|
|
123
|
+
logger.warning(
|
|
124
|
+
f"Error while parsing '{states[0]}'. \nPlease report this to the developer with URL: {self.url}",
|
|
125
|
+
)
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
async def broadcasting_date(self) -> date | None:
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
|
|
131
|
+
info = await self._info_values()
|
|
132
|
+
diffusions = info.get("diffusion", [])
|
|
133
|
+
|
|
134
|
+
if not diffusions:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
import time
|
|
138
|
+
|
|
139
|
+
return date(
|
|
140
|
+
*(time.strptime(diffusions[0], "%d %b. %Y")[0:3])
|
|
141
|
+
) # issue with local: %b should be interpreted with french local
|
|
142
|
+
|
|
143
|
+
async def year(self) -> int | None:
|
|
144
|
+
info = await self._info_values()
|
|
145
|
+
for label in ("diffusion", "année", "année de sortie", "début"):
|
|
146
|
+
for value in info.get(label, []):
|
|
147
|
+
match = re.search(r"\b(?:19|20)\d{2}\b", value)
|
|
148
|
+
if match:
|
|
149
|
+
return int(match.group(0))
|
|
150
|
+
|
|
151
|
+
async def episode_count(self) -> int | None:
|
|
152
|
+
info = await self._info_values()
|
|
153
|
+
for value in info.get("épisodes", []):
|
|
154
|
+
match = re.search(r"\d+", value)
|
|
155
|
+
if match:
|
|
156
|
+
return int(match.group(0))
|
|
157
|
+
|
|
158
|
+
# Currently not provided by Anime Sama. We consider this as deprecated: TODO add a warning
|
|
159
|
+
async def chapter_count(self) -> int | None:
|
|
160
|
+
info = await self._info_values()
|
|
161
|
+
for value in info.get("chapitres", []):
|
|
162
|
+
match = re.search(r"\d+", value)
|
|
163
|
+
if match:
|
|
164
|
+
return int(match.group(0))
|
|
165
|
+
|
|
166
|
+
async def creator(self) -> str:
|
|
167
|
+
info = await self._info_values()
|
|
168
|
+
creators = info.get("auteur", [])
|
|
169
|
+
|
|
170
|
+
if not creators:
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
return creators[0]
|
|
174
|
+
|
|
175
|
+
async def studio(self) -> str:
|
|
176
|
+
info = await self._info_values()
|
|
177
|
+
studios = info.get("studio", [])
|
|
178
|
+
|
|
179
|
+
if not studios:
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
return studios[0]
|
|
183
|
+
|
|
184
|
+
async def correspondence(self) -> str:
|
|
185
|
+
page = await self.page()
|
|
186
|
+
match = re.search(
|
|
187
|
+
r"Correspondance\s*:.*?</strong>\s*(.*?)</span>",
|
|
188
|
+
page,
|
|
189
|
+
re.IGNORECASE | re.DOTALL,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not match:
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
return self._clean_html(match.group(1))
|
|
196
|
+
|
|
197
|
+
async def synopsis(self) -> str:
|
|
198
|
+
page = await self.page()
|
|
199
|
+
synopsis = re.search(
|
|
200
|
+
r"""<p\b[^>]*id=["'][^"']*\bsynopsisText\b[^"']*["'][^>]*>(.*?)</p\s*>""",
|
|
201
|
+
page,
|
|
202
|
+
re.IGNORECASE | re.DOTALL,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not synopsis:
|
|
206
|
+
return ""
|
|
207
|
+
|
|
208
|
+
return self._clean_html(synopsis.group(1))
|
|
209
|
+
|
|
210
|
+
async def is_mature(self) -> bool:
|
|
211
|
+
"""Return True if the catalogue contain a warning about adult content"""
|
|
212
|
+
return (
|
|
213
|
+
re.search(
|
|
214
|
+
r'<div class=".*?yellow.*?">[\W\w]+?public averti', await self.page()
|
|
215
|
+
)
|
|
216
|
+
is not None
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def is_anime(self) -> bool:
|
|
221
|
+
return "Anime" in self.types
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def is_manga(self) -> bool:
|
|
225
|
+
return "Scans" in self.types
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def is_film(self) -> bool:
|
|
229
|
+
return "Film" in self.types
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def is_other(self) -> bool:
|
|
233
|
+
return "Autres" in self.types
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def fancy_name(self) -> str:
|
|
237
|
+
names = [""] + list(self.alternative_names) if self.alternative_names else []
|
|
238
|
+
return f"{self.name}[bright_black]{' - '.join(names)} {' '.join(flags[lang] for lang in self.languages if lang != 'VOSTFR')}"
|
|
239
|
+
|
|
240
|
+
def __repr__(self) -> str:
|
|
241
|
+
return f"Catalogue({self.url!r}, {self.name!r})"
|
|
242
|
+
|
|
243
|
+
def __str__(self) -> str:
|
|
244
|
+
return self.fancy_name
|
|
245
|
+
|
|
246
|
+
def __eq__(self, value: object) -> bool:
|
|
247
|
+
if not isinstance(value, Catalogue):
|
|
248
|
+
return False
|
|
249
|
+
return self.url == value.url
|
|
250
|
+
|
|
251
|
+
def __hash__(self) -> int:
|
|
252
|
+
return hash(self.url + self.name + "".join(self.alternative_names))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI using the package's HTML backend and lazy API resolution."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from urllib.error import URLError
|
|
6
|
+
|
|
7
|
+
from httpx import AsyncClient, HTTPError
|
|
8
|
+
from rich import get_console
|
|
9
|
+
from rich.logging import RichHandler
|
|
10
|
+
|
|
11
|
+
from anime_sama_api.cli.downloader import download_episodes
|
|
12
|
+
from anime_sama_api.cli.utils import install_clean_prompts
|
|
13
|
+
from anime_sama_api.link_resolver import LinkError, RateLimitedError
|
|
14
|
+
from anime_sama_api.season import EpisodeDataUnavailableError
|
|
15
|
+
from anime_sama_api.top_level import AnimeSama, find_site_url
|
|
16
|
+
|
|
17
|
+
from . import utils as cli_utils
|
|
18
|
+
from .config import config
|
|
19
|
+
|
|
20
|
+
console = get_console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def async_main():
|
|
24
|
+
query = cli_utils.safe_input("Anime name: ", str)
|
|
25
|
+
async with AsyncClient(timeout=30, follow_redirects=True) as client:
|
|
26
|
+
url = config.url or await find_site_url(client, config.provider_url)
|
|
27
|
+
if not url:
|
|
28
|
+
raise URLError("Could not find the Anime-Sama domain")
|
|
29
|
+
with console.status("Searching..."):
|
|
30
|
+
catalogues = await AnimeSama(url, client).search(query)
|
|
31
|
+
if not catalogues:
|
|
32
|
+
console.print("No results found.")
|
|
33
|
+
return True
|
|
34
|
+
catalogue = cli_utils.select_one(catalogues)
|
|
35
|
+
with console.status("Loading seasons..."):
|
|
36
|
+
seasons = await catalogue.seasons()
|
|
37
|
+
if not seasons:
|
|
38
|
+
console.print("No seasons found.")
|
|
39
|
+
return True
|
|
40
|
+
selected = cli_utils.select_range(seasons, msg="Choose season(s)")
|
|
41
|
+
if not selected:
|
|
42
|
+
return True
|
|
43
|
+
episodes = []
|
|
44
|
+
for season in selected:
|
|
45
|
+
with console.status("Loading episodes: " + season.name):
|
|
46
|
+
available = await season.episodes()
|
|
47
|
+
if len(selected) == 1:
|
|
48
|
+
console.print(season.series_name + " - " + season.name, markup=False)
|
|
49
|
+
episodes = cli_utils.select_range(
|
|
50
|
+
available, msg="Choose episode(s)", print_choices=True
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
episodes.extend(available)
|
|
54
|
+
if not episodes:
|
|
55
|
+
return True
|
|
56
|
+
if config.download:
|
|
57
|
+
return await download_episodes(episodes, config, await catalogue.year())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
install_clean_prompts()
|
|
62
|
+
logging.basicConfig(
|
|
63
|
+
level=logging.INFO, format="%(message)s", handlers=[RichHandler()]
|
|
64
|
+
)
|
|
65
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
66
|
+
try:
|
|
67
|
+
return 0 if asyncio.run(async_main()) else 1
|
|
68
|
+
except (KeyboardInterrupt, asyncio.CancelledError, EOFError):
|
|
69
|
+
console.print("Interrupted.", style="red", markup=False)
|
|
70
|
+
return 130
|
|
71
|
+
except RateLimitedError as exc:
|
|
72
|
+
console.print("Server rate limit: " + str(exc), style="red", markup=False)
|
|
73
|
+
return 2
|
|
74
|
+
except (LinkError, EpisodeDataUnavailableError, HTTPError, URLError) as exc:
|
|
75
|
+
console.print("Error: " + str(exc), style="red", markup=False)
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
raise SystemExit(main())
|
|
@@ -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 tomllib
|
|
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
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PlayersConfig:
|
|
17
|
+
prefers: list[str] = field(default_factory=list)
|
|
18
|
+
bans: list[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Config:
|
|
23
|
+
prefer_languages: list[Lang]
|
|
24
|
+
download_path: Path
|
|
25
|
+
episode_path: str
|
|
26
|
+
download: bool
|
|
27
|
+
show_players: bool
|
|
28
|
+
max_retry_time: int
|
|
29
|
+
format: str
|
|
30
|
+
format_sort: str
|
|
31
|
+
internal_player_command: list[str]
|
|
32
|
+
url: str
|
|
33
|
+
provider_url: str
|
|
34
|
+
players_config: PlayersConfig
|
|
35
|
+
concurrent_downloads: dict[str, int]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_config() -> Config:
|
|
39
|
+
# 1. Load default config from the package
|
|
40
|
+
default_config_file = Path(__file__).parent / "config.toml"
|
|
41
|
+
if not default_config_file.exists():
|
|
42
|
+
raise FileNotFoundError(
|
|
43
|
+
f"The default config.toml could not be found at {default_config_file}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with open(default_config_file, "rb") as f:
|
|
47
|
+
config_dict = tomllib.load(f)
|
|
48
|
+
|
|
49
|
+
# 2. Determine user config path
|
|
50
|
+
# Order of priority:
|
|
51
|
+
# - current directory (./config.toml)
|
|
52
|
+
# - system config directory (e.g. ~/AppData/Local/anime-sama_api/config.toml)
|
|
53
|
+
local_config = Path("config.toml")
|
|
54
|
+
system_config_dir = Path(user_config_dir("anime-sama_api", appauthor=False))
|
|
55
|
+
system_config_file = system_config_dir / "config.toml"
|
|
56
|
+
|
|
57
|
+
user_config_data = {}
|
|
58
|
+
if local_config.exists():
|
|
59
|
+
with open(local_config, "rb") as f:
|
|
60
|
+
user_config_data = tomllib.load(f)
|
|
61
|
+
elif system_config_file.exists():
|
|
62
|
+
with open(system_config_file, "rb") as f:
|
|
63
|
+
user_config_data = tomllib.load(f)
|
|
64
|
+
else:
|
|
65
|
+
# Recreate default config if not found anywhere
|
|
66
|
+
from shutil import copy
|
|
67
|
+
|
|
68
|
+
system_config_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
copy(default_config_file, system_config_file)
|
|
70
|
+
logger.info("Default config created at %s", system_config_file)
|
|
71
|
+
|
|
72
|
+
# 3. Merge configs (user values override defaults)
|
|
73
|
+
config_dict.update(user_config_data)
|
|
74
|
+
|
|
75
|
+
# 4. Backward compatibility and data cleaning
|
|
76
|
+
# Languages conversion
|
|
77
|
+
new_langs: list[Lang] = []
|
|
78
|
+
for lang in config_dict.get("prefer_languages", []):
|
|
79
|
+
if lang == "VO":
|
|
80
|
+
lang = "VOSTFR"
|
|
81
|
+
if lang in lang2ids:
|
|
82
|
+
new_langs.append(lang)
|
|
83
|
+
else:
|
|
84
|
+
logger.warning("'%s' is not a valid language, ignoring it.", lang)
|
|
85
|
+
config_dict["prefer_languages"] = new_langs
|
|
86
|
+
|
|
87
|
+
# Path detection and expansion
|
|
88
|
+
raw_path = config_dict.get("download_path")
|
|
89
|
+
if not raw_path:
|
|
90
|
+
# Default to system Downloads / Anime-Sama if not specified
|
|
91
|
+
config_dict["download_path"] = Path(user_downloads_dir()) / "Anime-Sama"
|
|
92
|
+
else:
|
|
93
|
+
config_dict["download_path"] = Path(raw_path).expanduser()
|
|
94
|
+
|
|
95
|
+
# Internal player command string to list
|
|
96
|
+
player_cmd = config_dict.get("internal_player_command")
|
|
97
|
+
if isinstance(player_cmd, str):
|
|
98
|
+
config_dict["internal_player_command"] = player_cmd.split()
|
|
99
|
+
elif player_cmd is None:
|
|
100
|
+
config_dict["internal_player_command"] = []
|
|
101
|
+
|
|
102
|
+
# Players config mapping
|
|
103
|
+
players_data = config_dict.pop("players_hostname", {})
|
|
104
|
+
if "players" in config_dict: # Old key removal
|
|
105
|
+
del config_dict["players"]
|
|
106
|
+
config_dict["players_config"] = PlayersConfig(**players_data)
|
|
107
|
+
|
|
108
|
+
# Spelling
|
|
109
|
+
config_dict["episode_path"] = config_dict["episode_path"].replace(
|
|
110
|
+
"{serie}", "{series}"
|
|
111
|
+
)
|
|
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()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Languages at the start of the list are preferred over the ones that come after
|
|
2
|
+
# Possible values: "VASTFR", "VCN", "VF", "VJSTFR", "VKR", "VQC", "VOSTFR"
|
|
3
|
+
prefer_languages = ["VOSTFR"]
|
|
4
|
+
|
|
5
|
+
# Where to put the downloads
|
|
6
|
+
download_path = "~/Downloads/Anime-Sama"
|
|
7
|
+
# How to organize the downloads (you can use {series} {season} {episode} {release_year_parentheses})
|
|
8
|
+
episode_path = "{series}/{season}/{episode}"
|
|
9
|
+
|
|
10
|
+
# If it downloads or plays the selected episodes
|
|
11
|
+
download = true
|
|
12
|
+
show_players = false
|
|
13
|
+
max_retry_time = 1024
|
|
14
|
+
|
|
15
|
+
# See https://github.com/yt-dlp/yt-dlp#format-selection
|
|
16
|
+
# For example, if you want the lowest size, put "+size,+br,+res,+fps" in format_sort
|
|
17
|
+
format = "all"
|
|
18
|
+
format_sort = ""
|
|
19
|
+
|
|
20
|
+
# What application to use to play
|
|
21
|
+
internal_player_command = "mpv"
|
|
22
|
+
|
|
23
|
+
# url of anime-sama (You shouldn't touch that)
|
|
24
|
+
url = ""
|
|
25
|
+
provider_url = "https://anime-sama.pw/"
|
|
26
|
+
|
|
27
|
+
[concurrent_downloads]
|
|
28
|
+
# how many fragment of a video to download at once
|
|
29
|
+
fragment = 3
|
|
30
|
+
# how many video to download at once
|
|
31
|
+
video = 5
|
|
32
|
+
|
|
33
|
+
[players_hostname]
|
|
34
|
+
prefers = []
|
|
35
|
+
bans = []
|