DealabsBridge.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. <?php
  2. class DealabsBridge extends PepperBridgeAbstract {
  3. const NAME = 'Dealabs Bridge';
  4. const URI = 'https://www.dealabs.com/';
  5. const DESCRIPTION = 'Affiche les Deals de Dealabs';
  6. const MAINTAINER = 'sysadminstory';
  7. const PARAMETERS = array(
  8. 'Recherche par Mot(s) clé(s)' => array (
  9. 'q' => array(
  10. 'name' => 'Mot(s) clé(s)',
  11. 'type' => 'text',
  12. 'required' => true
  13. ),
  14. 'hide_expired' => array(
  15. 'name' => 'Masquer les éléments expirés',
  16. 'type' => 'checkbox',
  17. 'required' => 'true'
  18. ),
  19. 'hide_local' => array(
  20. 'name' => 'Masquer les deals locaux',
  21. 'type' => 'checkbox',
  22. 'title' => 'Masquer les deals en magasins physiques',
  23. 'required' => 'true'
  24. ),
  25. 'priceFrom' => array(
  26. 'name' => 'Prix minimum',
  27. 'type' => 'text',
  28. 'title' => 'Prix mnimum en euros',
  29. 'required' => 'false',
  30. 'defaultValue' => ''
  31. ),
  32. 'priceTo' => array(
  33. 'name' => 'Prix maximum',
  34. 'type' => 'text',
  35. 'title' => 'Prix maximum en euros',
  36. 'required' => 'false',
  37. 'defaultValue' => ''
  38. ),
  39. ),
  40. 'Deals par groupe' => array(
  41. 'group' => array(
  42. 'name' => 'Groupe',
  43. 'type' => 'list',
  44. 'required' => 'true',
  45. 'title' => 'Groupe dont il faut afficher les deals',
  46. 'values' => array(
  47. 'Accessoires & gadgets' => 'accessoires-gadgets',
  48. 'Alimentation & boissons' => 'alimentation-boissons',
  49. 'Animaux' => 'animaux',
  50. 'Applis & logiciels' => 'applis-logiciels',
  51. 'Consoles & jeux vidéo' => 'consoles-jeux-video',
  52. 'Culture & divertissement' => 'culture-divertissement',
  53. 'Gratuit' => 'gratuit',
  54. 'Image, son & vidéo' => 'image-son-video',
  55. 'Informatique' => 'informatique',
  56. 'Jeux & jouets' => 'jeux-jouets',
  57. 'Maison & jardin' => 'maison-jardin',
  58. 'Mode & accessoires' => 'mode-accessoires',
  59. 'Santé & cosmétiques' => 'hygiene-sante-cosmetiques',
  60. 'Services divers' => 'services-divers',
  61. 'Sports & plein air' => 'sports-plein-air',
  62. 'Téléphonie' => 'telephonie',
  63. 'Voyages & sorties' => 'voyages-sorties-restaurants',
  64. )
  65. ),
  66. 'order' => array(
  67. 'name' => 'Trier par',
  68. 'type' => 'list',
  69. 'required' => 'true',
  70. 'title' => 'Ordre de tri des deals',
  71. 'values' => array(
  72. 'Du deal le plus Hot au moins Hot' => '',
  73. 'Du deal le plus récent au plus ancien' => '-nouveaux',
  74. 'Du deal le plus commentés au moins commentés' => '-commentes'
  75. )
  76. )
  77. )
  78. );
  79. public $lang = array(
  80. 'bridge-uri' => SELF::URI,
  81. 'bridge-name' => SELF::NAME,
  82. 'context-keyword' => 'Recherche par Mot(s) clé(s)',
  83. 'context-group' => 'Deals par groupe',
  84. 'uri-group' => '/groupe/',
  85. 'request-error' => 'Could not request Dealabs',
  86. 'no-results' => 'Il n&#039;y a rien à afficher pour le moment :(',
  87. 'relative-date-indicator' => array(
  88. 'il y a',
  89. ),
  90. 'price' => 'Prix',
  91. 'shipping' => 'Livraison',
  92. 'origin' => 'Origine',
  93. 'discount' => 'Réduction',
  94. 'title-keyword' => 'Recherche',
  95. 'title-group' => 'Groupe',
  96. 'local-months' => array(
  97. 'janvier',
  98. 'février',
  99. 'mars',
  100. 'avril',
  101. 'mai',
  102. 'juin',
  103. 'juillet',
  104. 'août',
  105. 'septembre',
  106. 'octobre',
  107. 'novembre',
  108. 'décembre'
  109. ),
  110. 'local-time-relative' => array(
  111. 'il y a ',
  112. 'min',
  113. 'h',
  114. 'jour',
  115. 'jours',
  116. 'mois',
  117. 'ans',
  118. 'et '
  119. ),
  120. 'date-prefixes' => array(
  121. 'Actualisé ',
  122. ),
  123. 'relative-date-alt-prefixes' => array(
  124. 'Actualisé ',
  125. ),
  126. 'relative-date-ignore-suffix' => array(
  127. ),
  128. 'localdeal' => array(
  129. 'Local',
  130. 'Pays d\'expédition'
  131. ),
  132. );
  133. }
  134. class PepperBridgeAbstract extends BridgeAbstract {
  135. const CACHE_TIMEOUT = 3600;
  136. public function collectData(){
  137. switch($this->queriedContext) {
  138. case $this->i8n('context-keyword'):
  139. return $this->collectDataKeywords();
  140. break;
  141. case $this->i8n('context-group'):
  142. return $this->collectDataGroup();
  143. break;
  144. }
  145. }
  146. /**
  147. * Get the Deal data from the choosen group in the choosed order
  148. */
  149. public function collectDataGroup()
  150. {
  151. $group = $this->getInput('group');
  152. $order = $this->getInput('order');
  153. $url = $this->i8n('bridge-uri')
  154. . $this->i8n('uri-group') . $group . $order;
  155. $this->collectDeals($url);
  156. }
  157. /**
  158. * Get the Deal data from the choosen keywords and parameters
  159. */
  160. public function collectDataKeywords()
  161. {
  162. $q = $this->getInput('q');
  163. $hide_expired = $this->getInput('hide_expired');
  164. $hide_local = $this->getInput('hide_local');
  165. $priceFrom = $this->getInput('priceFrom');
  166. $priceTo = $this->getInput('priceFrom');
  167. /* Even if the original website uses POST with the search page, GET works too */
  168. $url = $this->i8n('bridge-uri')
  169. . '/search/advanced?q='
  170. . urlencode($q)
  171. . '&hide_expired='. $hide_expired
  172. . '&hide_local='. $hide_local
  173. . '&priceFrom='. $priceFrom
  174. . '&priceTo='. $priceTo
  175. /* Some default parameters
  176. * search_fields : Search in Titres & Descriptions & Codes
  177. * sort_by : Sort the search by new deals
  178. * time_frame : Search will not be on a limited timeframe
  179. */
  180. . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
  181. $this->collectDeals($url);
  182. }
  183. /**
  184. * Get the Deal data using the given URL
  185. */
  186. public function collectDeals($url){
  187. $html = getSimpleHTMLDOM($url)
  188. or returnServerError($this->i8n('request-error'));
  189. $list = $html->find('article[id]');
  190. // Deal Image Link CSS Selector
  191. $selectorImageLink = implode(
  192. ' ', /* Notice this is a space! */
  193. array(
  194. 'cept-thread-image-link',
  195. 'imgFrame',
  196. 'imgFrame--noBorder',
  197. 'thread-listImgCell',
  198. )
  199. );
  200. // Deal Link CSS Selector
  201. $selectorLink = implode(
  202. ' ', /* Notice this is a space! */
  203. array(
  204. 'cept-tt',
  205. 'thread-link',
  206. 'linkPlain',
  207. )
  208. );
  209. // Deal Hotness CSS Selector
  210. $selectorHot = implode(
  211. ' ', /* Notice this is a space! */
  212. array(
  213. 'flex',
  214. 'flex--align-c',
  215. 'flex--justify-space-between',
  216. 'space--b-2',
  217. )
  218. );
  219. // Deal Description CSS Selector
  220. $selectorDescription = implode(
  221. ' ', /* Notice this is a space! */
  222. array(
  223. 'cept-description-container',
  224. 'overflow--wrap-break',
  225. 'size--all-s',
  226. 'size--fromW3-m'
  227. )
  228. );
  229. // Deal Date CSS Selector
  230. $selectorDate = implode(
  231. ' ', /* Notice this is a space! */
  232. array(
  233. 'size--all-s',
  234. 'flex',
  235. 'flex--justify-e',
  236. 'flex--grow-1',
  237. )
  238. );
  239. // If there is no results, we don't parse the content because it display some random deals
  240. $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
  241. if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
  242. $this->items = array();
  243. } else {
  244. foreach ($list as $deal) {
  245. $item = array();
  246. $item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
  247. $item['title'] = $deal->find('a[class*='. $selectorLink .']', 0
  248. )->plaintext;
  249. $item['author'] = $deal->find('span.thread-username', 0)->plaintext;
  250. $item['content'] = '<table><tr><td><a href="'
  251. . $deal->find(
  252. 'a[class*='. $selectorImageLink .']', 0)->href
  253. . '"><img src="'
  254. . $this->getImage($deal)
  255. . '"/></td><td><h2><a href="'
  256. . $deal->find('a[class*='. $selectorLink .']', 0)->href
  257. . '">'
  258. . $deal->find('a[class*='. $selectorLink .']', 0)->innertext
  259. . '</a></h2>'
  260. . $this->getPrice($deal)
  261. . $this->getDiscount($deal)
  262. . $this->getShipsFrom($deal)
  263. . $this->getShippingCost($deal)
  264. . $this->GetSource($deal)
  265. . $deal->find('div[class*='. $selectorDescription .']', 0)->innertext
  266. . '</td><td>'
  267. . $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
  268. . '</td></table>';
  269. $dealDateDiv = $deal->find('div[class*='. $selectorDate .']', 0)
  270. ->find('span[class=hide--toW3]');
  271. $itemDate = end($dealDateDiv)->plaintext;
  272. // In case of a Local deal, there is no date, but we can use
  273. // this case for other reason (like date not in the last field)
  274. if ($this->contains($itemDate, $this->i8n('localdeal'))) {
  275. $item['timestamp'] = time();
  276. } else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
  277. $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
  278. } else {
  279. $item['timestamp'] = $this->parseDate($itemDate);
  280. }
  281. $this->items[] = $item;
  282. }
  283. }
  284. }
  285. /**
  286. * Check if the string $str contains any of the string of the array $arr
  287. * @return boolean true if the string matched anything otherwise false
  288. */
  289. private function contains($str, array $arr)
  290. {
  291. foreach ($arr as $a) {
  292. if (stripos($str, $a) !== false) {
  293. return true;
  294. }
  295. }
  296. return false;
  297. }
  298. /**
  299. * Get the Price from a Deal if it exists
  300. * @return string String of the deal price
  301. */
  302. private function getPrice($deal)
  303. {
  304. if ($deal->find(
  305. 'span[class*=thread-price]', 0) != null) {
  306. return '<div>'.$this->i8n('price') .' : '
  307. . $deal->find(
  308. 'span[class*=thread-price]', 0
  309. )->plaintext
  310. . '</div>';
  311. } else {
  312. return '';
  313. }
  314. }
  315. /**
  316. * Get the Shipping costs from a Deal if it exists
  317. * @return string String of the deal shipping Cost
  318. */
  319. private function getShippingCost($deal)
  320. {
  321. if ($deal->find('span[class*=cept-shipping-price]', 0) != null) {
  322. if ($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
  323. return '<div>'. $this->i8n('shipping') .' : '
  324. . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
  325. . '</div>';
  326. } else {
  327. return '<div>'. $this->i8n('shipping') .' : '
  328. . $deal->find('span[class*=cept-shipping-price]', 0)->innertext
  329. . '</div>';
  330. }
  331. } else {
  332. return '';
  333. }
  334. }
  335. /**
  336. * Get the source of a Deal if it exists
  337. * @return string String of the deal source
  338. */
  339. private function GetSource($deal)
  340. {
  341. if ($deal->find('a[class=text--color-greyShade]', 0) != null) {
  342. return '<div>'. $this->i8n('origin') .' : '
  343. . $deal->find('a[class=text--color-greyShade]', 0)->outertext
  344. . '</div>';
  345. } else {
  346. return '';
  347. }
  348. }
  349. /**
  350. * Get the original Price and discout from a Deal if it exists
  351. * @return string String of the deal original price and discount
  352. */
  353. private function getDiscount($deal)
  354. {
  355. if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
  356. $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
  357. if ($discountHtml != null) {
  358. $discount = $discountHtml->plaintext;
  359. } else {
  360. $discount = '';
  361. }
  362. return '<div>'. $this->i8n('discount') .' : <span style="text-decoration: line-through;">'
  363. . $deal->find(
  364. 'span[class*=mute--text text--lineThrough]', 0
  365. )->plaintext
  366. . '</span>&nbsp;'
  367. . $discount
  368. . '</div>';
  369. } else {
  370. return '';
  371. }
  372. }
  373. /**
  374. * Get the Picture URL from a Deal if it exists
  375. * @return string String of the deal Picture URL
  376. */
  377. private function getImage($deal)
  378. {
  379. $selectorLazy = implode(
  380. ' ', /* Notice this is a space! */
  381. array(
  382. 'thread-image',
  383. 'width--all-auto',
  384. 'height--all-auto',
  385. 'imgFrame-img',
  386. 'cept-thread-img',
  387. 'img--dummy',
  388. 'js-lazy-img'
  389. )
  390. );
  391. $selectorPlain = implode(
  392. ' ', /* Notice this is a space! */
  393. array(
  394. 'thread-image',
  395. 'width--all-auto',
  396. 'height--all-auto',
  397. 'imgFrame-img',
  398. 'cept-thread-img'
  399. )
  400. );
  401. if ($deal->find('img[class='. $selectorLazy .']', 0) != null) {
  402. return json_decode(
  403. html_entity_decode(
  404. $deal->find('img[class='. $selectorLazy .']', 0)
  405. ->getAttribute('data-lazy-img')))->{'src'};
  406. } else {
  407. return $deal->find('img[class*='. $selectorPlain .']', 0 )->src;
  408. }
  409. }
  410. /**
  411. * Get the originating country from a Deal if it exists
  412. * @return string String of the deal originating country
  413. */
  414. private function getShipsFrom($deal)
  415. {
  416. $selector = implode(
  417. ' ', /* Notice this is a space! */
  418. array(
  419. 'meta-ribbon',
  420. 'overflow--wrap-off',
  421. 'space--l-3',
  422. 'text--color-greyShade'
  423. )
  424. );
  425. if ($deal->find('span[class='. $selector .']', 0) != null) {
  426. return '<div>'
  427. . $deal->find('span[class='. $selector .']', 0)->children(2)->plaintext
  428. . '</div>';
  429. } else {
  430. return '';
  431. }
  432. }
  433. /**
  434. * Transforms a local date into a timestamp
  435. * @return int timestamp of the input date
  436. */
  437. private function parseDate($string)
  438. {
  439. $month_local = $this->i8n('local-months');
  440. $month_en = array(
  441. 'January',
  442. 'February',
  443. 'March',
  444. 'April',
  445. 'May',
  446. 'June',
  447. 'July',
  448. 'August',
  449. 'September',
  450. 'October',
  451. 'November',
  452. 'December'
  453. );
  454. // A date can be prfixed with some words, we remove theme
  455. $string = $this->removeDatePrefixes($string);
  456. // We translate the local months name in the english one
  457. $date_str = trim(str_replace($month_local, $month_en, $string));
  458. // If the date does not contain any year, we add the current year
  459. if (!preg_match('/[0-9]{4}/', $string)) {
  460. $date_str .= ' ' . date('Y');
  461. }
  462. // Add the Hour and minutes
  463. $date_str .= ' 00:00';
  464. $date = DateTime::createFromFormat('j F Y H:i', $date_str);
  465. return $date->getTimestamp();
  466. }
  467. /**
  468. * Remove the prefix of a date if it has one
  469. * @return the date without prefiux
  470. */
  471. private function removeDatePrefixes($string)
  472. {
  473. $string = str_replace($this->i8n('date-prefixes'), array(), $string);
  474. return $string;
  475. }
  476. /**
  477. * Remove the suffix of a relative date if it has one
  478. * @return the relative date without suffixes
  479. */
  480. private function removeRelativeDateSuffixes($string)
  481. {
  482. if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
  483. $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
  484. }
  485. return $string;
  486. }
  487. /**
  488. * Transforms a relative local date into a timestamp
  489. * @return int timestamp of the input date
  490. */
  491. private function relativeDateToTimestamp($str) {
  492. $date = new DateTime();
  493. // In case of update date, replace it by the regular relative date first word
  494. $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
  495. $str = $this->removeRelativeDateSuffixes($str);
  496. $search = $this->i8n('local-time-relative');
  497. $replace = array(
  498. '-',
  499. 'minute',
  500. 'hour',
  501. 'day',
  502. 'month',
  503. 'year',
  504. ''
  505. );
  506. $date->modify(str_replace($search, $replace, $str));
  507. return $date->getTimestamp();
  508. }
  509. /**
  510. * Returns the RSS Feed title according to the parameters
  511. * @return string the RSS feed Tiyle
  512. */
  513. public function getName(){
  514. switch($this->queriedContext) {
  515. case $this->i8n('context-keyword'):
  516. return $this->i8n('bridge-name') . ' - '. $this->i8n('title-keyword') .' : '. $this->getInput('q');
  517. break;
  518. case $this->i8n('context-group'):
  519. $values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
  520. $group = array_search($this->getInput('group'), $values);
  521. return $this->i8n('bridge-name') . ' - '. $this->i8n('title-group'). ' : '. $group;
  522. break;
  523. default: // Return default value
  524. return static::NAME;
  525. }
  526. }
  527. /**
  528. * This is some "localisation" function that returns the needed content using
  529. * the "$lang" class variable in the local class
  530. * @return various the local content needed
  531. */
  532. public function i8n($key)
  533. {
  534. if (array_key_exists($key, $this->lang)) {
  535. return $this->lang[$key];
  536. } else {
  537. return null;
  538. }
  539. }
  540. }