DealabsBridge.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  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. protected 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. protected 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. protected 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. )
  226. );
  227. // Deal Date CSS Selector
  228. $selectorDate = implode(
  229. ' ', /* Notice this is a space! */
  230. array(
  231. 'size--all-s',
  232. 'flex',
  233. 'flex--justify-e',
  234. 'flex--grow-1',
  235. )
  236. );
  237. // If there is no results, we don't parse the content because it display some random deals
  238. $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
  239. if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
  240. $this->items = array();
  241. } else {
  242. foreach ($list as $deal) {
  243. $item = array();
  244. $item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
  245. $item['title'] = $deal->find('a[class*='. $selectorLink .']', 0
  246. )->plaintext;
  247. $item['author'] = $deal->find('span.thread-username', 0)->plaintext;
  248. $item['content'] = '<table><tr><td><a href="'
  249. . $deal->find(
  250. 'a[class*='. $selectorImageLink .']', 0)->href
  251. . '"><img src="'
  252. . $this->getImage($deal)
  253. . '"/></td><td><h2><a href="'
  254. . $deal->find('a[class*='. $selectorLink .']', 0)->href
  255. . '">'
  256. . $deal->find('a[class*='. $selectorLink .']', 0)->innertext
  257. . '</a></h2>'
  258. . $this->getPrice($deal)
  259. . $this->getDiscount($deal)
  260. . $this->getShipsFrom($deal)
  261. . $this->getShippingCost($deal)
  262. . $this->GetSource($deal)
  263. . $deal->find('div[class*='. $selectorDescription .']', 0)->innertext
  264. . '</td><td>'
  265. . $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
  266. . '</td></table>';
  267. $dealDateDiv = $deal->find('div[class*='. $selectorDate .']', 0)
  268. ->find('span[class=hide--toW3]');
  269. $itemDate = end($dealDateDiv)->plaintext;
  270. // In case of a Local deal, there is no date, but we can use
  271. // this case for other reason (like date not in the last field)
  272. if ($this->contains($itemDate, $this->i8n('localdeal'))) {
  273. $item['timestamp'] = time();
  274. } else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
  275. $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
  276. } else {
  277. $item['timestamp'] = $this->parseDate($itemDate);
  278. }
  279. $this->items[] = $item;
  280. }
  281. }
  282. }
  283. /**
  284. * Check if the string $str contains any of the string of the array $arr
  285. * @return boolean true if the string matched anything otherwise false
  286. */
  287. private function contains($str, array $arr)
  288. {
  289. foreach ($arr as $a) {
  290. if (stripos($str, $a) !== false) {
  291. return true;
  292. }
  293. }
  294. return false;
  295. }
  296. /**
  297. * Get the Price from a Deal if it exists
  298. * @return string String of the deal price
  299. */
  300. private function getPrice($deal)
  301. {
  302. if ($deal->find(
  303. 'span[class*=thread-price]', 0) != null) {
  304. return '<div>'.$this->i8n('price') .' : '
  305. . $deal->find(
  306. 'span[class*=thread-price]', 0
  307. )->plaintext
  308. . '</div>';
  309. } else {
  310. return '';
  311. }
  312. }
  313. /**
  314. * Get the Shipping costs from a Deal if it exists
  315. * @return string String of the deal shipping Cost
  316. */
  317. private function getShippingCost($deal)
  318. {
  319. if ($deal->find('span[class*=cept-shipping-price]', 0) != null) {
  320. if ($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
  321. return '<div>'. $this->i8n('shipping') .' : '
  322. . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
  323. . '</div>';
  324. } else {
  325. return '<div>'. $this->i8n('shipping') .' : '
  326. . $deal->find('span[class*=cept-shipping-price]', 0)->innertext
  327. . '</div>';
  328. }
  329. } else {
  330. return '';
  331. }
  332. }
  333. /**
  334. * Get the source of a Deal if it exists
  335. * @return string String of the deal source
  336. */
  337. private function GetSource($deal)
  338. {
  339. if ($deal->find('a[class=text--color-greyShade]', 0) != null) {
  340. return '<div>'. $this->i8n('origin') .' : '
  341. . $deal->find('a[class=text--color-greyShade]', 0)->outertext
  342. . '</div>';
  343. } else {
  344. return '';
  345. }
  346. }
  347. /**
  348. * Get the original Price and discout from a Deal if it exists
  349. * @return string String of the deal original price and discount
  350. */
  351. private function getDiscount($deal)
  352. {
  353. if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
  354. $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
  355. if ($discountHtml != null) {
  356. $discount = $discountHtml->plaintext;
  357. } else {
  358. $discount = '';
  359. }
  360. return '<div>'. $this->i8n('discount') .' : <span style="text-decoration: line-through;">'
  361. . $deal->find(
  362. 'span[class*=mute--text text--lineThrough]', 0
  363. )->plaintext
  364. . '</span>&nbsp;'
  365. . $discount
  366. . '</div>';
  367. } else {
  368. return '';
  369. }
  370. }
  371. /**
  372. * Get the Picture URL from a Deal if it exists
  373. * @return string String of the deal Picture URL
  374. */
  375. private function getImage($deal)
  376. {
  377. $selectorLazy = implode(
  378. ' ', /* Notice this is a space! */
  379. array(
  380. 'thread-image',
  381. 'width--all-auto',
  382. 'height--all-auto',
  383. 'imgFrame-img',
  384. 'cept-thread-img',
  385. 'img--dummy',
  386. 'js-lazy-img'
  387. )
  388. );
  389. $selectorPlain = implode(
  390. ' ', /* Notice this is a space! */
  391. array(
  392. 'thread-image',
  393. 'width--all-auto',
  394. 'height--all-auto',
  395. 'imgFrame-img',
  396. 'cept-thread-img'
  397. )
  398. );
  399. if ($deal->find('img[class='. $selectorLazy .']', 0) != null) {
  400. return json_decode(
  401. html_entity_decode(
  402. $deal->find('img[class='. $selectorLazy .']', 0)
  403. ->getAttribute('data-lazy-img')))->{'src'};
  404. } else {
  405. return $deal->find('img[class*='. $selectorPlain .']', 0 )->src;
  406. }
  407. }
  408. /**
  409. * Get the originating country from a Deal if it exists
  410. * @return string String of the deal originating country
  411. */
  412. private function getShipsFrom($deal)
  413. {
  414. $selector = implode(
  415. ' ', /* Notice this is a space! */
  416. array(
  417. 'meta-ribbon',
  418. 'overflow--wrap-off',
  419. 'space--l-3',
  420. 'text--color-greyShade'
  421. )
  422. );
  423. if ($deal->find('span[class='. $selector .']', 0) != null) {
  424. return '<div>'
  425. . $deal->find('span[class='. $selector .']', 0)->children(2)->plaintext
  426. . '</div>';
  427. } else {
  428. return '';
  429. }
  430. }
  431. /**
  432. * Transforms a local date into a timestamp
  433. * @return int timestamp of the input date
  434. */
  435. private function parseDate($string)
  436. {
  437. $month_local = $this->i8n('local-months');
  438. $month_en = array(
  439. 'January',
  440. 'February',
  441. 'March',
  442. 'April',
  443. 'May',
  444. 'June',
  445. 'July',
  446. 'August',
  447. 'September',
  448. 'October',
  449. 'November',
  450. 'December'
  451. );
  452. // A date can be prfixed with some words, we remove theme
  453. $string = $this->removeDatePrefixes($string);
  454. // We translate the local months name in the english one
  455. $date_str = trim(str_replace($month_local, $month_en, $string));
  456. // If the date does not contain any year, we add the current year
  457. if (!preg_match('/[0-9]{4}/', $string)) {
  458. $date_str .= ' ' . date('Y');
  459. }
  460. // Add the Hour and minutes
  461. $date_str .= ' 00:00';
  462. $date = DateTime::createFromFormat('j F Y H:i', $date_str);
  463. return $date->getTimestamp();
  464. }
  465. /**
  466. * Remove the prefix of a date if it has one
  467. * @return the date without prefiux
  468. */
  469. private function removeDatePrefixes($string)
  470. {
  471. $string = str_replace($this->i8n('date-prefixes'), array(), $string);
  472. return $string;
  473. }
  474. /**
  475. * Remove the suffix of a relative date if it has one
  476. * @return the relative date without suffixes
  477. */
  478. private function removeRelativeDateSuffixes($string)
  479. {
  480. if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
  481. $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
  482. }
  483. return $string;
  484. }
  485. /**
  486. * Transforms a relative local date into a timestamp
  487. * @return int timestamp of the input date
  488. */
  489. private function relativeDateToTimestamp($str) {
  490. $date = new DateTime();
  491. // In case of update date, replace it by the regular relative date first word
  492. $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
  493. $str = $this->removeRelativeDateSuffixes($str);
  494. $search = $this->i8n('local-time-relative');
  495. $replace = array(
  496. '-',
  497. 'minute',
  498. 'hour',
  499. 'day',
  500. 'month',
  501. 'year',
  502. ''
  503. );
  504. $date->modify(str_replace($search, $replace, $str));
  505. return $date->getTimestamp();
  506. }
  507. /**
  508. * Returns the RSS Feed title according to the parameters
  509. * @return string the RSS feed Tiyle
  510. */
  511. public function getName(){
  512. switch($this->queriedContext) {
  513. case $this->i8n('context-keyword'):
  514. return $this->i8n('bridge-name') . ' - '. $this->i8n('title-keyword') .' : '. $this->getInput('q');
  515. break;
  516. case $this->i8n('context-group'):
  517. $values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
  518. $group = array_search($this->getInput('group'), $values);
  519. return $this->i8n('bridge-name') . ' - '. $this->i8n('title-group'). ' : '. $group;
  520. break;
  521. default: // Return default value
  522. return static::NAME;
  523. }
  524. }
  525. /**
  526. * This is some "localisation" function that returns the needed content using
  527. * the "$lang" class variable in the local class
  528. * @return various the local content needed
  529. */
  530. protected function i8n($key)
  531. {
  532. if (array_key_exists($key, $this->lang)) {
  533. return $this->lang[$key];
  534. } else {
  535. return null;
  536. }
  537. }
  538. }