Skip to content

Commit 0744bd6

Browse files
committed
feat: add anidbapp provider
1 parent 84afb18 commit 0744bd6

11 files changed

Lines changed: 356 additions & 43 deletions

File tree

api/src/anipy_api/provider/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class LanguageTypeEnum(Enum):
2323

2424
SUB = "sub"
2525
DUB = "dub"
26+
UND = "undfined"
2627

2728
def __repr__(self) -> str:
2829
return self.value
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from anipy_api.provider.providers.animehub_provider import AnimeHubProvider
22
from anipy_api.provider.providers.native_provider import NativeProvider
3+
from anipy_api.provider.providers.anidbapp_provider import AniDBAppProvider
34

4-
__all__ = ["NativeProvider", "AnimeHubProvider"]
5+
__all__ = ["NativeProvider", "AnimeHubProvider", "AniDBAppProvider"]
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import re
2+
from typing import List
3+
from urllib.parse import urljoin
4+
5+
import Levenshtein
6+
import m3u8
7+
from anipy_api.provider import (BaseProvider, Episode, ProviderInfoResult,
8+
ProviderSearchResult, ProviderStream)
9+
from anipy_api.provider.base import LanguageTypeEnum
10+
from anipy_api.provider.filter import (BaseFilter, FilterCapabilities, Filters,
11+
MediaType, Season, Status)
12+
from anipy_api.provider.utils import parsenum
13+
from anipy_api.provider import Episode
14+
from anipy_api.error import LangTypeNotAvailableError
15+
16+
17+
from bs4 import BeautifulSoup
18+
19+
from requests import Request
20+
21+
HLS_RE = re.compile(
22+
r"(?:file\s*:\s*|source\s*=\s*)[\"']([^\"']+\.m3u8(?:\?[^\"']*)?)[\"']",
23+
re.IGNORECASE,
24+
)
25+
26+
class AniDBAppFilter(BaseFilter):
27+
def _apply_query(self, query: str):
28+
self._request.params["q"] = query
29+
30+
def _apply_year(self, year: int):
31+
self._request.params["year"] = year
32+
33+
def _apply_season(self, season: Season):
34+
self._request.params["season"] = season.name.lower()
35+
36+
def _apply_status(self, status: Status):
37+
mapping = {
38+
Status.COMPLETED: "Finished Airing",
39+
Status.ONGOING: "Currently Airing",
40+
Status.UPCOMING: ""
41+
}
42+
self._request.params["status"] = mapping[status]
43+
44+
def _apply_media_type(self, media_type: MediaType):
45+
mapping = {
46+
MediaType.TV: "TV",
47+
MediaType.MOVIE: "Movie",
48+
MediaType.OVA: "OVA",
49+
MediaType.ONA: "ONA",
50+
MediaType.SPECIAL: "Special",
51+
MediaType.MUSIC: "Music"
52+
}
53+
self._request.params["type"] = mapping[media_type]
54+
55+
class AniDBAppProvider(BaseProvider):
56+
NAME: str = "anidbapp"
57+
BASE_URL: str = "https://anidb.app"
58+
FILTER_CAPS: FilterCapabilities = FilterCapabilities.ALL
59+
60+
def get_search(
61+
self, query: str, filters: Filters = Filters()
62+
) -> List[ProviderSearchResult]:
63+
req = Request("GET", f"{self.BASE_URL}/browse")
64+
req = AniDBAppFilter(req).apply(query, filters)
65+
res = self._request_page(req)
66+
67+
current_page = BeautifulSoup(res.text, "html.parser")
68+
pages = current_page.find("span", attrs={"class": "text-sm text-muted"})
69+
if pages:
70+
pages = parsenum(pages.findChildren()[-1].text)
71+
else:
72+
pages = 1
73+
74+
results: list[ProviderSearchResult] = []
75+
76+
for p in range(2 if pages > 1 else 1, pages + 1):
77+
anime = current_page.find("div", attrs={"class": "anime-grid"}).findAll(
78+
"a", attrs={"class": "anime-card"}
79+
)
80+
81+
for a in anime:
82+
if a is None:
83+
continue
84+
85+
name = a.p.text
86+
link = a["href"]
87+
ident = link.split("-")[-1]
88+
results.append(ProviderSearchResult(
89+
identifier=ident, name=name, languages={LanguageTypeEnum.UND}
90+
))
91+
92+
req.params["page"] = p
93+
res = self._request_page(req)
94+
current_page = BeautifulSoup(res.text, "html.parser")
95+
96+
results.sort(
97+
key=lambda x: Levenshtein.ratio(query, x.name, processor=str.lower),
98+
reverse=True,
99+
)
100+
101+
return results
102+
103+
def get_info(self, identifier: str) -> ProviderInfoResult:
104+
req = Request(
105+
"GET",
106+
f"{self.BASE_URL}/anime/anime-{identifier}",
107+
)
108+
res = self._request_page(req)
109+
110+
soup = BeautifulSoup(res.text, "html.parser")
111+
112+
name = soup.find("h1", attrs={"class": "leading-tight"})
113+
if name:
114+
name = name.text
115+
116+
image = soup.find("img", attrs={"class": "object-cover"})
117+
if image:
118+
image = image["src"]
119+
120+
year = soup.find("a", attrs={"href": re.compile(r"\/browse\?season=\w+&year=\d+")})
121+
if year:
122+
year = parsenum(year.text.split()[-1])
123+
124+
genre_container = soup.findAll("div", attrs={"class": "flex flex-wrap gap-1.5 mb-4"})
125+
126+
genres = genre_container[-1].findAll("a", attrs={"href": re.compile(r"\/(genres|themes)\/.+")})
127+
genres = [g.text for g in genres]
128+
129+
status = soup.find("a", attrs={"href": re.compile(r"\/browse\?status=.+")})
130+
if status:
131+
mapping = {
132+
"Finished Airing": Status.COMPLETED,
133+
"Currently Airing": Status.ONGOING,
134+
}
135+
status = mapping.get(status.text, None)
136+
137+
synopsis = soup.find("p", attrs={"class": "text-sm text-faint leading-relaxed"})
138+
if synopsis:
139+
synopsis = synopsis.text
140+
141+
synonyms = soup.find("dt", text=re.compile(r"Synonyms"))
142+
if synonyms:
143+
synonyms = [s.strip() for s in synonyms.findNextSibling().text.split(",")]
144+
145+
return ProviderInfoResult(
146+
name = name,
147+
image = image,
148+
genres = genres,
149+
synopsis = synopsis,
150+
status = status,
151+
release_year = year,
152+
alternative_names = synonyms
153+
)
154+
155+
def get_episodes(self, identifier: str, lang: LanguageTypeEnum) -> List[Episode]:
156+
req = Request(
157+
"GET",
158+
f"{self.BASE_URL}/api/frontend/anime/{identifier}/episodes",
159+
)
160+
res = self._request_page(req).json()
161+
return [e["number"] for e in res["episodes"]]
162+
163+
def get_video(
164+
self, identifier: str, episode: Episode, lang: LanguageTypeEnum
165+
) -> List[ProviderStream]:
166+
req = Request(
167+
"GET",
168+
f"{self.BASE_URL}/api/frontend/anime/{identifier}/episodes",
169+
)
170+
res = self._request_page(req).json().get("episodes", [])
171+
episode_id = next(filter(lambda e: e["number"] == episode, res))["id"]
172+
173+
req = Request(
174+
"GET",
175+
f"{self.BASE_URL}/api/frontend/episode/{episode_id}/languages",
176+
)
177+
res = self._request_page(req).json().get("languages", [])
178+
if not res:
179+
return []
180+
181+
lang_short = "eng" if lang == LanguageTypeEnum.DUB else "jpn"
182+
183+
embed_url = list(filter(lambda l: l["code"] == lang_short, res))
184+
if not embed_url:
185+
raise LangTypeNotAvailableError(identifier, self.NAME, lang)
186+
else:
187+
embed_url = embed_url[0]["embed_url"]
188+
189+
req = Request(
190+
"GET",
191+
embed_url,
192+
)
193+
res = self._request_page(req)
194+
195+
streams = HLS_RE.findall(res.text)
196+
if not streams:
197+
return []
198+
199+
sub_playlists = []
200+
201+
for s in streams:
202+
req = Request("GET", s)
203+
res = self._request_page(req)
204+
205+
content = m3u8.M3U8(res.text, base_uri=urljoin(res.url, "."))
206+
207+
if len(content.playlists) == 0:
208+
sub_playlists.append(
209+
ProviderStream(
210+
url=s,
211+
resolution=1080,
212+
episode=episode,
213+
language=lang,
214+
container="hls",
215+
)
216+
)
217+
218+
for sub_playlist in content.playlists:
219+
sub_playlists.append(
220+
ProviderStream(
221+
url=urljoin(content.base_uri, sub_playlist.uri),
222+
resolution=sub_playlist.stream_info.resolution[1],
223+
episode=episode,
224+
language=lang,
225+
container="hls",
226+
)
227+
)
228+
return sub_playlists

cli/src/anipy_cli/clis/binge_cli.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
22
from typing import TYPE_CHECKING
33

4+
from anipy_api.provider.base import LanguageTypeEnum
5+
from anipy_api.error import LangTypeNotAvailableError
46
from anipy_api.locallist import LocalList
57
from anipy_cli.clis.base_cli import CliBase
68
from anipy_cli.colors import colors, cprint
@@ -76,9 +78,22 @@ def show(self):
7678
e,
7779
"...",
7880
) as s:
79-
stream = self.anime.get_video(
80-
e, self.lang, preferred_quality=self.options.quality
81-
)
81+
try:
82+
stream = self.anime.get_video(
83+
e, self.lang, preferred_quality=self.options.quality
84+
)
85+
except LangTypeNotAvailableError:
86+
if self.lang == LanguageTypeEnum.SUB:
87+
new_lang = LanguageTypeEnum.DUB
88+
else:
89+
new_lang = LanguageTypeEnum.SUB
90+
91+
error(f"Language {self.lang} not available, changing to {new_lang} for this episode!")
92+
93+
stream = self.anime.get_video(
94+
e, new_lang, preferred_quality=self.options.quality
95+
)
96+
8297
if stream is None:
8398
error("Could not find stream for requested episode, skipping")
8499
continue

cli/src/anipy_cli/config.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def providers(self) -> Dict[str, List[str]]:
7575
7676
For an updated list of providers look here: https://sdaqo.github.io/anipy-cli/availabilty
7777
78-
Supported providers (as of $version): animehub
78+
Supported providers (as of $version): animehub, anidbapp
7979
8080
Examples:
8181
providers:
@@ -87,12 +87,12 @@ def providers(self) -> Dict[str, List[str]]:
8787
anilist: ["provider1"]
8888
"""
8989
defaults = {
90-
"default": ["animehub"],
91-
"download": ["animehub"],
92-
"seasonal": ["animehub"],
93-
"binge": ["animehub"],
94-
"mal": ["animehub"],
95-
"anilist": ["animehub"],
90+
"default": ["anidbapp"],
91+
"download": ["anidbapp"],
92+
"seasonal": ["anidbapp"],
93+
"binge": ["anidbapp"],
94+
"mal": ["anidbapp"],
95+
"anilist": ["anidbapp"],
9696
}
9797

9898
value = self._get_value("providers", defaults, dict)

cli/src/anipy_cli/menus/anilist_menu.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from anipy_api.anilist import AniList, AniListAnime, AniListMyListStatusEnum
66
from anipy_api.anime import Anime
7+
from anipy_api.error import LangTypeNotAvailableError
78
from anipy_api.locallist import LocalList, LocalListEntry
89
from anipy_api.provider import LanguageTypeEnum
910
from anipy_api.provider.base import Episode
@@ -292,13 +293,25 @@ def binge_latest(self):
292293
ep,
293294
"...",
294295
) as s:
295-
stream = anime.get_video(
296-
ep, lang, preferred_quality=self.options.quality
297-
)
298-
if stream is None:
299-
error("Could not find stream for requested episode, skipping")
296+
try:
297+
stream = anime.get_video(
298+
ep, lang, preferred_quality=self.options.quality
299+
)
300+
except LangTypeNotAvailableError:
301+
if lang == LanguageTypeEnum.SUB:
302+
new_lang = LanguageTypeEnum.DUB
303+
else:
304+
new_lang = LanguageTypeEnum.SUB
305+
306+
error(f"Language {lang} not available, changing to {new_lang} for this episode!")
307+
308+
stream = anime.get_video(
309+
ep, new_lang, preferred_quality=self.options.quality
310+
)
311+
300312
s.ok("✔")
301313

314+
302315
self.player.play_title(anime, stream)
303316
self.player.wait()
304317

@@ -357,7 +370,7 @@ def sync_anilist_seasonls(self):
357370
else LanguageTypeEnum.SUB
358371
)
359372

360-
if pref_lang in v.languages:
373+
if pref_lang in v.languages or LanguageTypeEnum.UND in v.languages:
361374
lang = pref_lang
362375
else:
363376
lang = next(iter(v.languages))
@@ -463,7 +476,7 @@ def _choose_latest(
463476
else LanguageTypeEnum.SUB
464477
)
465478

466-
if pref_lang in result.languages:
479+
if pref_lang in result.languages and LanguageTypeEnum.UND in result.languages:
467480
lang = pref_lang
468481
s.write(
469482
f"> Looking for {lang} episodes because of config/tag preference"

0 commit comments

Comments
 (0)