41f07b6ad8
Browser-based feed selection layered on top of config/feeds.php: - manage.php (password + CSRF gated, session based): add subreddits (name / r/name / URL) and YouTube channels (@handle / UC id / URL, handle resolved to channel_id once, youtube.com only); enable/disable toggles; remove manual feeds; config feeds are hidden, not deleted. - New feeds are validated with one immediate fetch (bypassing the per-refresh host cap): HTTP 404 rejects the entry, 429 keeps it for a later refresh. YouTube labels adopt the real channel title. - feeds table gains origin (config|manual) and enabled columns, with a guarded ALTER migration for existing databases. - Only enabled feeds are refreshed and served: getItems/countItems filter by feed_url in enabled feeds, so deselected feeds' cached stories are hidden, not deleted; re-adding a URL re-enables it. - Dashboard header gets a Manage link; manage page styled with the existing design tokens; README documents the feature.
118 lines
3.5 KiB
PHP
118 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class FeedService
|
|
{
|
|
/**
|
|
* @param array<int, array<string, mixed>> $feeds
|
|
*/
|
|
public function __construct(
|
|
private FeedRepository $repository,
|
|
private FeedFetcher $fetcher,
|
|
private array $feeds,
|
|
private int $ttlSeconds,
|
|
private int $errorRetryTtlSeconds = 300,
|
|
private array $throttledHosts = [],
|
|
private int $throttledHostMax = 1,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Refresh every feed whose cached copy is stale. Failures are recorded and
|
|
* skipped so previously cached items remain available.
|
|
*
|
|
* @return array{attempted: int, succeeded: int, failed: int, new_items: int, errors: array<int, string>}
|
|
*/
|
|
public function refreshStale(): array
|
|
{
|
|
$this->repository->syncFeeds($this->feeds);
|
|
|
|
$report = [
|
|
'attempted' => 0,
|
|
'succeeded' => 0,
|
|
'failed' => 0,
|
|
'new_items' => 0,
|
|
'errors' => [],
|
|
];
|
|
|
|
$hostCounts = [];
|
|
$activeFeeds = $this->repository->enabledFeeds();
|
|
|
|
foreach ($this->repository->staleFeeds($activeFeeds, $this->ttlSeconds, $this->errorRetryTtlSeconds) as $feed) {
|
|
$url = (string) $feed['url'];
|
|
$host = (string) parse_url($url, PHP_URL_HOST);
|
|
|
|
if (in_array($host, $this->throttledHosts, true)) {
|
|
$seen = $hostCounts[$host] ?? 0;
|
|
if ($seen >= $this->throttledHostMax) {
|
|
// Leave it stale so a later request picks it up.
|
|
continue;
|
|
}
|
|
$hostCounts[$host] = $seen + 1;
|
|
}
|
|
|
|
$report['attempted']++;
|
|
|
|
try {
|
|
$result = $this->fetcher->fetch($feed);
|
|
$report['new_items'] += $this->repository->upsertItems($result['items']);
|
|
$this->repository->recordAttempt($url, true);
|
|
$report['succeeded']++;
|
|
} catch (\Throwable $e) {
|
|
$this->repository->recordAttempt($url, false, $e->getMessage());
|
|
$report['failed']++;
|
|
$report['errors'][] = ($feed['label'] ?? $url) . ': ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $report;
|
|
}
|
|
|
|
/**
|
|
* @return array{items: array<int, array<string, mixed>>, page: int, page_size: int, total: int, has_more: bool}
|
|
*/
|
|
public function page(?string $source, int $page, int $pageSize, ?string $since = null): array
|
|
{
|
|
$page = max(1, $page);
|
|
$pageSize = max(1, $pageSize);
|
|
$offset = ($page - 1) * $pageSize;
|
|
|
|
$items = $this->repository->getItems($pageSize, $offset, $source, $since);
|
|
$total = $this->repository->countItems($source, $since);
|
|
|
|
return [
|
|
'items' => $items,
|
|
'page' => $page,
|
|
'page_size' => $pageSize,
|
|
'total' => $total,
|
|
'has_more' => ($offset + count($items)) < $total,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function feedsStatus(): array
|
|
{
|
|
return $this->repository->feedsStatus();
|
|
}
|
|
|
|
/**
|
|
* Sources that currently have at least one enabled feed.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
public function sources(): array
|
|
{
|
|
$sources = [];
|
|
foreach ($this->repository->enabledFeeds() as $feed) {
|
|
$sources[(string) $feed['source']] = true;
|
|
}
|
|
|
|
return array_keys($sources);
|
|
}
|
|
}
|