From 1c772e63cb9e1f0675d2772e81c199a7a43988dc Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 10 Aug 2025 21:47:10 +0200 Subject: [PATCH] Fix languages and feeds --- .../Controllers/Api/V1/FeedsController.php | 3 - .../Api/V1/OnboardingController.php | 7 +++ backend/app/Models/Feed.php | 2 + .../Factories/ArticleParserFactory.php | 20 +++++++ .../Factories/HomepageParserFactory.php | 16 +++++- backend/config/feed.php | 56 +++++++++++++++++++ backend/config/languages.php | 51 +++++++++++++++++ backend/database/factories/FeedFactory.php | 17 ++++++ ...4_01_01_000004_create_feeds_and_routes.php | 1 + backend/database/seeders/DatabaseSeeder.php | 8 +-- backend/database/seeders/LanguageSeeder.php | 7 +-- backend/tests/Unit/Models/FeedTest.php | 3 +- 12 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 backend/config/feed.php create mode 100644 backend/config/languages.php diff --git a/backend/app/Http/Controllers/Api/V1/FeedsController.php b/backend/app/Http/Controllers/Api/V1/FeedsController.php index 8dcc504..f8b07f0 100644 --- a/backend/app/Http/Controllers/Api/V1/FeedsController.php +++ b/backend/app/Http/Controllers/Api/V1/FeedsController.php @@ -57,9 +57,6 @@ public function store(StoreFeedRequest $request): JsonResponse $validated['url'] = $adapter->getHomepageUrl(); $validated['type'] = 'website'; - // Remove provider from validated data as it's not a database column - unset($validated['provider']); - $feed = Feed::create($validated); return $this->sendResponse( diff --git a/backend/app/Http/Controllers/Api/V1/OnboardingController.php b/backend/app/Http/Controllers/Api/V1/OnboardingController.php index ad891e4..454cd2f 100644 --- a/backend/app/Http/Controllers/Api/V1/OnboardingController.php +++ b/backend/app/Http/Controllers/Api/V1/OnboardingController.php @@ -90,11 +90,17 @@ public function options(): JsonResponse ->orderBy('name') ->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']); + // Get feed providers from config + $feedProviders = collect(config('feed.providers', [])) + ->filter(fn($provider) => $provider['is_active']) + ->values(); + return $this->sendResponse([ 'languages' => $languages, 'platform_instances' => $platformInstances, 'feeds' => $feeds, 'platform_channels' => $platformChannels, + 'feed_providers' => $feedProviders, ], 'Onboarding options retrieved successfully.'); } @@ -215,6 +221,7 @@ public function createFeed(Request $request): JsonResponse 'name' => $validated['name'], 'url' => $url, 'type' => $type, + 'provider' => $provider, 'language_id' => $validated['language_id'], 'description' => $validated['description'] ?? null, 'is_active' => true, diff --git a/backend/app/Models/Feed.php b/backend/app/Models/Feed.php index ef53672..6fefbec 100644 --- a/backend/app/Models/Feed.php +++ b/backend/app/Models/Feed.php @@ -15,6 +15,7 @@ * @property string $name * @property string $url * @property string $type + * @property string $provider * @property int $language_id * @property Language|null $language * @property string $description @@ -38,6 +39,7 @@ class Feed extends Model 'name', 'url', 'type', + 'provider', 'language_id', 'description', 'settings', diff --git a/backend/app/Services/Factories/ArticleParserFactory.php b/backend/app/Services/Factories/ArticleParserFactory.php index 682feac..765994a 100644 --- a/backend/app/Services/Factories/ArticleParserFactory.php +++ b/backend/app/Services/Factories/ArticleParserFactory.php @@ -3,6 +3,7 @@ namespace App\Services\Factories; use App\Contracts\ArticleParserInterface; +use App\Models\Feed; use App\Services\Parsers\VrtArticleParser; use App\Services\Parsers\BelgaArticleParser; use Exception; @@ -33,6 +34,25 @@ public static function getParser(string $url): ArticleParserInterface throw new Exception("No parser found for URL: {$url}"); } + public static function getParserForFeed(Feed $feed, string $parserType = 'article'): ?ArticleParserInterface + { + if (!$feed->provider) { + return null; + } + + $providerConfig = config("feed.providers.{$feed->provider}"); + if (!$providerConfig || !isset($providerConfig['parsers'][$parserType])) { + return null; + } + + $parserClass = $providerConfig['parsers'][$parserType]; + if (!class_exists($parserClass)) { + return null; + } + + return new $parserClass(); + } + /** * @return array */ diff --git a/backend/app/Services/Factories/HomepageParserFactory.php b/backend/app/Services/Factories/HomepageParserFactory.php index 0e5e90b..7215961 100644 --- a/backend/app/Services/Factories/HomepageParserFactory.php +++ b/backend/app/Services/Factories/HomepageParserFactory.php @@ -36,10 +36,20 @@ public static function getParser(string $url): HomepageParserInterface public static function getParserForFeed(Feed $feed): ?HomepageParserInterface { - try { - return self::getParser($feed->url); - } catch (Exception) { + if (!$feed->provider) { return null; } + + $providerConfig = config("feed.providers.{$feed->provider}"); + if (!$providerConfig || !isset($providerConfig['parsers']['homepage'])) { + return null; + } + + $parserClass = $providerConfig['parsers']['homepage']; + if (!class_exists($parserClass)) { + return null; + } + + return new $parserClass(); } } diff --git a/backend/config/feed.php b/backend/config/feed.php new file mode 100644 index 0000000..2c4288d --- /dev/null +++ b/backend/config/feed.php @@ -0,0 +1,56 @@ + [ + 'vrt' => [ + 'code' => 'vrt', + 'name' => 'VRT News', + 'description' => 'Belgian public broadcaster news', + 'type' => 'website', + 'is_active' => true, + 'parsers' => [ + 'homepage' => \App\Services\Parsers\VrtHomepageParserAdapter::class, + 'article' => \App\Services\Parsers\VrtArticleParser::class, + 'article_page' => \App\Services\Parsers\VrtArticlePageParser::class, + ], + ], + 'belga' => [ + 'code' => 'belga', + 'name' => 'Belga News Agency', + 'description' => 'Belgian national news agency', + 'type' => 'rss', + 'is_active' => true, + 'parsers' => [ + 'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class, + 'article' => \App\Services\Parsers\BelgaArticleParser::class, + 'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class, + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Default Feed Settings + |-------------------------------------------------------------------------- + | + | Default configuration values for feed processing + | + */ + + 'defaults' => [ + 'fetch_interval' => 3600, // 1 hour in seconds + 'max_articles_per_fetch' => 50, + 'article_retention_days' => 30, + ], +]; \ No newline at end of file diff --git a/backend/config/languages.php b/backend/config/languages.php new file mode 100644 index 0000000..d0f8c7f --- /dev/null +++ b/backend/config/languages.php @@ -0,0 +1,51 @@ + [ + 'en' => [ + 'short_code' => 'en', + 'name' => 'English', + 'native_name' => 'English', + 'is_active' => true, + ], + 'nl' => [ + 'short_code' => 'nl', + 'name' => 'Dutch', + 'native_name' => 'Nederlands', + 'is_active' => true, + ], + 'fr' => [ + 'short_code' => 'fr', + 'name' => 'French', + 'native_name' => 'Français', + 'is_active' => true, + ], + 'de' => [ + 'short_code' => 'de', + 'name' => 'German', + 'native_name' => 'Deutsch', + 'is_active' => true, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Default Language + |-------------------------------------------------------------------------- + | + | The default language code when no language is specified + | + */ + + 'default' => 'en', +]; \ No newline at end of file diff --git a/backend/database/factories/FeedFactory.php b/backend/database/factories/FeedFactory.php index 810d1be..d07c3f0 100644 --- a/backend/database/factories/FeedFactory.php +++ b/backend/database/factories/FeedFactory.php @@ -19,6 +19,7 @@ public function definition(): array 'name' => $this->faker->words(3, true), 'url' => $this->faker->url(), 'type' => $this->faker->randomElement(['website', 'rss']), + 'provider' => $this->faker->randomElement(['vrt', 'belga']), 'language_id' => null, 'description' => $this->faker->optional()->sentence(), 'settings' => [], @@ -61,4 +62,20 @@ public function language(Language $language): static 'language_id' => $language->id, ]); } + + public function vrt(): static + { + return $this->state(fn (array $attributes) => [ + 'provider' => 'vrt', + 'url' => 'https://www.vrt.be/vrtnws/en/', + ]); + } + + public function belga(): static + { + return $this->state(fn (array $attributes) => [ + 'provider' => 'belga', + 'url' => 'https://www.belganewsagency.eu/', + ]); + } } \ No newline at end of file diff --git a/backend/database/migrations/2024_01_01_000004_create_feeds_and_routes.php b/backend/database/migrations/2024_01_01_000004_create_feeds_and_routes.php index 2425fbb..1bf820a 100644 --- a/backend/database/migrations/2024_01_01_000004_create_feeds_and_routes.php +++ b/backend/database/migrations/2024_01_01_000004_create_feeds_and_routes.php @@ -14,6 +14,7 @@ public function up(): void $table->string('name'); // "VRT News", "Belga News Agency" $table->string('url'); // "https://vrt.be" or "https://feeds.example.com/rss.xml" $table->enum('type', ['website', 'rss']); // Feed type + $table->string('provider'); // Feed provider code (vrt, belga, etc.) $table->foreignId('language_id')->nullable()->constrained(); $table->text('description')->nullable(); $table->json('settings')->nullable(); // Custom settings per feed type diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index 2a37b0c..d074784 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -10,13 +10,7 @@ public function run(): void { $this->call([ SettingsSeeder::class, + LanguageSeeder::class, ]); - - // Seed languages in local/dev environment only to avoid conflicts in tests - if (app()->environment('local')) { - $this->call([ - LanguageSeeder::class, - ]); - } } } diff --git a/backend/database/seeders/LanguageSeeder.php b/backend/database/seeders/LanguageSeeder.php index 36c4a85..389d452 100644 --- a/backend/database/seeders/LanguageSeeder.php +++ b/backend/database/seeders/LanguageSeeder.php @@ -9,12 +9,7 @@ class LanguageSeeder extends Seeder { public function run(): void { - $languages = [ - ['short_code' => 'en', 'name' => 'English', 'native_name' => 'English', 'is_active' => true], - ['short_code' => 'nl', 'name' => 'Dutch', 'native_name' => 'Nederlands', 'is_active' => true], - ['short_code' => 'fr', 'name' => 'French', 'native_name' => 'Français', 'is_active' => true], - ['short_code' => 'de', 'name' => 'German', 'native_name' => 'Deutsch', 'is_active' => true], - ]; + $languages = config('languages.supported', []); foreach ($languages as $language) { DB::table('languages')->updateOrInsert( diff --git a/backend/tests/Unit/Models/FeedTest.php b/backend/tests/Unit/Models/FeedTest.php index e82448a..94beef8 100644 --- a/backend/tests/Unit/Models/FeedTest.php +++ b/backend/tests/Unit/Models/FeedTest.php @@ -16,7 +16,7 @@ class FeedTest extends TestCase public function test_fillable_fields(): void { - $fillableFields = ['name', 'url', 'type', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at']; + $fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at']; $feed = new Feed(); $this->assertEquals($fillableFields, $feed->getFillable()); @@ -240,6 +240,7 @@ public function test_feed_creation_with_explicit_values(): void 'name' => 'Test Feed', 'url' => 'https://example.com/feed', 'type' => 'rss', + 'provider' => 'vrt', 'language_id' => $language->id, 'description' => 'Test description', 'settings' => $settings,