Spaces:
Paused
Paused
| import { NextResponse } from 'next/server'; | |
| import * as cheerio from 'cheerio'; | |
| import { validateApiKey, createUnauthorizedResponse } from '@/lib/middleware/api-auth'; | |
| // Function to remove duplicate posts based on postUrl | |
| function deduplicatePosts(posts: any[]) { | |
| const seen = new Set(); | |
| return posts.filter(post => { | |
| if (seen.has(post.postUrl)) { | |
| return false; | |
| } | |
| seen.add(post.postUrl); | |
| return true; | |
| }); | |
| } | |
| // Function to build category URL | |
| function buildCategoryUrl(category?: string): string { | |
| const baseUrl = 'https://animesalt.cc' | |
| if (!category || category === 'all') { | |
| return `${baseUrl}/category/language/hindi/` // Default to hindi | |
| } | |
| // Language categories | |
| if (['hindi', 'english', 'tamil'].includes(category)) { | |
| return `${baseUrl}/category/language/${category}/` | |
| } | |
| // Network categories | |
| if (['crunchyroll', 'disney', 'hotstar'].includes(category)) { | |
| return `${baseUrl}/category/network/${category}/` | |
| } | |
| // Fallback to hindi if unknown category | |
| return `${baseUrl}/category/language/hindi/` | |
| } | |
| // Function to extract path from full URL | |
| function extractPathFromUrl(fullUrl: string): string { | |
| if (!fullUrl) return ''; | |
| try { | |
| const url = new URL(fullUrl); | |
| return url.pathname; | |
| } catch { | |
| // If URL parsing fails, try to extract path manually | |
| const pathMatch = fullUrl.match(/https?:\/\/[^\/]+(.+)/); | |
| return pathMatch ? pathMatch[1] : fullUrl; | |
| } | |
| } | |
| // Function to fetch and parse HTML content from category page | |
| async function scrapeAnimeData(category?: string) { | |
| try { | |
| const url = buildCategoryUrl(category) | |
| // Make a request to the anime website | |
| const response = await fetch(url, { | |
| cache: 'no-cache', | |
| headers: { | |
| 'User-Agent': | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch posts: ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| const $ = cheerio.load(html); | |
| const posts = []; | |
| // Parse each post based on the HTML structure we observed | |
| $('li[class*="post-"]').each((_, element) => { | |
| const $element = $(element); | |
| // Extract the image URL | |
| let imageUrl = $element.find('img').data('src') || $element.find('img').attr('src'); | |
| // Normalize protocol-relative URLs | |
| if (imageUrl && imageUrl.startsWith('//')) { | |
| imageUrl = 'https:' + imageUrl; | |
| } | |
| // Extract the title | |
| const title = $element.find('h2.entry-title').text().trim(); | |
| // Extract the post URL and convert to path only | |
| const fullPostUrl = $element.find('a.lnk-blk').attr('href') || $element.find('a').attr('href'); | |
| const postUrl = extractPathFromUrl(fullPostUrl || ''); | |
| // Only add posts with all required fields | |
| if (imageUrl && title && postUrl) { | |
| posts.push({ | |
| imageUrl, | |
| title, | |
| postUrl, | |
| }); | |
| } | |
| }); | |
| return deduplicatePosts(posts); | |
| } catch (error) { | |
| console.error('Error scraping anime data:', error); | |
| throw error; | |
| } | |
| } | |
| // Function to search anime using the search page with category support | |
| async function searchAnimeData(searchQuery: string, category?: string) { | |
| try { | |
| let searchUrl = `https://animesalt.cc/?s=${encodeURIComponent(searchQuery)}` | |
| // Add category filter to search if specified | |
| if (category && category !== 'all') { | |
| // For search, we'll still use the general search but filter results | |
| // The website's search doesn't seem to support category filtering directly | |
| } | |
| const response = await fetch(searchUrl, { | |
| cache: 'no-cache', | |
| headers: { | |
| 'User-Agent': | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch search results: ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| const $ = cheerio.load(html); | |
| const posts = []; | |
| // Parse search results - they might have a different structure | |
| $('li[class*="post-"], .search-result, article').each((_, element) => { | |
| const $element = $(element); | |
| // Try multiple selectors for image | |
| let imageUrl = $element.find('img').data('src') || | |
| $element.find('img').attr('src') || | |
| $element.find('.post-thumbnail img').attr('src') || | |
| $element.find('.entry-image img').attr('src'); | |
| // Normalize protocol-relative URLs | |
| if (imageUrl && imageUrl.startsWith('//')) { | |
| imageUrl = 'https:' + imageUrl; | |
| } | |
| // Try multiple selectors for title | |
| const title = $element.find('h2.entry-title').text().trim() || | |
| $element.find('h3.entry-title').text().trim() || | |
| $element.find('.entry-title').text().trim() || | |
| $element.find('h2 a').text().trim() || | |
| $element.find('h3 a').text().trim(); | |
| // Try multiple selectors for post URL and convert to path only | |
| const fullPostUrl = $element.find('a.lnk-blk').attr('href') || | |
| $element.find('a').attr('href') || | |
| $element.find('.entry-title a').attr('href'); | |
| const postUrl = extractPathFromUrl(fullPostUrl || ''); | |
| // Only add posts with all required fields | |
| if (imageUrl && title && postUrl) { | |
| posts.push({ | |
| imageUrl, | |
| title, | |
| postUrl, | |
| }); | |
| } | |
| }); | |
| return deduplicatePosts(posts); | |
| } catch (error) { | |
| console.error('Error searching anime data:', error); | |
| throw error; | |
| } | |
| } | |
| export async function GET(request: Request) { | |
| try { | |
| // Validate API key | |
| const authResult = await validateApiKey(request); | |
| if (!authResult.isValid) { | |
| return createUnauthorizedResponse(authResult.error || 'Invalid API key'); | |
| } | |
| const { searchParams } = new URL(request.url); | |
| const searchQuery = searchParams.get('search'); | |
| const category = searchParams.get('category'); | |
| let posts = []; | |
| if (searchQuery) { | |
| // If there's a search query, first try to get category posts and filter | |
| const categoryPosts = await scrapeAnimeData(category || undefined); | |
| // Filter posts that match the search query | |
| const filteredPosts = categoryPosts.filter(post => | |
| post.title.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ); | |
| if (filteredPosts.length > 0) { | |
| // If we found matches in the category data, return them | |
| posts = filteredPosts; | |
| } else { | |
| // If no matches found in category data, use search API | |
| posts = await searchAnimeData(searchQuery, category || undefined); | |
| } | |
| } else { | |
| // No search query, return posts from category page | |
| posts = await scrapeAnimeData(category || undefined); | |
| } | |
| // Apply final deduplication as an extra safety measure | |
| posts = deduplicatePosts(posts); | |
| return NextResponse.json({ | |
| success: true, | |
| count: posts.length, | |
| posts, | |
| searchQuery: searchQuery || null, | |
| category: category || 'all', | |
| source: searchQuery && posts.length > 0 ? 'search' : 'category', | |
| remainingRequests: authResult.apiKey ? (authResult.apiKey.requestsLimit - authResult.apiKey.requestsUsed) : 0 | |
| }); | |
| } catch (error) { | |
| return NextResponse.json( | |
| { | |
| success: false, | |
| error: error instanceof Error ? error.message : 'Failed to fetch posts', | |
| }, | |
| { status: 500 } | |
| ); | |
| } | |
| } | |