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,376 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import AsyncIterator, Generator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from html import unescape
|
|
8
|
+
from html.parser import HTMLParser
|
|
9
|
+
from typing import Any, cast, get_args
|
|
10
|
+
from urllib.parse import urljoin
|
|
11
|
+
|
|
12
|
+
from httpx import AsyncClient
|
|
13
|
+
|
|
14
|
+
from .catalogue import Catalogue, Type
|
|
15
|
+
from .episode import Episode
|
|
16
|
+
from .langs import Lang, flagid2lang, flags
|
|
17
|
+
from .season import Season
|
|
18
|
+
from .utils import filter_literal, is_Literal
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _CatalogueCardParser(HTMLParser):
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
super().__init__(convert_charrefs=True)
|
|
26
|
+
self.cards: list[dict[str, Any]] = []
|
|
27
|
+
self._card: dict[str, Any] | None = None
|
|
28
|
+
self._div_depth = 0
|
|
29
|
+
self._title_active = False
|
|
30
|
+
self._title_text = ""
|
|
31
|
+
self._badge_active = False
|
|
32
|
+
self._badge_text = ""
|
|
33
|
+
|
|
34
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
35
|
+
attributes = dict(attrs)
|
|
36
|
+
classes = (attributes.get("class") or "").split()
|
|
37
|
+
|
|
38
|
+
if tag == "div" and self._card is None and "as-carte" in classes:
|
|
39
|
+
self._card = {
|
|
40
|
+
"url": attributes.get("data-url") or "",
|
|
41
|
+
"name": attributes.get("data-nom") or "",
|
|
42
|
+
"image_url": attributes.get("data-img") or "",
|
|
43
|
+
"types": [],
|
|
44
|
+
"languages": [],
|
|
45
|
+
}
|
|
46
|
+
self._div_depth = 1
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if self._card is None:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if tag == "div":
|
|
53
|
+
self._div_depth += 1
|
|
54
|
+
elif tag == "h3":
|
|
55
|
+
self._title_active = True
|
|
56
|
+
self._title_text = ""
|
|
57
|
+
elif tag == "span" and any(name.startswith("as-badge") for name in classes):
|
|
58
|
+
self._badge_active = True
|
|
59
|
+
self._badge_text = ""
|
|
60
|
+
elif tag == "img":
|
|
61
|
+
language_name = (attributes.get("title") or "").strip().casefold()
|
|
62
|
+
language_by_name = {
|
|
63
|
+
language.casefold(): language for language in get_args(Lang)
|
|
64
|
+
}
|
|
65
|
+
language = language_by_name.get(language_name)
|
|
66
|
+
if language is None:
|
|
67
|
+
language = next(
|
|
68
|
+
(
|
|
69
|
+
value
|
|
70
|
+
for flag_id, value in flagid2lang.items()
|
|
71
|
+
if flag_id.casefold() == language_name
|
|
72
|
+
),
|
|
73
|
+
None,
|
|
74
|
+
)
|
|
75
|
+
if language is not None:
|
|
76
|
+
self._card["languages"].append(language)
|
|
77
|
+
|
|
78
|
+
def handle_data(self, data: str) -> None:
|
|
79
|
+
if self._title_active:
|
|
80
|
+
self._title_text += data
|
|
81
|
+
if self._badge_active:
|
|
82
|
+
self._badge_text += data
|
|
83
|
+
|
|
84
|
+
def handle_endtag(self, tag: str) -> None:
|
|
85
|
+
if self._card is None:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if tag == "h3" and self._title_active:
|
|
89
|
+
if self._title_text.strip():
|
|
90
|
+
self._card["name"] = self._title_text.strip()
|
|
91
|
+
self._title_active = False
|
|
92
|
+
elif tag == "span" and self._badge_active:
|
|
93
|
+
badge = self._badge_text.strip()
|
|
94
|
+
if badge:
|
|
95
|
+
self._card["types"].append(badge)
|
|
96
|
+
self._badge_active = False
|
|
97
|
+
elif tag == "div":
|
|
98
|
+
self._div_depth -= 1
|
|
99
|
+
if self._div_depth == 0:
|
|
100
|
+
if self._card["name"] and self._card["url"]:
|
|
101
|
+
self.cards.append(self._card)
|
|
102
|
+
self._card = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def find_site_url(
|
|
106
|
+
client: AsyncClient | None = None, provider_url="https://anime-sama.pw/"
|
|
107
|
+
) -> str | None:
|
|
108
|
+
client = client or AsyncClient()
|
|
109
|
+
|
|
110
|
+
response = await client.get(provider_url)
|
|
111
|
+
|
|
112
|
+
if response.is_error:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# * Sometimes need to check for the great word "anime-sama" in lowercase or uppercase but if add re.IGNORECASE it will work
|
|
116
|
+
match = re.search(
|
|
117
|
+
r"href=\"(.+?)\">Accéder à Anime-Sama", response.text, re.IGNORECASE
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# * 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
|
|
121
|
+
|
|
122
|
+
if match:
|
|
123
|
+
redirected = await client.get(match.group(1), follow_redirects=True)
|
|
124
|
+
return str(redirected.url) + "/"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class EpisodeRelease:
|
|
129
|
+
page_url: str
|
|
130
|
+
image_url: str
|
|
131
|
+
series_name: str
|
|
132
|
+
types: tuple[Type]
|
|
133
|
+
language: Lang
|
|
134
|
+
episode_name: str
|
|
135
|
+
timestamp: datetime
|
|
136
|
+
|
|
137
|
+
def get_real_episodes(self) -> list[Episode]:
|
|
138
|
+
raise NotImplementedError
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def fancy_name(self) -> str:
|
|
142
|
+
return (
|
|
143
|
+
f"{self.series_name} - {self.episode_name} {flags.get(self.language, '')}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AnimeSama:
|
|
148
|
+
def __init__(self, site_url: str, client: AsyncClient | None = None) -> None:
|
|
149
|
+
self.site_url = site_url
|
|
150
|
+
self.client = client or AsyncClient()
|
|
151
|
+
|
|
152
|
+
async def _get_homepage_section(self, section_name: str, how_many: int = 1) -> str:
|
|
153
|
+
homepage = await self.client.get(self.site_url)
|
|
154
|
+
|
|
155
|
+
if homepage.is_error:
|
|
156
|
+
return ""
|
|
157
|
+
|
|
158
|
+
sections = homepage.text.split("<!--")
|
|
159
|
+
for index, section in enumerate(sections):
|
|
160
|
+
comment_end_pos = section.find("-->")
|
|
161
|
+
if section_name in section[:comment_end_pos]:
|
|
162
|
+
return "<!--" + "<!--".join(sections[index : index + how_many])
|
|
163
|
+
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
def _yield_catalogues_from(self, html: str) -> Generator[Catalogue]:
|
|
167
|
+
parser = _CatalogueCardParser()
|
|
168
|
+
parser.feed(html)
|
|
169
|
+
if parser.cards:
|
|
170
|
+
for card in parser.cards:
|
|
171
|
+
url = urljoin(self.site_url, card["url"])
|
|
172
|
+
|
|
173
|
+
def not_in_literal(value: Any, url=url) -> None:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Error while parsing '%s'. Please report this with URL: %s",
|
|
176
|
+
value,
|
|
177
|
+
url,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
card_types = cast(
|
|
181
|
+
set[Type],
|
|
182
|
+
set(filter_literal(card["types"], Type, not_in_literal)),
|
|
183
|
+
)
|
|
184
|
+
yield Catalogue(
|
|
185
|
+
url=url,
|
|
186
|
+
name=card["name"],
|
|
187
|
+
types=card_types,
|
|
188
|
+
languages=set(card["languages"]),
|
|
189
|
+
image_url=card["image_url"],
|
|
190
|
+
client=self.client,
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
text_without_script = re.sub(r"<script[\W\w]+?</script>", "", html)
|
|
195
|
+
for match in re.finditer(
|
|
196
|
+
rf"href=\"({self.site_url}catalogue/.+)\"[\W\w]+?src=\"(.+?)\"[\W\w]+?<h2.+?>(.*)\n?<[\W\w]+?<p.+?>(.*)\n?<[\W\w]+?<div class=\"genre-tags\">([\W\w]*?)</div[\W\w]+?<p.+?>(.*)\n?<[\W\w]+?<div class=\"lang-flags\">([\W\w]*?)</div",
|
|
197
|
+
text_without_script,
|
|
198
|
+
):
|
|
199
|
+
(
|
|
200
|
+
url,
|
|
201
|
+
image_url,
|
|
202
|
+
name,
|
|
203
|
+
alternative_names_str,
|
|
204
|
+
genres_str,
|
|
205
|
+
types_str,
|
|
206
|
+
flags_str,
|
|
207
|
+
) = (unescape(item) for item in match.groups())
|
|
208
|
+
|
|
209
|
+
alternative_names = (
|
|
210
|
+
alternative_names_str.split(", ") if alternative_names_str else []
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
genres = re.findall(r">(.+?)<", genres_str)
|
|
214
|
+
types = types_str.split(", ") if types_str else []
|
|
215
|
+
flags = re.findall(r"title=\"(.+?)\"", flags_str)
|
|
216
|
+
|
|
217
|
+
def not_in_literal(value: Any, url=url) -> None:
|
|
218
|
+
logger.warning(
|
|
219
|
+
f"Error while parsing '{value}'. \nPlease report this to the developer with URL: {url}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
types_checked = cast(
|
|
223
|
+
set[Type], set(filter_literal(types, Type, not_in_literal))
|
|
224
|
+
)
|
|
225
|
+
languages = {
|
|
226
|
+
flagid2lang[flag.lower()]
|
|
227
|
+
for flag in flags
|
|
228
|
+
if flag.lower() in flagid2lang
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
yield Catalogue(
|
|
232
|
+
url=url,
|
|
233
|
+
name=name,
|
|
234
|
+
alternative_names=alternative_names,
|
|
235
|
+
genres=genres,
|
|
236
|
+
types=types_checked,
|
|
237
|
+
languages=languages,
|
|
238
|
+
image_url=image_url,
|
|
239
|
+
client=self.client,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _yield_release_episodes_from(self, html: str) -> Generator[EpisodeRelease]:
|
|
243
|
+
for match in re.finditer(
|
|
244
|
+
r"href=\"/(catalogue\/.+)\"[\W\w]+?src=\"(.+?)\"[\W\w]+?>.*\n?<[\W\w]+?>.*\n?<[\W\w]+?>(.*)\n?<[\W\w]+?>.*\n?<[\W\w]+?\">([\W\w]*?)\n?</[\W\w]+?>(.*)\n?<[\W\w]+?>.*\n?<[\W\w]+?>(.*)\n?<[\W\w]+?>(.*)\n?<",
|
|
245
|
+
html,
|
|
246
|
+
):
|
|
247
|
+
(
|
|
248
|
+
season_url,
|
|
249
|
+
image_url,
|
|
250
|
+
types,
|
|
251
|
+
language_str,
|
|
252
|
+
series_name,
|
|
253
|
+
episode_name,
|
|
254
|
+
timestamp,
|
|
255
|
+
) = match.groups()
|
|
256
|
+
season_url = self.site_url + season_url
|
|
257
|
+
|
|
258
|
+
types = types.split(", ") if types else ["Anime"]
|
|
259
|
+
|
|
260
|
+
def not_in_literal(value: Any, season_url=season_url) -> None:
|
|
261
|
+
logger.warning(
|
|
262
|
+
f"Error while parsing '{value}'. \nPlease report this to the developer with URL: {season_url} (from homepage)"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
types_checked = cast(
|
|
266
|
+
tuple[Type],
|
|
267
|
+
tuple(filter_literal(types, Type, not_in_literal)),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
language = re.findall(r"title=\"(.+?)\"", language_str)[0]
|
|
271
|
+
is_Literal(language, Lang, not_in_literal)
|
|
272
|
+
|
|
273
|
+
yield EpisodeRelease(
|
|
274
|
+
page_url=season_url,
|
|
275
|
+
image_url=image_url,
|
|
276
|
+
series_name=series_name,
|
|
277
|
+
types=types_checked,
|
|
278
|
+
language=language,
|
|
279
|
+
episode_name=episode_name,
|
|
280
|
+
timestamp=datetime.strptime(timestamp, "%d/%m/%Y %H:%M"),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def search(self, query: str) -> list[Catalogue]:
|
|
284
|
+
response = (
|
|
285
|
+
await self.client.get(
|
|
286
|
+
f"{self.site_url}catalogue/", params={"search": query}
|
|
287
|
+
)
|
|
288
|
+
).raise_for_status()
|
|
289
|
+
|
|
290
|
+
pages_regex = re.findall(r"page=(\d+)", response.text)
|
|
291
|
+
|
|
292
|
+
last_page = max((int(page) for page in pages_regex), default=1)
|
|
293
|
+
|
|
294
|
+
responses = [response] + await asyncio.gather(
|
|
295
|
+
*(
|
|
296
|
+
self.client.get(
|
|
297
|
+
f"{self.site_url}catalogue/",
|
|
298
|
+
params={"search": query, "page": num},
|
|
299
|
+
)
|
|
300
|
+
for num in range(2, last_page + 1)
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
catalogues = []
|
|
305
|
+
for response in responses:
|
|
306
|
+
if response.is_error:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# catalogues += list(self._yield_catalogues_from(response.text))
|
|
310
|
+
# catalogues = [
|
|
311
|
+
# *catalogue
|
|
312
|
+
# for response in response
|
|
313
|
+
# if not response.is_error
|
|
314
|
+
# for catalogue in self._yield_catalogues_from(response.text)
|
|
315
|
+
# ]
|
|
316
|
+
|
|
317
|
+
catalogues.extend(self._yield_catalogues_from(response.text))
|
|
318
|
+
|
|
319
|
+
return catalogues
|
|
320
|
+
|
|
321
|
+
async def search_iter(self, query: str) -> AsyncIterator[Catalogue]:
|
|
322
|
+
response = (
|
|
323
|
+
await self.client.get(
|
|
324
|
+
f"{self.site_url}catalogue/", params={"search": query}
|
|
325
|
+
)
|
|
326
|
+
).raise_for_status()
|
|
327
|
+
|
|
328
|
+
pages_regex = re.findall(r"page=(\d+)", response.text)
|
|
329
|
+
|
|
330
|
+
last_page = max((int(page) for page in pages_regex), default=1)
|
|
331
|
+
|
|
332
|
+
for catalogue in self._yield_catalogues_from(response.text):
|
|
333
|
+
yield catalogue
|
|
334
|
+
|
|
335
|
+
for number in range(2, last_page + 1):
|
|
336
|
+
response = await self.client.get(
|
|
337
|
+
f"{self.site_url}catalogue/",
|
|
338
|
+
params={"search": query, "page": number},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if response.is_error:
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
for catalogue in self._yield_catalogues_from(response.text):
|
|
345
|
+
yield catalogue
|
|
346
|
+
|
|
347
|
+
async def catalogues_iter(self) -> AsyncIterator[Catalogue]:
|
|
348
|
+
async for catalogue in self.search_iter(""):
|
|
349
|
+
yield catalogue
|
|
350
|
+
|
|
351
|
+
async def all_catalogues(self) -> list[Catalogue]:
|
|
352
|
+
return await self.search("")
|
|
353
|
+
|
|
354
|
+
async def planning(self) -> list[list[Season]]:
|
|
355
|
+
# Get from homepage, return value should be change
|
|
356
|
+
raise NotImplementedError
|
|
357
|
+
|
|
358
|
+
async def new_episodes(self) -> list[EpisodeRelease]:
|
|
359
|
+
"""
|
|
360
|
+
Return the new available episodes on anime-sama using the homepage sorted from oldest to newest.
|
|
361
|
+
"""
|
|
362
|
+
section = await self._get_homepage_section("ajouts animes", 4)
|
|
363
|
+
release_episodes = list(self._yield_release_episodes_from(section))
|
|
364
|
+
return release_episodes[::-1]
|
|
365
|
+
|
|
366
|
+
"""async def new_scans(self) -> list[Scan]:
|
|
367
|
+
raise NotImplementedError"""
|
|
368
|
+
|
|
369
|
+
async def new_content(self) -> list[Catalogue]:
|
|
370
|
+
raise NotImplementedError
|
|
371
|
+
|
|
372
|
+
async def classics(self) -> list[Catalogue]:
|
|
373
|
+
raise NotImplementedError
|
|
374
|
+
|
|
375
|
+
async def highlights(self) -> list[Catalogue]:
|
|
376
|
+
raise NotImplementedError
|
anime_sama_api/utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import operator
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Generator, Iterable
|
|
5
|
+
from itertools import zip_longest
|
|
6
|
+
from typing import Any, TypeVar, cast, get_args
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
NO_VALUE = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def zip_varlen(*iterables: Iterable[T]) -> list[list[T]]:
|
|
13
|
+
return cast(
|
|
14
|
+
list[list[T]],
|
|
15
|
+
[
|
|
16
|
+
[entry for entry in row if entry is not NO_VALUE]
|
|
17
|
+
for row in zip_longest(*iterables, fillvalue=NO_VALUE)
|
|
18
|
+
],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def split_and_strip(string: str, delimiters: Iterable[str] | str) -> list[str]:
|
|
23
|
+
if isinstance(delimiters, str):
|
|
24
|
+
return [part.strip() for part in string.split(delimiters)]
|
|
25
|
+
string_list = [string]
|
|
26
|
+
for delimiter in delimiters:
|
|
27
|
+
string_list = functools.reduce(
|
|
28
|
+
operator.iadd, (part.split(delimiter) for part in string_list), []
|
|
29
|
+
)
|
|
30
|
+
return [part.strip() for part in string_list]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove_some_js_comments(string: str) -> str:
|
|
34
|
+
string = re.sub(r"/\*[\W\w]*?\*/", "", string)
|
|
35
|
+
return re.sub(r"<!--[\W\w]*?-->", "", string)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_Literal(
|
|
39
|
+
value: Any,
|
|
40
|
+
Lit: Any,
|
|
41
|
+
callback_when_false: Callable[[Any], None] = lambda _: None,
|
|
42
|
+
) -> bool:
|
|
43
|
+
if value in get_args(Lit):
|
|
44
|
+
return True
|
|
45
|
+
callback_when_false(value)
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def filter_literal(
|
|
50
|
+
iterable: Iterable[Any],
|
|
51
|
+
Lit: T,
|
|
52
|
+
callback_when_false: Callable[[Any], None] = lambda _: None,
|
|
53
|
+
) -> Generator[T, None, None]:
|
|
54
|
+
return (value for value in iterable if is_Literal(value, Lit, callback_when_false))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anime-sama_cli_api
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Api for anime-sama
|
|
5
|
+
Project-URL: Repository, https://github.com/Mathieux83/anime-sama_api
|
|
6
|
+
Author-email: NiniKo <pcninik@gmail.com>, Mathieux83 <car.math@live.fr>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: anime,anime-sama,downloader
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
21
|
+
Classifier: Topic :: System :: Archiving
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx2>=2.16.0
|
|
24
|
+
Requires-Dist: httpx>=0.28.1
|
|
25
|
+
Provides-Extra: cli
|
|
26
|
+
Requires-Dist: platformdirs>=4.13.5; extra == 'cli'
|
|
27
|
+
Requires-Dist: rich>=15.0.0; extra == 'cli'
|
|
28
|
+
Requires-Dist: yt-dlp>=2026.8.19; extra == 'cli'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
> [!WARNING]
|
|
32
|
+
> This project is an updated fork of the main repo of Niniko, with his authorization I only work in this fork no PR was send for now.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Anime-Sama API
|
|
37
|
+
|
|
38
|
+
An API for anime-sama, also provides a CLI to download videos.
|
|
39
|
+
|
|
40
|
+
I have implemented all the features I care about. This project is now in maintenance mode.
|
|
41
|
+
|
|
42
|
+
# Installation
|
|
43
|
+
|
|
44
|
+
Requirements:
|
|
45
|
+
|
|
46
|
+
- Python **3.11** or higher
|
|
47
|
+
|
|
48
|
+
You can simply install it with (note that you can use tools like pipx to isolate the installation):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install anime-sama-api[cli]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
And to run it:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
anime-sama
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
You can customize the config at :
|
|
63
|
+
|
|
64
|
+
- Linux: `~/.config/anime-sama_cli/config.toml`
|
|
65
|
+
- Windows: `%USER%/AppData/Local/anime-sama_cli/config.toml`
|
|
66
|
+
- macOS: `~/Library/Application Support/anime-sama_api/config.toml`
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# For developers
|
|
71
|
+
|
|
72
|
+
## Requirements
|
|
73
|
+
|
|
74
|
+
- git
|
|
75
|
+
- [uv](https://docs.astral.sh/uv/#installation)
|
|
76
|
+
|
|
77
|
+
## Install locally
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/Sky-NiniKo/anime-sama_api.git
|
|
81
|
+
cd anime-sama_downloader
|
|
82
|
+
uv sync --extra cli
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Run
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv run anime-sama
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Update
|
|
92
|
+
|
|
93
|
+
In the `anime_sama` folder:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git pull
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Contribution
|
|
102
|
+
|
|
103
|
+
I am open to contribution. Please only open a PR for ONE change. AKA, don't do "Various improvements" and explain your motivation behind your improvement ("Various typos fix"/"Cleanup" is fine).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
anime_sama_api/__init__.py,sha256=unxLlRekkszdKueHtqU6-wC_D9VZ0nOPlBdUxzcI99Y,1246
|
|
2
|
+
anime_sama_api/catalogue.py,sha256=VIpVfl0xV9OiMlSUfhInC-PYNxJEiz6F3kC-lKl8buQ,7737
|
|
3
|
+
anime_sama_api/episode.py,sha256=2E8ZzvgqXhX_SrRGAqjNDk2eW0e9CAflPPcZ0cKzc7U,7027
|
|
4
|
+
anime_sama_api/langs.py,sha256=K8jjV4oIXTNfvbldPWlb0fA7AHB2JlzM22mp8eFmqO0,2072
|
|
5
|
+
anime_sama_api/link_resolver.py,sha256=h5U6nPSvk2erTFc35LupaWFIa--zGIx7hDY0cVWi_nQ,6241
|
|
6
|
+
anime_sama_api/season.py,sha256=5a2zsi6qmOvo0XH2QKLimam1Q0iaIQbawbazk1-T0X0,9317
|
|
7
|
+
anime_sama_api/top_level.py,sha256=zwasoI_-DCxcokgaGEBUgOoPu2keBq6w5S96ZS_kh48,12981
|
|
8
|
+
anime_sama_api/utils.py,sha256=0AmJAPigX8u4OPbY4PpcI7zksAxOQHsbEHtTEuI-jvM,1523
|
|
9
|
+
anime_sama_api/cli/__main__.py,sha256=SMZU4ixVPQKP9tc_HSQBCMDZjqr4Nu-WjCGK0yLTs6c,2982
|
|
10
|
+
anime_sama_api/cli/config.py,sha256=jVk5u727R8-TKREdeBwtm1Ncvbzs_2l3AWcgV2RRFbc,4061
|
|
11
|
+
anime_sama_api/cli/config.toml,sha256=_vAsf5ZSTNYMIvBU7p_9OSj9STG8Oeg7YgN1RRNnDXM,1028
|
|
12
|
+
anime_sama_api/cli/downloader.py,sha256=zjyQeUMpvYEcW0nsQlujhtGoCAPbR-R-RUglrtj3t14,10908
|
|
13
|
+
anime_sama_api/cli/error_handling.py,sha256=PnWIlrwvYGVh8Hfh1shYhLYVJEP7XDd6IhOrypihiEY,2064
|
|
14
|
+
anime_sama_api/cli/internal_player.py,sha256=Pqwiv0dcfZYr-H3xNLiqhBKVBYA6cri7lv2NSDVj1ho,1535
|
|
15
|
+
anime_sama_api/cli/play_menu.py,sha256=nhvagx6Us3QbTYTJ9NfMYj45hRFDtZBFCefHnxPf_Hg,1637
|
|
16
|
+
anime_sama_api/cli/utils.py,sha256=LVlcZzBn9lh3eFs2ekBpdeAchYuFQDp-OTGBIrnPtVk,4395
|
|
17
|
+
anime_sama_cli_api-0.4.0.dist-info/METADATA,sha256=povEg3R0_18RuKNoqlr37KM0NYDp546q1cX8nnmrtpo,2579
|
|
18
|
+
anime_sama_cli_api-0.4.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
19
|
+
anime_sama_cli_api-0.4.0.dist-info/entry_points.txt,sha256=AoSk4oUFjzp4TwPxqXFOfg7R7rTGtakEHrHUk_XeNQ0,51
|
|
20
|
+
anime_sama_cli_api-0.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
+
anime_sama_cli_api-0.4.0.dist-info/RECORD,,
|