UNPKG

@hanivanrizky/nestjs-html-parser

Version:

āš ļø ARCHIVED - This package is no longer maintained. Please migrate to @hanivanrizky/nestjs-xpath-parser (https://github.com/Hanivan/nestjs-xpath-parser)

311 lines • 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.demonstrateOtakudesuParser = demonstrateOtakudesuParser; const html_parser_service_1 = require("../html-parser.service"); async function demonstrateOtakudesuParser(verbose = false) { const parser = new html_parser_service_1.HtmlParserService(); console.log('šŸŽŒ Otakudesu.cloud - Indonesian Anime Parser Demo'); console.log('='.repeat(60)); try { // Fetch the main page with random user agent for better success rate console.log('šŸ“” Fetching Otakudesu homepage...'); const response = await parser.fetchHtml('https://otakudesu.cloud/', { useRandomUserAgent: true, timeout: 15000, verbose, headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'id-ID,id;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache', Pragma: 'no-cache', }, }); console.log(`āœ… Successfully fetched page (${response.status} ${response.statusText})`); console.log(`šŸ“Š Page size: ${response.data.length} characters`); console.log(`🌐 Content-Type: ${response.headers['content-type']}`); console.log(); // Extract ongoing anime information using the exact structure from the website console.log('šŸ” Extracting ongoing anime information...'); // Schema for extracting individual anime entries from li elements const ongoingAnimeSchema = { title: { selector: './/h2[@class="jdlflm"]/text()', type: 'xpath', transform: (title) => title.trim(), }, link: { selector: './/a[@href]', type: 'xpath', attribute: 'href', }, episode: { selector: './/div[@class="epz"]/text()', type: 'xpath', transform: (text) => { const match = text.match(/Episode\s+(\d+)/); return match ? parseInt(match[1]) : 0; }, }, releaseDay: { selector: './/div[@class="epztipe"]/text()', type: 'xpath', transform: (text) => text.trim(), }, releaseDate: { selector: './/div[@class="newnime"]/text()', type: 'xpath', transform: (text) => text.trim(), }, imageUrl: { selector: './/img', type: 'xpath', attribute: 'src', }, imageAlt: { selector: './/img', type: 'xpath', attribute: 'alt', }, }; // Extract ongoing anime using the specific container structure const ongoingAnimeItems = parser.extractStructuredList(response.data, '//div[@class="venz"]//ul//li', ongoingAnimeSchema, 'xpath', { verbose }); console.log(`šŸŽÆ Found ${ongoingAnimeItems.length} anime items in the ongoing section`); // Process and clean the extracted data const processedAnime = ongoingAnimeItems .filter((item) => item.title && item.link && item.episode) .map((item) => { const cleanTitle = item.title.replace(/\s*Sub Indo\s*$/i, '').trim(); const hasSubIndo = item.title.toLowerCase().includes('sub indo') || (item.imageAlt && item.imageAlt.toLowerCase().includes('sub indo')); return { title: item.title, cleanTitle: cleanTitle, episode: item.episode || 0, releaseDay: item.releaseDay || 'Unknown', releaseDate: item.releaseDate || 'Unknown', link: item.link, imageUrl: item.imageUrl || '', hasSubIndo: hasSubIndo, status: 'ongoing', }; }); // Display results console.log('šŸ“‹ ONGOING ANIME EXTRACTION RESULTS'); console.log('='.repeat(50)); if (processedAnime.length > 0) { console.log(`āœ… Successfully extracted ${processedAnime.length} ongoing anime`); console.log(); // Display first 10 anime with details console.log('šŸŽÆ Latest Ongoing Anime (Top 10):'); console.log('-'.repeat(40)); processedAnime.slice(0, 10).forEach((anime, index) => { console.log(`${index + 1}. ${anime.cleanTitle}`); console.log(` šŸ“ŗ Episode: ${anime.episode}`); console.log(` šŸ“… Release: ${anime.releaseDay}, ${anime.releaseDate}`); console.log(` šŸ”— Link: ${anime.link}`); console.log(` šŸ–¼ļø Image: ${anime.imageUrl}`); console.log(` šŸŒ Sub Indonesia: ${anime.hasSubIndo ? 'āœ…' : 'āŒ'}`); console.log(); }); // Generate statistics const stats = { totalOngoingAnime: processedAnime.length, latestEpisodes: processedAnime.slice(0, 5), releaseSchedule: {}, popularTitles: processedAnime .filter((anime) => anime.episode > 5) .map((anime) => anime.cleanTitle) .slice(0, 5), averageEpisode: Math.round(processedAnime.reduce((sum, anime) => sum + anime.episode, 0) / processedAnime.length), }; // Count release schedule by day processedAnime.forEach((anime) => { const day = anime.releaseDay; if (day && day !== 'Unknown') { stats.releaseSchedule[day] = (stats.releaseSchedule[day] || 0) + 1; } }); console.log('šŸ“Š OTAKUDESU STATISTICS'); console.log('='.repeat(40)); console.log(`šŸ“ˆ Total Ongoing Anime: ${stats.totalOngoingAnime}`); console.log(`šŸ“Š Average Episode: ${stats.averageEpisode}`); console.log(); console.log('šŸ“… Release Schedule:'); Object.entries(stats.releaseSchedule) .sort(([, a], [, b]) => b - a) .forEach(([day, count]) => { console.log(` ${day}: ${count} anime`); }); console.log(); console.log('šŸ”„ Popular Ongoing Titles (Episode > 5):'); stats.popularTitles.forEach((title, index) => { console.log(` ${index + 1}. ${title}`); }); console.log(); // Show anime by release day distribution console.log('šŸ“Š Anime Distribution by Release Day:'); const dayOrder = [ 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu', ]; dayOrder.forEach((day) => { const count = stats.releaseSchedule[day] || 0; if (count > 0) { const animeList = processedAnime .filter((anime) => anime.releaseDay === day) .map((anime) => anime.cleanTitle) .slice(0, 3); console.log(` ${day}: ${count} anime - ${animeList.join(', ')}${animeList.length < count ? '...' : ''}`); } }); // Show highest episode numbers console.log('\nšŸ† Anime with Highest Episodes:'); const sortedByEpisode = processedAnime .sort((a, b) => b.episode - a.episode) .slice(0, 5); sortedByEpisode.forEach((anime, index) => { console.log(` ${index + 1}. ${anime.cleanTitle} - Episode ${anime.episode}`); }); } else { console.log('āš ļø No ongoing anime found. Possible reasons:'); console.log(' - Website structure has changed'); console.log(' - Content is loaded dynamically'); console.log(' - Network or access issues'); console.log(); // Debug information - check for the container elements const venzContainer = parser.exists(response.data, '//div[@class="venz"]', 'xpath', { verbose }); const ulElements = parser.count(response.data, '//div[@class="venz"]//ul', 'xpath', { verbose }); const liElements = parser.count(response.data, '//div[@class="venz"]//ul//li', 'xpath', { verbose }); const detpostElements = parser.count(response.data, '//div[@class="detpost"]', 'xpath', { verbose }); console.log('šŸ”§ DEBUG INFORMATION'); console.log(` Venz container found: ${venzContainer ? 'āœ…' : 'āŒ'}`); console.log(` UL elements found: ${ulElements}`); console.log(` LI elements found: ${liElements}`); console.log(` Detpost elements found: ${detpostElements}`); // Try to find any anime-related content const anyAnimeLinks = parser.extractMultiple(response.data, '//a[contains(@href, "anime")]', 'xpath', 'href', { verbose }); const anyAnimeTitles = parser.extractMultiple(response.data, '//h2[@class="jdlflm"]/text()', 'xpath', undefined, { verbose }); console.log(` Any anime links found: ${anyAnimeLinks.length}`); console.log(` Any anime titles found: ${anyAnimeTitles.length}`); if (anyAnimeTitles.length > 0) { console.log('\n Sample titles found:'); anyAnimeTitles.slice(0, 3).forEach((title, i) => { console.log(` ${i + 1}. ${title}`); }); } } // === Advanced Transform Example === // --- Example class definitions --- class UppercasePipe { transform(value) { return value.toUpperCase(); } } class SuffixPipe { suffix = ''; transform(value) { return value + this.suffix; } } // --- Example usage: with and without 'new' keyword --- // Type assertion is used here to allow class constructors in the transform array for demonstration. const advancedSchema = { title: { selector: './/h2[@class="jdlflm"]/text()', type: 'xpath', transform: [ (title) => title.trim(), { class: UppercasePipe }, // <-- class, will be instantiated automatically { class: SuffixPipe, payload: { suffix: ' [ADVANCED]' } }, // <-- object-based configuration ], }, episode: { selector: './/div[@class="epz"]', type: 'xpath', transform: [ (text) => { if (typeof text !== 'string') return 0; let match = text.match(/Episode\s+(\d+)/i); if (!match) match = text.match(/(\d+)/); return match ? parseInt(match[1]) : 0; }, { class: SuffixPipe, payload: { suffix: ' (ep)' } }, // <-- object-based configuration ], }, }; const advancedAnime = parser.extractStructuredList(response.data, '//div[@class="venz"]//ul//li', advancedSchema, 'xpath', { verbose }); if (advancedAnime.length > 0) { console.log('\n=== ADVANCED TRANSFORM EXAMPLE ==='); advancedAnime.slice(0, 3).forEach((anime, i) => { console.log(`${i + 1}. ${anime.title} | Episode: ${anime.episode}`); }); } // Demonstrate advanced parsing techniques console.log('\nšŸ”¬ ADVANCED PARSING TECHNIQUES'); console.log('='.repeat(50)); // Check for specific anime genres or types in titles const actionAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('action') || anime.cleanTitle.toLowerCase().includes('battle') || anime.cleanTitle.toLowerCase().includes('hero')).length; const schoolAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('school') || anime.cleanTitle.toLowerCase().includes('academia')).length; const fantasyAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('magic') || anime.cleanTitle.toLowerCase().includes('witch') || anime.cleanTitle.toLowerCase().includes('fantasy')).length; console.log('šŸŽ­ Anime Categories Detection:'); console.log(` Action/Hero: ${actionAnime} anime`); console.log(` School/Academia: ${schoolAnime} anime`); console.log(` Magic/Fantasy: ${fantasyAnime} anime`); // Extract page metadata const pageTitle = parser.extractSingle(response.data, '//title/text()', 'xpath', undefined, { verbose }); const metaDescription = parser.extractSingle(response.data, '//meta[@name="description"]', 'xpath', 'content', { verbose }); console.log('\nšŸ“„ Page Metadata:'); console.log(` Title: ${pageTitle || 'Not found'}`); console.log(` Description: ${metaDescription || 'Not found'}`); // Check for responsive design elements const hasViewport = parser.exists(response.data, '//meta[@name="viewport"]', 'xpath', { verbose }); const hasBootstrap = parser.exists(response.data, '//*[contains(@href, "bootstrap") or contains(@src, "bootstrap")]', 'xpath', { verbose }); console.log('\nšŸ“± Technical Features:'); console.log(` Responsive Design: ${hasViewport ? 'āœ…' : 'āŒ'}`); console.log(` Bootstrap Framework: ${hasBootstrap ? 'āœ…' : 'āŒ'}`); // Additional technical analysis const hasJQuery = parser.exists(response.data, '//*[contains(@src, "jquery")]', 'xpath', { verbose }); const totalImages = parser.count(response.data, '//img', 'xpath', { verbose, }); const lazyLoadImages = parser.count(response.data, '//img[@loading="lazy"]', 'xpath', { verbose }); console.log(` jQuery: ${hasJQuery ? 'āœ…' : 'āŒ'}`); console.log(` Total Images: ${totalImages}`); console.log(` Lazy Loading Images: ${lazyLoadImages}`); } catch (error) { console.error('āŒ Error parsing Otakudesu:'); console.error(` ${error instanceof Error ? error.message : 'Unknown error'}`); console.log('\nšŸ’” Troubleshooting tips:'); console.log(' - Check if website is accessible'); console.log(' - Try using different user agents'); console.log(' - Website might have anti-scraping measures'); console.log(' - Consider using proxy if blocked'); console.log(' - Check if the HTML structure has changed'); console.log(' - Use verbose mode for detailed error output: demonstrateOtakudesuParser(true)'); } } // Run the demonstration if this file is executed directly if (require.main === module) { // Check for verbose flag in command line arguments const verbose = process.argv.includes('--verbose') || process.argv.includes('-v'); demonstrateOtakudesuParser(verbose) .then(() => console.log('\nšŸŽŒ Otakudesu parsing demo completed!')) .catch(console.error); } //# sourceMappingURL=otakudesu.cloud.js.map