mwask commited on
Commit
03df875
·
1 Parent(s): 6578bb5
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
- query = '''
67
- query ($genre: String, $page: Int, $perPage: Int) {
68
- Page(page: $page, perPage: $perPage) {
69
- pageInfo {
 
70
  total
71
  hasNextPage
72
  lastPage
73
- }
74
- media(type: ANIME, genre: $genre, sort: SCORE_DESC, isAdult: false) {
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
- filtered_results = [
98
- item for item in media_list
99
- if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
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
- query = '''
140
- query ($page: Int, $perPage: Int, $format: MediaFormat, $status: MediaStatus, $sort: [MediaSort]) {
141
- Page(page: $page, perPage: $perPage) {
142
- pageInfo { total hasNextPage lastPage }
143
- media(type: ANIME, format: $format, status: $status, sort: $sort, isAdult: false) {
 
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
- filtered_results = [
174
- item for item in media_list
175
- if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
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
- filtered_results = [
229
- item for item in media_list
230
- if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
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 media or media.get("isAdult", False) or "Hentai" in media.get("genres", []):
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
- filtered_results = [
117
- item for item in results
118
- if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
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
- filtered_suggestions = [
178
- s for s in suggestions
179
- if not s.get("isAdult", False) and "Hentai" not in s.get("genres", [])
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
- filtered_results = [
256
- item for item in results
257
- if not item.get("isAdult", False) and "Hentai" not in item.get("genres", [])
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:009ee45b99fda328b8bab3c98d5112c73975777bee58e13cbea79ff2cfe875db
3
- size 21333
 
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
- <!-- Manga 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="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
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
- Manga
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>