Done
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ArticleSelector
|
||||
{
|
||||
public function forCategory(Category $category, int $limit = 10): Collection
|
||||
{
|
||||
$feeds = $category->feeds()->with([
|
||||
'articles' => fn ($q) => $q->orderByDesc('published_at')->limit($limit),
|
||||
])->get();
|
||||
|
||||
// Round robin: take article[0] from every feed, then article[1] from every feed, etc.
|
||||
$buckets = $feeds->map(fn ($feed) => $feed->articles->values());
|
||||
$maxDepth = $buckets->max(fn ($b) => $b->count()) ?? 0;
|
||||
|
||||
$selected = collect();
|
||||
for ($i = 0; $i < $maxDepth && $selected->count() < $limit; $i++) {
|
||||
foreach ($buckets as $bucket) {
|
||||
if ($selected->count() >= $limit) break;
|
||||
if ($bucket->has($i)) $selected->push($bucket[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
return $selected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use SimplePie\SimplePie;
|
||||
|
||||
class FeedFetcher
|
||||
{
|
||||
public function __construct(private OgImageResolver $ogResolver) {}
|
||||
public function fetch(Feed $feed): void
|
||||
{
|
||||
$simplepie = new SimplePie();
|
||||
$simplepie->set_feed_url($feed->url);
|
||||
$simplepie->enable_cache(false); // you're doing your own caching in the DB
|
||||
$simplepie->init();
|
||||
|
||||
if ($simplepie->error()) {
|
||||
report(new \RuntimeException("Feed fetch failed for {$feed->url}: {$simplepie->error()}"));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($simplepie->get_items() as $item) {
|
||||
$imageUrl = $this->extractImage($item);
|
||||
|
||||
$attributes = [
|
||||
'title' => strip_tags($item->get_title()),
|
||||
'description' => strip_tags($item->get_description() ?? ''),
|
||||
'link' => $item->get_permalink(),
|
||||
'author' => $item->get_author()?->get_name(),
|
||||
'published_at' => $item->get_date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
if ($imageUrl) {
|
||||
$attributes['image_url'] = $imageUrl;
|
||||
}
|
||||
|
||||
$article = Article::updateOrCreate(
|
||||
['feed_id' => $feed->id, 'guid' => $item->get_id()],
|
||||
$attributes
|
||||
);
|
||||
|
||||
if (! $article->image_url && ! $article->image_checked_at) {
|
||||
$og = $this->ogResolver->resolve($article->link);
|
||||
$article->update([
|
||||
'image_url' => $og,
|
||||
'image_checked_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extractImage($item): ?string
|
||||
{
|
||||
$mediaContents = $item->get_item_tags('http://search.yahoo.com/mrss/', 'content');
|
||||
|
||||
if (empty($mediaContents)) {
|
||||
$enclosure = $item->get_enclosure();
|
||||
$link = $enclosure?->get_thumbnail() ?? $enclosure?->get_link();
|
||||
return $link ? html_entity_decode($link) : null;
|
||||
}
|
||||
|
||||
usort($mediaContents, fn ($a, $b) =>
|
||||
(int) ($b['attribs']['']['width'] ?? 0) <=> (int) ($a['attribs']['']['width'] ?? 0)
|
||||
);
|
||||
|
||||
return $mediaContents[0]['attribs']['']['url'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class OgImageResolver
|
||||
{
|
||||
public function resolve(string $pageUrl): ?string
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'User-Agent' => 'Mozilla/5.0 (compatible; DredgeNewsBot/1.0)',
|
||||
])->timeout(8)->get($pageUrl);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true); // suppress warnings from imperfect HTML
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadHTML($response->body());
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach (['og:image', 'twitter:image', 'twitter:image:src'] as $property) {
|
||||
$node = $xpath->query("//meta[@property='{$property}']/@content")->item(0)
|
||||
?? $xpath->query("//meta[@name='{$property}']/@content")->item(0);
|
||||
|
||||
if ($node && filled($node->nodeValue)) {
|
||||
return $node->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user