From 872ef5c31a3c73a091c5538cf244b9c256a0a853 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 16 Apr 2026 13:07:46 +0200 Subject: [PATCH] feat: implement backend podcast subscription controllers and frontend library page with optimistic updates --- backend/src/controllers/podcastController.ts | 14 ++-- backend/src/models/Podcast.ts | 10 +++ backend/src/services/rssParserService.ts | 19 +++++- frontend/src/pages/Library.tsx | 72 +++++++++++++++----- frontend/src/services/podcastService.ts | 2 + 5 files changed, 92 insertions(+), 25 deletions(-) diff --git a/backend/src/controllers/podcastController.ts b/backend/src/controllers/podcastController.ts index 00a6054..ea94766 100644 --- a/backend/src/controllers/podcastController.ts +++ b/backend/src/controllers/podcastController.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; +import mongoose from 'mongoose'; import { Podcast } from '../models/Podcast'; import { UserSubscription } from '../models/UserSubscription'; import { Episode } from '../models/Episode'; @@ -106,15 +107,20 @@ export const unsubscribe = async (req: Request, res: Response) => { const { podcastId } = req.params; + // Log for debugging on the server + console.log(`[Unsubscribe] User ${req.user.id} attempting to unsubscribe from podcast ${podcastId}`); + const subscription = await UserSubscription.findOneAndDelete({ - userId: req.user.id, - podcastId, + userId: new mongoose.Types.ObjectId(req.user.id), + podcastId: new mongoose.Types.ObjectId(podcastId), }); if (!subscription) { + console.warn(`[Unsubscribe] No subscription found for user ${req.user.id} and podcast ${podcastId}`); return res.status(404).json({ message: 'Subscription not found' }); } + console.log(`[Unsubscribe] Successfully removed subscription for user ${req.user.id}`); res.json({ message: 'Unsubscribed successfully' }); } catch (error) { console.error('Unsubscribe error:', error); @@ -198,12 +204,10 @@ export const subscribeFromDiscovery = async (req: Request, res: Response) => { try { const result = await rssParserService.createPodcastFromRss(rssUrl, { title: title || 'Unknown Podcast', - description: '', author: author || '', imageUrl: imageUrl, - episodes: [], }); - // Refetch as a proper Mongoose document (result.podcast is a plain object) + // Refetch as a proper Mongoose document podcast = await Podcast.findById(result.podcast._id); } catch (error: any) { if (error.message.includes('already in database')) { diff --git a/backend/src/models/Podcast.ts b/backend/src/models/Podcast.ts index 07977e2..89e6135 100644 --- a/backend/src/models/Podcast.ts +++ b/backend/src/models/Podcast.ts @@ -10,6 +10,8 @@ export interface IPodcast extends Document { language: string; lastFetched: Date; episodeCount: number; + lastEpisodeTitle?: string; + lastEpisodeDate?: Date; createdAt: Date; updatedAt: Date; } @@ -55,6 +57,14 @@ const podcastSchema = new Schema( type: Number, default: 0, }, + lastEpisodeTitle: { + type: String, + default: '', + }, + lastEpisodeDate: { + type: Date, + default: null, + }, }, { timestamps: true, diff --git a/backend/src/services/rssParserService.ts b/backend/src/services/rssParserService.ts index 36770c3..8c8cdc7 100644 --- a/backend/src/services/rssParserService.ts +++ b/backend/src/services/rssParserService.ts @@ -195,6 +195,12 @@ export const rssParserService = { // Update podcast metadata podcast.lastFetched = new Date(); podcast.episodeCount = await Episode.countDocuments({ podcastId }); + + if (episodes.length > 0) { + podcast.lastEpisodeTitle = episodes[0].title; + podcast.lastEpisodeDate = episodes[0].pubDate; + } + await podcast.save(); const result = { @@ -231,9 +237,10 @@ export const rssParserService = { throw new Error('Podcast already in database'); } - // Parse feed if not provided - if (!feedData) { - feedData = await rssParserService.parseFeed(rssUrl); + // Always parse feed to get fresh description and episodes, even if partial data provided + if (!feedData || !feedData.description || !feedData.episodes || feedData.episodes.length === 0) { + const freshData = await rssParserService.parseFeed(rssUrl); + feedData = { ...feedData, ...freshData }; } // Create podcast @@ -275,6 +282,12 @@ export const rssParserService = { podcast.episodeCount = await Episode.countDocuments({ podcastId: podcast._id }); podcast.lastFetched = new Date(); + + if (episodes.length > 0) { + podcast.lastEpisodeTitle = episodes[0].title; + podcast.lastEpisodeDate = episodes[0].pubDate; + } + await podcast.save(); return { diff --git a/frontend/src/pages/Library.tsx b/frontend/src/pages/Library.tsx index 9a1e549..a7e8919 100644 --- a/frontend/src/pages/Library.tsx +++ b/frontend/src/pages/Library.tsx @@ -39,9 +39,10 @@ export default function Library() { queryClient.setQueryData( ['podcasts', 'subscriptions'], (old: any) => { - if (!old) return old; - const filtered = old.podcasts.filter((p: any) => p._id !== podcastId); - return { podcasts: filtered, count: filtered.length }; + if (!old || !old.podcasts) return old; + // Ensure we compare strings to avoid type issues + const filtered = old.podcasts.filter((p: any) => String(p._id) !== String(podcastId)); + return { ...old, podcasts: filtered, count: filtered.length }; } ); @@ -52,10 +53,12 @@ export default function Library() { queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); }, - onError: (error: any, podcastId, context) => { + onError: (error: any) => { // Rollback: re-fetch the real data from server if the mutation failed queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); - alert(`Erreur : ${error.response?.data?.message || 'Impossible de se désabonner'}`); + + const errorMessage = error.response?.data?.message || 'Erreur inconnue'; + alert(`Erreur lors du désabonnement : ${errorMessage}`); }, }); @@ -149,20 +152,55 @@ export default function Library() { )} {/* Info */} -
-

- {podcast.title} -

- {podcast.author && ( -

{podcast.author}

- )} -
- {podcast.episodeCount > 0 && ( - {podcast.episodeCount} épisodes +
+

+ {podcast.title} +

+ {podcast.author && ( +

{podcast.author}

+ )} + + {/* Description preview */} + {podcast.description && ( +

+ {podcast.description} +

+ )} + +
+ {podcast.episodeCount > 0 && ( + + {podcast.episodeCount} épisodes + + )} + {podcast.language && ( + + {podcast.language} + + )} +
+ + {/* Last Episode info */} + {podcast.lastEpisodeTitle && ( +
+

+ Dernier épisode +

+

+ {podcast.lastEpisodeTitle} +

+ {podcast.lastEpisodeDate && ( +

+ {new Date(podcast.lastEpisodeDate).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'short', + year: 'numeric' + })} +

+ )} +
)} - {podcast.language && {podcast.language}}
-
{/* Unsubscribe button */} {confirmDeleteId === podcast._id ? ( diff --git a/frontend/src/services/podcastService.ts b/frontend/src/services/podcastService.ts index 8fd1cfa..4eccfe0 100644 --- a/frontend/src/services/podcastService.ts +++ b/frontend/src/services/podcastService.ts @@ -10,6 +10,8 @@ export interface Podcast { category: string[]; language: string; episodeCount: number; + lastEpisodeTitle?: string; + lastEpisodeDate?: string; lastFetched: string; }