scraper.js 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337
  1. import fetch from 'node-fetch'
  2. import axios from 'axios';
  3. import { wrapper } from 'axios-cookiejar-support';
  4. import FormData from 'form-data';
  5. import WebSocket from 'ws';
  6. import * as cheerio from 'cheerio';
  7. import { CookieJar } from 'tough-cookie';
  8. import crypto from 'crypto';
  9. const perplexity = {
  10. api: {
  11. base: 'https://api.perplexity.ai/chat/completions',
  12. models: {
  13. 'sonar-medium-online': {
  14. description: 'Online-enabled medium model',
  15. context: 12000
  16. },
  17. 'sonar-small-online': {
  18. description: 'Online-enabled small model',
  19. context: 12000
  20. },
  21. 'sonar-medium-chat': {
  22. description: 'Optimized medium chat model',
  23. context: 12000
  24. },
  25. 'sonar-small-chat': {
  26. description: 'Optimized small chat model',
  27. context: 12000
  28. },
  29. 'sonar-reasoning-pro': {
  30. description: 'Advanced reasoning model with enhanced capabilities',
  31. context: 16384
  32. },
  33. 'sonar-reasoning': {
  34. description: 'Balanced reasoning model',
  35. context: 8192
  36. },
  37. 'sonar-pro': {
  38. description: 'Enhanced general purpose model',
  39. context: 8192
  40. },
  41. 'sonar': {
  42. description: 'Fast and efficient model',
  43. context: 4096
  44. },
  45. 'mixtral-8x7b-instruct': {
  46. description: 'Mixtral instruction model',
  47. context: 8192
  48. },
  49. 'codellama-70b-instruct': {
  50. description: 'Code specialized model',
  51. context: 8192
  52. },
  53. 'llama-2-70b-chat': {
  54. description: 'LLaMA 2 chat model',
  55. context: 4096
  56. }
  57. },
  58. headers: {
  59. 'Content-Type': 'application/json',
  60. 'Accept': 'application/json',
  61. 'User-Agent': 'Postify/1.0.0'
  62. },
  63. keys: [
  64. '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',
  65. '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',
  66. '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',
  67. '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'
  68. ],
  69. retry: {
  70. maxAttempts: 3,
  71. delayMs: 2000,
  72. timeoutMs: 60000
  73. }
  74. },
  75. isParams: (messages, model, temperature) => {
  76. const errors = [];
  77. if (!messages || !Array.isArray(messages) || messages.length === 0) {
  78. errors.push({
  79. param: 'messages',
  80. error: '[ ❌ ] ¡Ya me cansé de decirte, llena el input al menos, por favor!',
  81. example: [{
  82. role: 'user',
  83. content: 'el input va aquí, ¿ok?'
  84. }]
  85. });
  86. } else {
  87. messages.forEach((msg, index) => {
  88. if (!msg.role || !msg.content) {
  89. errors.push({
  90. param: `messages[${index}]`,
  91. error: '[ ❌ ] ¡El formato de tu mensaje está mal, qué desastre!',
  92. example: {
  93. role: 'user/assistant',
  94. content: 'el input va aquí, ¿ok?'
  95. }
  96. });
  97. }
  98. });
  99. }
  100. if (!model) {
  101. errors.push({
  102. param: 'model',
  103. error: '[ ❌ ] ¿En serio no llenaste el modelo? ¡Mínimo pon uno, por favor!',
  104. available: Object.keys(perplexity.api.models)
  105. });
  106. } else if (!perplexity.api.models[model]) {
  107. errors.push({
  108. param: 'model',
  109. error: '[ ❌ ] ¡El modelo que elegiste no existe! Escoge uno de la lista, ¿ya?',
  110. available: Object.keys(perplexity.api.models)
  111. });
  112. }
  113. if (temperature === undefined || temperature === null) {
  114. errors.push({
  115. param: 'temperature',
  116. error: '[ ❌ ] ¿Dónde está la temperatura? ¡No puede estar vacío!',
  117. range: '0.0 - 1.0',
  118. recommended: 0.7
  119. });
  120. } else if (temperature < 0 || temperature > 1) {
  121. errors.push({
  122. param: 'temperature',
  123. error: '[ ❌ ] ¡La temperatura está fuera de rango! Solo de 0 a 1, ¿ok?',
  124. range: '0.0 - 1.0',
  125. recommended: 0.7
  126. });
  127. }
  128. return errors;
  129. },
  130. key: () => perplexity.api.keys[Math.floor(Math.random() * perplexity.api.keys.length)],
  131. delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
  132. retry: async (operation, attempt = 1) => {
  133. try {
  134. return await operation();
  135. } catch (error) {
  136. if (attempt >= perplexity.api.retry.maxAttempts) {
  137. throw error;
  138. }
  139. console.log(`🔄 Intentando de nuevo el intento ${attempt}, espera ${perplexity.api.retry.delayMs}ms, ¡aguanta!`);
  140. console.error(error.message);
  141. await perplexity.delay(perplexity.api.retry.delayMs * attempt);
  142. return await perplexity.retry(operation, attempt + 1);
  143. }
  144. },
  145. createAxiosInstance: () => axios.create({
  146. baseURL: perplexity.api.base,
  147. timeout: perplexity.api.retry.timeoutMs,
  148. maxContentLength: Infinity,
  149. maxBodyLength: Infinity
  150. }),
  151. getHeaders: (apiKey) => {
  152. return {
  153. 'Authorization': `Bearer ${apiKey}`,
  154. ...perplexity.api.headers
  155. };
  156. },
  157. chat: async (messages, model = 'sonar', temperature = 0.7) => {
  158. const ve = perplexity.isParams(messages, model, temperature);
  159. if (ve.length > 0) {
  160. return {
  161. status: false,
  162. code: 400,
  163. result: {
  164. error: '[ ❌ ] ¡Todos tus parámetros están mal, qué desastre!',
  165. details: ve
  166. }
  167. };
  168. }
  169. return await perplexity.retry(async () => {
  170. const axiosInstance = perplexity.createAxiosInstance();
  171. const perplexityKey = perplexity.key();
  172. try {
  173. const response = await axiosInstance.post('', {
  174. model: model,
  175. messages: messages,
  176. temperature: temperature,
  177. max_tokens: 4096,
  178. stream: false
  179. }, {
  180. headers: perplexity.getHeaders(perplexityKey)
  181. });
  182. return {
  183. status: true,
  184. code: 200,
  185. result: {
  186. response: response.data.choices[0].message.content,
  187. model: {
  188. name: model,
  189. ...perplexity.api.models[model]
  190. }
  191. }
  192. };
  193. } catch (error) {
  194. const e = {
  195. status: false,
  196. code: error.response?.status || 500,
  197. result: {
  198. error: '[ ❌ ] ¡Error, hermano!',
  199. details: `${error.message}`,
  200. solution: '[ ❌ ] Intenta de nuevo más tarde, a ver si funciona'
  201. }
  202. };
  203. throw e;
  204. }
  205. });
  206. },
  207. stream: async (messages, model = 'sonar', temperature = 0.7, onChunk) => {
  208. const ve = perplexity.isParams(messages, model, temperature);
  209. if (ve.length > 0) {
  210. return {
  211. status: false,
  212. code: 400,
  213. result: {
  214. error: '[ ❌ ] ¡Todos tus parámetros están mal, qué fastidio!',
  215. details: ve
  216. }
  217. };
  218. }
  219. if (typeof onChunk !== 'function') {
  220. return {
  221. status: false,
  222. code: 400,
  223. result: {
  224. error: '[ ❌ ] ¿Dónde está la función de callback? ¡Falta!',
  225. details: [{
  226. param: 'onChunk',
  227. error: '[ ❌ ] ¡Necesitas una función callback para el streaming!',
  228. example: '(chunk) => console.log(chunk)'
  229. }]
  230. }
  231. };
  232. }
  233. return await perplexity.retry(async () => {
  234. const axiosInstance = perplexity.createAxiosInstance();
  235. const perplexityKey = perplexity.key();
  236. try {
  237. const response = await axiosInstance.post('', {
  238. model: model,
  239. messages: messages,
  240. temperature: temperature,
  241. max_tokens: 4096,
  242. stream: true
  243. }, {
  244. headers: perplexity.getHeaders(perplexityKey),
  245. responseType: 'stream'
  246. });
  247. let pull = '';
  248. for await (const chunk of response.data) {
  249. const lines = chunk.toString().split('\n');
  250. for (const line of lines) {
  251. if (line.startsWith('data: ')) {
  252. try {
  253. const result = JSON.parse(line.slice(5));
  254. if (result.choices?.[0]?.delta?.content) {
  255. const content = result.choices[0].delta.content;
  256. pull += content;
  257. onChunk(content);
  258. }
  259. } catch (e) {
  260. if (!line.includes('[DONE]')) {
  261. console.warn('[ ❌ ] Falló al analizar el chunk, hermano: ', e);
  262. }
  263. }
  264. }
  265. }
  266. }
  267. return {
  268. status: true,
  269. code: 200,
  270. result: {
  271. response: pull,
  272. model: {
  273. name: model,
  274. ...perplexity.api.models[model]
  275. }
  276. }
  277. };
  278. } catch (error) {
  279. const e = {
  280. status: false,
  281. code: error.response?.status || 500,
  282. result: {
  283. error: '[ ❌ ] ¡El streaming falló, qué lata!',
  284. details: error.message,
  285. solution: '[ ❌ ] Reinicia el streaming, ¡a ver si funciona!'
  286. }
  287. };
  288. throw e;
  289. }
  290. });
  291. }
  292. };
  293. //----------------------[Pinterest]---------------------------
  294. const pinterest = {
  295. api: {
  296. base: "https://www.pinterest.com",
  297. endpoints: {
  298. search: "/resource/BaseSearchResource/get/",
  299. pin: "/resource/PinResource/get/",
  300. user: "/resource/UserResource/get/"
  301. }
  302. },
  303. headers: {
  304. 'accept': 'application/json, text/javascript, */*, q=0.01',
  305. 'referer': 'https://www.pinterest.com/',
  306. 'user-agent': 'Postify/1.0.0',
  307. 'x-app-version': 'a9522f',
  308. 'x-pinterest-appstate': 'active',
  309. 'x-pinterest-pws-handler': 'www/[username]/[slug].js',
  310. 'x-pinterest-source-url': '/search/pins/?rs=typed&q=kucing%20anggora/',
  311. 'x-requested-with': 'XMLHttpRequest'
  312. },
  313. isUrl: (str) => {
  314. try {
  315. new URL(str);
  316. return true;
  317. } catch (_) {
  318. return false;
  319. }
  320. },
  321. isPin: (url) => {
  322. if (!url) return false;
  323. const patterns = [
  324. /^https?:\/\/(?:www\.)?pinterest\.com\/pin\/[\w.-]+/,
  325. /^https?:\/\/(?:www\.)?pinterest\.[\w.]+\/pin\/[\w.-]+/,
  326. /^https?:\/\/(?:www\.)?pinterest\.(?:ca|co\.uk|com\.au|de|fr|id|es|mx|br|pt|jp|kr|nz|ru|at|be|ch|cl|dk|fi|gr|ie|nl|no|pl|pt|se|th|tr)\/pin\/[\w.-]+/,
  327. /^https?:\/\/pin\.it\/[\w.-]+/,
  328. /^https?:\/\/(?:www\.)?pinterest\.com\/amp\/pin\/[\w.-]+/,
  329. /^https?:\/\/(?:[a-z]{2}|www)\.pinterest\.com\/pin\/[\w.-]+/,
  330. /^https?:\/\/(?:www\.)?pinterest\.com\/pin\/[\d]+(?:\/)?$/,
  331. /^https?:\/\/(?:www\.)?pinterest\.[\w.]+\/pin\/[\d]+(?:\/)?$/,
  332. /^https?:\/\/(?:www\.)?pinterestcn\.com\/pin\/[\w.-]+/,
  333. /^https?:\/\/(?:www\.)?pinterest\.com\.[\w.]+\/pin\/[\w.-]+/
  334. ];
  335. const clean = url.trim().toLowerCase();
  336. return patterns.some(pattern => pattern.test(clean));
  337. },
  338. getCookies: async () => {
  339. try {
  340. const response = await axios.get(pinterest.api.base);
  341. const setHeaders = response.headers['set-cookie'];
  342. if (setHeaders) {
  343. const cookies = setHeaders.map(cookieString => {
  344. const cp = cookieString.split(';');
  345. const cv = cp[0].trim();
  346. return cv;
  347. });
  348. return cookies.join('; ');
  349. }
  350. return null;
  351. } catch (error) {
  352. console.error(error);
  353. return null;
  354. }
  355. },
  356. search: async (query, limit = 10) => {
  357. if (!query) {
  358. return {
  359. status: false,
  360. code: 400,
  361. result: {
  362. message: "[ ❌ ] ¡Hermano, qué escribiste? ¿El query está literalmente vacío? ¿Crees que tengo un tercer ojo para adivinar? ¡Esfuérzate un poco, por favor!"
  363. }
  364. };
  365. }
  366. try {
  367. const cookies = await pinterest.getCookies();
  368. if (!cookies) {
  369. return {
  370. status: false,
  371. code: 400,
  372. result: {
  373. message: "[ ❌ ] No pude obtener las cookies, intenta de nuevo más tarde, ¿ya?"
  374. }
  375. };
  376. }
  377. const params = {
  378. source_url: `/search/pins/?q=${query}`,
  379. data: JSON.stringify({
  380. options: {
  381. isPrefetch: false,
  382. query: query,
  383. scope: "pins",
  384. bookmarks: [""],
  385. no_fetch_context_on_resource: false,
  386. page_size: limit
  387. },
  388. context: {}
  389. }),
  390. _: Date.now()
  391. };
  392. const { data } = await axios.get(`${pinterest.api.base}${pinterest.api.endpoints.search}`, {
  393. headers: { ...pinterest.headers, 'cookie': cookies },
  394. params: params
  395. });
  396. const container = [];
  397. const results = data.resource_response.data.results.filter((v) => v.images?.orig);
  398. results.forEach((result) => {
  399. container.push({
  400. id: result.id,
  401. title: result.title || "",
  402. description: result.description,
  403. pin_url: `https://pinterest.com/pin/${result.id}`,
  404. media: {
  405. images: {
  406. orig: result.images.orig,
  407. small: result.images['236x'],
  408. medium: result.images['474x'],
  409. large: result.images['736x']
  410. },
  411. video: result.videos ? {
  412. video_list: result.videos.video_list,
  413. duration: result.videos.duration,
  414. video_url: result.videos.video_url
  415. } : null
  416. },
  417. uploader: {
  418. username: result.pinner.username,
  419. full_name: result.pinner.full_name,
  420. profile_url: `https://pinterest.com/${result.pinner.username}`
  421. }
  422. });
  423. });
  424. if (container.length === 0) {
  425. return {
  426. status: false,
  427. code: 404,
  428. result: {
  429. message: `[ ❌ ] ¡Qué desastre, hermano! No encontré nada con "${query}". En serio, tus habilidades de búsqueda necesitan mejorar, sin ofender, ¡esfuérzate más!`
  430. }
  431. };
  432. }
  433. return {
  434. status: true,
  435. code: 200,
  436. result: {
  437. query: query,
  438. total: container.length,
  439. pins: container
  440. }
  441. };
  442. } catch (error) {
  443. return {
  444. status: false,
  445. code: error.response?.status || 500,
  446. result: {
  447. message: "[ ❌ ] ¡El servidor está en caos, hermano! Me molestas todo el tiempo, necesita un descanso. Intenta de nuevo más tarde, ¿ok?"
  448. }
  449. };
  450. }
  451. },
  452. download: async (pinUrl) => {
  453. if (!pinUrl) {
  454. return {
  455. status: false,
  456. code: 400,
  457. result: {
  458. message: "[ ❌ ] ¿Me diste un link vacío, hermano? ¿En serio? ¿Quieres que descargue aire? ¡Esfuérzate un poco, estoy cansado!"
  459. }
  460. };
  461. }
  462. if (!pinterest.isUrl(pinUrl)) {
  463. return {
  464. status: false,
  465. code: 400,
  466. result: {
  467. message: "[ ❌ ] ¿Qué link es este? ¡No sabes ni lo básico de URLs, qué locura!"
  468. }
  469. };
  470. }
  471. if (!pinterest.isPin(pinUrl)) {
  472. return {
  473. status: false,
  474. code: 400,
  475. result: {
  476. message: "[ ❌ ] ¡Por favor, esto no es un link de Pinterest, hermano!"
  477. }
  478. };
  479. }
  480. try {
  481. const pinId = pinUrl.split('/pin/')[1].replace('/', '');
  482. const cookies = await pinterest.getCookies();
  483. if (!cookies) {
  484. return {
  485. status: false,
  486. code: 400,
  487. result: {
  488. message: "[ ❌ ] No pude obtener las cookies, intenta de nuevo más tarde, ¿ya?"
  489. }
  490. };
  491. }
  492. const params = {
  493. source_url: `/pin/${pinId}/`,
  494. data: JSON.stringify({
  495. options: {
  496. field_set_key: "detailed",
  497. id: pinId,
  498. },
  499. context: {}
  500. }),
  501. _: Date.now()
  502. };
  503. const { data } = await axios.get(`${pinterest.api.base}${pinterest.api.endpoints.pin}`, {
  504. headers: { ...pinterest.headers, 'cookie': cookies },
  505. params: params
  506. });
  507. if (!data.resource_response.data) {
  508. return {
  509. status: false,
  510. code: 404,
  511. result: {
  512. message: "[ ❌ ] El pin ya no existe, hermano, se fue, expiró, ¡borrado del planeta! Busca algo que exista, me cansé de explicarte."
  513. }
  514. };
  515. }
  516. const pd = data.resource_response.data;
  517. const mediaUrls = [];
  518. if (pd.videos) {
  519. const videoFormats = Object.values(pd.videos.video_list)
  520. .sort((a, b) => b.width - a.width);
  521. videoFormats.forEach(video => {
  522. mediaUrls.push({
  523. type: 'video',
  524. quality: `${video.width}x${video.height}`,
  525. width: video.width,
  526. height: video.height,
  527. duration: pd.videos.duration || null,
  528. url: video.url,
  529. file_size: video.file_size || null,
  530. thumbnail: pd.images.orig.url
  531. });
  532. });
  533. }
  534. if (pd.images) {
  535. const imge = {
  536. 'original': pd.images.orig,
  537. 'large': pd.images['736x'],
  538. 'medium': pd.images['474x'],
  539. 'small': pd.images['236x'],
  540. 'thumbnail': pd.images['170x']
  541. };
  542. Object.entries(imge).forEach(([quality, image]) => {
  543. if (image) {
  544. mediaUrls.push({
  545. type: 'image',
  546. quality: quality,
  547. width: image.width,
  548. height: image.height,
  549. url: image.url,
  550. size: `${image.width}x${image.height}`
  551. });
  552. }
  553. });
  554. }
  555. if (mediaUrls.length === 0) {
  556. return {
  557. status: false,
  558. code: 404,
  559. result: {
  560. message: "[ ❌ ] ¡Qué desastre, hermano! El pin no tiene medios. ¿Qué esperas que descargue, solo vibes? ¡Qué locura!"
  561. }
  562. };
  563. }
  564. return {
  565. status: true,
  566. code: 200,
  567. result: {
  568. id: pd.id,
  569. title: pd.title || pd.grid_title || "",
  570. description: pd.description || "",
  571. created_at: pd.created_at,
  572. dominant_color: pd.dominant_color || null,
  573. link: pd.link || null,
  574. category: pd.category || null,
  575. media_urls: mediaUrls,
  576. statistics: {
  577. saves: pd.repin_count || 0,
  578. comments: pd.comment_count || 0,
  579. reactions: pd.reaction_counts || {},
  580. total_reactions: pd.total_reaction_count || 0,
  581. views: pd.view_count || 0,
  582. saves_by_category: pd.aggregated_pin_data?.aggregated_stats || {},
  583. },
  584. source: {
  585. name: pd.domain || null,
  586. url: pd.link || null,
  587. favicon: pd.favicon_url || null,
  588. provider: pd.provider_name || null,
  589. rating: pd.embed?.src_rating || null
  590. },
  591. board: {
  592. id: pd.board?.id || null,
  593. name: pd.board?.name || null,
  594. url: pd.board?.url ? `https://pinterest.com${pd.board.url}` : null,
  595. owner: {
  596. id: pd.board?.owner?.id || null,
  597. username: pd.board?.owner?.username || null
  598. }
  599. },
  600. uploader: {
  601. id: pd.pinner?.id || null,
  602. username: pd.pinner?.username || null,
  603. full_name: pd.pinner?.full_name || null,
  604. profile_url: pd.pinner?.username ? `https://pinterest.com/${pd.pinner.username}` : null,
  605. image: {
  606. small: pd.pinner?.image_small_url || null,
  607. medium: pd.pinner?.image_medium_url || null,
  608. large: pd.pinner?.image_large_url || null,
  609. original: pd.pinner?.image_xlarge_url || null
  610. },
  611. type: pd.pinner?.type || "user",
  612. is_verified: pd.pinner?.verified_identity || false
  613. },
  614. metadata: {
  615. article: pd.article || null,
  616. product: {
  617. price: pd.price_value || null,
  618. currency: pd.price_currency || null,
  619. availability: pd.shopping_flags || null,
  620. ratings: pd.rating || null,
  621. reviews_count: pd.review_count || null
  622. },
  623. recipe: pd.recipe || null,
  624. video: pd.videos ? {
  625. duration: pd.videos.duration || null,
  626. views: pd.videos.video_view_count || null,
  627. cover: pd.videos.cover_image_url || null
  628. } : null
  629. },
  630. is_promoted: pd.is_promoted || false,
  631. is_downloadable: pd.is_downloadable || true,
  632. is_playable: pd.is_playable || false,
  633. is_repin: pd.is_repin || false,
  634. is_video: pd.is_video || false,
  635. has_required_attribution: pd.attribution || null,
  636. privacy_level: pd.privacy || "public",
  637. tags: pd.pin_join?.annotations || [],
  638. hashtags: pd.hashtags || [],
  639. did_it_data: pd.did_it_data || null,
  640. native_creator: pd.native_creator || null,
  641. sponsor: pd.sponsor || null,
  642. visual_search_objects: pd.visual_search_objects || []
  643. }
  644. };
  645. } catch (error) {
  646. if (error.response?.status === 404) {
  647. return {
  648. status: false,
  649. code: 404,
  650. result: {
  651. message: "[ ❌ ] El pin ya no existe, hermano, se fue, expiró, ¡borrado del planeta! Busca algo que exista, me cansé de explicarte."
  652. }
  653. };
  654. }
  655. return {
  656. status: false,
  657. code: error.response?.status || 500,
  658. result: {
  659. message: "[ ❌ ] ¡El servidor está en caos, hermano! Me molestas todo el tiempo, necesita un descanso. Intenta de nuevo más tarde, ¿ok?"
  660. }
  661. };
  662. }
  663. },
  664. profile: async (username) => {
  665. if (!username) {
  666. return {
  667. status: false,
  668. code: 400,
  669. result: {
  670. message: "[ ❌ ] ¿Dónde está el username, hermano? ¿Esperas que sea adivino? ¡Dame un username al menos, por favor!"
  671. }
  672. };
  673. }
  674. try {
  675. const cookies = await pinterest.getCookies();
  676. if (!cookies) {
  677. return {
  678. status: false,
  679. code: 400,
  680. result: {
  681. message: "[ ❌ ] No pude obtener las cookies, intenta de nuevo más tarde, ¿ya?"
  682. }
  683. };
  684. }
  685. const params = {
  686. source_url: `/${username}/`,
  687. data: JSON.stringify({
  688. options: {
  689. username: username,
  690. field_set_key: "profile",
  691. isPrefetch: false,
  692. },
  693. context: {}
  694. }),
  695. _: Date.now()
  696. };
  697. const { data } = await axios.get(`${pinterest.api.base}${pinterest.api.endpoints.user}`, {
  698. headers: { ...pinterest.headers, 'cookie': cookies },
  699. params: params
  700. });
  701. if (!data.resource_response.data) {
  702. return {
  703. status: false,
  704. code: 404,
  705. result: {
  706. message: "[ ❌ ] ¡El usuario no existe, hermano! ¿A quién estás buscando en realidad?"
  707. }
  708. };
  709. }
  710. const userx = data.resource_response.data;
  711. return {
  712. status: true,
  713. code: 200,
  714. result: {
  715. id: userx.id,
  716. username: userx.username,
  717. full_name: userx.full_name || "",
  718. bio: userx.about || "",
  719. email: userx.email || null,
  720. type: userx.type || "user",
  721. profile_url: `https://pinterest.com/${userx.username}`,
  722. image: {
  723. small: userx.image_small_url || null,
  724. medium: userx.image_medium_url || null,
  725. large: userx.image_large_url || null,
  726. original: userx.image_xlarge_url || null
  727. },
  728. stats: {
  729. pins: userx.pin_count || 0,
  730. followers: userx.follower_count || 0,
  731. following: userx.following_count || 0,
  732. boards: userx.board_count || 0,
  733. likes: userx.like_count || 0,
  734. saves: userx.save_count || 0
  735. },
  736. website: userx.website_url || null,
  737. domain_url: userx.domain_url || null,
  738. domain_verified: userx.domain_verified || false,
  739. explicitly_followed_by_me: userx.explicitly_followed_by_me || false,
  740. implicitly_followed_by_me: userx.implicitly_followed_by_me || false,
  741. location: userx.location || null,
  742. country: userx.country || null,
  743. is_verified: userx.verified_identity || false,
  744. is_partner: userx.is_partner || false,
  745. is_indexed: userx.indexed || false,
  746. is_tastemaker: userx.is_tastemaker || false,
  747. is_employee: userx.is_employee || false,
  748. is_blocked: userx.blocked_by_me || false,
  749. meta: {
  750. first_name: userx.first_name || null,
  751. last_name: userx.last_name || null,
  752. full_name: userx.full_name || "",
  753. locale: userx.locale || null,
  754. gender: userx.gender || null,
  755. partner: {
  756. is_partner: userx.is_partner || false,
  757. partner_type: userx.partner_type || null
  758. }
  759. },
  760. account_type: userx.account_type || null,
  761. personalize_pins: userx.personalize || false,
  762. connected_to_etsy: userx.connected_to_etsy || false,
  763. has_password: userx.has_password || true,
  764. has_mfa: userx.has_mfa || false,
  765. created_at: userx.created_at || null,
  766. last_login: userx.last_login || null,
  767. social_links: {
  768. twitter: userx.twitter_url || null,
  769. facebook: userx.facebook_url || null,
  770. instagram: userx.instagram_url || null,
  771. youtube: userx.youtube_url || null,
  772. etsy: userx.etsy_url || null
  773. },
  774. custom_gender: userx.custom_gender || null,
  775. pronouns: userx.pronouns || null,
  776. board_classifications: userx.board_classifications || {},
  777. interests: userx.interests || []
  778. }
  779. };
  780. } catch (error) {
  781. if (error.response?.status === 404) {
  782. return {
  783. status: false,
  784. code: 404,
  785. result: {
  786. message: "[ ❌ ] ¡El username no es válido, hermano! Buscas a lo loco, mejor googlea primero."
  787. }
  788. };
  789. }
  790. return {
  791. status: false,
  792. code: error.response?.status || 500,
  793. result: {
  794. message: "[ ❌ ] ¡El servidor está en caos, hermano! Me molestas todo el tiempo, necesita un descanso. Intenta de nuevo más tarde, ¿ok?"
  795. }
  796. };
  797. }
  798. }
  799. };
  800. //-------------------[YTDL AMDL]--------------------
  801. const amdl = {
  802. api: {
  803. base: {
  804. video: 'https://amp4.cc',
  805. audio: 'https://amp3.cc'
  806. }
  807. },
  808. headers: {
  809. Accept: 'application/json',
  810. 'User-Agent': 'Postify/1.0.0',
  811. },
  812. jar: new CookieJar(),
  813. client: wrapper(axios.create({ jar: new CookieJar() })),
  814. ytRegex: /^((?:https?:)?\/\/)?((?:www|m|music)\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(?:embed\/)?(?:v\/)?(?:shorts\/)?([a-zA-Z0-9_-]{11})/,
  815. formats: {
  816. video: ['144p', '240p', '360p', '480p', '720p', '1080p'],
  817. audio: ['64k', '128k', '192k', '256k', '320k']
  818. },
  819. captcha: {
  820. hashChallenge: async function(salt, number, algorithm) {
  821. return crypto.createHash(algorithm.toLowerCase()).update(salt + number).digest('hex');
  822. },
  823. verifyChallenge: async function(challengeData, salt, algorithm, maxNumber) {
  824. for (let i = 0; i <= maxNumber; i++) {
  825. if (await this.hashChallenge(salt, i, algorithm) === challengeData) {
  826. return { number: i, took: Date.now() };
  827. }
  828. }
  829. throw new Error('Fallo en la verificación de Captcha.');
  830. },
  831. solve: async function(challenge) {
  832. const { algorithm, challenge: challengeData, salt, maxnumber, signature } = challenge;
  833. const solution = await this.verifyChallenge(challengeData, salt, algorithm, maxnumber);
  834. return Buffer.from(
  835. JSON.stringify({
  836. algorithm,
  837. challenge: challengeData,
  838. number: solution.number,
  839. salt,
  840. signature,
  841. took: solution.took,
  842. })
  843. ).toString('base64');
  844. },
  845. },
  846. isUrl: async function(url) {
  847. if (!url) {
  848. return {
  849. status: false,
  850. code: 400,
  851. result: {
  852. error: "[ ❌ ] ¿Dónde está el link? ¡No puedo descargar sin un link, por favor!"
  853. }
  854. };
  855. }
  856. if (!this.ytRegex.test(url)) {
  857. return {
  858. status: false,
  859. code: 400,
  860. result: {
  861. error: "[ ❌ ] ¿Qué link metiste, hermano? ¡Solo links de YouTube, que eso es lo que quieres descargar!"
  862. }
  863. };
  864. }
  865. return {
  866. status: true,
  867. code: 200,
  868. id: url.match(this.ytRegex)[3]
  869. };
  870. },
  871. convert: async function(url, format, quality, isAudio = false) {
  872. try {
  873. const linkx = await this.isUrl(url);
  874. if (!linkx.status) return linkx;
  875. const formatx = isAudio ? this.formats.audio : this.formats.video;
  876. if (!quality || !formatx.includes(quality)) {
  877. return {
  878. status: false,
  879. code: 400,
  880. result: {
  881. error: "[ ❌ ] ¡Ese formato no existe, hermano! Elige uno de los disponibles, no busques lo que no hay.",
  882. available_fmt: formatx
  883. }
  884. };
  885. }
  886. const fixedURL = `https://youtu.be/${linkx.id}`;
  887. const base = isAudio ? this.api.base.audio : this.api.base.video;
  888. const pages = await this.client.get(`${base}/`);
  889. const $ = cheerio.load(pages.data);
  890. const csrfToken = $('meta[name="csrf-token"]').attr('content');
  891. if (!csrfToken) {
  892. return {
  893. status: false,
  894. code: 500,
  895. result: {
  896. error: "[ ❌ ] ¡No hay CSRF, hermano! Parece que hay un problema..."
  897. }
  898. };
  899. }
  900. const form = new FormData();
  901. form.append('url', fixedURL);
  902. form.append('format', format);
  903. form.append('quality', quality);
  904. form.append('service', 'youtube');
  905. if (isAudio) {
  906. form.append('playlist', 'false');
  907. }
  908. form.append('_token', csrfToken);
  909. const captchaX = await this.client.get(`${base}/captcha`, {
  910. headers: {
  911. ...this.headers,
  912. Origin: base,
  913. Referer: `${base}/`
  914. },
  915. });
  916. if (captchaX.data) {
  917. const solvedCaptcha = await this.captcha.solve(captchaX.data);
  918. form.append('altcha', solvedCaptcha);
  919. }
  920. const endpoint = isAudio ? '/convertAudio' : '/convertVideo';
  921. const res = await this.client.post(`${base}${endpoint}`, form, {
  922. headers: {
  923. ...form.getHeaders(),
  924. ...this.headers,
  925. Origin: base,
  926. Referer: `${base}/`
  927. },
  928. });
  929. if (!res.data.success) {
  930. return {
  931. status: false,
  932. code: 400,
  933. result: {
  934. error: res.data.message
  935. }
  936. };
  937. }
  938. const ws = await this.connect(res.data.message, isAudio);
  939. const dlink = `${base}/dl/${ws.worker}/${res.data.message}/${encodeURIComponent(ws.file)}`;
  940. return {
  941. status: true,
  942. code: 200,
  943. result: {
  944. title: ws.title || "[ ❌ ] No sé",
  945. type: isAudio ? 'audio' : 'video',
  946. format: format,
  947. thumbnail: ws.thumbnail || `https://i.ytimg.com/vi/${linkx.id}/maxresdefault.jpg`,
  948. download: dlink,
  949. id: linkx.id,
  950. duration: ws.duration,
  951. quality: quality,
  952. uploader: ws.uploader
  953. }
  954. };
  955. } catch (error) {
  956. return {
  957. status: false,
  958. code: 500,
  959. result: {
  960. error: "[ ❌ ] ¡Hubo un error, qué risa!"
  961. }
  962. };
  963. }
  964. },
  965. connect: async function(id, isAudio = false) {
  966. return new Promise((resolve, reject) => {
  967. const ws = new WebSocket(`wss://${isAudio ? 'amp3' : 'amp4'}.cc/ws`, ['json'], {
  968. headers: {
  969. ...this.headers,
  970. Origin: `https://${isAudio ? 'amp3' : 'amp4'}.cc`
  971. },
  972. rejectUnauthorized: false,
  973. });
  974. let fileInfo = {};
  975. let timeoutId = setTimeout(() => {
  976. ws.close();
  977. reject({
  978. status: false,
  979. code: 408,
  980. result: {
  981. error: "[ ❌ ] Se acabó el tiempo, el servidor no responde, ¡qué risa!"
  982. }
  983. });
  984. }, 30000);
  985. ws.on('open', () => ws.send(id));
  986. ws.on('message', (data) => {
  987. const res = JSON.parse(data);
  988. if (res.event === 'query' || res.event === 'queue') {
  989. fileInfo = { thumbnail: res.thumbnail, title: res.title, duration: res.duration, uploader: res.uploader };
  990. } else if (res.event === 'file' && res.done) {
  991. clearTimeout(timeoutId);
  992. ws.close();
  993. resolve({ ...fileInfo, ...res });
  994. }
  995. });
  996. ws.on('error', (err) => {
  997. clearTimeout(timeoutId);
  998. reject({
  999. status: false,
  1000. code: 500,
  1001. result: {
  1002. error: "[ ❌ ] ¡Qué mal, este servidor es pésimo! Falló la conexión otra vez, qué desastre."
  1003. }
  1004. });
  1005. });
  1006. });
  1007. },
  1008. download: async function(url, format = '720p') {
  1009. try {
  1010. const isAudio = format === 'mp3';
  1011. return await this.convert(
  1012. url,
  1013. isAudio ? 'mp3' : 'mp4',
  1014. isAudio ? '128k' : format,
  1015. isAudio
  1016. );
  1017. } catch (error) {
  1018. return {
  1019. status: false,
  1020. code: 500,
  1021. result: {
  1022. error: "[ ❌ ] ¡Error, qué locura!"
  1023. }
  1024. };
  1025. }
  1026. }
  1027. };
  1028. const ytdown = {
  1029. api: {
  1030. base: "https://p.oceansaver.in/ajax/",
  1031. progress: "https://p.oceansaver.in/ajax/progress.php"
  1032. },
  1033. headers: {
  1034. 'authority': 'p.oceansaver.in',
  1035. 'origin': 'https://y2down.cc',
  1036. 'referer': 'https://y2down.cc/',
  1037. 'user-agent': 'Postify/1.0.0'
  1038. },
  1039. formats: ['360', '480', '720', '1080', '1440', '2160', 'mp3', 'm4a', 'wav', 'aac', 'flac', 'opus', 'ogg'],
  1040. isUrl: str => { try { new URL(str); return true; } catch (_) { return false; } },
  1041. youtube: url => {
  1042. if (!url) return null;
  1043. const a = [
  1044. /youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,
  1045. /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
  1046. /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
  1047. /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
  1048. /youtu\.be\/([a-zA-Z0-9_-]{11})/
  1049. ];
  1050. for (let b of a) {
  1051. if (b.test(url)) return url.match(b)[1];
  1052. }
  1053. return null;
  1054. },
  1055. request: async (endpoint, params = {}) => {
  1056. try {
  1057. const { data } = await axios.get(`${ytdown.api.base}${endpoint}`, {
  1058. params, headers: ytdown.headers, withCredentials: true
  1059. });
  1060. return data;
  1061. } catch (error) {
  1062. console.error(error.message, error.response?.data);
  1063. throw error;
  1064. }
  1065. },
  1066. download: async (link, format) => {
  1067. if (!link) return { error: "[ ❌ ] ¿Dónde está el link? ¡No puedo descargar sin un link, por favor!" };
  1068. if (!ytdown.isUrl(link)) return { error: "[ ❌ ] ¿Qué link metiste, hermano? ¡Solo links de YouTube, que eso es lo que quieres descargar!" };
  1069. if (!format || !ytdown.formats.includes(format)) return {
  1070. error: "[ ❌ ] ¡Ese formato no existe, hermano! Elige uno de los disponibles, no busques lo que no hay.",
  1071. availableFormats: ytdown.formats
  1072. };
  1073. const id = ytdown.youtube(link);
  1074. if (!id) return { error: "[ ❌ ] No pude extraer el link de YouTube, usa un link correcto para que no pase esto otra vez, ¡qué risa!" };
  1075. try {
  1076. const response = await ytdown.request("download.php", { format, url: `https://www.youtube.com/watch?v=${id}` });
  1077. return ytdown.handler(response, format, id);
  1078. } catch (error) {
  1079. return {
  1080. error: `[ ❌ ] ${error.message}`,
  1081. details: error.response?.data
  1082. };
  1083. }
  1084. },
  1085. handler: async (data, format, id) => {
  1086. if (!data.success) return { error: data.message || "[ ❌ ] Error" };
  1087. if (!data.id) return { error: "[ ❌ ] ¡No hay ID de descarga, hermano! Así no puedo continuar el proceso, ¡qué risa!" };
  1088. try {
  1089. const pr = await ytdown.checkProgress(data.id);
  1090. return pr.success ? ytdown.final(data, pr, format, id) : pr;
  1091. } catch (error) {
  1092. return { error: `[ ❌ ] ${error.message}` };
  1093. }
  1094. },
  1095. checkProgress: async (id) => {
  1096. let attempts = 0, lastProgress = -1;
  1097. process.stdout.write("[ ✨ ] Progreso: [ ] 0%");
  1098. while (attempts < 100) {
  1099. try {
  1100. const { data } = await axios.get(ytdown.api.progress, {
  1101. params: { id }, headers: ytdown.headers, withCredentials: true
  1102. });
  1103. const currentProgress = Math.round(data.progress / 10);
  1104. if (currentProgress !== lastProgress) {
  1105. ytdown.updateBar(currentProgress);
  1106. lastProgress = currentProgress;
  1107. }
  1108. if (data.download_url && data.success) {
  1109. return { success: true, ...data };
  1110. } else if (!data.download_url && data.success) {
  1111. return { error: data.text };
  1112. }
  1113. await new Promise(resolve => setTimeout(resolve, 1000));
  1114. attempts++;
  1115. } catch (error) {
  1116. console.error("\n", error);
  1117. attempts++;
  1118. await new Promise(resolve => setTimeout(resolve, 1000));
  1119. }
  1120. }
  1121. return { error: "[ ❌ ] El proceso de descarga no pudo continuar, hermano, ¡se acabó el tiempo!" };
  1122. },
  1123. updateBar: (progress) => {
  1124. const barLength = 30;
  1125. const filledLength = Math.round(barLength * progress / 100);
  1126. const bar = '█'.repeat(filledLength) + ' '.repeat(barLength - filledLength);
  1127. process.stdout.clearLine();
  1128. process.stdout.cursorTo(0);
  1129. process.stdout.write(`[ ✨ ] Progreso: [${bar}] ${progress}%\n\n`);
  1130. },
  1131. final: (init, pro, formats, id) => ({
  1132. success: true,
  1133. title: init.title || "[ ❌ ] No sé",
  1134. type: ['360', '480', '720', '1080', '1440', '2160'].includes(formats) ? 'video' : 'audio',
  1135. formats,
  1136. thumbnail: init.info?.image || `https://img.youtube.com/vi/${id}/hqdefault.jpg`,
  1137. download: pro.download_url || "[ ❌ ] No sé",
  1138. id: id
  1139. })
  1140. };
  1141. //-----
  1142. async function sekaikomikDl(url) {
  1143. let res = await fetch(url)
  1144. let $ = cheerio.load(await res.text())
  1145. let data = $('script').map((idx, el) => $(el).html()).toArray()
  1146. data = data.filter(v => /wp-content/i.test(v))
  1147. data = eval(data[0].split('"images":')[1].split('}],')[0])
  1148. return data.map(v => encodeURI(v))
  1149. }
  1150. async function facebookDl(url) {
  1151. let res = await fetch('https://fdownloader.net/')
  1152. let $ = cheerio.load(await res.text())
  1153. let token = $('input[name="__RequestVerificationToken"]').attr('value')
  1154. let json = await (await fetch('https://fdownloader.net/api/ajaxSearch', {
  1155. method: 'post',
  1156. headers: {
  1157. cookie: res.headers.get('set-cookie'),
  1158. 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  1159. referer: 'https://fdownloader.net/'
  1160. },
  1161. body: new URLSearchParams(Object.entries({ __RequestVerificationToken: token, q: url }))
  1162. })).json()
  1163. let $$ = cheerio.load(json.data)
  1164. let result = {}
  1165. $$('.button.is-success.is-small.download-link-fb').each(function () {
  1166. let quality = $$(this).attr('title').split(' ')[1]
  1167. let link = $$(this).attr('href')
  1168. if (link) result[quality] = link
  1169. })
  1170. return result
  1171. }
  1172. //--
  1173. async function tiktokStalk(user) {
  1174. let res = await axios.get(`https://urlebird.com/user/${user}/`)
  1175. let $ = cheerio.load(res.data), obj = {}
  1176. obj.pp_user = $('div[class="col-md-auto justify-content-center text-center"] > img').attr('src')
  1177. obj.name = $('h1.user').text().trim()
  1178. obj.username = $('div.content > h5').text().trim()
  1179. obj.followers = $('div[class="col-7 col-md-auto text-truncate"]').text().trim().split(' ')[1]
  1180. obj.following = $('div[class="col-auto d-none d-sm-block text-truncate"]').text().trim().split(' ')[1]
  1181. obj.description = $('div.content > p').text().trim()
  1182. return obj
  1183. }
  1184. //--
  1185. async function igStalk(username) {
  1186. username = username.replace(/^@/, '')
  1187. const html = await (await fetch(`https://dumpor.com/v/${username}`)).text()
  1188. const $$ = cheerio.load(html)
  1189. const name = $$('div.user__title > a > h1').text().trim()
  1190. const Uname = $$('div.user__title > h4').text().trim()
  1191. const description = $$('div.user__info-desc').text().trim()
  1192. const profilePic = $$('div.user__img').attr('style')?.replace("background-image: url('", '').replace("');", '')
  1193. const row = $$('#user-page > div.container > div > div > div:nth-child(1) > div > a')
  1194. const postsH = row.eq(0).text().replace(/Posts/i, '').trim()
  1195. const followersH = row.eq(2).text().replace(/Followers/i, '').trim()
  1196. const followingH = row.eq(3).text().replace(/Following/i, '').trim()
  1197. const list = $$('ul.list > li.list__item')
  1198. const posts = parseInt(list.eq(0).text().replace(/Posts/i, '').trim().replace(/\s/g, ''))
  1199. const followers = parseInt(list.eq(1).text().replace(/Followers/i, '').trim().replace(/\s/g, ''))
  1200. const following = parseInt(list.eq(2).text().replace(/Following/i, '').trim().replace(/\s/g, ''))
  1201. return {
  1202. name,
  1203. username: Uname,
  1204. description,
  1205. postsH,
  1206. posts,
  1207. followersH,
  1208. followers,
  1209. followingH,
  1210. following,
  1211. profilePic
  1212. }
  1213. }
  1214. export { perplexity, amdl, pinterest, ytdown, sekaikomikDl, igStalk, facebookDl, tiktokStalk }