From dddb6ade28b4c1af5eef076a0838ef62973d1c61 Mon Sep 17 00:00:00 2001 From: astria Date: Fri, 22 Aug 2025 22:42:54 +0200 Subject: [PATCH] Add top creators and recommendation --- .../controllers/recommendation.controller.js | 87 +++++++++++++++++-- backend/app/routes/redommendation.route.js | 4 +- backend/logs/access.log | 84 ++++++++++++++++++ backend/requests/top-tags-videos.http | 17 ++++ frontend/src/components/Recommendations.jsx | 8 +- frontend/src/components/TopCreators.jsx | 21 +++-- frontend/src/pages/Home.jsx | 13 ++- .../src/services/recommendation.service.js | 13 ++- 8 files changed, 225 insertions(+), 22 deletions(-) create mode 100644 backend/requests/top-tags-videos.http diff --git a/backend/app/controllers/recommendation.controller.js b/backend/app/controllers/recommendation.controller.js index d7f043c..9aa6340 100644 --- a/backend/app/controllers/recommendation.controller.js +++ b/backend/app/controllers/recommendation.controller.js @@ -5,14 +5,65 @@ export async function getRecommendations(req, res) { const token = req.headers.authorization?.split(' ')[1]; - if (!token) { + if (!req.headers.authorization || !token) { - // GET MOST USED TOKEN + // GET MOST USED TAGS let client = await getClient(); - let queryMostUsedToken = `SELECT id, name FROM tags ORDER BY usage_count DESC LIMIT 3;`; - let result = await client.query(queryMostUsedToken); - - const recommendations = result.rows; + let queryMostUsedTags = `SELECT id, name FROM tags ORDER BY usage_count DESC LIMIT 3;`; + let result = await client.query(queryMostUsedTags); + + // GET 10 VIDEOS WITH THE TAGS + + let tagIds = result.rows.map(tag => tag.id); + let queryVideosWithTags = ` + + SELECT + v.id, + v.title, + v.thumbnail, + v.description AS video_description, + v.channel, + v.visibility, + v.file, + v.slug, + v.release_date, + v.channel AS channel_id, + c.owner, + COUNT(h.id) AS views, + json_build_object( + 'name', c.name, + 'profilePicture', u.picture, + 'description', c.description + ) AS creator, + 'video' AS type + FROM public.videos v + INNER JOIN public.video_tags vt ON v.id = vt.video + INNER JOIN public.tags t ON vt.tag = t.id + INNER JOIN public.channels c ON v.channel = c.id + INNER JOIN public.users u ON c.owner = u.id + LEFT JOIN public.history h ON h.video = v.id + WHERE t.id = ANY($1::int[]) + AND v.visibility = 'public' + GROUP BY + v.id, + v.title, + v.thumbnail, + v.description, + v.channel, + v.visibility, + v.file, + v.slug, + v.release_date, + c.owner, + c.name, + u.picture, + c.description + ORDER BY views DESC, v.release_date DESC + LIMIT 10; + + `; + let videoResult = await client.query(queryVideosWithTags, [tagIds]); + const recommendations = videoResult.rows; res.status(200).json(recommendations); } else { @@ -88,4 +139,28 @@ export async function getTrendingVideos(req, res) { console.error("Error fetching trending videos:", error); res.status(500).json({error: "Internal server error while fetching trending videos."}); } +} + +export async function getTopCreators(req, res) { + try { + // GET TOP 5 CREATORS BASED ON NUMBER OF SUBSCRIBERS + let client = await getClient(); + let queryTopCreators = ` + SELECT c.id, c.name, c.description, u.picture AS profilePicture, COUNT(s.id) AS subscriber_count + FROM channels c + JOIN users u ON c.owner = u.id + LEFT JOIN subscriptions s ON c.id = s.channel + GROUP BY c.id, u.picture + ORDER BY subscriber_count DESC + LIMIT 10; + `; + let result = await client.query(queryTopCreators); + const topCreators = result.rows; + + client.end(); + res.status(200).json(topCreators); + } catch (error) { + console.error("Error fetching top creators:", error); + res.status(500).json({error: "Internal server error while fetching top creators."}); + } } \ No newline at end of file diff --git a/backend/app/routes/redommendation.route.js b/backend/app/routes/redommendation.route.js index dbb7f2c..4757b0f 100644 --- a/backend/app/routes/redommendation.route.js +++ b/backend/app/routes/redommendation.route.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import {getRecommendations, getTrendingVideos} from "../controllers/recommendation.controller.js"; +import {getRecommendations, getTrendingVideos, getTopCreators} from "../controllers/recommendation.controller.js"; const router = Router(); @@ -7,4 +7,6 @@ router.get('/', [], getRecommendations); router.get('/trending', [], getTrendingVideos); +router.get("/creators", [], getTopCreators) + export default router; \ No newline at end of file diff --git a/backend/logs/access.log b/backend/logs/access.log index d76fa21..20a1a00 100644 --- a/backend/logs/access.log +++ b/backend/logs/access.log @@ -8440,3 +8440,87 @@ [2025-08-22 17:20:25.400] [undefined] GET(/:id/history): try to retrieve history of user 1 [2025-08-22 17:20:25.407] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 [2025-08-22 17:20:25.418] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 17:45:26.016] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:45:26.020] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-22 17:45:26.024] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404 +[2025-08-22 17:45:26.028] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 +[2025-08-22 17:45:26.038] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 17:45:31.307] [undefined] POST(/): try to create new channel with owner 1 and name Astri4 +[2025-08-22 17:45:31.311] [undefined] POST(/): Successfully created new channel with name Astri4 with status 200 +[2025-08-22 17:45:31.326] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:45:31.329] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 17:45:32.380] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 17:45:32.390] [undefined] GET(/:id/stats): try to get stats +[2025-08-22 17:45:32.396] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 17:45:32.405] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-22 17:45:33.382] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:45:33.387] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 17:48:00.202] [undefined] POST(/): try to upload video with status undefined +[2025-08-22 17:48:00.209] [undefined] POST(/): successfully uploaded video with status 200 +[2025-08-22 17:48:00.320] [undefined] POST(/thumbnail): try to add thumbnail to video 1 +[2025-08-22 17:48:00.326] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-08-22 17:48:00.348] [undefined] PUT(/:id/tags): try to add tags to video 1 +[2025-08-22 17:48:00.363] [undefined] PUT(/:id/tags): successfully added tags to video 1 with status 200 +[2025-08-22 17:52:47.227] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:52:47.232] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 17:52:47.237] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-22 17:52:47.242] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 +[2025-08-22 17:52:47.251] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 17:52:48.223] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 17:52:48.233] [undefined] GET(/:id/stats): try to get stats +[2025-08-22 17:52:48.237] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 17:52:48.247] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-22 17:52:49.012] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:52:49.016] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 17:53:03.301] [undefined] POST(/): try to upload video with status undefined +[2025-08-22 17:53:03.307] [undefined] POST(/): successfully uploaded video with status 200 +[2025-08-22 17:53:03.417] [undefined] POST(/thumbnail): try to add thumbnail to video 2 +[2025-08-22 17:53:03.423] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-08-22 17:53:03.445] [undefined] PUT(/:id/tags): try to add tags to video 2 +[2025-08-22 17:53:03.458] [undefined] PUT(/:id/tags): successfully added tags to video 2 with status 200 +[2025-08-22 17:53:12.167] [undefined] GET(/:id): try to get video 1 +[2025-08-22 17:53:12.171] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 17:53:12.184] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-22 17:53:12.201] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-22 17:53:12.228] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-22 17:53:12.255] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-22 17:53:12.300] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-22 17:53:14.457] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 17:53:14.462] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-22 17:53:14.466] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 17:53:14.471] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-22 17:53:14.482] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 17:53:15.772] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 17:53:15.783] [undefined] GET(/:id/stats): try to get stats +[2025-08-22 17:53:15.789] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 17:53:15.800] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-22 17:53:16.871] [undefined] GET(/:id/likes/day): try to get likes per day +[2025-08-22 17:53:16.882] [undefined] GET(/:id): try to get video 2 +[2025-08-22 17:53:16.886] [undefined] GET(/:id/likes/day): successfully retrieved likes per day with status 200 +[2025-08-22 17:53:16.897] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-08-22 17:53:19.714] [undefined] PUT(/:id/tags): try to add tags to video 2 +[2025-08-22 17:53:19.724] [undefined] PUT(/:id/tags): successfully added tags to video 2 with status 200 +[2025-08-22 17:53:23.383] [undefined] PUT(/:id/tags): try to add tags to video 2 +[2025-08-22 17:53:23.393] [undefined] PUT(/:id/tags): Tag wankil studio already exists for video 2 with status 200 +[2025-08-22 17:53:23.400] [undefined] PUT(/:id/tags): successfully added tags to video 2 with status 200 +[2025-08-22 20:37:48.060] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-22 20:37:48.067] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-22 20:37:48.072] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-22 20:37:48.077] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-22 20:37:48.087] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-22 20:37:52.902] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 20:37:52.914] [undefined] GET(/:id/stats): try to get stats +[2025-08-22 20:37:52.919] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 20:37:52.928] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-22 20:37:58.472] [undefined] PUT(/:id): try to update channel with id 1 +[2025-08-22 20:37:58.478] [undefined] PUT(/:id): Successfully updated channel with status 200 +[2025-08-22 20:40:57.463] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 20:40:57.475] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 20:40:57.491] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1 +[2025-08-22 20:40:57.495] [undefined] GET(/:id/channel/subscribed): user 1 is not subscribed to channel 1 with status 200 +[2025-08-22 20:41:04.433] [undefined] POST(/:id/subscribe): try to toggle subscription for channel with id 1 +[2025-08-22 20:41:04.446] [undefined] POST(/:id/subscribe): Successfully subscribed to channel with status 200 +[2025-08-22 20:42:05.622] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-22 20:42:05.633] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-22 20:42:05.685] [undefined] GET(/:id/channel/subscribed): check if user 1 is subscribed to channel 1 +[2025-08-22 20:42:05.690] [undefined] GET(/:id/channel/subscribed): user 1 is subscribed to channel 1 with status 200 diff --git a/backend/requests/top-tags-videos.http b/backend/requests/top-tags-videos.http new file mode 100644 index 0000000..1891010 --- /dev/null +++ b/backend/requests/top-tags-videos.http @@ -0,0 +1,17 @@ +### Get all videos from the three most watched tags +GET http://127.0.0.1:8000/api/videos/top-tags/videos + +### Alternative localhost URL +GET http://localhost:8000/api/videos/top-tags/videos + +### With frontend URL (if using nginx proxy) +GET http://localhost/api/videos/top-tags/videos + +### +# This endpoint returns: +# - topTags: The 3 most used tags with their usage count +# - videos: All public videos that have any of these top 3 tags +# - totalVideos: Count of videos returned +# +# Videos are ordered by popularity score (calculated from views, likes, comments) +# and then by release date (newest first) diff --git a/frontend/src/components/Recommendations.jsx b/frontend/src/components/Recommendations.jsx index 6a118dc..03819a1 100644 --- a/frontend/src/components/Recommendations.jsx +++ b/frontend/src/components/Recommendations.jsx @@ -1,14 +1,12 @@ - +import VideoCard from "./VideoCard"; export default function Recommendations({videos}) { - - - + console.log(videos); return (

Recommendations

-
+
{videos && videos.map((video, index) => ( ))} diff --git a/frontend/src/components/TopCreators.jsx b/frontend/src/components/TopCreators.jsx index aa3305e..65349e8 100644 --- a/frontend/src/components/TopCreators.jsx +++ b/frontend/src/components/TopCreators.jsx @@ -1,15 +1,22 @@ -export default function TopCreators({ creators }) { +export default function TopCreators({ creators, navigate }) { return (
-

Top Creators

-
+

Top Créateurs

+
{creators && creators.map((creator, index) => ( -
- {creator.name} -

{creator.name}

- {creator.subscribers} subscribers +
navigate(`/channel/${creator.id}`)} + > + {creator.name} +

{creator.name}

+ {creator.subscriber_count} abonné{creator.subscriber_count > 1 ? 's' : ''} +

+ {creator.description.slice(0, 100) + (creator.description.length > 100 ? '...' : '')} +

))}
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 7899e9f..8e36173 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -5,7 +5,8 @@ import {useState, useEffect} from "react"; import { useAuth } from '../contexts/AuthContext'; import TopCreators from "../components/TopCreators.jsx"; import TrendingVideos from "../components/TrendingVideos.jsx"; -import { getRecommendations, getTrendingVideos } from '../services/recommendation.service.js'; +import { getRecommendations, getTrendingVideos, getTopCreators } from '../services/recommendation.service.js'; +import { useNavigate } from 'react-router-dom'; export default function Home() { const { isAuthenticated, user } = useAuth(); @@ -15,6 +16,8 @@ export default function Home() { const [trendingVideos, setTrendingVideos] = useState([]); const [alerts, setAlerts] = useState([]); + const navigate = useNavigate(); + useEffect(() => { // Fetch recommendations, top creators, and trending videos const fetchData = async () => { @@ -30,6 +33,12 @@ export default function Home() { setLoading(false); } + try { + setTopCreators(await getTopCreators(addAlert)); + } finally { + setLoading(false); + } + }; fetchData(); }, []); @@ -82,7 +91,7 @@ export default function Home() { {/* Top Creators section */} - + {/* Trending Videos section */} diff --git a/frontend/src/services/recommendation.service.js b/frontend/src/services/recommendation.service.js index 7935e43..03825d5 100644 --- a/frontend/src/services/recommendation.service.js +++ b/frontend/src/services/recommendation.service.js @@ -4,7 +4,7 @@ export async function getRecommendations(addAlert) { try { const response = await fetch('/api/recommendations'); const data = await response.json(); - return data.recommendations; + return data; } catch (error) { console.error('Error fetching data:', error); addAlert('error', 'Erreur lors du chargement des recommandations'); @@ -20,4 +20,15 @@ export async function getTrendingVideos(addAlert) { console.error('Error fetching data:', error); addAlert('error', 'Erreur lors du chargement des vidéos tendance'); } +} + +export async function getTopCreators(addAlert) { + try { + const response = await fetch('/api/recommendations/creators'); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching data:', error); + addAlert('error', 'Erreur lors du chargement des créateurs'); + } } \ No newline at end of file