mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2024-12-23 21:00:24 +03:00
281 lines
9.2 KiB
PHP
281 lines
9.2 KiB
PHP
<?php
|
|
|
|
class AssociatedPressNewsBridge extends BridgeAbstract
|
|
{
|
|
const NAME = 'Associated Press News Bridge';
|
|
const URI = 'https://apnews.com/';
|
|
const DESCRIPTION = 'Returns newest articles by topic';
|
|
const MAINTAINER = 'VerifiedJoseph';
|
|
const PARAMETERS = [
|
|
'Standard Topics' => [
|
|
'topic' => [
|
|
'name' => 'Topic',
|
|
'type' => 'list',
|
|
'values' => [
|
|
'AP Top News' => 'apf-topnews',
|
|
'Sports' => 'apf-sports',
|
|
'Entertainment' => 'apf-entertainment',
|
|
'Oddities' => 'apf-oddities',
|
|
'Travel' => 'apf-Travel',
|
|
'Technology' => 'apf-technology',
|
|
'Lifestyle' => 'apf-lifestyle',
|
|
'Business' => 'apf-business',
|
|
'U.S. News' => 'apf-usnews',
|
|
'Health' => 'apf-Health',
|
|
'Science' => 'apf-science',
|
|
'World News' => 'apf-WorldNews',
|
|
'Politics' => 'apf-politics',
|
|
'Religion' => 'apf-religion',
|
|
'Photo Galleries' => 'PhotoGalleries',
|
|
'Fact Checks' => 'APFactCheck',
|
|
'Videos' => 'apf-videos',
|
|
],
|
|
'defaultValue' => 'apf-topnews',
|
|
],
|
|
],
|
|
'Custom Topic' => [
|
|
'topic' => [
|
|
'name' => 'Topic',
|
|
'type' => 'text',
|
|
'required' => true,
|
|
'exampleValue' => 'europe'
|
|
],
|
|
]
|
|
];
|
|
|
|
const CACHE_TIMEOUT = 900; // 15 mins
|
|
|
|
private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
|
|
private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
|
|
private $feedName = '';
|
|
|
|
public function detectParameters($url)
|
|
{
|
|
$params = [];
|
|
|
|
if (preg_match($this->detectParamRegex, $url, $matches) > 0) {
|
|
$params['topic'] = $matches[1];
|
|
$params['context'] = 'Custom Topic';
|
|
return $params;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function collectData()
|
|
{
|
|
switch ($this->getInput('topic')) {
|
|
case 'Podcasts':
|
|
returnClientError('Podcasts topic feed is not supported');
|
|
break;
|
|
case 'PressReleases':
|
|
returnClientError('PressReleases topic feed is not supported');
|
|
break;
|
|
default:
|
|
$this->collectCardData();
|
|
}
|
|
}
|
|
|
|
public function getURI()
|
|
{
|
|
if (!is_null($this->getInput('topic'))) {
|
|
return self::URI . $this->getInput('topic');
|
|
}
|
|
|
|
return parent::getURI();
|
|
}
|
|
|
|
public function getName()
|
|
{
|
|
if (!empty($this->feedName)) {
|
|
return $this->feedName . ' - Associated Press';
|
|
}
|
|
|
|
return parent::getName();
|
|
}
|
|
|
|
private function getTagURI()
|
|
{
|
|
if (!is_null($this->getInput('topic'))) {
|
|
return $this->tagEndpoint . $this->getInput('topic');
|
|
}
|
|
|
|
return parent::getURI();
|
|
}
|
|
|
|
private function collectCardData()
|
|
{
|
|
$json = getContents($this->getTagURI())
|
|
or returnServerError('Could not request: ' . $this->getTagURI());
|
|
|
|
$tagContents = json_decode($json, true);
|
|
|
|
if (empty($tagContents['tagObjs'])) {
|
|
returnClientError('Topic not found: ' . $this->getInput('topic'));
|
|
}
|
|
|
|
$this->feedName = $tagContents['tagObjs'][0]['name'];
|
|
|
|
foreach ($tagContents['cards'] as $card) {
|
|
$item = [];
|
|
|
|
// skip hub peeks & Notifications
|
|
if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
|
|
continue;
|
|
}
|
|
|
|
$storyContent = $card['contents'][0];
|
|
|
|
switch ($storyContent['contentType']) {
|
|
case 'web': // Skip link only content
|
|
continue 2;
|
|
|
|
case 'video':
|
|
$html = $this->processVideo($storyContent);
|
|
|
|
$item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
|
|
. $storyContent['media'][0]['id'] . '/800.jpeg';
|
|
break;
|
|
default:
|
|
if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML
|
|
continue 2;
|
|
}
|
|
|
|
$html = defaultLinkTo($storyContent['storyHTML'], self::URI);
|
|
$html = str_get_html($html);
|
|
|
|
$this->processMediaPlaceholders($html, $storyContent['id']);
|
|
$this->processHubLinks($html, $storyContent);
|
|
$this->processIframes($html);
|
|
|
|
if (!is_null($storyContent['leadPhotoId'])) {
|
|
$leadPhotoUrl = sprintf('https://storage.googleapis.com/afs-prod/media/%s/800.jpeg', $storyContent['leadPhotoId']);
|
|
$leadPhotoImageTag = sprintf('<img src="%s">', $leadPhotoUrl);
|
|
// Move the image to the beginning of the content
|
|
$html = $leadPhotoImageTag . $html;
|
|
// Explicitly not adding it to the item's enclosures!
|
|
}
|
|
}
|
|
|
|
$item['title'] = $card['contents'][0]['headline'];
|
|
$item['uri'] = self::URI . $card['shortId'];
|
|
|
|
if ($card['contents'][0]['localLinkUrl']) {
|
|
$item['uri'] = $card['contents'][0]['localLinkUrl'];
|
|
}
|
|
|
|
$item['timestamp'] = $storyContent['published'];
|
|
|
|
if (is_null($storyContent['bylines']) === false) {
|
|
// Remove 'By' from the bylines
|
|
if (substr($storyContent['bylines'], 0, 2) == 'By') {
|
|
$item['author'] = ltrim($storyContent['bylines'], 'By ');
|
|
} else {
|
|
$item['author'] = $storyContent['bylines'];
|
|
}
|
|
}
|
|
|
|
$item['content'] = $html;
|
|
|
|
foreach ($storyContent['tagObjs'] as $tag) {
|
|
$item['categories'][] = $tag['name'];
|
|
}
|
|
|
|
$this->items[] = $item;
|
|
|
|
if (count($this->items) >= 15) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function processMediaPlaceholders($html, $id)
|
|
{
|
|
if ($html->find('div.media-placeholder', 0)) {
|
|
// Fetch page content
|
|
$json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id);
|
|
$storyContent = json_decode($json, true);
|
|
|
|
foreach ($html->find('div.media-placeholder') as $div) {
|
|
$key = array_search($div->id, $storyContent['mediumIds']);
|
|
|
|
if (!isset($storyContent['media'][$key])) {
|
|
continue;
|
|
}
|
|
|
|
$media = $storyContent['media'][$key];
|
|
|
|
if ($media['type'] === 'Photo') {
|
|
$mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension'];
|
|
$mediaCaption = $media['caption'];
|
|
|
|
$div->outertext = <<<EOD
|
|
<figure><img loading="lazy" src="{$mediaUrl}"/><figcaption>{$mediaCaption}</figcaption></figure>
|
|
EOD;
|
|
}
|
|
|
|
if ($media['type'] === 'YouTube') {
|
|
$div->outertext = <<<EOD
|
|
<iframe src="https://www.youtube.com/embed/{$media['externalId']}" width="560" height="315">
|
|
</iframe>
|
|
EOD;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Create full coverage links (HubLinks)
|
|
*/
|
|
private function processHubLinks($html, $storyContent)
|
|
{
|
|
if (!empty($storyContent['richEmbeds'])) {
|
|
foreach ($storyContent['richEmbeds'] as $embed) {
|
|
if ($embed['type'] === 'Hub Link') {
|
|
$url = self::URI . $embed['tag']['id'];
|
|
$div = $html->find('div[id=' . $embed['id'] . ']', 0);
|
|
|
|
if ($div) {
|
|
$div->outertext = <<<EOD
|
|
<p><a href="{$url}">{$embed['calloutText']} {$embed['displayName']}</a></p>
|
|
EOD;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function processVideo($storyContent)
|
|
{
|
|
$video = $storyContent['media'][0];
|
|
|
|
if ($video['type'] === 'YouTube') {
|
|
$url = 'https://www.youtube.com/embed/' . $video['externalId'];
|
|
$html = <<<EOD
|
|
<iframe width="560" height="315" src="{$url}" frameborder="0" allowfullscreen></iframe>
|
|
EOD;
|
|
} else {
|
|
$html = <<<EOD
|
|
<video controls poster="https://storage.googleapis.com/afs-prod/media/{$video['id']}/800.jpeg" preload="none">
|
|
<source src="{$video['gcsBaseUrl']} {$video['videoRenderedSizes'][0]} {$video['videoFileExtension']}" type="video/mp4">
|
|
</video>
|
|
EOD;
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
// Remove datawrapper.dwcdn.net iframes and related javaScript
|
|
private function processIframes($html)
|
|
{
|
|
foreach ($html->find('iframe') as $index => $iframe) {
|
|
if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) {
|
|
$iframe->outertext = '';
|
|
|
|
if ($html->find('script', $index)) {
|
|
$html->find('script', $index)->outertext = '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|