mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-23 23:18:05 +02:00
feat: implement backend podcast subscription controllers and frontend library page with optimistic updates
This commit is contained in:
1 parent
38521c45dc
commit
872ef5c31a
5 files changed
+92
-25
No files matched your search
@@ -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')) {
|
||||
|
||||
@@ -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<IPodcast>(
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastEpisodeTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
lastEpisodeDate: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-bold text-light-900 mb-1 line-clamp-2">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-sm text-light-500 mb-2">{podcast.author}</p>
|
||||
)}
|
||||
<div className="flex gap-3 text-xs text-light-400 mb-4">
|
||||
{podcast.episodeCount > 0 && (
|
||||
<span>{podcast.episodeCount} épisodes</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-bold text-light-900 mb-1 truncate">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-xs text-light-500 mb-2 truncate">{podcast.author}</p>
|
||||
)}
|
||||
|
||||
{/* Description preview */}
|
||||
{podcast.description && (
|
||||
<p className="text-xs text-light-400 line-clamp-2 mb-3 h-8 leading-relaxed">
|
||||
{podcast.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-[10px] text-light-400 mb-4">
|
||||
{podcast.episodeCount > 0 && (
|
||||
<span className="bg-light-100 px-2 py-0.5 rounded-full">
|
||||
{podcast.episodeCount} épisodes
|
||||
</span>
|
||||
)}
|
||||
{podcast.language && (
|
||||
<span className="bg-light-100 px-2 py-0.5 rounded-full uppercase">
|
||||
{podcast.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last Episode info */}
|
||||
{podcast.lastEpisodeTitle && (
|
||||
<div className="bg-blue-50/50 rounded-lg p-2.5 mb-2 border border-blue-100/50">
|
||||
<p className="text-[10px] font-semibold text-blue-600 uppercase tracking-wider mb-1">
|
||||
Dernier épisode
|
||||
</p>
|
||||
<p className="text-xs font-medium text-light-800 line-clamp-1 mb-0.5">
|
||||
{podcast.lastEpisodeTitle}
|
||||
</p>
|
||||
{podcast.lastEpisodeDate && (
|
||||
<p className="text-[10px] text-light-400">
|
||||
{new Date(podcast.lastEpisodeDate).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{podcast.language && <span>{podcast.language}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unsubscribe button */}
|
||||
{confirmDeleteId === podcast._id ? (
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface Podcast {
|
||||
category: string[];
|
||||
language: string;
|
||||
episodeCount: number;
|
||||
lastEpisodeTitle?: string;
|
||||
lastEpisodeDate?: string;
|
||||
lastFetched: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user