Skip to content

Automatic feed channel icons feature for xExtension-YouTube #337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions xExtension-YouTube/configure.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,51 @@
declare(strict_types=1);
/** @var YouTubeExtension $this */
?>
<form action="<?php echo _url('extension', 'configure', 'e', urlencode($this->getName())); ?>" method="post">
<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
<form action="<?= _url('extension', 'configure', 'e', urlencode($this->getName())) ?>" method="post">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<div class="form-group">

<label class="group-name" for="yt_height"><?php echo _t('ext.yt_videos.height'); ?></label>
<label class="group-name" for="yt_height"><?= _t('ext.yt_videos.height') ?></label>
<div class="group-controls">
<input type="number" id="yt_height" name="yt_height" value="<?php echo $this->getHeight(); ?>" min="50" data-leave-validation="1">
<input type="number" id="yt_height" name="yt_height" value="<?= $this->getHeight() ?>" min="50" data-leave-validation="1">
</div>

<label class="group-name" for="yt_width"><?php echo _t('ext.yt_videos.width'); ?></label>
<label class="group-name" for="yt_width"><?= _t('ext.yt_videos.width') ?></label>
<div class="group-controls">
<input type="number" id="yt_width" name="yt_width" value="<?php echo $this->getWidth(); ?>" min="100" data-leave-validation="1">
<input type="number" id="yt_width" name="yt_width" value="<?= $this->getWidth() ?>" min="100" data-leave-validation="1">
</div>

<div class="group-controls">
<label class="checkbox" for="yt_show_content">
<input type="checkbox" id="yt_show_content" name="yt_show_content" value="1" <?php echo $this->isShowContent() ? 'checked' : ''; ?>>
<?php echo _t('ext.yt_videos.show_content'); ?>
<input type="checkbox" id="yt_show_content" name="yt_show_content" value="1" <?= $this->isShowContent() ? 'checked' : '' ?>>
<?= _t('ext.yt_videos.show_content') ?>
</label>
</div>

<div class="group-controls">
<label class="checkbox" for="yt_download_channel_icons">
<input type="checkbox" id="yt_download_channel_icons" name="yt_download_channel_icons" value="1" <?= $this->isDownloadIcons() ? 'checked' : '' ?>>
<?= _t('ext.yt_videos.download_channel_icons') ?>
</label>
</div>

<div class="group-controls">
<label class="checkbox" for="yt_nocookie">
<input type="checkbox" id="yt_nocookie" name="yt_nocookie" value="1" <?php echo $this->isUseNoCookieDomain() ? 'checked' : ''; ?>>
<?php echo _t('ext.yt_videos.use_nocookie'); ?>
<input type="checkbox" id="yt_nocookie" name="yt_nocookie" value="1" <?= $this->isUseNoCookieDomain() ? 'checked' : '' ?>>
<?= _t('ext.yt_videos.use_nocookie') ?>
</label>
</div>

<div class="group-controls">
<button id="yt_action_btn" name="yt_action_btn" value="iconFetchFinish" type="submit" class="btn" disabled="disabled" title="<?= _t('gen.js.should_be_activated') ?>"><?= _t('ext.yt_videos.fetch_channel_icons') ?></button>
<button id="yt_action_btn" name="yt_action_btn" value="resetIcons" type="submit" class="btn"><?= _t('ext.yt_videos.reset_channel_icons') ?></button>
</div>
</div>

<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
</div>
</div>
</form>

<p>
<?php echo _t('ext.yt_videos.updates'); ?>
<a href="https://github.com/kevinpapst/freshrss-youtube" target="_blank">GitHub</a>.
</p>
191 changes: 189 additions & 2 deletions xExtension-YouTube/extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
/**
* Class YouTubeExtension
*
* Latest version can be found at https://github.com/kevinpapst/freshrss-youtube
*
* @author Kevin Papst
*/
final class YouTubeExtension extends Minz_Extension
Expand All @@ -21,6 +19,10 @@
* Whether we display the original feed content
*/
private bool $showContent = false;
/**
* Wheter channel icons should be automatically downloaded and set for feeds
*/
private bool $downloadIcons = false;
/**
* Switch to enable the Youtube No-Cookie domain
*/
Expand All @@ -34,9 +36,165 @@
{
$this->registerHook('entry_before_display', [$this, 'embedYouTubeVideo']);
$this->registerHook('check_url_before_add', [self::class, 'convertYoutubeFeedUrl']);
$this->registerHook('custom_favicon_hash', [$this, 'iconHashParams']);
$this->registerHook('feed_before_insert', [$this, 'setIconForFeed']);
if (Minz_Request::controllerName() === 'extension') {
$this->registerHook('js_vars', [self::class, 'jsVars']);
Minz_View::appendScript($this->getFileUrl('fetchIcons.js'));
}
$this->registerTranslates();
}

public static function jsVars(array $vars): array {

Check failure on line 48 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::jsVars() return type has no value type specified in iterable type array.

Check failure on line 48 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::jsVars() has parameter $vars with no value type specified in iterable type array.
$vars['yt_i18n'] = [
'fetching_icons' => _t('ext.yt_videos.fetching_icons'),
];
return $vars;
}

public function iconHashParams(FreshRSS_Feed $feed): ?string {
if ($feed->customFaviconExt() !== $this->getName()) {

Check failure on line 56 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Call to an undefined method FreshRSS_Feed::customFaviconExt().
return null;
}
return 'yt' . $feed->website() . $feed->proxyParam();

Check failure on line 59 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Call to an undefined method FreshRSS_Feed::proxyParam().

Check failure on line 59 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Binary operation "." between non-falsy-string and mixed results in an error.
}

public function ajaxGetYtFeeds() {

Check failure on line 62 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::ajaxGetYtFeeds() has no return type specified.
$feedDAO = FreshRSS_Factory::createFeedDao();

Check failure on line 63 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::ajaxGetYtFeeds() throws checked exception Minz_PDOConnectionException but it's missing from the PHPDoc @throws tag.

Check failure on line 63 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::ajaxGetYtFeeds() throws checked exception Minz_ConfigurationNamespaceException but it's missing from the PHPDoc @throws tag.
$ids = $feedDAO->listFeedsIds();

$feeds = [];

foreach ($ids as $feedId) {
$feed = $feedDAO->searchById($feedId);
if ($feed === null) {
continue;
}
if (str_starts_with($feed->website(), 'https://www.youtube.com/')) {
$feeds[] = [
'id' => $feed->id(),
'title' => $feed->name(true),
];
}
}

header('Content-Type: application/json; charset=UTF-8');
exit(json_encode($feeds));
}

public function ajaxFetchIcon() {

Check failure on line 85 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::ajaxFetchIcon() has no return type specified.
$feedDAO = FreshRSS_Factory::createFeedDao();

Check failure on line 86 in xExtension-YouTube/extension.php

View workflow job for this annotation

GitHub Actions / tests

Method YouTubeExtension::ajaxFetchIcon() throws checked exception Minz_ConfigurationNamespaceException but it's missing from the PHPDoc @throws tag.

$feed = $feedDAO->searchById(Minz_Request::paramInt('id'));
if ($feed === null) {
Minz_Error::error(404);
return;
}
$this->setIconForFeed($feed, setValues: true);

exit('OK');
}

public function resetAllIcons() {
$feedDAO = FreshRSS_Factory::createFeedDao();
$ids = $feedDAO->listFeedsIds();

foreach ($ids as $feedId) {
$feed = $feedDAO->searchById($feedId);
if ($feed === null) {
continue;
}
if ($feed->customFaviconExt() === $this->getName()) {
$v = [];
try {
$feed->resetCustomFavicon(values: $v);
} catch (FreshRSS_Feed_Exception $_) {
$this->warnLog('failed to reset favicon for feed "' . $feed->name(true) . '": feed error');
}
}
}
}

public function warnLog(string $s) {
Minz_Log::warning('[' . $this->getName() . '] ' . $s);
}
public function debugLog(string $s) {
Minz_Log::debug('[' . $this->getName() . '] ' . $s);
}

public function setIconForFeed(FreshRSS_Feed $feed, bool $setValues = false): FreshRSS_Feed {
$this->loadConfigValues();

if (!($this->downloadIcons || $setValues) || !str_starts_with($feed->website(), 'https://www.youtube.com/')) {
return $feed;
}

// Return early if the icon had already been downloaded before
$v = $setValues ? [] : null;
$oldAttributes = $feed->attributes();
try {
$path = $feed->setCustomFavicon(extName: $this->getName(), disallowDelete: true, values: $v);
if ($path === null) {
$feed->_attributes($oldAttributes);
return $feed;
} elseif (file_exists($path)) {
$this->debugLog('icon had already been downloaded before for feed "' . $feed->name(true) . '" - returning early!');
return $feed;
}
} catch (FreshRSS_Feed_Exception $_) {
$this->warnLog('failed to set custom favicon for feed "' . $feed->name(true) . '": feed error');
$feed->_attributes($oldAttributes);
return $feed;
}

$feed->_attributes($oldAttributes);
$this->debugLog('downloading icon for feed "' . $feed->name(true) . '"');

$url = $feed->website();
/** @var $curlOptions array<int,int|bool|string> */
$curlOptions = $feed->attributeArray('curl_params') ?? [];
$html = downloadHttp($url, $curlOptions);

$dom = new DOMDocument();

if ($html == '' || !@$dom->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
$this->warnLog('fail while downloading icon for feed "' . $feed->name(true) . '": failed to load HTML');
return $feed;
}

$xpath = new DOMXPath($dom);
$iconElem = $xpath->query('//meta[@name="twitter:image"]');

if ($iconElem === false) {
$this->warnLog('fail while downloading icon for feed "' . $feed->name(true) . '": icon URL couldn\'t be found');
return $feed;
}

$iconUrl = $iconElem->item(0) !== null ? $iconElem->item(0)->getAttribute('content') : null;
if ($iconUrl === null) {
$this->warnLog('fail while downloading icon for feed "' . $feed->name(true) . '": icon URL couldn\'t be found');
return $feed;
}

$contents = downloadHttp($iconUrl, $curlOptions);
if ($contents == '') {
$this->warnLog('fail while downloading icon for feed "' . $feed->name(true) . '": empty contents');
return $feed;
}

try {
$feed->setCustomFavicon($contents, extName: $this->getName(), disallowDelete: true, values: $v, overrideCustomIcon: true);
} catch (FreshRSS_UnsupportedImageFormat_Exception $_) {
$this->warnLog('failed to set custom favicon for feed "' . $feed->name(true) . '": unsupported image format');
return $feed;
} catch (FreshRSS_Feed_Exception $_) {
$this->warnLog('failed to set custom favicon for feed "' . $feed->name(true) . '": feed error');
return $feed;
}

return $feed;
}

public static function convertYoutubeFeedUrl(string $url): string
{
$matches = [];
Expand Down Expand Up @@ -78,6 +236,11 @@
$this->showContent = $showContent;
}

$downloadIcons = FreshRSS_Context::userConf()->attributeBool('yt_download_channel_icons');
if ($downloadIcons !== null) {
$this->downloadIcons = $downloadIcons;
}

$noCookie = FreshRSS_Context::userConf()->attributeBool('yt_nocookie');
if ($noCookie !== null) {
$this->useNoCookie = $noCookie;
Expand Down Expand Up @@ -111,6 +274,15 @@
return $this->showContent;
}

/**
* Returns whether the automatic icon download option is enabled.
* You have to call loadConfigValues() before this one, otherwise you get default values.
*/
public function isDownloadIcons(): bool
{
return $this->downloadIcons;
}

/**
* Returns if this extension should use youtube-nocookie.com instead of youtube.com.
* You have to call loadConfigValues() before this one, otherwise you get default values.
Expand Down Expand Up @@ -252,9 +424,24 @@
$this->registerTranslates();

if (Minz_Request::isPost()) {
switch (Minz_Request::paramString('yt_action_btn')) {
case 'ajaxGetYtFeeds':
$this->ajaxGetYtFeeds();
return;
case 'ajaxFetchIcon':
$this->ajaxFetchIcon();
return;
case 'iconFetchFinish':
Minz_Request::good(_t('ext.yt_videos.finished_fetching_icons'), ['c' => 'extension']);
break;
case 'resetIcons':
$this->resetAllIcons();
break;
}
FreshRSS_Context::userConf()->_attribute('yt_player_height', Minz_Request::paramInt('yt_height'));
FreshRSS_Context::userConf()->_attribute('yt_player_width', Minz_Request::paramInt('yt_width'));
FreshRSS_Context::userConf()->_attribute('yt_show_content', Minz_Request::paramBoolean('yt_show_content'));
FreshRSS_Context::userConf()->_attribute('yt_download_channel_icons', Minz_Request::paramBoolean('yt_download_channel_icons'));
FreshRSS_Context::userConf()->_attribute('yt_nocookie', Minz_Request::paramInt('yt_nocookie'));
FreshRSS_Context::userConf()->save();
}
Expand Down
6 changes: 5 additions & 1 deletion xExtension-YouTube/i18n/de/ext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
'yt_videos' => array(
'height' => 'Höhe des Players',
'width' => 'Breite des Players',
'updates' => 'Die neueste Version des Plugins findest Du bei',
'show_content' => 'Zeige den Inhalt des Feeds an',
'download_channel_icons' => 'Automatically use the channels’ icons',
'fetch_channel_icons' => 'Fetch icons of all channels',
'reset_channel_icons' => 'Reset icons of all channels',
'fetching_icons' => 'Fetching icons',
'finished_fetching_icons' => 'Finished fetching icons.',
'use_nocookie' => 'Verwende die Cookie-freie Domain www.youtube-nocookie.com',
),
);
6 changes: 5 additions & 1 deletion xExtension-YouTube/i18n/en/ext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
'yt_videos' => array(
'height' => 'Player height',
'width' => 'Player width',
'updates' => 'You can find the latest extension version at',
'show_content' => 'Display the feeds content',
'download_channel_icons' => 'Automatically use the channels’ icons',
'fetch_channel_icons' => 'Fetch icons of all channels',
'reset_channel_icons' => 'Reset icons of all channels',
'fetching_icons' => 'Fetching icons',
'finished_fetching_icons' => 'Finished fetching icons.',
'use_nocookie' => 'Use the cookie-free domain www.youtube-nocookie.com',
),
);
6 changes: 5 additions & 1 deletion xExtension-YouTube/i18n/fr/ext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
'yt_videos' => array(
'height' => 'Hauteur du lecteur',
'width' => 'Largeur du lecteur',
'updates' => 'Vous pouvez trouver la dernière mise à jour de l’extension sur ',
'show_content' => 'Afficher le contenu du flux',
'download_channel_icons' => 'Utiliser automatiquement les icônes des chaînes',
'fetch_channel_icons' => 'Fetch icons of all channels',
'reset_channel_icons' => 'Reset icons of all channels',
'fetching_icons' => 'Fetching icons',
'finished_fetching_icons' => 'Finished fetching icons.',
'use_nocookie' => 'Utiliser le domaine www.youtube-nocookie.com pour éviter les cookies',
),
);
16 changes: 16 additions & 0 deletions xExtension-YouTube/i18n/pl/ext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

return array(
'yt_videos' => array(
'height' => 'Wysokość odtwarzacza wideo',
'width' => 'Szerokość odtwarzacza wideo',
'show_content' => 'Wyświetlaj zawartość kanałów',
'download_channel_icons' => 'Automatycznie używaj ikon kanałów',
'fetch_channel_icons' => 'Pobierz ikony wszystkich kanałów',
'reset_channel_icons' => 'Przywróć domyślne ikony wszystkich kanałów',
'fetching_icons' => 'Pobieranie ikon',
'finished_fetching_icons' => 'Zakończono pobieranie ikon.',
'use_nocookie' => 'Używaj domeny bez ciasteczek www.youtube-nocookie.com',
),
);

6 changes: 5 additions & 1 deletion xExtension-YouTube/i18n/tr/ext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
'yt_videos' => array(
'height' => 'Oynatıcı yükseklik',
'width' => 'Oynatıcı genişlik',
'updates' => 'En son uzantı sürümünü şu adreste bulabilirsiniz:',
'show_content' => 'Yayın içeriğini görüntüle',
'download_channel_icons' => 'Automatically use the channels’ icons',
'fetch_channel_icons' => 'Fetch icons of all channels',
'reset_channel_icons' => 'Reset icons of all channels',
'fetching_icons' => 'Fetching icons',
'finished_fetching_icons' => 'Finished fetching icons.',
'use_nocookie' => 'Çerezsiz olan "www.youtube-nocookie.com" alan adını kullanın',
),
);
Loading