nsfw
Browse files- api/providers/anilist_home.py +5 -3
- api/providers/mal_fallback.py +12 -4
- api/providers/miruro/catalog.py +51 -40
- api/providers/miruro/home.py +5 -3
- api/providers/miruro/search.py +25 -15
- api/providers/unified.py +21 -21
- api/routes/anime/catalog_routes.py +8 -4
- api/routes/shared/home_routes.py +4 -3
- api/routes/shared/search_routes.py +12 -3
- api/static/js/search.js +2 -2
- api/templates/anime/genre.html +2 -1
- api/templates/anime/results.html +2 -1
- api/templates/shared/base.html +29 -0
- api/templates/shared/index.html +3 -3
- api/templates/shared/settings.html +11 -4
api/providers/anilist_home.py
CHANGED
|
@@ -111,7 +111,7 @@ class AnilistHomeService:
|
|
| 111 |
logger.error(f"AniList request failed: {e}")
|
| 112 |
return {}
|
| 113 |
|
| 114 |
-
async def _fetch_home_data(self) -> Dict[str, Any]:
|
| 115 |
"""Fetch trending, popular, and recent from AniList GraphQL API using a single combined query"""
|
| 116 |
now = time.time()
|
| 117 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
@@ -168,6 +168,8 @@ class AnilistHomeService:
|
|
| 168 |
return data.get(key, {}).get("media", [])
|
| 169 |
|
| 170 |
def filter_adult(items):
|
|
|
|
|
|
|
| 171 |
return [
|
| 172 |
item for item in items
|
| 173 |
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
|
@@ -267,9 +269,9 @@ class AnilistHomeService:
|
|
| 267 |
|
| 268 |
return animes
|
| 269 |
|
| 270 |
-
async def home(self) -> Dict[str, Any]:
|
| 271 |
"""Get unified home response with all sections + metadata"""
|
| 272 |
-
data = await self._fetch_home_data()
|
| 273 |
return {
|
| 274 |
"success": True,
|
| 275 |
"data": {key: value for key, value in data.items()},
|
|
|
|
| 111 |
logger.error(f"AniList request failed: {e}")
|
| 112 |
return {}
|
| 113 |
|
| 114 |
+
async def _fetch_home_data(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 115 |
"""Fetch trending, popular, and recent from AniList GraphQL API using a single combined query"""
|
| 116 |
now = time.time()
|
| 117 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
|
|
| 168 |
return data.get(key, {}).get("media", [])
|
| 169 |
|
| 170 |
def filter_adult(items):
|
| 171 |
+
if include_adult:
|
| 172 |
+
return items
|
| 173 |
return [
|
| 174 |
item for item in items
|
| 175 |
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
|
|
|
| 269 |
|
| 270 |
return animes
|
| 271 |
|
| 272 |
+
async def home(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 273 |
"""Get unified home response with all sections + metadata"""
|
| 274 |
+
data = await self._fetch_home_data(include_adult=include_adult)
|
| 275 |
return {
|
| 276 |
"success": True,
|
| 277 |
"data": {key: value for key, value in data.items()},
|
api/providers/mal_fallback.py
CHANGED
|
@@ -482,7 +482,7 @@ class MalFallbackService:
|
|
| 482 |
# HOME
|
| 483 |
# ======================================================================
|
| 484 |
|
| 485 |
-
async def home(self) -> Dict[str, Any]:
|
| 486 |
"""Fetch home page data from Jikan as fallback."""
|
| 487 |
now = time.time()
|
| 488 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
@@ -507,6 +507,8 @@ class MalFallbackService:
|
|
| 507 |
return resp.get("data", [])
|
| 508 |
|
| 509 |
def filter_adult(items):
|
|
|
|
|
|
|
| 510 |
return [
|
| 511 |
item for item in items
|
| 512 |
if not (item.get("rating") or "").startswith("Rx")
|
|
@@ -647,7 +649,7 @@ class MalFallbackService:
|
|
| 647 |
)
|
| 648 |
return info
|
| 649 |
|
| 650 |
-
async def search(self, q: str, page: int = 1, **kwargs) -> Dict[str, Any]:
|
| 651 |
"""Search anime using Jikan fallback."""
|
| 652 |
await self._ensure_mapping()
|
| 653 |
|
|
@@ -670,6 +672,8 @@ class MalFallbackService:
|
|
| 670 |
|
| 671 |
# Filter adult and hentai
|
| 672 |
def filter_adult(items):
|
|
|
|
|
|
|
| 673 |
return [
|
| 674 |
item for item in items
|
| 675 |
if not (item.get("rating") or "").startswith("Rx")
|
|
@@ -705,7 +709,7 @@ class MalFallbackService:
|
|
| 705 |
"searchQuery": q,
|
| 706 |
}
|
| 707 |
|
| 708 |
-
async def search_suggestions(self, q: str) -> Dict[str, Any]:
|
| 709 |
"""Search autocomplete suggestions using Jikan fallback."""
|
| 710 |
await self._ensure_mapping()
|
| 711 |
|
|
@@ -717,6 +721,8 @@ class MalFallbackService:
|
|
| 717 |
data = resp.get("data", [])
|
| 718 |
|
| 719 |
def filter_adult(items):
|
|
|
|
|
|
|
| 720 |
return [
|
| 721 |
item for item in items
|
| 722 |
if not (item.get("rating") or "").startswith("Rx")
|
|
@@ -769,7 +775,7 @@ class MalFallbackService:
|
|
| 769 |
|
| 770 |
return {"suggestions": suggestions}
|
| 771 |
|
| 772 |
-
async def az_list(self, sort_option: str = "all", page: int = 1) -> Dict[str, Any]:
|
| 773 |
"""A-Z anime list using Jikan fallback."""
|
| 774 |
await self._ensure_mapping()
|
| 775 |
|
|
@@ -789,6 +795,8 @@ class MalFallbackService:
|
|
| 789 |
has_next = pagination.get("has_next_page", False)
|
| 790 |
|
| 791 |
def filter_adult(items):
|
|
|
|
|
|
|
| 792 |
return [
|
| 793 |
item for item in items
|
| 794 |
if not (item.get("rating") or "").startswith("Rx")
|
|
|
|
| 482 |
# HOME
|
| 483 |
# ======================================================================
|
| 484 |
|
| 485 |
+
async def home(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 486 |
"""Fetch home page data from Jikan as fallback."""
|
| 487 |
now = time.time()
|
| 488 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
|
|
| 507 |
return resp.get("data", [])
|
| 508 |
|
| 509 |
def filter_adult(items):
|
| 510 |
+
if include_adult:
|
| 511 |
+
return items
|
| 512 |
return [
|
| 513 |
item for item in items
|
| 514 |
if not (item.get("rating") or "").startswith("Rx")
|
|
|
|
| 649 |
)
|
| 650 |
return info
|
| 651 |
|
| 652 |
+
async def search(self, q: str, page: int = 1, include_adult: bool = False, **kwargs) -> Dict[str, Any]:
|
| 653 |
"""Search anime using Jikan fallback."""
|
| 654 |
await self._ensure_mapping()
|
| 655 |
|
|
|
|
| 672 |
|
| 673 |
# Filter adult and hentai
|
| 674 |
def filter_adult(items):
|
| 675 |
+
if include_adult:
|
| 676 |
+
return items
|
| 677 |
return [
|
| 678 |
item for item in items
|
| 679 |
if not (item.get("rating") or "").startswith("Rx")
|
|
|
|
| 709 |
"searchQuery": q,
|
| 710 |
}
|
| 711 |
|
| 712 |
+
async def search_suggestions(self, q: str, include_adult: bool = False) -> Dict[str, Any]:
|
| 713 |
"""Search autocomplete suggestions using Jikan fallback."""
|
| 714 |
await self._ensure_mapping()
|
| 715 |
|
|
|
|
| 721 |
data = resp.get("data", [])
|
| 722 |
|
| 723 |
def filter_adult(items):
|
| 724 |
+
if include_adult:
|
| 725 |
+
return items
|
| 726 |
return [
|
| 727 |
item for item in items
|
| 728 |
if not (item.get("rating") or "").startswith("Rx")
|
|
|
|
| 775 |
|
| 776 |
return {"suggestions": suggestions}
|
| 777 |
|
| 778 |
+
async def az_list(self, sort_option: str = "all", page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 779 |
"""A-Z anime list using Jikan fallback."""
|
| 780 |
await self._ensure_mapping()
|
| 781 |
|
|
|
|
| 795 |
has_next = pagination.get("has_next_page", False)
|
| 796 |
|
| 797 |
def filter_adult(items):
|
| 798 |
+
if include_adult:
|
| 799 |
+
return items
|
| 800 |
return [
|
| 801 |
item for item in items
|
| 802 |
if not (item.get("rating") or "").startswith("Rx")
|
api/providers/miruro/catalog.py
CHANGED
|
@@ -58,33 +58,34 @@ class MiruroCatalogService:
|
|
| 58 |
logger.error(f"AniList fallback query failed: {e}")
|
| 59 |
return {}
|
| 60 |
|
| 61 |
-
async def genre(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 62 |
"""Get anime by genre via AniList GraphQL"""
|
| 63 |
resp = None
|
| 64 |
if not resp:
|
| 65 |
logger.info(f"Miruro /filter failed for genre '{name}'. Using AniList fallback.")
|
| 66 |
-
|
| 67 |
-
query
|
| 68 |
-
|
| 69 |
-
|
|
|
|
| 70 |
total
|
| 71 |
hasNextPage
|
| 72 |
lastPage
|
| 73 |
-
}
|
| 74 |
-
media(type: ANIME, genre: $genre, sort: SCORE_DESC,
|
| 75 |
id
|
| 76 |
-
title { romaji english native }
|
| 77 |
-
coverImage { extraLarge large }
|
| 78 |
episodes
|
| 79 |
-
nextAiringEpisode { episode }
|
| 80 |
format
|
| 81 |
duration
|
| 82 |
averageScore
|
| 83 |
genres
|
| 84 |
isAdult
|
| 85 |
-
}
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
'''
|
| 89 |
variables = {"genre": name.title(), "page": page, "perPage": 24}
|
| 90 |
fallback_data = await self._fallback_anilist_query(query, variables)
|
|
@@ -94,10 +95,13 @@ class MiruroCatalogService:
|
|
| 94 |
media_list = page_data.get("media", [])
|
| 95 |
page_info = page_data.get("pageInfo", {})
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 102 |
|
| 103 |
return {
|
|
@@ -111,7 +115,7 @@ class MiruroCatalogService:
|
|
| 111 |
|
| 112 |
|
| 113 |
|
| 114 |
-
async def category(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 115 |
"""Get anime by category via AniList API"""
|
| 116 |
category_map = {
|
| 117 |
"trending": {"sort": "TRENDING_DESC"},
|
|
@@ -136,24 +140,25 @@ class MiruroCatalogService:
|
|
| 136 |
graphql_status = extra_params.get("status")
|
| 137 |
graphql_sort = extra_params.get("sort")
|
| 138 |
|
| 139 |
-
|
| 140 |
-
query
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
| 144 |
id
|
| 145 |
-
title { romaji english native }
|
| 146 |
-
coverImage { extraLarge large }
|
| 147 |
episodes
|
| 148 |
-
nextAiringEpisode { episode }
|
| 149 |
format
|
| 150 |
duration
|
| 151 |
averageScore
|
| 152 |
genres
|
| 153 |
isAdult
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
}
|
| 157 |
'''
|
| 158 |
variables = {"page": page, "perPage": 24}
|
| 159 |
if graphql_format:
|
|
@@ -170,10 +175,13 @@ class MiruroCatalogService:
|
|
| 170 |
media_list = page_data.get("media", [])
|
| 171 |
page_info = page_data.get("pageInfo", {})
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 178 |
|
| 179 |
return {
|
|
@@ -185,7 +193,7 @@ class MiruroCatalogService:
|
|
| 185 |
}
|
| 186 |
return {}
|
| 187 |
|
| 188 |
-
async def producer(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 189 |
"""
|
| 190 |
Get anime by producer/studio — Use AniList GraphQL
|
| 191 |
"""
|
|
@@ -225,10 +233,13 @@ class MiruroCatalogService:
|
|
| 225 |
page_info = page_data.get("pageInfo", {})
|
| 226 |
if studios and "media" in studios[0]:
|
| 227 |
media_list = studios[0]["media"].get("nodes", [])
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
| 232 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 233 |
|
| 234 |
return {
|
|
@@ -239,7 +250,7 @@ class MiruroCatalogService:
|
|
| 239 |
"currentPage": page,
|
| 240 |
}
|
| 241 |
|
| 242 |
-
async def schedule(self, date: str = None) -> Dict[str, Any]:
|
| 243 |
"""Get anime airing schedule via AniList GraphQL endpoint"""
|
| 244 |
import time
|
| 245 |
now = int(time.time())
|
|
@@ -280,7 +291,7 @@ class MiruroCatalogService:
|
|
| 280 |
schedules = fallback_data["data"]["Page"].get("airingSchedules", [])
|
| 281 |
for sched in schedules:
|
| 282 |
media = sched.get("media", {})
|
| 283 |
-
if not
|
| 284 |
continue
|
| 285 |
normalized = self._normalize_anime(media)
|
| 286 |
normalized["next_episode"] = sched.get("episode")
|
|
|
|
| 58 |
logger.error(f"AniList fallback query failed: {e}")
|
| 59 |
return {}
|
| 60 |
|
| 61 |
+
async def genre(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 62 |
"""Get anime by genre via AniList GraphQL"""
|
| 63 |
resp = None
|
| 64 |
if not resp:
|
| 65 |
logger.info(f"Miruro /filter failed for genre '{name}'. Using AniList fallback.")
|
| 66 |
+
isAdult_filter = "" if include_adult else "isAdult: false"
|
| 67 |
+
query = f'''
|
| 68 |
+
query ($genre: String, $page: Int, $perPage: Int) {{
|
| 69 |
+
Page(page: $page, perPage: $perPage) {{
|
| 70 |
+
pageInfo {{
|
| 71 |
total
|
| 72 |
hasNextPage
|
| 73 |
lastPage
|
| 74 |
+
}}
|
| 75 |
+
media(type: ANIME, genre: $genre, sort: SCORE_DESC{', ' + isAdult_filter if isAdult_filter else ''}) {{
|
| 76 |
id
|
| 77 |
+
title {{ romaji english native }}
|
| 78 |
+
coverImage {{ extraLarge large }}
|
| 79 |
episodes
|
| 80 |
+
nextAiringEpisode {{ episode }}
|
| 81 |
format
|
| 82 |
duration
|
| 83 |
averageScore
|
| 84 |
genres
|
| 85 |
isAdult
|
| 86 |
+
}}
|
| 87 |
+
}}
|
| 88 |
+
}}
|
| 89 |
'''
|
| 90 |
variables = {"genre": name.title(), "page": page, "perPage": 24}
|
| 91 |
fallback_data = await self._fallback_anilist_query(query, variables)
|
|
|
|
| 95 |
media_list = page_data.get("media", [])
|
| 96 |
page_info = page_data.get("pageInfo", {})
|
| 97 |
|
| 98 |
+
if not include_adult:
|
| 99 |
+
filtered_results = [
|
| 100 |
+
item for item in media_list
|
| 101 |
+
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
| 102 |
+
]
|
| 103 |
+
else:
|
| 104 |
+
filtered_results = media_list
|
| 105 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 106 |
|
| 107 |
return {
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
|
| 118 |
+
async def category(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 119 |
"""Get anime by category via AniList API"""
|
| 120 |
category_map = {
|
| 121 |
"trending": {"sort": "TRENDING_DESC"},
|
|
|
|
| 140 |
graphql_status = extra_params.get("status")
|
| 141 |
graphql_sort = extra_params.get("sort")
|
| 142 |
|
| 143 |
+
isAdult_filter = "" if include_adult else "isAdult: false"
|
| 144 |
+
query = f'''
|
| 145 |
+
query ($page: Int, $perPage: Int, $format: MediaFormat, $status: MediaStatus, $sort: [MediaSort]) {{
|
| 146 |
+
Page(page: $page, perPage: $perPage) {{
|
| 147 |
+
pageInfo {{ total hasNextPage lastPage }}
|
| 148 |
+
media(type: ANIME, format: $format, status: $status, sort: $sort{', ' + isAdult_filter if isAdult_filter else ''}) {{
|
| 149 |
id
|
| 150 |
+
title {{ romaji english native }}
|
| 151 |
+
coverImage {{ extraLarge large }}
|
| 152 |
episodes
|
| 153 |
+
nextAiringEpisode {{ episode }}
|
| 154 |
format
|
| 155 |
duration
|
| 156 |
averageScore
|
| 157 |
genres
|
| 158 |
isAdult
|
| 159 |
+
}}
|
| 160 |
+
}}
|
| 161 |
+
}}
|
| 162 |
'''
|
| 163 |
variables = {"page": page, "perPage": 24}
|
| 164 |
if graphql_format:
|
|
|
|
| 175 |
media_list = page_data.get("media", [])
|
| 176 |
page_info = page_data.get("pageInfo", {})
|
| 177 |
|
| 178 |
+
if not include_adult:
|
| 179 |
+
filtered_results = [
|
| 180 |
+
item for item in media_list
|
| 181 |
+
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
| 182 |
+
]
|
| 183 |
+
else:
|
| 184 |
+
filtered_results = media_list
|
| 185 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 186 |
|
| 187 |
return {
|
|
|
|
| 193 |
}
|
| 194 |
return {}
|
| 195 |
|
| 196 |
+
async def producer(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 197 |
"""
|
| 198 |
Get anime by producer/studio — Use AniList GraphQL
|
| 199 |
"""
|
|
|
|
| 233 |
page_info = page_data.get("pageInfo", {})
|
| 234 |
if studios and "media" in studios[0]:
|
| 235 |
media_list = studios[0]["media"].get("nodes", [])
|
| 236 |
+
if not include_adult:
|
| 237 |
+
filtered_results = [
|
| 238 |
+
item for item in media_list
|
| 239 |
+
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
| 240 |
+
]
|
| 241 |
+
else:
|
| 242 |
+
filtered_results = media_list
|
| 243 |
animes = [self._normalize_anime(item) for item in filtered_results]
|
| 244 |
|
| 245 |
return {
|
|
|
|
| 250 |
"currentPage": page,
|
| 251 |
}
|
| 252 |
|
| 253 |
+
async def schedule(self, date: str = None, include_adult: bool = False) -> Dict[str, Any]:
|
| 254 |
"""Get anime airing schedule via AniList GraphQL endpoint"""
|
| 255 |
import time
|
| 256 |
now = int(time.time())
|
|
|
|
| 291 |
schedules = fallback_data["data"]["Page"].get("airingSchedules", [])
|
| 292 |
for sched in schedules:
|
| 293 |
media = sched.get("media", {})
|
| 294 |
+
if not include_adult and (media.get("isAdult", False) or "Hentai" in media.get("genres", [])):
|
| 295 |
continue
|
| 296 |
normalized = self._normalize_anime(media)
|
| 297 |
normalized["next_episode"] = sched.get("episode")
|
api/providers/miruro/home.py
CHANGED
|
@@ -84,7 +84,7 @@ class MiruroHomeService:
|
|
| 84 |
base["nextEpisode"] = next_ep.get("episode") or None
|
| 85 |
return base
|
| 86 |
|
| 87 |
-
async def _fetch_home_data(self) -> Dict[str, Any]:
|
| 88 |
"""Fetch trending, popular, and recent from Miruro API in parallel"""
|
| 89 |
now = time.time()
|
| 90 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
@@ -107,6 +107,8 @@ class MiruroHomeService:
|
|
| 107 |
return resp.get("results", [])
|
| 108 |
|
| 109 |
def filter_adult(items):
|
|
|
|
|
|
|
| 110 |
return [
|
| 111 |
item for item in items
|
| 112 |
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
|
@@ -189,9 +191,9 @@ class MiruroHomeService:
|
|
| 189 |
"latestEpisodeAnimes": [],
|
| 190 |
}
|
| 191 |
|
| 192 |
-
async def home(self) -> Dict[str, Any]:
|
| 193 |
"""Get unified home response with all sections + metadata"""
|
| 194 |
-
data = await self._fetch_home_data()
|
| 195 |
return {
|
| 196 |
"success": True,
|
| 197 |
"data": {key: value for key, value in data.items()},
|
|
|
|
| 84 |
base["nextEpisode"] = next_ep.get("episode") or None
|
| 85 |
return base
|
| 86 |
|
| 87 |
+
async def _fetch_home_data(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 88 |
"""Fetch trending, popular, and recent from Miruro API in parallel"""
|
| 89 |
now = time.time()
|
| 90 |
if self._home_cache and (now - self._home_cache_ts) < self._home_cache_ttl:
|
|
|
|
| 107 |
return resp.get("results", [])
|
| 108 |
|
| 109 |
def filter_adult(items):
|
| 110 |
+
if include_adult:
|
| 111 |
+
return items
|
| 112 |
return [
|
| 113 |
item for item in items
|
| 114 |
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
|
|
|
| 191 |
"latestEpisodeAnimes": [],
|
| 192 |
}
|
| 193 |
|
| 194 |
+
async def home(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 195 |
"""Get unified home response with all sections + metadata"""
|
| 196 |
+
data = await self._fetch_home_data(include_adult)
|
| 197 |
return {
|
| 198 |
"success": True,
|
| 199 |
"data": {key: value for key, value in data.items()},
|
api/providers/miruro/search.py
CHANGED
|
@@ -73,7 +73,8 @@ class MiruroSearchService:
|
|
| 73 |
rating: Optional[str] = None,
|
| 74 |
start_date: Optional[str] = None,
|
| 75 |
end_date: Optional[str] = None,
|
| 76 |
-
score: Optional[str] = None
|
|
|
|
| 77 |
) -> Dict[str, Any]:
|
| 78 |
"""
|
| 79 |
Search anime via Miruro /search endpoint
|
|
@@ -113,10 +114,13 @@ class MiruroSearchService:
|
|
| 113 |
page_data = {}
|
| 114 |
|
| 115 |
results = page_data.get("media", [])
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
page_info = page_data.get("pageInfo", {})
|
| 122 |
total = page_info.get("total", 0)
|
|
@@ -139,7 +143,7 @@ class MiruroSearchService:
|
|
| 139 |
"searchQuery": q,
|
| 140 |
}
|
| 141 |
|
| 142 |
-
async def search_suggestions(self, q: str) -> Dict[str, Any]:
|
| 143 |
"""
|
| 144 |
Get search suggestions via Miruro /suggestions endpoint
|
| 145 |
Returns data in standard format
|
|
@@ -174,10 +178,13 @@ class MiruroSearchService:
|
|
| 174 |
logger.error(f"Anilist suggestions fetch failed: {e}")
|
| 175 |
suggestions = []
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
normalized = []
|
| 183 |
for s in filtered_suggestions:
|
|
@@ -213,7 +220,7 @@ class MiruroSearchService:
|
|
| 213 |
|
| 214 |
return {"suggestions": normalized}
|
| 215 |
|
| 216 |
-
async def az_list(self, sort_option: str = "all", page: int = 1) -> Dict[str, Any]:
|
| 217 |
"""
|
| 218 |
Miruro doesn't have a direct A-Z list endpoint.
|
| 219 |
Use /filter with alphabet sorting as a workaround.
|
|
@@ -252,10 +259,13 @@ class MiruroSearchService:
|
|
| 252 |
page_data = {}
|
| 253 |
|
| 254 |
results = page_data.get("media", [])
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
page_info = page_data.get("pageInfo", {})
|
| 261 |
|
|
|
|
| 73 |
rating: Optional[str] = None,
|
| 74 |
start_date: Optional[str] = None,
|
| 75 |
end_date: Optional[str] = None,
|
| 76 |
+
score: Optional[str] = None,
|
| 77 |
+
include_adult: bool = False
|
| 78 |
) -> Dict[str, Any]:
|
| 79 |
"""
|
| 80 |
Search anime via Miruro /search endpoint
|
|
|
|
| 114 |
page_data = {}
|
| 115 |
|
| 116 |
results = page_data.get("media", [])
|
| 117 |
+
if not include_adult:
|
| 118 |
+
filtered_results = [
|
| 119 |
+
item for item in results
|
| 120 |
+
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
| 121 |
+
]
|
| 122 |
+
else:
|
| 123 |
+
filtered_results = results
|
| 124 |
|
| 125 |
page_info = page_data.get("pageInfo", {})
|
| 126 |
total = page_info.get("total", 0)
|
|
|
|
| 143 |
"searchQuery": q,
|
| 144 |
}
|
| 145 |
|
| 146 |
+
async def search_suggestions(self, q: str, include_adult: bool = False) -> Dict[str, Any]:
|
| 147 |
"""
|
| 148 |
Get search suggestions via Miruro /suggestions endpoint
|
| 149 |
Returns data in standard format
|
|
|
|
| 178 |
logger.error(f"Anilist suggestions fetch failed: {e}")
|
| 179 |
suggestions = []
|
| 180 |
|
| 181 |
+
if not include_adult:
|
| 182 |
+
filtered_suggestions = [
|
| 183 |
+
s for s in suggestions
|
| 184 |
+
if not s.get("isAdult", False) and "Hentai" not in s.get("genres", [])
|
| 185 |
+
]
|
| 186 |
+
else:
|
| 187 |
+
filtered_suggestions = suggestions
|
| 188 |
|
| 189 |
normalized = []
|
| 190 |
for s in filtered_suggestions:
|
|
|
|
| 220 |
|
| 221 |
return {"suggestions": normalized}
|
| 222 |
|
| 223 |
+
async def az_list(self, sort_option: str = "all", page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 224 |
"""
|
| 225 |
Miruro doesn't have a direct A-Z list endpoint.
|
| 226 |
Use /filter with alphabet sorting as a workaround.
|
|
|
|
| 259 |
page_data = {}
|
| 260 |
|
| 261 |
results = page_data.get("media", [])
|
| 262 |
+
if not include_adult:
|
| 263 |
+
filtered_results = [
|
| 264 |
+
item for item in results
|
| 265 |
+
if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
|
| 266 |
+
]
|
| 267 |
+
else:
|
| 268 |
+
filtered_results = results
|
| 269 |
|
| 270 |
page_info = page_data.get("pageInfo", {})
|
| 271 |
|
api/providers/unified.py
CHANGED
|
@@ -39,10 +39,10 @@ class UnifiedScraper:
|
|
| 39 |
# =========================================================================
|
| 40 |
# HOME
|
| 41 |
# =========================================================================
|
| 42 |
-
async def home(self) -> Dict[str, Any]:
|
| 43 |
"""Get home page data from AniList GraphQL with fallback to Miruro API"""
|
| 44 |
try:
|
| 45 |
-
result = await self.anilist_home.home()
|
| 46 |
if (
|
| 47 |
result
|
| 48 |
and result.get("success")
|
|
@@ -62,7 +62,7 @@ class UnifiedScraper:
|
|
| 62 |
|
| 63 |
try:
|
| 64 |
logger.info("[UnifiedScraper] Home: Falling back to Miruro API")
|
| 65 |
-
miruro_result = await self.miruro.home()
|
| 66 |
if (
|
| 67 |
miruro_result
|
| 68 |
and miruro_result.get("success")
|
|
@@ -82,7 +82,7 @@ class UnifiedScraper:
|
|
| 82 |
# Third tier: Jikan (MAL) fallback
|
| 83 |
try:
|
| 84 |
logger.info("[UnifiedScraper] Home: Falling back to Jikan (MAL)")
|
| 85 |
-
mal_result = await self.mal_fallback.home()
|
| 86 |
if (
|
| 87 |
mal_result
|
| 88 |
and mal_result.get("success")
|
|
@@ -726,10 +726,10 @@ class UnifiedScraper:
|
|
| 726 |
# =========================================================================
|
| 727 |
# SEARCH
|
| 728 |
# =========================================================================
|
| 729 |
-
async def search(self, q: str, page: int = 1, **kwargs) -> Dict[str, Any]:
|
| 730 |
"""Search anime — Miruro with Jikan fallback"""
|
| 731 |
try:
|
| 732 |
-
result = await self.miruro.search(q, page, **kwargs)
|
| 733 |
if result and result.get("animes"):
|
| 734 |
logger.debug(
|
| 735 |
f"[UnifiedScraper] Search (Miruro): {len(result.get('animes', []))} results"
|
|
@@ -740,7 +740,7 @@ class UnifiedScraper:
|
|
| 740 |
|
| 741 |
try:
|
| 742 |
logger.info("[UnifiedScraper] Search: Falling back to Jikan (MAL)")
|
| 743 |
-
result = await self.mal_fallback.search(q, page, **kwargs)
|
| 744 |
if result and result.get("animes"):
|
| 745 |
return result
|
| 746 |
except Exception as e:
|
|
@@ -755,10 +755,10 @@ class UnifiedScraper:
|
|
| 755 |
"searchQuery": q,
|
| 756 |
}
|
| 757 |
|
| 758 |
-
async def search_suggestions(self, q: str) -> Dict[str, Any]:
|
| 759 |
"""Get search suggestions — Miruro with Jikan fallback"""
|
| 760 |
try:
|
| 761 |
-
result = await self.miruro.search_suggestions(q)
|
| 762 |
if result and result.get("suggestions"):
|
| 763 |
logger.debug(
|
| 764 |
f"[UnifiedScraper] Suggestions (Miruro): {len(result.get('suggestions', []))} results"
|
|
@@ -769,7 +769,7 @@ class UnifiedScraper:
|
|
| 769 |
|
| 770 |
try:
|
| 771 |
logger.info("[UnifiedScraper] Suggestions: Falling back to Jikan (MAL)")
|
| 772 |
-
result = await self.mal_fallback.search_suggestions(q)
|
| 773 |
if result and result.get("suggestions"):
|
| 774 |
return result
|
| 775 |
except Exception as e:
|
|
@@ -777,10 +777,10 @@ class UnifiedScraper:
|
|
| 777 |
|
| 778 |
return {"suggestions": []}
|
| 779 |
|
| 780 |
-
async def az_list(self, sort_option: str = "all", page: int = 1) -> Dict[str, Any]:
|
| 781 |
"""Get A-Z anime list — Miruro with Jikan fallback"""
|
| 782 |
try:
|
| 783 |
-
result = await self.miruro.az_list(sort_option, page)
|
| 784 |
if result and result.get("animes"):
|
| 785 |
return result
|
| 786 |
except Exception as e:
|
|
@@ -788,7 +788,7 @@ class UnifiedScraper:
|
|
| 788 |
|
| 789 |
try:
|
| 790 |
logger.info("[UnifiedScraper] az_list: Falling back to Jikan (MAL)")
|
| 791 |
-
result = await self.mal_fallback.az_list(sort_option, page)
|
| 792 |
if result and result.get("animes"):
|
| 793 |
return result
|
| 794 |
except Exception as e:
|
|
@@ -799,10 +799,10 @@ class UnifiedScraper:
|
|
| 799 |
# =========================================================================
|
| 800 |
# CATALOG
|
| 801 |
# =========================================================================
|
| 802 |
-
async def producer(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 803 |
"""Get anime by producer"""
|
| 804 |
try:
|
| 805 |
-
result = await self.miruro.producer(name, page)
|
| 806 |
if result and result.get("animes"):
|
| 807 |
return result
|
| 808 |
except Exception:
|
|
@@ -816,10 +816,10 @@ class UnifiedScraper:
|
|
| 816 |
except Exception:
|
| 817 |
return {"success": False, "message": "Failed to fetch studio details"}
|
| 818 |
|
| 819 |
-
async def genre(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 820 |
"""Get anime by genre"""
|
| 821 |
try:
|
| 822 |
-
result = await self.miruro.genre(name, page)
|
| 823 |
if result and result.get("animes"):
|
| 824 |
logger.debug(
|
| 825 |
f"[UnifiedScraper] Genre (Miruro, {name}): {len(result.get('animes', []))} results"
|
|
@@ -830,10 +830,10 @@ class UnifiedScraper:
|
|
| 830 |
|
| 831 |
return {}
|
| 832 |
|
| 833 |
-
async def category(self, name: str, page: int = 1) -> Dict[str, Any]:
|
| 834 |
"""Get anime by category"""
|
| 835 |
try:
|
| 836 |
-
result = await self.miruro.category(name, page)
|
| 837 |
if result and result.get("animes"):
|
| 838 |
logger.debug(
|
| 839 |
f"[UnifiedScraper] Category (Miruro, {name}): {len(result.get('animes', []))} results"
|
|
@@ -844,10 +844,10 @@ class UnifiedScraper:
|
|
| 844 |
|
| 845 |
return {}
|
| 846 |
|
| 847 |
-
async def schedule(self, date: str = None) -> Dict[str, Any]:
|
| 848 |
"""Get anime schedule"""
|
| 849 |
try:
|
| 850 |
-
result = await self.miruro.schedule(date)
|
| 851 |
if result and (result.get("scheduledAnimes") or result.get("animes")):
|
| 852 |
return result
|
| 853 |
except Exception:
|
|
|
|
| 39 |
# =========================================================================
|
| 40 |
# HOME
|
| 41 |
# =========================================================================
|
| 42 |
+
async def home(self, include_adult: bool = False) -> Dict[str, Any]:
|
| 43 |
"""Get home page data from AniList GraphQL with fallback to Miruro API"""
|
| 44 |
try:
|
| 45 |
+
result = await self.anilist_home.home(include_adult=include_adult)
|
| 46 |
if (
|
| 47 |
result
|
| 48 |
and result.get("success")
|
|
|
|
| 62 |
|
| 63 |
try:
|
| 64 |
logger.info("[UnifiedScraper] Home: Falling back to Miruro API")
|
| 65 |
+
miruro_result = await self.miruro.home(include_adult=include_adult)
|
| 66 |
if (
|
| 67 |
miruro_result
|
| 68 |
and miruro_result.get("success")
|
|
|
|
| 82 |
# Third tier: Jikan (MAL) fallback
|
| 83 |
try:
|
| 84 |
logger.info("[UnifiedScraper] Home: Falling back to Jikan (MAL)")
|
| 85 |
+
mal_result = await self.mal_fallback.home(include_adult=include_adult)
|
| 86 |
if (
|
| 87 |
mal_result
|
| 88 |
and mal_result.get("success")
|
|
|
|
| 726 |
# =========================================================================
|
| 727 |
# SEARCH
|
| 728 |
# =========================================================================
|
| 729 |
+
async def search(self, q: str, page: int = 1, include_adult: bool = False, **kwargs) -> Dict[str, Any]:
|
| 730 |
"""Search anime — Miruro with Jikan fallback"""
|
| 731 |
try:
|
| 732 |
+
result = await self.miruro.search(q, page, include_adult=include_adult, **kwargs)
|
| 733 |
if result and result.get("animes"):
|
| 734 |
logger.debug(
|
| 735 |
f"[UnifiedScraper] Search (Miruro): {len(result.get('animes', []))} results"
|
|
|
|
| 740 |
|
| 741 |
try:
|
| 742 |
logger.info("[UnifiedScraper] Search: Falling back to Jikan (MAL)")
|
| 743 |
+
result = await self.mal_fallback.search(q, page, include_adult=include_adult, **kwargs)
|
| 744 |
if result and result.get("animes"):
|
| 745 |
return result
|
| 746 |
except Exception as e:
|
|
|
|
| 755 |
"searchQuery": q,
|
| 756 |
}
|
| 757 |
|
| 758 |
+
async def search_suggestions(self, q: str, include_adult: bool = False) -> Dict[str, Any]:
|
| 759 |
"""Get search suggestions — Miruro with Jikan fallback"""
|
| 760 |
try:
|
| 761 |
+
result = await self.miruro.search_suggestions(q, include_adult=include_adult)
|
| 762 |
if result and result.get("suggestions"):
|
| 763 |
logger.debug(
|
| 764 |
f"[UnifiedScraper] Suggestions (Miruro): {len(result.get('suggestions', []))} results"
|
|
|
|
| 769 |
|
| 770 |
try:
|
| 771 |
logger.info("[UnifiedScraper] Suggestions: Falling back to Jikan (MAL)")
|
| 772 |
+
result = await self.mal_fallback.search_suggestions(q, include_adult=include_adult)
|
| 773 |
if result and result.get("suggestions"):
|
| 774 |
return result
|
| 775 |
except Exception as e:
|
|
|
|
| 777 |
|
| 778 |
return {"suggestions": []}
|
| 779 |
|
| 780 |
+
async def az_list(self, sort_option: str = "all", page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 781 |
"""Get A-Z anime list — Miruro with Jikan fallback"""
|
| 782 |
try:
|
| 783 |
+
result = await self.miruro.az_list(sort_option, page, include_adult=include_adult)
|
| 784 |
if result and result.get("animes"):
|
| 785 |
return result
|
| 786 |
except Exception as e:
|
|
|
|
| 788 |
|
| 789 |
try:
|
| 790 |
logger.info("[UnifiedScraper] az_list: Falling back to Jikan (MAL)")
|
| 791 |
+
result = await self.mal_fallback.az_list(sort_option, page, include_adult=include_adult)
|
| 792 |
if result and result.get("animes"):
|
| 793 |
return result
|
| 794 |
except Exception as e:
|
|
|
|
| 799 |
# =========================================================================
|
| 800 |
# CATALOG
|
| 801 |
# =========================================================================
|
| 802 |
+
async def producer(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 803 |
"""Get anime by producer"""
|
| 804 |
try:
|
| 805 |
+
result = await self.miruro.producer(name, page, include_adult=include_adult)
|
| 806 |
if result and result.get("animes"):
|
| 807 |
return result
|
| 808 |
except Exception:
|
|
|
|
| 816 |
except Exception:
|
| 817 |
return {"success": False, "message": "Failed to fetch studio details"}
|
| 818 |
|
| 819 |
+
async def genre(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 820 |
"""Get anime by genre"""
|
| 821 |
try:
|
| 822 |
+
result = await self.miruro.genre(name, page, include_adult=include_adult)
|
| 823 |
if result and result.get("animes"):
|
| 824 |
logger.debug(
|
| 825 |
f"[UnifiedScraper] Genre (Miruro, {name}): {len(result.get('animes', []))} results"
|
|
|
|
| 830 |
|
| 831 |
return {}
|
| 832 |
|
| 833 |
+
async def category(self, name: str, page: int = 1, include_adult: bool = False) -> Dict[str, Any]:
|
| 834 |
"""Get anime by category"""
|
| 835 |
try:
|
| 836 |
+
result = await self.miruro.category(name, page, include_adult=include_adult)
|
| 837 |
if result and result.get("animes"):
|
| 838 |
logger.debug(
|
| 839 |
f"[UnifiedScraper] Category (Miruro, {name}): {len(result.get('animes', []))} results"
|
|
|
|
| 844 |
|
| 845 |
return {}
|
| 846 |
|
| 847 |
+
async def schedule(self, date: str = None, include_adult: bool = False) -> Dict[str, Any]:
|
| 848 |
"""Get anime schedule"""
|
| 849 |
try:
|
| 850 |
+
result = await self.miruro.schedule(date, include_adult=include_adult)
|
| 851 |
if result and (result.get("scheduledAnimes") or result.get("animes")):
|
| 852 |
return result
|
| 853 |
except Exception:
|
api/routes/anime/catalog_routes.py
CHANGED
|
@@ -15,9 +15,10 @@ catalog_routes_bp = Blueprint('catalog_routes', __name__)
|
|
| 15 |
def genre(genre_name):
|
| 16 |
"""Display anime list for a specific genre"""
|
| 17 |
genre_name = escape(genre_name)
|
|
|
|
| 18 |
|
| 19 |
try:
|
| 20 |
-
data = asyncio.run(current_app.ha_scraper.genre(genre_name))
|
| 21 |
animes = data.get("animes", [])
|
| 22 |
if not animes:
|
| 23 |
return render_template('shared/404.html', error_message=f"No animes found for genre: {genre_name}"), 404
|
|
@@ -53,6 +54,7 @@ def genre(genre_name):
|
|
| 53 |
"duration": anime.get("duration") or "N/A",
|
| 54 |
"type": anime.get("type") or "Unknown",
|
| 55 |
"rating": anime.get("rating"),
|
|
|
|
| 56 |
"episodes": {
|
| 57 |
"sub": sub,
|
| 58 |
"dub": dub
|
|
@@ -60,7 +62,7 @@ def genre(genre_name):
|
|
| 60 |
}
|
| 61 |
genre_data['animes'].append(mapped_anime)
|
| 62 |
|
| 63 |
-
return render_template('anime/genre.html', **genre_data)
|
| 64 |
|
| 65 |
except Exception as e:
|
| 66 |
current_app.logger.exception(f"Error fetching genre {genre_name}")
|
|
@@ -72,9 +74,10 @@ def genre(genre_name):
|
|
| 72 |
def category(category_name):
|
| 73 |
"""Display anime list for a specific category"""
|
| 74 |
category_name_escaped = escape(category_name)
|
|
|
|
| 75 |
|
| 76 |
try:
|
| 77 |
-
data = asyncio.run(current_app.ha_scraper.category(category_name_escaped))
|
| 78 |
animes = data.get("animes", [])
|
| 79 |
if not animes:
|
| 80 |
return render_template('shared/404.html', error_message=f"No animes found for category: {category_name}"), 404
|
|
@@ -110,6 +113,7 @@ def category(category_name):
|
|
| 110 |
"duration": anime.get("duration") or "N/A",
|
| 111 |
"type": anime.get("type") or "Unknown",
|
| 112 |
"rating": anime.get("rating"),
|
|
|
|
| 113 |
"episodes": {
|
| 114 |
"sub": sub,
|
| 115 |
"dub": dub
|
|
@@ -117,7 +121,7 @@ def category(category_name):
|
|
| 117 |
}
|
| 118 |
category_data['animes'].append(mapped_anime)
|
| 119 |
|
| 120 |
-
return render_template('anime/genre.html', **category_data)
|
| 121 |
|
| 122 |
except Exception as e:
|
| 123 |
current_app.logger.exception(f"Error fetching category {category_name}")
|
|
|
|
| 15 |
def genre(genre_name):
|
| 16 |
"""Display anime list for a specific genre"""
|
| 17 |
genre_name = escape(genre_name)
|
| 18 |
+
include_adult = request.args.get('include_adult', '0') == '1'
|
| 19 |
|
| 20 |
try:
|
| 21 |
+
data = asyncio.run(current_app.ha_scraper.genre(genre_name, include_adult=include_adult))
|
| 22 |
animes = data.get("animes", [])
|
| 23 |
if not animes:
|
| 24 |
return render_template('shared/404.html', error_message=f"No animes found for genre: {genre_name}"), 404
|
|
|
|
| 54 |
"duration": anime.get("duration") or "N/A",
|
| 55 |
"type": anime.get("type") or "Unknown",
|
| 56 |
"rating": anime.get("rating"),
|
| 57 |
+
"isAdult": anime.get("isAdult", False),
|
| 58 |
"episodes": {
|
| 59 |
"sub": sub,
|
| 60 |
"dub": dub
|
|
|
|
| 62 |
}
|
| 63 |
genre_data['animes'].append(mapped_anime)
|
| 64 |
|
| 65 |
+
return render_template('anime/genre.html', include_adult=include_adult, **genre_data)
|
| 66 |
|
| 67 |
except Exception as e:
|
| 68 |
current_app.logger.exception(f"Error fetching genre {genre_name}")
|
|
|
|
| 74 |
def category(category_name):
|
| 75 |
"""Display anime list for a specific category"""
|
| 76 |
category_name_escaped = escape(category_name)
|
| 77 |
+
include_adult = request.args.get('include_adult', '0') == '1'
|
| 78 |
|
| 79 |
try:
|
| 80 |
+
data = asyncio.run(current_app.ha_scraper.category(category_name_escaped, include_adult=include_adult))
|
| 81 |
animes = data.get("animes", [])
|
| 82 |
if not animes:
|
| 83 |
return render_template('shared/404.html', error_message=f"No animes found for category: {category_name}"), 404
|
|
|
|
| 113 |
"duration": anime.get("duration") or "N/A",
|
| 114 |
"type": anime.get("type") or "Unknown",
|
| 115 |
"rating": anime.get("rating"),
|
| 116 |
+
"isAdult": anime.get("isAdult", False),
|
| 117 |
"episodes": {
|
| 118 |
"sub": sub,
|
| 119 |
"dub": dub
|
|
|
|
| 121 |
}
|
| 122 |
category_data['animes'].append(mapped_anime)
|
| 123 |
|
| 124 |
+
return render_template('anime/genre.html', include_adult=include_adult, **category_data)
|
| 125 |
|
| 126 |
except Exception as e:
|
| 127 |
current_app.logger.exception(f"Error fetching category {category_name}")
|
api/routes/shared/home_routes.py
CHANGED
|
@@ -17,12 +17,13 @@ def index():
|
|
| 17 |
def home():
|
| 18 |
"""Display home page with anime sections"""
|
| 19 |
info = "Home"
|
|
|
|
| 20 |
try:
|
| 21 |
async def _fetch_all():
|
| 22 |
scraper = current_app.ha_scraper
|
| 23 |
home_data, movie_data = await asyncio.gather(
|
| 24 |
-
scraper.home(),
|
| 25 |
-
scraper.category("movie"),
|
| 26 |
return_exceptions=True,
|
| 27 |
)
|
| 28 |
if isinstance(home_data, Exception):
|
|
@@ -38,7 +39,7 @@ def home():
|
|
| 38 |
|
| 39 |
movies = (movie_data or {}).get("animes", [])
|
| 40 |
current_app.logger.debug("home counts: %s", data.get("counts"))
|
| 41 |
-
return render_template("shared/index.html", suggestions=data, movies=movies, info=info)
|
| 42 |
except Exception as e:
|
| 43 |
current_app.logger.exception("Unhandled error in /home")
|
| 44 |
empty = {
|
|
|
|
| 17 |
def home():
|
| 18 |
"""Display home page with anime sections"""
|
| 19 |
info = "Home"
|
| 20 |
+
include_adult = request.args.get('include_adult', '0') == '1'
|
| 21 |
try:
|
| 22 |
async def _fetch_all():
|
| 23 |
scraper = current_app.ha_scraper
|
| 24 |
home_data, movie_data = await asyncio.gather(
|
| 25 |
+
scraper.home(include_adult=include_adult),
|
| 26 |
+
scraper.category("movie", include_adult=include_adult),
|
| 27 |
return_exceptions=True,
|
| 28 |
)
|
| 29 |
if isinstance(home_data, Exception):
|
|
|
|
| 39 |
|
| 40 |
movies = (movie_data or {}).get("animes", [])
|
| 41 |
current_app.logger.debug("home counts: %s", data.get("counts"))
|
| 42 |
+
return render_template("shared/index.html", suggestions=data, movies=movies, info=info, include_adult=include_adult)
|
| 43 |
except Exception as e:
|
| 44 |
current_app.logger.exception("Unhandled error in /home")
|
| 45 |
empty = {
|
api/routes/shared/search_routes.py
CHANGED
|
@@ -12,6 +12,7 @@ def search():
|
|
| 12 |
"""Handle search request"""
|
| 13 |
|
| 14 |
search_query = request.args.get('q', '').strip()
|
|
|
|
| 15 |
|
| 16 |
if not search_query:
|
| 17 |
return redirect(url_for('home_routes.home'))
|
|
@@ -20,7 +21,7 @@ def search():
|
|
| 20 |
loop = asyncio.new_event_loop()
|
| 21 |
asyncio.set_event_loop(loop)
|
| 22 |
results = loop.run_until_complete(
|
| 23 |
-
current_app.ha_scraper.search(search_query)
|
| 24 |
)
|
| 25 |
loop.close()
|
| 26 |
|
|
@@ -40,13 +41,20 @@ def search():
|
|
| 40 |
if not poster and not sub and not dub:
|
| 41 |
continue
|
| 42 |
|
|
|
|
|
|
|
|
|
|
| 43 |
mapped.append(anime)
|
| 44 |
|
| 45 |
return render_template(
|
| 46 |
'anime/results.html',
|
| 47 |
query=search_query,
|
| 48 |
-
animes=mapped
|
|
|
|
| 49 |
)
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
except Exception as e:
|
| 52 |
print("Search error:", e)
|
|
@@ -57,6 +65,7 @@ def search():
|
|
| 57 |
@search_routes_bp.route('/search/suggestions', methods=['GET'], strict_slashes=False)
|
| 58 |
def search_suggestions_route():
|
| 59 |
query = request.args.get('q', '').strip()
|
|
|
|
| 60 |
|
| 61 |
if not query:
|
| 62 |
return jsonify({"suggestions": []})
|
|
@@ -65,7 +74,7 @@ def search_suggestions_route():
|
|
| 65 |
loop = asyncio.new_event_loop()
|
| 66 |
asyncio.set_event_loop(loop)
|
| 67 |
suggestions = loop.run_until_complete(
|
| 68 |
-
current_app.ha_scraper.search_suggestions(query)
|
| 69 |
)
|
| 70 |
loop.close()
|
| 71 |
|
|
|
|
| 12 |
"""Handle search request"""
|
| 13 |
|
| 14 |
search_query = request.args.get('q', '').strip()
|
| 15 |
+
include_adult = request.args.get('include_adult', '0') == '1'
|
| 16 |
|
| 17 |
if not search_query:
|
| 18 |
return redirect(url_for('home_routes.home'))
|
|
|
|
| 21 |
loop = asyncio.new_event_loop()
|
| 22 |
asyncio.set_event_loop(loop)
|
| 23 |
results = loop.run_until_complete(
|
| 24 |
+
current_app.ha_scraper.search(search_query, include_adult=include_adult)
|
| 25 |
)
|
| 26 |
loop.close()
|
| 27 |
|
|
|
|
| 41 |
if not poster and not sub and not dub:
|
| 42 |
continue
|
| 43 |
|
| 44 |
+
# Preserve isAdult for client-side filtering
|
| 45 |
+
anime["isAdult"] = anime.get("isAdult", False)
|
| 46 |
+
|
| 47 |
mapped.append(anime)
|
| 48 |
|
| 49 |
return render_template(
|
| 50 |
'anime/results.html',
|
| 51 |
query=search_query,
|
| 52 |
+
animes=mapped,
|
| 53 |
+
include_adult=include_adult
|
| 54 |
)
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print("Search error:", e)
|
| 57 |
+
return redirect(url_for('home_routes.home'))
|
| 58 |
|
| 59 |
except Exception as e:
|
| 60 |
print("Search error:", e)
|
|
|
|
| 65 |
@search_routes_bp.route('/search/suggestions', methods=['GET'], strict_slashes=False)
|
| 66 |
def search_suggestions_route():
|
| 67 |
query = request.args.get('q', '').strip()
|
| 68 |
+
include_adult = request.args.get('include_adult', '0') == '1'
|
| 69 |
|
| 70 |
if not query:
|
| 71 |
return jsonify({"suggestions": []})
|
|
|
|
| 74 |
loop = asyncio.new_event_loop()
|
| 75 |
asyncio.set_event_loop(loop)
|
| 76 |
suggestions = loop.run_until_complete(
|
| 77 |
+
current_app.ha_scraper.search_suggestions(query, include_adult=include_adult)
|
| 78 |
)
|
| 79 |
loop.close()
|
| 80 |
|
api/static/js/search.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:19bdbb99cd501fd81f93cadd8b4537ed102a0d95fcc41027fa84647cb985b28e
|
| 3 |
+
size 21665
|
api/templates/anime/genre.html
CHANGED
|
@@ -22,11 +22,12 @@
|
|
| 22 |
{% if animes and animes | length > 0 %}
|
| 23 |
<div class="anime-grid">
|
| 24 |
{% for anime in animes %}
|
| 25 |
-
<a href="/anime/{{ anime.id }}" class="anime-card">
|
| 26 |
<div class="anime-card-poster">
|
| 27 |
<img src="{{ anime.poster }}" alt="{{ anime.name }}" loading="lazy"
|
| 28 |
onerror="this.src='https://via.placeholder.com/200x280?text=No+Image'">
|
| 29 |
<div class="anime-card-badges">
|
|
|
|
| 30 |
{% if anime.episodes and anime.episodes.sub %}
|
| 31 |
<span class="badge badge-sub">{% if anime.episodes.released and anime.episodes.released !=
|
| 32 |
anime.episodes.sub %}{{ anime.episodes.released }}/{{ anime.episodes.sub }}{% else %}{{
|
|
|
|
| 22 |
{% if animes and animes | length > 0 %}
|
| 23 |
<div class="anime-grid">
|
| 24 |
{% for anime in animes %}
|
| 25 |
+
<a href="/anime/{{ anime.id }}" class="anime-card" {% if anime.isAdult %}data-is-adult="true"{% endif %}>
|
| 26 |
<div class="anime-card-poster">
|
| 27 |
<img src="{{ anime.poster }}" alt="{{ anime.name }}" loading="lazy"
|
| 28 |
onerror="this.src='https://via.placeholder.com/200x280?text=No+Image'">
|
| 29 |
<div class="anime-card-badges">
|
| 30 |
+
{% if anime.isAdult %}<span class="badge" style="background:var(--error);color:white;">18+</span>{% endif %}
|
| 31 |
{% if anime.episodes and anime.episodes.sub %}
|
| 32 |
<span class="badge badge-sub">{% if anime.episodes.released and anime.episodes.released !=
|
| 33 |
anime.episodes.sub %}{{ anime.episodes.released }}/{{ anime.episodes.sub }}{% else %}{{
|
api/templates/anime/results.html
CHANGED
|
@@ -39,11 +39,12 @@
|
|
| 39 |
{% if animes and animes | length > 0 %}
|
| 40 |
<div class="anime-grid">
|
| 41 |
{% for anime in animes %}
|
| 42 |
-
<a href="/anime/{{ anime.id }}" class="anime-card">
|
| 43 |
<div class="anime-card-poster">
|
| 44 |
<img src="{{ anime.poster }}" alt="{{ anime.name }}" loading="lazy"
|
| 45 |
onerror="this.src='https://via.placeholder.com/200x280?text=No+Image'">
|
| 46 |
<div class="anime-card-badges">
|
|
|
|
| 47 |
{% if anime.episodes and anime.episodes.sub %}
|
| 48 |
<span class="badge badge-sub">{% if anime.episodes.released and anime.episodes.released !=
|
| 49 |
anime.episodes.sub %}{{ anime.episodes.released }}/{{ anime.episodes.sub }}{% else %}{{
|
|
|
|
| 39 |
{% if animes and animes | length > 0 %}
|
| 40 |
<div class="anime-grid">
|
| 41 |
{% for anime in animes %}
|
| 42 |
+
<a href="/anime/{{ anime.id }}" class="anime-card" {% if anime.isAdult %}data-is-adult="true"{% endif %}>
|
| 43 |
<div class="anime-card-poster">
|
| 44 |
<img src="{{ anime.poster }}" alt="{{ anime.name }}" loading="lazy"
|
| 45 |
onerror="this.src='https://via.placeholder.com/200x280?text=No+Image'">
|
| 46 |
<div class="anime-card-badges">
|
| 47 |
+
{% if anime.isAdult %}<span class="badge" style="background:var(--error);color:white;">18+</span>{% endif %}
|
| 48 |
{% if anime.episodes and anime.episodes.sub %}
|
| 49 |
<span class="badge badge-sub">{% if anime.episodes.released and anime.episodes.released !=
|
| 50 |
anime.episodes.sub %}{{ anime.episodes.released }}/{{ anime.episodes.sub }}{% else %}{{
|
api/templates/shared/base.html
CHANGED
|
@@ -853,6 +853,35 @@
|
|
| 853 |
<script src="{{ url_for('static', filename='js/core.js') }}"></script>
|
| 854 |
<script src="{{ url_for('static', filename='js/popup.js') }}"></script>
|
| 855 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
<!-- Login Widget Script -->
|
| 857 |
<!-- Login Widget Script (Removed - now handled inline) -->
|
| 858 |
<script src="{{ url_for('static', filename='js/search.js') }}"></script>
|
|
|
|
| 853 |
<script src="{{ url_for('static', filename='js/core.js') }}"></script>
|
| 854 |
<script src="{{ url_for('static', filename='js/popup.js') }}"></script>
|
| 855 |
|
| 856 |
+
<!-- Anime Adult Content Toggle (client-side filtering) -->
|
| 857 |
+
<script>
|
| 858 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 859 |
+
const ADULT_KEY = 'yume_show_adult_anime';
|
| 860 |
+
|
| 861 |
+
function applyAnimeAdultFilter() {
|
| 862 |
+
// Show adult content only if the toggle is ON (true)
|
| 863 |
+
const showAdult = localStorage.getItem(ADULT_KEY) === 'true';
|
| 864 |
+
document.querySelectorAll('[data-is-adult="true"]').forEach(function(el) {
|
| 865 |
+
el.style.display = showAdult ? '' : 'none';
|
| 866 |
+
});
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
applyAnimeAdultFilter();
|
| 870 |
+
|
| 871 |
+
// Listen for storage changes from other tabs
|
| 872 |
+
window.addEventListener('storage', function(e) {
|
| 873 |
+
if (e.key === ADULT_KEY) {
|
| 874 |
+
applyAnimeAdultFilter();
|
| 875 |
+
}
|
| 876 |
+
});
|
| 877 |
+
|
| 878 |
+
// Expose for settings page to call
|
| 879 |
+
window.AnimeAdultFilter = {
|
| 880 |
+
apply: applyAnimeAdultFilter
|
| 881 |
+
};
|
| 882 |
+
});
|
| 883 |
+
</script>
|
| 884 |
+
|
| 885 |
<!-- Login Widget Script -->
|
| 886 |
<!-- Login Widget Script (Removed - now handled inline) -->
|
| 887 |
<script src="{{ url_for('static', filename='js/search.js') }}"></script>
|
api/templates/shared/index.html
CHANGED
|
@@ -235,7 +235,7 @@
|
|
| 235 |
{% if suggestions and suggestions.data and suggestions.data.trendingAnimes %}
|
| 236 |
<div class="anime-grid">
|
| 237 |
{% for anime in suggestions.data.trendingAnimes[:14] %}
|
| 238 |
-
<a href="/watch/{{ anime.id }}" class="anime-card">
|
| 239 |
<div class="anime-card-poster">
|
| 240 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 241 |
loading="lazy">
|
|
@@ -273,7 +273,7 @@
|
|
| 273 |
{% if suggestions and suggestions.data and suggestions.data.mostPopularAnimes %}
|
| 274 |
<div class="anime-grid">
|
| 275 |
{% for anime in suggestions.data.mostPopularAnimes[:12] %}
|
| 276 |
-
<a href="/watch/{{ anime.id }}" class="anime-card">
|
| 277 |
<div class="anime-card-poster">
|
| 278 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 279 |
loading="lazy">
|
|
@@ -311,7 +311,7 @@
|
|
| 311 |
{% if movies %}
|
| 312 |
<div class="anime-grid">
|
| 313 |
{% for anime in movies[:12] %}
|
| 314 |
-
<a href="/watch/{{ anime.id }}" class="anime-card">
|
| 315 |
<div class="anime-card-poster">
|
| 316 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 317 |
loading="lazy">
|
|
|
|
| 235 |
{% if suggestions and suggestions.data and suggestions.data.trendingAnimes %}
|
| 236 |
<div class="anime-grid">
|
| 237 |
{% for anime in suggestions.data.trendingAnimes[:14] %}
|
| 238 |
+
<a href="/watch/{{ anime.id }}" class="anime-card" {% if anime.isAdult %}data-is-adult="true"{% endif %}>
|
| 239 |
<div class="anime-card-poster">
|
| 240 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 241 |
loading="lazy">
|
|
|
|
| 273 |
{% if suggestions and suggestions.data and suggestions.data.mostPopularAnimes %}
|
| 274 |
<div class="anime-grid">
|
| 275 |
{% for anime in suggestions.data.mostPopularAnimes[:12] %}
|
| 276 |
+
<a href="/watch/{{ anime.id }}" class="anime-card" {% if anime.isAdult %}data-is-adult="true"{% endif %}>
|
| 277 |
<div class="anime-card-poster">
|
| 278 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 279 |
loading="lazy">
|
|
|
|
| 311 |
{% if movies %}
|
| 312 |
<div class="anime-grid">
|
| 313 |
{% for anime in movies[:12] %}
|
| 314 |
+
<a href="/watch/{{ anime.id }}" class="anime-card" {% if anime.isAdult %}data-is-adult="true"{% endif %}>
|
| 315 |
<div class="anime-card-poster">
|
| 316 |
<img src="{{ anime.poster or anime.image }}" alt="{{ anime.name or anime.title }}"
|
| 317 |
loading="lazy">
|
api/templates/shared/settings.html
CHANGED
|
@@ -243,16 +243,23 @@
|
|
| 243 |
</div>
|
| 244 |
</div>
|
| 245 |
|
| 246 |
-
<!--
|
| 247 |
<div class="settings-section">
|
| 248 |
<h2 class="settings-section-title">
|
| 249 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 250 |
-
<path d="
|
| 251 |
-
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
| 252 |
</svg>
|
| 253 |
-
|
| 254 |
</h2>
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
<div class="settings-row">
|
| 257 |
<div class="settings-label">
|
| 258 |
<span class="settings-label-text">Hide NSFW Manga</span>
|
|
|
|
| 243 |
</div>
|
| 244 |
</div>
|
| 245 |
|
| 246 |
+
<!-- Content Settings -->
|
| 247 |
<div class="settings-section">
|
| 248 |
<h2 class="settings-section-title">
|
| 249 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 250 |
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
|
|
| 251 |
</svg>
|
| 252 |
+
Content
|
| 253 |
</h2>
|
| 254 |
|
| 255 |
+
<div class="settings-row">
|
| 256 |
+
<div class="settings-label">
|
| 257 |
+
<span class="settings-label-text">Show 18+ Anime</span>
|
| 258 |
+
<span class="settings-label-desc">Show adult anime content in search and browse results</span>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="toggle" id="anime-adult-toggle" data-setting="show_adult_anime"></div>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
<div class="settings-row">
|
| 264 |
<div class="settings-label">
|
| 265 |
<span class="settings-label-text">Hide NSFW Manga</span>
|