@@ -2,15 +2,19 @@ import puppeteer from 'puppeteer-extra';
22import StealthPlugin from 'puppeteer-extra-plugin-stealth' ;
33import { create } from 'xmlbuilder2' ;
44import { XMLParser } from 'fast-xml-parser' ;
5- import fs from 'fs' ;
5+ import fs from 'fs/promises ' ;
66import { executablePath } from "puppeteer" ;
77
88const puppeteerStealth = StealthPlugin ( ) ;
99puppeteerStealth . enabledEvasions . delete ( 'user-agent-override' ) ;
1010puppeteer . use ( puppeteerStealth ) ;
1111
12+ const xmlPath = 'docs/index.xml' ;
13+ const URL = 'https://apkpure.com/android-device-policy/com.google.android.apps.work.clouddpc/versions' ;
14+
1215( async ( ) => {
1316 console . log ( '🛫 - Launching browser...' ) ;
17+
1418 const browser = await puppeteer . launch ( {
1519 executablePath : executablePath ( ) ,
1620 readTimeout : 5 * 60 * 1000 ,
@@ -21,129 +25,122 @@ puppeteer.use(puppeteerStealth);
2125 '--disable-dev-shm-usage' ,
2226 ] ,
2327 } ) ;
24- console . log ( '✔️ - Browser launched' ) ;
28+ console . log ( '🤖 - Browser launched' ) ;
2529
26- let versions = [ ] ;
2730 const page = await browser . newPage ( ) ;
28- try {
29- await page . setUserAgent ( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' )
31+ await page . setUserAgent ( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' )
3032
31- const URL = 'https://apkpure.com/android-device-policy/com.google.android.apps.work.clouddpc/versions' ;
33+ try {
34+ console . log ( '🌐 - Loading page...' ) ;
35+ await page . goto ( URL , { waitUntil : 'domcontentloaded' } ) ;
3236
33- await page . goto ( URL , { waitUntil : 'domcontentloaded' } ) ;
34-
3537 console . log ( '✔️ - Page loaded' ) ;
3638
3739 await page . waitForFunction ( ( ) => {
3840 return ! document . querySelector ( 'form#challenge-form, .cf-browser-verification' ) &&
3941 ! / A t t e n t i o n R e q u i r e d / . test ( document . title ) ;
40- } , { timeout : 20000 } ) . catch ( ( ) => {
42+ } , { timeout : 20000 } ) . catch ( ( ) => {
4143 throw new Error ( '⛔ - Cloudflare challenge did not resolve in time' ) ;
4244 } ) ;
43-
44- versions = await page . evaluate ( ( ) => {
45- const items = document . querySelectorAll ( 'li a.ver_download_link' ) ;
46- const result = [ ] ;
47-
48- items . forEach ( link => {
49- const versionDiv = link . querySelector ( '.ver-item-n' ) ;
50- const dateSpan = link . querySelector ( '.update-on' ) ;
51-
52- const version = versionDiv ? versionDiv . textContent . trim ( ) . replace ( / ^ A n d r o i d D e v i c e P o l i c y \s * / i, '' ) . replace ( / \s + / g, ' ' ) : null ;
53- const date = dateSpan ? dateSpan . textContent . trim ( ) : null ;
54- const href = link . getAttribute ( 'href' ) ;
55-
56- if ( version && date && href ) {
57- result . push ( {
58- version,
59- date,
60- link : href . startsWith ( 'http' ) ? href : `https://apkpure.com${ href } `
61- } ) ;
62- }
63- } ) ;
6445
65- return result ;
46+ const versions = await page . evaluate ( ( ) => {
47+ return Array . from ( document . querySelectorAll ( 'li a.ver_download_link' ) )
48+ . map ( link => {
49+ const version = link . querySelector ( '.ver-item-n' ) ?. textContent
50+ ?. trim ( )
51+ ?. replace ( / ^ A n d r o i d D e v i c e P o l i c y \s * / i, '' )
52+ ?. replace ( / \s + / g, ' ' ) ;
53+
54+ const date = link . querySelector ( '.update-on' ) ?. textContent . trim ( ) ;
55+ const href = link . getAttribute ( 'href' ) ;
56+
57+ return version && date && href
58+ ? { version, date, link : href . startsWith ( 'http' ) ? href : `https://apkpure.com${ href } ` }
59+ : null ;
60+ } )
61+ . filter ( Boolean ) ;
6662 } ) ;
6763
6864 if ( ! versions || versions . length === 0 ) {
6965 console . warn ( '⚠️ - No versions scraped — dumping page content for debugging' ) ;
7066
71- const fs = await import ( 'fs/promises' ) ;
7267 const html = await page . content ( ) ;
73- await fs . mkdir ( 'docs' , { recursive : true } ) ;
68+ await fs . mkdir ( 'docs' , { recursive : true } ) ;
7469 await fs . writeFile ( 'debug.html' , html ) ;
75- await page . screenshot ( { path : 'screenshot .png' , fullPage : true } ) ;
70+ await page . screenshot ( { path : 'debug .png' , fullPage : true } ) ;
7671
7772 console . log ( '📝 - Saved debug.html and screenshot.png to docs/' ) ;
78- process . exit ( 0 ) ;
73+ return ;
7974 }
80-
81- if ( versions . length ) console . log ( `🦝 - Scraped ${ versions . length } versions` ) ;
82- } catch ( e ) {
83- console . error ( '🏴☠️ - Page load failed:' , e ) ;
84- const errorHtml = await page . content ( ) ;
85- await page . screenshot ( { path : 'screenshot.png' , fullPage : true } ) ;
8675
87- const fs = await import ( 'fs/promises' ) ;
88- await fs . mkdir ( 'docs' , { recursive : true } ) ;
89- await fs . writeFile ( 'docs/debug.html' , errorHtml ) ;
76+ if ( versions . length ) console . log ( `🦝 - Scraped ${ versions . length } versions` ) ;
9077
91- console . log ( '📝 - Saved debug.html and screenshot.png to docs/' ) ;
92- }
78+ await browser . close ( ) ;
9379
94- await browser . close ( ) ;
95-
96- //console.log(versions);
80+ let existingItems = [ ] ;
9781
98- let existingItems = [ ] ;
99- const xmlPath = 'docs/index.xml'
82+ if ( ( await fs . stat ( xmlPath ) ) . isFile ( ) ) {
83+ const xmlData = await fs . readFile ( xmlPath , 'utf-8' ) ;
84+ const parser = new XMLParser ( ) ;
85+ const parsed = parser . parse ( xmlData ) ;
86+ const items = parsed ?. rss ?. channel ?. item || [ ] ;
87+ existingItems = Array . isArray ( items ) ? items : [ items ] ;
88+ }
10089
101- if ( fs . existsSync ( xmlPath ) ) {
102- const xmlData = fs . readFileSync ( xmlPath , 'utf-8' ) ;
103- const parser = new XMLParser ( ) ;
104- const parsed = parser . parse ( xmlData ) ;
105- const items = parsed ?. rss ?. channel ?. item || [ ] ;
106- existingItems = Array . isArray ( items ) ? items : [ items ] ;
107- }
90+ // Use a Set for fast version-date matching
91+ const existingKeys = new Set ( existingItems . map ( i => `${ i . guid } ` ) ) ;
92+
93+ // Add only new entries
94+ const newItems = versions
95+ . filter ( v => ! existingKeys . has ( v . version ) )
96+ . map ( v => {
97+ const pubDate = new Date ( ) . toUTCString ( ) ;
98+ const guid = v . version ;
99+ return {
100+ title : `Android Device Policy ${ v . version } - ${ v . date } ` ,
101+ link : v . link ,
102+ description : `Version ${ v . version } released on ${ v . date } ` ,
103+ pubDate,
104+ guid
105+ } ;
106+ } )
107+
108+ console . log ( `🔎 - Found ${ newItems . length } new versions` ) ;
109+
110+ if ( newItems . length === 0 ) {
111+ return
112+ }
108113
109- // Use a Set for fast version-date matching
110- const existingKeys = new Set (
111- existingItems . map ( i => `${ i . guid } ` )
112- ) ;
113-
114- // Add only new entries
115- const newItems = versions
116- . filter ( v => ! existingKeys . has ( v . version ) )
117- . map ( v => {
118- const pubDate = new Date ( ) . toUTCString ( ) ;
119- const guid = v . version ;
120- return {
121- title : `Android Device Policy ${ v . version } - ${ v . date } ` ,
122- link : v . link ,
123- description : `Version ${ v . version } released on ${ v . date } ` ,
124- pubDate,
125- guid
126- } ;
127- } )
128- . filter ( item => ! existingKeys . has ( item . title ) ) ;
129-
130- console . log ( `🔎 - Found ${ newItems . length } new versions` ) ;
131-
132- // Combine and sort
133- const allItems = [ ...newItems , ...existingItems ] . slice ( 0 , 50 ) ; // keep only latest 50
134-
135- const rss = {
136- rss : {
137- '@version' : '2.0' ,
138- channel : {
139- title : 'Android Device Policy Versions' ,
140- link : 'https://apkpure.com/android-device-policy/com.google.android.apps.work.clouddpc/versions' ,
141- description : 'Tracks version updates on APKPure' ,
142- item : allItems
114+ // Combine and sort
115+ const allItems = [ ...newItems , ...existingItems ] . slice ( 0 , 50 ) ; // keep only latest 50
116+
117+ const rss = {
118+ rss : {
119+ '@version' : '2.0' ,
120+ channel : {
121+ title : 'Android Device Policy Versions' ,
122+ link : 'https://apkpure.com/android-device-policy/com.google.android.apps.work.clouddpc/versions' ,
123+ description : 'Tracks version updates on APKPure' ,
124+ item : allItems
125+ }
143126 }
144- }
145- } ;
127+ } ;
128+
129+ console . log ( `📝 - Saving xml feed...` )
130+ const xml = create ( { version : '1.0' , encoding : 'UTF-8' } , rss ) . end ( { prettyPrint : true } ) ;
131+ await fs . writeFile ( xmlPath , xml ) ;
132+ console . log ( `💾 - Saving done` )
133+ } catch ( err ) {
134+ console . error ( '💥 Scraper failed:' , err ) ;
135+
136+ const errorHtml = await page . content ( ) ;
137+ await page . screenshot ( { path : 'debud.png' , fullPage : true } ) ;
146138
147- const xml = create ( { version : '1.0' , encoding : 'UTF-8' } , rss ) . end ( { prettyPrint : true } ) ;
148- fs . writeFileSync ( xmlPath , xml ) ;
139+ await fs . mkdir ( 'docs' , { recursive : true } ) ;
140+ await fs . writeFile ( 'debug.html' , errorHtml ) ;
141+
142+ console . log ( '📝 - Saved debug.html and debug.png to docs/' ) ;
143+ } finally {
144+ await browser . close ( ) ;
145+ }
149146} ) ( ) ;
0 commit comments