From e075434dd70053b07fcdd7bd2349e68943029b33 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 16 Apr 2026 13:55:18 +0200 Subject: [PATCH] feat: implement RSS parsing service and podcast controller for feed ingestion and episode synchronization --- backend/src/controllers/podcastController.ts | 26 +++++++++++ backend/src/routes/podcastRoutes.ts | 2 + backend/src/services/rssParserService.ts | 40 +++++++++++------ frontend/src/pages/Library.tsx | 45 ++++++++++++++++++-- frontend/src/services/podcastService.ts | 5 +++ 5 files changed, 102 insertions(+), 16 deletions(-) diff --git a/backend/src/controllers/podcastController.ts b/backend/src/controllers/podcastController.ts index 1a8a713..d9e7ce9 100644 --- a/backend/src/controllers/podcastController.ts +++ b/backend/src/controllers/podcastController.ts @@ -56,6 +56,9 @@ export const subscribe = async (req: Request, res: Response) => { let podcast = await Podcast.findOne({ rssUrl }); if (!podcast) { + // Parse RSS feed + const feedData = await rssParserService.parseFeed(rssUrl); + const episodes = rssParserService.parseEpisodes(feedData.items, feedData.image?.url || '', rssUrl); try { const result = await rssParserService.createPodcastFromRss(rssUrl); podcast = await Podcast.findById(result.podcast._id); @@ -146,6 +149,29 @@ export const unsubscribe = async (req: Request, res: Response) => { } }; +export const syncPodcast = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { podcastId } = req.params; + const podcast = await Podcast.findById(podcastId); + + if (!podcast) { + return res.status(404).json({ message: 'Podcast not found' }); + } + + console.log(`[Sync] Triggering manual sync for podcast: ${podcast.title} (${podcastId})`); + const result = await rssParserService.syncPodcastEpisodes(podcastId, podcast.rssUrl, true); + + res.json(result); + } catch (error: any) { + console.error('Manual sync error:', error); + res.status(500).json({ message: error.message || 'Failed to sync podcast' }); + } +}; + export const searchPodcasts = async (req: Request, res: Response) => { try { const { q, limit = 20, skip = 0 } = req.query; diff --git a/backend/src/routes/podcastRoutes.ts b/backend/src/routes/podcastRoutes.ts index 9b09be4..a03dc90 100644 --- a/backend/src/routes/podcastRoutes.ts +++ b/backend/src/routes/podcastRoutes.ts @@ -3,6 +3,7 @@ import { getUserSubscriptions, subscribe, unsubscribe, + syncPodcast, searchPodcasts, getPodcast, discoverPodcasts, @@ -18,6 +19,7 @@ router.get('/subscriptions', authenticate, getUserSubscriptions); router.post('/subscribe', authenticate, subscribe); router.post('/subscribe-discovery', authenticate, subscribeFromDiscovery); router.delete('/:podcastId/unsubscribe', authenticate, unsubscribe); +router.post('/:podcastId/sync', authenticate, syncPodcast); // Public/discovery routes router.get('/discover', discoverPodcasts); diff --git a/backend/src/services/rssParserService.ts b/backend/src/services/rssParserService.ts index a8687f1..21302d4 100644 --- a/backend/src/services/rssParserService.ts +++ b/backend/src/services/rssParserService.ts @@ -138,13 +138,24 @@ export const rssParserService = { /** * Parse episodes from RSS feed items */ - parseEpisodes: (items: any[], podcastImage?: string): ParsedEpisode[] => { - return items + parseEpisodes: (items: any[], podcastImage?: string, rssUrl: string = ''): ParsedEpisode[] => { + const parsed = items .filter((item) => { const audioUrl = rssParserService.extractAudioUrl(item); - return !!audioUrl && !!item.title; // Must have both audio and title - }) - .map((item) => ({ + const hasTitle = !!item.title; + + if (!audioUrl || !hasTitle) { + // Log only a sample to avoid flooding, but enough to debug + if (items.indexOf(item) === 0) { + console.log(`[RSS Debug] Item check failed for "${item.title || 'Untitled'}": Audio: ${!!audioUrl}, RSS URL Sample: ${rssUrl.substring(0, 30)}...`); + if (!audioUrl) console.log(`[RSS Debug] Available keys for item: ${Object.keys(item).join(', ')}`); + } + } + + return !!audioUrl && hasTitle; + }); + + return parsed.map((item) => ({ title: item.title || 'Untitled Episode', description: item.contentSnippet || item.description || '', audioUrl: rssParserService.extractAudioUrl(item) || '', @@ -159,15 +170,20 @@ export const rssParserService = { /** * Fetch and sync podcast episodes */ - syncPodcastEpisodes: async (podcastId: string, rssUrl: string) => { + syncPodcastEpisodes: async (podcastId: string, rssUrl: string, force: boolean = false) => { try { const redis = getRedis(); - - // Check cache first const cacheKey = `podcast:${podcastId}:sync`; - const cached = await redis.get(cacheKey); - if (cached) { - return JSON.parse(cached); + + // Check cache unless forced + if (!force) { + const cached = await redis.get(cacheKey); + if (cached) { + console.log(`[RSS Sync] Returning cached result for ${podcastId}`); + return JSON.parse(cached); + } + } else { + console.log(`[RSS Sync] Force sync requested for ${podcastId}`); } const podcast = await Podcast.findById(podcastId); @@ -274,7 +290,7 @@ export const rssParserService = { await podcast.save(); // Parse and create episodes - const episodes = rssParserService.parseEpisodes(feedData.episodes, feedData.imageUrl); + const episodes = rssParserService.parseEpisodes(feedData.episodes, feedData.imageUrl, rssUrl); for (const episodeData of episodes) { const episode = new Episode({ diff --git a/frontend/src/pages/Library.tsx b/frontend/src/pages/Library.tsx index bc6d56c..cb6e1df 100644 --- a/frontend/src/pages/Library.tsx +++ b/frontend/src/pages/Library.tsx @@ -65,6 +65,20 @@ export default function Library() { }, }); + const syncMutation = useMutation({ + mutationFn: (podcastId: string) => podcastService.syncPodcast(podcastId), + onSuccess: (data, podcastId) => { + console.log(`[Sync SUCCESS] Podcast ID: ${podcastId}`, data); + queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); + queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); + }, + onError: (error: any, podcastId) => { + console.error(`[Sync FAILED] Podcast ID: ${podcastId}`, error); + const errorMessage = error.response?.data?.message || 'Erreur de synchronisation'; + alert(`Erreur : ${errorMessage}`); + }, + }); + const handleLogout = async () => { try { await authService.logout(); @@ -233,10 +247,33 @@ export default function Library() { ) : (
{podcast.episodeCount === 0 && ( -

- - Episodes en cours de synchronisation... -

+
+

+ {syncMutation.isPending && syncMutation.variables === (podcast.subscriptionId || podcast._id) ? ( + + ) : ( + + )} + Aucun épisode trouvé +

+ +
)}