This commit is contained in:
ld3zian 2025-05-03 11:56:22 -07:00
parent bc0f10ab1d
commit 2d3eeeaeba
5 changed files with 1540 additions and 3 deletions

View file

@ -0,0 +1,267 @@
import type { APIRoute } from "astro";
import puppeteer from 'puppeteer';
// Helper function to extract comic slugs from a ComicK user list page
async function scrapeComicList(userId: string, limit: number = 10): Promise<string[]> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// Set a user agent to mimic a real browser
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36');
console.log(`Navigating to ComicK user page: ${userId}`);
// Navigate to the user's list page
await page.goto(`https://comick.io/user/${userId}/list`, {
waitUntil: 'networkidle0', // Wait until there are no network connections for at least 500ms
timeout: 60000 // Longer timeout to ensure everything loads
});
// Try different waiting strategies
console.log('Waiting for page to load completely...');
// First try waiting for specific elements that might be present
const selectors = ['.book', '[data-testid="comic-item"]', '.comics-grid', '.comic-list', '.md-list'];
let foundSelector = false;
for (const selector of selectors) {
try {
console.log(`Trying to wait for selector: ${selector}`);
await page.waitForSelector(selector, { timeout: 5000 });
console.log(`Found element with selector: ${selector}`);
foundSelector = true;
break;
} catch (e) {
console.log(`Selector ${selector} not found`);
}
}
// If no specific selector worked, wait for any links that might be comics
if (!foundSelector) {
console.log('Waiting for any comic links to appear...');
try {
await page.waitForSelector('a[href*="/comic/"]', { timeout: 5000 });
console.log('Found comic links on the page');
} catch (e) {
console.log('No comic links found, continuing anyway');
}
}
// Interact with the page to ensure all content is loaded
console.log('Interacting with page to ensure content is loaded...');
// Scroll down to trigger any lazy loading
await page.evaluate(() => {
// Scroll to the bottom of the page
window.scrollTo(0, document.body.scrollHeight);
});
await new Promise(resolve => setTimeout(resolve, 2000));
// Scroll back up
await page.evaluate(() => {
window.scrollTo(0, 0);
});
await new Promise(resolve => setTimeout(resolve, 1000));
// Try clicking any buttons that might reveal the content
try {
const buttons = await page.$$('button, .button, [role="button"]');
console.log(`Found ${buttons.length} buttons on the page`);
// Try clicking buttons that might be related to viewing the list
for (const button of buttons.slice(0, 5)) { // Limit to first 5 buttons to avoid clicking too many
const buttonText = await page.evaluate(el => el.textContent?.toLowerCase() || '', button);
console.log(`Found button with text: ${buttonText}`);
if (buttonText.includes('list') || buttonText.includes('view') ||
buttonText.includes('show') || buttonText.includes('load')) {
console.log(`Clicking button: ${buttonText}`);
await button.click().catch(e => console.log(`Failed to click button: ${e.message}`));
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
} catch (e) {
console.log(`Error interacting with buttons: ${e.message}`);
}
// Give extra time for JavaScript to execute and everything to render
console.log('Waiting additional time for page to stabilize...');
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('Page loaded, extracting comic data...');
// Extract the HTML structure for debugging
const pageHtml = await page.content();
console.log('Page HTML length:', pageHtml.length);
// Extract comic slugs from the page with multiple selector attempts
const slugs = await page.evaluate(() => {
// Try multiple selectors to find comics
const selectors = [
// Try different selectors that might match comics
'.book a[href^="/comic/"]',
'a[href^="/comic/"]',
'.md-comic',
'.comics-grid a',
'.comic-list a',
'.md-list a',
'[data-testid="comic-item"] a',
// Generic link selector targeting comic URLs as fallback
'a[href*="/comic/"]'
];
// Try each selector and use the first one that returns results
for (const selector of selectors) {
console.log('Trying selector:', selector);
const elements = document.querySelectorAll(selector);
console.log(`Found ${elements.length} elements with selector: ${selector}`);
if (elements.length > 0) {
// We found matching elements, extract the slugs
const results = Array.from(elements).map(element => {
const href = element.getAttribute('href');
if (!href || !href.includes('/comic/')) return null;
return href.replace('/comic/', '').split('/')[0]; // Get just the slug part
}).filter(Boolean);
if (results.length > 0) {
console.log(`Found ${results.length} comics with selector: ${selector}`);
return results;
}
}
}
// If all selectors failed, try a generic approach to find any links to comics
const allLinks = document.querySelectorAll('a');
console.log(`Trying generic approach with ${allLinks.length} links`);
const comicLinks = Array.from(allLinks).filter(link => {
const href = link.getAttribute('href');
return href && href.includes('/comic/');
});
console.log(`Found ${comicLinks.length} potential comic links using generic approach`);
return comicLinks.map(link => {
const href = link.getAttribute('href');
return href ? href.replace('/comic/', '').split('/')[0] : null;
}).filter(Boolean);
});
// Limit the number of comics to the specified limit
const limitedSlugs = slugs.slice(0, limit);
console.log(`Extracted ${slugs.length} comics from page, returning ${limitedSlugs.length} (limit: ${limit})`);
return limitedSlugs as string[];
} finally {
await browser.close();
}
}
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const userId = url.searchParams.get('userId');
// Get limit parameter from query string (default to 10 if not provided)
const limitParam = url.searchParams.get('limit');
const limit = limitParam ? parseInt(limitParam, 10) : 10;
// Ensure limit is a valid number
const validLimit = isNaN(limit) || limit <= 0 ? 10 : Math.min(limit, 100);
if (!userId) {
return new Response(JSON.stringify({ error: 'Missing user ID' }), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
});
}
// Extract user ID from URL if full URL was provided
let cleanUserId = userId;
if (userId.includes('comick.io/user/')) {
const match = userId.match(/comick\.io\/user\/([a-zA-Z0-9-]+)/);
if (match && match[1]) {
cleanUserId = match[1];
}
}
// Validate the UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(cleanUserId)) {
return new Response(JSON.stringify({ error: 'Invalid user ID format. Must be a UUID like: 8da7d6cf-a2ac-4972-a465-770ba975ee2a' }), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
});
}
console.log(`Scraping comics for user: ${cleanUserId} with limit: ${validLimit}`);
// Use Puppeteer to scrape the user's comic list with the specified limit
const comics = await scrapeComicList(cleanUserId, validLimit);
// Calculate total
const totalComics = comics.length;
// Check if the list is empty
if (totalComics === 0) {
return new Response(JSON.stringify({
comics: [],
totalComics: 0,
message: "No comics found in user's list"
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}
return new Response(JSON.stringify({ comics, totalComics }), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=3600' // Cache for 1 hour
}
});
} catch (error) {
console.error('Error fetching user comics:', error);
// Try to generate a helpful error message based on the error
let errorMessage = 'Failed to fetch user comics';
let statusCode = 500;
if (error instanceof Error) {
// Handle specific error types
if (error.message.includes('Failed to fetch') ||
error.message.includes('network') ||
error.message.includes('ECONNREFUSED')) {
errorMessage = 'Could not connect to ComicK API. Please try again later.';
} else if (error.message.includes('timeout')) {
errorMessage = 'Request timed out. The ComicK API might be experiencing high load.';
} else if (error.message.includes('parsing')) {
errorMessage = 'Received invalid data from ComicK API. Their API structure might have changed.';
}
}
return new Response(
JSON.stringify({
error: errorMessage,
details: error instanceof Error ? error.message : String(error)
}), {
status: statusCode,
headers: {
'Content-Type': 'application/json'
}
}
);
}
};