diff --git a/backend/app/controllers/playlist.controller.js b/backend/app/controllers/playlist.controller.js index dc90b56..cd78d46 100644 --- a/backend/app/controllers/playlist.controller.js +++ b/backend/app/controllers/playlist.controller.js @@ -227,4 +227,76 @@ export async function del(req, res) { client.end(); res.status(500).json({ error: "Internal server error" }); } +} + +export async function getSeeLater(req, res) { + + const token = req.headers.authorization.split(' ')[1]; + const userId = jwt.decode(token)["id"]; + const logger = req.body.logger; + + const client = await getClient(); + const query = ` + SELECT + JSON_AGG( + json_build_object( + 'video_id', videos.id, + 'title', videos.title, + 'thumbnail', videos.thumbnail, + 'video_decscription', videos.description, + 'channel', videos.channel, + 'visibility', videos.visibility, + 'file', videos.file, + 'format', videos.format, + 'release_date', videos.release_date, + 'channel_id', channels.id, + 'owner', channels.owner, + 'views', COALESCE(video_views.view_count, 0), + 'creator', json_build_object( + 'name', channels.name, + 'profilePicture', users.picture, + 'description', channels.description + ) + ) + ) AS videos + FROM + public.playlists + LEFT JOIN public.playlist_elements ON public.playlists.id = public.playlist_elements.playlist + LEFT JOIN ( + SELECT + * + FROM public.videos + LIMIT 10 + ) videos ON public.playlist_elements.video = videos.id + LEFT JOIN public.channels ON videos.channel = public.channels.id + LEFT JOIN public.users ON public.channels.owner = public.users.id + LEFT JOIN ( + SELECT video, COUNT(*) as view_count + FROM public.history + GROUP BY video + ) video_views ON videos.id = video_views.video + WHERE + playlists.owner = $1 + GROUP BY playlists.id, playlists.name + ORDER BY + playlists.id ASC + LIMIT 1; + `; + try { + const result = await client.query(query, [userId]); + if (result.rows.length === 0) { + logger.write("No 'See Later' playlist found for user with id " + userId, 404); + client.end(); + res.status(404).json({ error: "'See Later' playlist not found" }); + return; + } + logger.write("'See Later' playlist retrieved for user with id " + userId, 200); + client.end(); + res.status(200).json(result.rows[0].videos); + } catch (error) { + logger.write("Error retrieving 'See Later' playlist: " + error.message, 500); + client.end(); + res.status(500).json({ error: "Internal server error" }); + } + } \ No newline at end of file diff --git a/backend/app/controllers/recommendation.controller.js b/backend/app/controllers/recommendation.controller.js index d7f043c..5e6897c 100644 --- a/backend/app/controllers/recommendation.controller.js +++ b/backend/app/controllers/recommendation.controller.js @@ -1,39 +1,195 @@ import {getClient} from "../utils/database.js"; - +import jwt from 'jsonwebtoken'; 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); + 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 - const recommendations = result.rows; + 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 { - // Recuperer les 20 derniere vu de l'historique let client = await getClient(); - let queryLastVideos = `SELECT video_id FROM history WHERE user_id = $1 ORDER BY viewed_at DESC LIMIT 20;`; - // TODO: Implement retrieval of recommendations based on user history and interactions - - // Recuperer les likes de l'utilisateur sur les 20 derniere videos recuperees - - // Recuperer les commentaires de l'utilisateur sur les 20 derniere videos recuperees + const claims = jwt.decode(token) + const query = ` + -- Recommandation de contenu similaire non vu basée sur les interactions utilisateur + -- Paramètre: $1 = user_id + WITH user_interactions AS ( + -- Récupérer tous les contenus avec lesquels l'utilisateur a interagi + SELECT DISTINCT v.id as video_id, v.channel, t.id as tag_id, t.name as tag_name + FROM videos v + JOIN video_tags vt ON v.id = vt.video + JOIN tags t ON vt.tag = t.id + WHERE v.id IN ( + -- Vidéos likées par l'utilisateur + SELECT DISTINCT l.video FROM likes l WHERE l.owner = $1 + UNION + -- Vidéos commentées par l'utilisateur + SELECT DISTINCT c.video FROM comments c WHERE c.author = $1 + UNION + -- Vidéos ajoutées aux playlists de l'utilisateur + SELECT DISTINCT pe.video + FROM playlist_elements pe + JOIN playlists p ON pe.playlist = p.id + WHERE p.owner = $1 + ) + ), + user_preferred_tags AS ( + -- Tags préférés basés sur les interactions + SELECT tag_id, tag_name, COUNT(*) as interaction_count + FROM user_interactions + GROUP BY tag_id, tag_name + ), + user_preferred_channels AS ( + -- Chaînes préférées basées sur les interactions + SELECT channel, COUNT(*) as interaction_count + FROM user_interactions + GROUP BY channel + ), + unseen_videos AS ( + -- Vidéos que l'utilisateur n'a jamais vues + SELECT v.id, v.title, v.thumbnail, v.description, v.channel, v.visibility, + v.file, v.slug, v.format, v.release_date, ch.owner + FROM videos v + JOIN channels ch ON v.channel = ch.id + WHERE v.visibility = 'public' + AND v.id NOT IN ( + -- Exclure les vidéos déjà vues + SELECT DISTINCT h.video FROM history h WHERE h.user_id = $1 + UNION + -- Exclure les vidéos déjà likées + SELECT DISTINCT l.video FROM likes l WHERE l.owner = $1 + UNION + -- Exclure les vidéos déjà commentées + SELECT DISTINCT c.video FROM comments c WHERE c.author = $1 + UNION + -- Exclure les vidéos déjà ajoutées aux playlists + SELECT DISTINCT pe.video + FROM playlist_elements pe + JOIN playlists p ON pe.playlist = p.id + WHERE p.owner = $1 + ) + ) + -- Requête principale : recommander du contenu similaire + SELECT + uv.id, + uv.title, + uv.thumbnail, + uv.description as video_description, + uv.channel, + uv.visibility, + uv.file, + uv.slug, + uv.format, + uv.release_date, + uv.channel as channel_id, + uv.owner, + COALESCE(view_counts.views::text, '0') as views, + json_build_object( + 'name', u.username, + 'profilePicture', u.picture, + 'description', ch.description + ) as creator, + 'video' as type + FROM unseen_videos uv + JOIN channels ch ON uv.channel = ch.id + JOIN users u ON ch.owner = u.id + -- Compter les vues + LEFT JOIN ( + SELECT video, COUNT(*) as views + FROM history + GROUP BY video + ) view_counts ON uv.id = view_counts.video + -- Score basé sur les tags similaires + LEFT JOIN ( + SELECT + vt.video, + SUM(upt.interaction_count * 0.7) as score + FROM video_tags vt + JOIN user_preferred_tags upt ON vt.tag = upt.tag_id + GROUP BY vt.video + ) tag_score ON uv.id = tag_score.video + -- Score basé sur les chaînes similaires + LEFT JOIN ( + SELECT + uv2.channel, + MAX(upc.interaction_count * 0.3) as score + FROM unseen_videos uv2 + JOIN user_preferred_channels upc ON uv2.channel = upc.channel + GROUP BY uv2.channel + ) channel_score ON uv.channel = channel_score.channel + WHERE (tag_score.score > 0 OR channel_score.score > 0) -- Au moins une similarité + GROUP BY uv.id, uv.title, uv.thumbnail, uv.description, uv.channel, uv.visibility, + uv.file, uv.slug, uv.format, uv.release_date, uv.owner, u.username, u.picture, + ch.description, view_counts.views, tag_score.score, channel_score.score + ORDER BY (COALESCE(tag_score.score, 0) + COALESCE(channel_score.score, 0)) DESC, uv.release_date DESC + LIMIT 20; - // Recuperer les 3 tags avec lesquels l'utilisateur a le plus interagi + `; + let result = await client.query(query, [claims.id]); - // Recuperer 10 videos avec les 3 tags ayant le plus d'interaction avec l'utilisateur client.end() - res.status(200).json({ - message: "Recommendations based on user history and interactions are not yet implemented." - }); + res.status(200).json(result.rows); } } @@ -88,4 +244,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/playlist.route.js b/backend/app/routes/playlist.route.js index ee5c030..00b8cd0 100644 --- a/backend/app/routes/playlist.route.js +++ b/backend/app/routes/playlist.route.js @@ -3,7 +3,7 @@ import {addLogger} from "../middlewares/logger.middleware.js"; import {isTokenValid} from "../middlewares/jwt.middleware.js"; import {doPlaylistExists, Playlist, isOwner, isVideoInPlaylist} from "../middlewares/playlist.middleware.js"; import validator from "../middlewares/error.middleware.js"; -import {addVideo, create, del, deleteVideo, getById, getByUser, update} from "../controllers/playlist.controller.js"; +import {addVideo, create, del, deleteVideo, getById, getByUser, update, getSeeLater} from "../controllers/playlist.controller.js"; import {doVideoExists, Video} from "../middlewares/video.middleware.js"; import {doUserExists, User, isOwner as isOwnerUser} from "../middlewares/user.middleware.js"; @@ -12,6 +12,9 @@ const router = new Router(); // CREATE PLAYLIST router.post("/", [addLogger, isTokenValid, Playlist.name, validator], create); +// GET SEE LATER PLAYLIST +router.get("/see-later", [addLogger, isTokenValid], getSeeLater); + // ADD VIDEO TO PLAYLIST router.post("/:id", [addLogger, isTokenValid, Playlist.id, Video.idBody, validator, doPlaylistExists, isOwner, doVideoExists], addVideo); @@ -30,4 +33,5 @@ router.delete("/:id/video/:videoId", [addLogger, isTokenValid, Video.id, Playlis // DELETE PLAYLIST router.delete("/:id", [addLogger, isTokenValid, Playlist.id, validator, doPlaylistExists, isOwner], del); + export default router; \ 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/app/utils/database.js b/backend/app/utils/database.js index ad85431..780d8ec 100644 --- a/backend/app/utils/database.js +++ b/backend/app/utils/database.js @@ -2,10 +2,10 @@ import pg from "pg"; export async function getClient() { const client = new pg.Client({ - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - host: process.env.DB_HOST, - database: process.env.DB_NAME, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DB, port: 5432 }) diff --git a/backend/logs/access.log b/backend/logs/access.log index d76fa21..df1d83c 100644 --- a/backend/logs/access.log +++ b/backend/logs/access.log @@ -8440,3 +8440,369 @@ [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 +[2025-08-23 10:58:17.412] [undefined] GET(/:id): try to get video 1 +[2025-08-23 10:58:17.417] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-23 10:58:17.430] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-23 10:58:17.449] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-23 10:58:17.462] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-23 10:58:17.490] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-23 10:58:17.503] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-23 10:58:36.017] [undefined] GET(/:id/like): try to toggle like on video 1 +[2025-08-23 10:58:36.027] [undefined] GET(/:id/like): no likes found adding likes for video 1 with status 200 +[2025-08-23 10:58:41.944] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-23 10:58:41.949] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-23 10:58:41.953] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-23 10:58:41.958] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-23 10:58:41.967] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-23 10:58:47.879] [undefined] PUT(/:id): try to update user 1 +[2025-08-23 10:58:47.883] [undefined] PUT(/:id): failed to update profile picture with status 500 +[2025-08-23 11:11:35.296] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-23 11:11:35.302] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-23 11:11:35.306] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-23 11:11:35.311] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-23 11:11:35.323] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-23 11:11:38.491] [undefined] GET(/:id): try to get video 1 +[2025-08-23 11:11:38.519] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-23 11:11:38.531] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-23 11:11:38.551] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-23 11:11:38.568] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-23 11:11:38.606] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-23 11:11:38.623] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-23 11:11:42.769] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-23 11:11:42.773] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-23 11:11:42.824] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-23 11:11:42.829] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-23 11:11:42.837] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 08:39:21.217] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 08:39:21.222] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404 +[2025-08-24 08:39:21.226] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 08:39:21.231] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 +[2025-08-24 08:39:21.239] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 08:39:33.320] [undefined] POST(/): try to create new channel with owner 1 and name Astri4-4 +[2025-08-24 08:39:33.324] [undefined] POST(/): Successfully created new channel with name Astri4-4 with status 200 +[2025-08-24 08:39:33.422] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 08:39:33.427] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 08:39:34.528] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-24 08:39:34.541] [undefined] GET(/:id/stats): try to get stats +[2025-08-24 08:39:34.548] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-24 08:39:34.558] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-24 08:39:44.708] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 08:39:44.712] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 08:40:20.863] [undefined] POST(/): try to upload video with status undefined +[2025-08-24 08:40:20.868] [undefined] POST(/): successfully uploaded video with status 200 +[2025-08-24 08:40:20.975] [undefined] POST(/thumbnail): try to add thumbnail to video 1 +[2025-08-24 08:40:20.980] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-08-24 08:40:21.090] [undefined] PUT(/:id/tags): try to add tags to video 1 +[2025-08-24 08:40:21.098] [undefined] PUT(/:id/tags): successfully added tags to video 1 with status 200 +[2025-08-24 08:42:13.079] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 08:42:13.084] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 +[2025-08-24 08:42:13.088] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 08:42:13.092] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 08:42:13.102] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 08:42:14.233] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-24 08:42:14.243] [undefined] GET(/:id/stats): try to get stats +[2025-08-24 08:42:14.248] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-24 08:42:14.256] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-24 08:42:15.169] [undefined] GET(/:id/likes/day): try to get likes per day +[2025-08-24 08:42:15.181] [undefined] GET(/:id): try to get video 1 +[2025-08-24 08:42:15.186] [undefined] GET(/:id/likes/day): successfully retrieved likes per day with status 200 +[2025-08-24 08:42:15.199] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 08:42:18.349] [undefined] PUT(/:id/tags): try to add tags to video 1 +[2025-08-24 08:42:18.359] [undefined] PUT(/:id/tags): successfully added tags to video 1 with status 200 +[2025-08-24 09:09:41.627] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 09:09:41.632] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 09:09:41.636] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404 +[2025-08-24 09:09:41.641] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 09:09:41.650] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 09:09:46.047] [undefined] POST(/): Playlist created with id 2 with status 200 +[2025-08-24 09:09:46.102] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 09:09:49.883] [undefined] POST(/): Playlist created with id 3 with status 200 +[2025-08-24 09:09:49.901] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 09:11:01.101] [undefined] GET(/:id): try to get video 1 +[2025-08-24 09:11:01.106] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 09:11:01.117] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 09:11:01.135] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 09:11:01.150] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 09:11:01.215] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-24 09:11:01.227] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-24 09:11:03.606] [undefined] POST(/:id): Video added to playlist with id 1 with status 200 +[2025-08-24 09:34:20.753] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 09:34:20.757] [undefined] GET(/:id): try to get video 1 +[2025-08-24 09:34:20.769] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 09:34:20.794] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 09:34:20.808] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 09:34:20.863] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-24 09:34:20.874] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-24 09:54:38.736] [undefined] GET(/:id): failed due to invalid values with status 400 +[2025-08-24 09:54:44.233] [undefined] GET(/:id): failed due to invalid values with status 400 +[2025-08-24 09:55:34.639] [undefined] GET(/see-later): Error retrieving 'See Later' playlist: bind message supplies 1 parameters, but prepared statement "" requires 0 with status 500 +[2025-08-24 09:56:07.897] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 09:57:45.228] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 09:58:20.325] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 09:59:01.147] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 09:59:13.221] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 09:59:23.788] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 10:26:50.298] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 10:26:50.302] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 10:26:50.308] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 10:26:50.315] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 10:26:50.324] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 10:26:52.180] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-24 10:26:52.191] [undefined] GET(/:id/stats): try to get stats +[2025-08-24 10:26:52.196] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-24 10:26:52.204] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-24 10:26:54.334] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 10:26:54.338] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 10:27:16.503] [undefined] POST(/): try to upload video with status undefined +[2025-08-24 10:27:16.534] [undefined] POST(/): successfully uploaded video with status 200 +[2025-08-24 10:27:16.645] [undefined] POST(/thumbnail): try to add thumbnail to video 2 +[2025-08-24 10:27:16.650] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-08-24 10:27:16.675] [undefined] PUT(/:id/tags): try to add tags to video 2 +[2025-08-24 10:27:16.685] [undefined] PUT(/:id/tags): Tag wankil already exists for video 2 with status 200 +[2025-08-24 10:27:16.691] [undefined] PUT(/:id/tags): successfully added tags to video 2 with status 200 +[2025-08-24 10:27:19.733] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 10:56:34.988] [undefined] GET(/:id): try to get video 2 +[2025-08-24 10:56:34.991] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 10:56:35.003] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-08-24 10:56:35.025] [undefined] GET(/:id/similar): try to get similar videos for video 2 +[2025-08-24 10:56:35.042] [undefined] GET(/:id/similar): successfully get similar videos for video 2 with status 200 +[2025-08-24 10:56:35.146] [undefined] GET(/:id/views): try to add views for video 2 +[2025-08-24 10:56:35.187] [undefined] GET(/:id/views): successfully added views for video 2 with status 200 +[2025-08-24 10:56:41.980] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:42:15.221] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:42:18.258] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 12:42:18.262] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 12:42:18.266] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 12:42:18.271] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 12:42:18.279] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:42:18.806] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200 +[2025-08-24 12:42:19.274] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 12:42:19.278] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 12:42:19.282] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 12:42:19.287] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 12:42:19.296] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:42:19.928] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:48:46.238] [undefined] GET(/:id): try to get video 2 +[2025-08-24 12:48:46.242] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:48:46.254] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-08-24 12:48:46.271] [undefined] GET(/:id/similar): try to get similar videos for video 2 +[2025-08-24 12:48:46.283] [undefined] GET(/:id/similar): successfully get similar videos for video 2 with status 200 +[2025-08-24 12:48:46.326] [undefined] GET(/:id/views): try to add views for video 2 +[2025-08-24 12:48:46.337] [undefined] GET(/:id/views): successfully added views for video 2 with status 200 +[2025-08-24 12:48:47.143] [undefined] GET(/:id/like): try to toggle like on video 2 +[2025-08-24 12:48:47.153] [undefined] GET(/:id/like): no likes found adding likes for video 2 with status 200 +[2025-08-24 12:48:48.072] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:48:48.816] [undefined] GET(/:id): try to get video 2 +[2025-08-24 12:48:48.819] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:48:48.830] [undefined] GET(/:id): successfully get video 2 with status 200 +[2025-08-24 12:48:48.851] [undefined] GET(/:id/similar): try to get similar videos for video 2 +[2025-08-24 12:48:48.865] [undefined] GET(/:id/similar): successfully get similar videos for video 2 with status 200 +[2025-08-24 12:48:48.898] [undefined] GET(/:id/views): try to add views for video 2 +[2025-08-24 12:48:48.907] [undefined] GET(/:id/views): successfully added views for video 2 with status 200 +[2025-08-24 12:48:49.852] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:48:50.610] [undefined] GET(/:id): try to get video 1 +[2025-08-24 12:48:50.613] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:48:50.624] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 12:48:50.644] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 12:48:50.658] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 12:48:50.704] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-24 12:48:50.713] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-24 12:48:51.443] [undefined] GET(/:id/like): try to toggle like on video 1 +[2025-08-24 12:48:51.452] [undefined] GET(/:id/like): no likes found adding likes for video 1 with status 200 +[2025-08-24 12:48:51.747] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 12:52:54.497] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 12:52:54.503] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 12:52:54.507] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 12:52:54.512] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 12:52:54.523] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 12:52:56.045] [undefined] GET(/:id): try to get channel with id 1 +[2025-08-24 12:52:56.055] [undefined] GET(/:id/stats): try to get stats +[2025-08-24 12:52:56.060] [undefined] GET(/:id): Successfully get channel with id 1 with status 200 +[2025-08-24 12:52:56.067] [undefined] GET(/:id/stats): Successfully get stats with status 200 +[2025-08-24 12:52:56.859] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 12:52:56.864] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 12:53:15.671] [undefined] POST(/): try to upload video with status undefined +[2025-08-24 12:53:15.678] [undefined] POST(/): successfully uploaded video with status 200 +[2025-08-24 12:53:15.783] [undefined] POST(/thumbnail): try to add thumbnail to video 3 +[2025-08-24 12:53:15.789] [undefined] POST(/thumbnail): successfully uploaded thumbnail with status 200 +[2025-08-24 12:53:15.816] [undefined] PUT(/:id/tags): try to add tags to video 3 +[2025-08-24 12:53:15.837] [undefined] PUT(/:id/tags): Tag wankil already exists for video 3 with status 200 +[2025-08-24 12:53:15.842] [undefined] PUT(/:id/tags): successfully added tags to video 3 with status 200 +[2025-08-24 12:53:18.269] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:31:53.485] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:33:34.728] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:34:06.863] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:56:14.294] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:56:45.221] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 14:56:52.544] [undefined] GET(/:id): try to get video 3 +[2025-08-24 14:56:52.548] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 14:56:52.607] [undefined] GET(/:id): successfully get video 3 with status 200 +[2025-08-24 14:56:52.625] [undefined] GET(/:id/similar): try to get similar videos for video 3 +[2025-08-24 14:56:52.640] [undefined] GET(/:id/similar): successfully get similar videos for video 3 with status 200 +[2025-08-24 14:56:52.708] [undefined] GET(/:id/views): try to add views for video 3 +[2025-08-24 14:56:52.719] [undefined] GET(/:id/views): successfully added views for video 3 with status 200 +[2025-08-24 14:56:53.526] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:04.764] [undefined] GET(/:id): failed due to invalid values with status 400 +[2025-08-24 15:00:04.774] [undefined] GET(/:id/similar): failed due to invalid values with status 400 +[2025-08-24 15:00:04.783] [undefined] GET(/:id/views): failed due to invalid values with status 400 +[2025-08-24 15:00:04.792] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 15:00:07.941] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:09.836] [undefined] GET(/:id): failed due to invalid values with status 400 +[2025-08-24 15:00:09.848] [undefined] GET(/:id/similar): failed due to invalid values with status 400 +[2025-08-24 15:00:09.858] [undefined] GET(/:id/views): failed due to invalid values with status 400 +[2025-08-24 15:00:09.869] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 15:00:13.276] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:14.341] [undefined] GET(/:id): try to get video 1 +[2025-08-24 15:00:14.345] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 15:00:14.356] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 15:00:14.379] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 15:00:14.396] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 15:00:14.443] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-24 15:00:14.452] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-24 15:00:15.454] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:17.865] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 15:00:17.868] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 15:00:17.872] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 15:00:17.877] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 15:00:17.886] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 15:00:18.411] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200 +[2025-08-24 15:00:19.030] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 15:00:19.036] [undefined] GET(/:id): try to get video 1 +[2025-08-24 15:00:19.047] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200 +[2025-08-24 15:00:19.052] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 15:00:19.072] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 15:00:19.086] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 15:00:19.122] [undefined] GET(/:id/views): try to add views for video 1 +[2025-08-24 15:00:19.131] [undefined] GET(/:id/views): successfully added views for video 1 with status 200 +[2025-08-24 15:00:21.743] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200 +[2025-08-24 15:00:22.774] [undefined] DELETE(/:id/video/:videoId): Video deleted from playlist with id 1 with status 200 +[2025-08-24 15:00:22.844] [undefined] GET(/:id): Playlist retrieved with id 1 with status 200 +[2025-08-24 15:00:24.102] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:26.909] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:00:35.516] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 15:19:55.359] [undefined] GET(/:id): failed due to invalid values with status 400 +[2025-08-24 15:19:55.364] [undefined] GET(/:id/stats): failed due to invalid values with status 400 +[2025-08-24 15:20:00.400] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:16:06.897] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:30:27.130] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:30:53.040] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:31:03.350] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:31:40.409] [undefined] GET(/:id): try to get video 1 +[2025-08-24 18:31:40.421] [undefined] GET(/:id): successfully get video 1 with status 200 +[2025-08-24 18:31:40.483] [undefined] GET(/:id/similar): try to get similar videos for video 1 +[2025-08-24 18:31:40.493] [undefined] GET(/:id/similar): successfully get similar videos for video 1 with status 200 +[2025-08-24 18:32:05.446] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 18:32:05.450] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 18:32:05.454] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 18:32:05.462] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 18:32:05.474] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 18:32:06.968] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 18:32:06.972] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 18:32:06.976] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 18:32:06.981] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 18:32:06.990] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 18:32:07.865] [undefined] GET(/:id/channel): try to retrieve channel of user 1 +[2025-08-24 18:32:07.869] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200 +[2025-08-24 18:32:07.873] [undefined] GET(/:id/history): try to retrieve history of user 1 +[2025-08-24 18:32:07.880] [undefined] GET(/:id/history): successfully retrieved history of user 1 with status 200 +[2025-08-24 18:32:07.888] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200 +[2025-08-24 18:32:10.777] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:16.357] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:17.190] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:17.648] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:25.811] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:39.691] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:32:49.962] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:33:46.919] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:01.427] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:25.992] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:42.706] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:43.614] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:44.171] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:48.277] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:34:54.944] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 1 with status 200 +[2025-08-24 18:35:28.834] [undefined] GET(/see-later): 'See Later' playlist retrieved for user with id 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/backend/server.js b/backend/server.js index ffd5df4..1b3aeb0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -19,6 +19,7 @@ import { Strategy as GitHubStrategy } from "passport-github2"; console.clear(); dotenv.config(); +console.log(process.env) const app = express(); diff --git a/create_db.sql b/create_db.sql new file mode 100644 index 0000000..d4129fd --- /dev/null +++ b/create_db.sql @@ -0,0 +1,4 @@ + + CREATE ROLE 'sacha' WITH PASSWORD 'sacha'; + CREATE DATABASE 'sacha' OWNER 'sacha'; + diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..d4129fd --- /dev/null +++ b/db.sql @@ -0,0 +1,4 @@ + + CREATE ROLE 'sacha' WITH PASSWORD 'sacha'; + CREATE DATABASE 'sacha' OWNER 'sacha'; + diff --git a/default.conf b/default.conf new file mode 100644 index 0000000..65c5154 --- /dev/null +++ b/default.conf @@ -0,0 +1,68 @@ +server { + server_name localhost; + listen 80; + + return 301 https://$host$request_uri; +} + +server { + server_name localhost; + listen 443 ssl; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Allow large file uploads for videos (up to 500MB) + client_max_body_size 500M; + + ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; + ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # API routes - proxy to backend (MUST come before static file rules) + location /api/ { + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '$http_origin' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Max-Age' 1728000 always; + add_header 'Content-Type' 'text/plain; charset=utf-8' always; + add_header 'Content-Length' 0 always; + return 204; + } + + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Origin $http_origin; + proxy_buffering off; + + # CORS headers for actual requests + add_header 'Access-Control-Allow-Origin' '$http_origin' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + + # Also set timeout for large uploads + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # Static assets - NO CACHING for development + location ~* ^/(?!api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; + try_files $uri =404; + } + + # Handle React Router - all other routes should serve index.html + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..93566dc --- /dev/null +++ b/deploy.sh @@ -0,0 +1,100 @@ +#!/bin/bash +if [[ $EUID -ne 0 ]]; then + echo "Ce script doit être lancé avec les droits root" + exit 1 +fi + +pwd=$PWD + +# Environment variables +echo -e "\033[0;32m Création des variables d'environnement \033[0m" +echo -e "\033[1;33m Nom d'utilisateur de la base de données \033[0m" +read POSTGRES_USER +echo -e "\033[1;33m Mot de passe de la base de données \033[0m" +read POSTGRES_PASSWORD +echo -e "\033[1;33m Nom de la base de données \033[0m" +read POSTGRES_DB + +echo -e "\033[1;33m Clé d'encryption des JWTs \033[0m" +read JWT_SECRET + +echo -e "\033[1;33m Utilisateur Gmail \033[0m" +read GMAIL_USER +echo -e "\033[1;33m Mot de passe de l'application Gmail \033[0m" +read GMAIL_PASSWORD + +echo -e "\033[1;33m Url du site web \033[0m" +read FRONTEND_URL + +echo -e "\033[1;33m Client ID de l'application Github OAuth \033[0m" +read GITHUB_ID +echo -e "\033[1;33m Client secret de l'application Github OAuth \033[0m" +read GITHUB_PASSWORD + +touch $pwd/backend/.env +echo " + POSTGRES_USER=$POSTGRES_USER + POSTGRES_PASSWORD=$POSTGRES_PASSWORD + POSTGRES_DB=$POSTGRES_DB + POSTGRES_HOST=localhost + + BACKEND_PORT=8000 + + JWT_SECRET=$JWT_SECRET + + LOG_FILE=/var/log/freetube/access.log + + GMAIL_USER=$GMAIL_USER + GMAIL_PASSWORD=$GMAIL_PASSWORD + + FRONTEND_URL=$FRONTEND_URL + + GITHUB_ID=$GITHUB_ID + GITHUB_SECRET=$GITHUB_PASSWORD +" > $pwd/backend/.env + +# Install dependencies (NodeJS 22/PostgreSQL/Nginx) +echo -e "\033[0;32m Installation des dépendances... \033[0m" +apt install postgresql nginx openssl curl && + +# Install NVM and NodeJS & NPM +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && +export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +\. "$HOME/.nvm/nvm.sh" && +nvm install 22 && + +# Install node packages for backend +echo -e "\033[0;32m Installation des paquets NodeJS \033[0m" +cd $pwd/backend && npm install && +cd $pwd/frontend && npm install && + +echo "Construction du frontend" +npx vite build +cd $pwd + +# Create Nginx configuration +echo -e "\033[0;32m Création de la configuration Nginx \033[0m" +mkdir /etc/nginx/ssl/ +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx-selfsigned.key -out /etc/nginx/ssl/nginx-selfsigned.crt +touch /etc/nginx/conf.d/freetube.conf +cat $pwd/default.conf > /etc/nginx/conf.d/freetube.conf + +echo -e "\033[0;32m Copie des fichiers vers /usr/share/nginx/html \033[0m" +rm /usr/share/nginx/html/index.html +mv $pwd/frontend/dist/* /usr/share/nginx/html/ + +# Create PostgreSQL database +echo -e "\033[0;32m Création de l'utilisateur $POSTGRES_USER \033[0m" + +sudo -u postgres psql -c "CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';" +sudo -u postgres psql -c "CREATE ROLE $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';" +sudo -u postgres psql -c "CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER;" + +# Add log file +mkdir /var/log/freetube +touch /var/log/freetube/access.log +systemctl enable nginx +systemctl enable postgresql +systemctl start nginx +systemctl start postgresql \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 1f2014d..9daba49 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,10 +9,10 @@ services: ports: - "8000:8000" environment: - DB_USER: ${POSTGRES_USER} - DB_NAME: ${POSTGRES_DB} - DB_HOST: db - DB_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_HOST: db + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} JWT_SECRET: ${JWT_SECRET} LOG_FILE: ${LOG_FILE} PORT: ${BACKEND_PORT} diff --git a/freetube.sh b/freetube.sh new file mode 100755 index 0000000..4a4af30 --- /dev/null +++ b/freetube.sh @@ -0,0 +1,2 @@ +#!/bin/bash +node ./backend/server.js \ No newline at end of file diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 3b64e47..195a98f 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -7,6 +7,7 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert const { user, logout, isAuthenticated } = useAuth(); const [searchQuery, setSearchQuery] = useState(''); const [internalAlerts, setInternalAlerts] = useState([]); + const [isNavbarOpen, setIsNavbarOpen] = useState(false); const navigate = useNavigate(); const handleLogout = () => { @@ -40,70 +41,167 @@ export default function Navbar({ isSearchPage = false, alerts = [], onCloseAlert return ( <> - ) diff --git a/frontend/src/components/Recommendations.jsx b/frontend/src/components/Recommendations.jsx index 6a118dc..a8a0f4d 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/SeeLater.jsx b/frontend/src/components/SeeLater.jsx new file mode 100644 index 0000000..e5f8e45 --- /dev/null +++ b/frontend/src/components/SeeLater.jsx @@ -0,0 +1,19 @@ +import VideoCard from "./VideoCard.jsx"; + + +export default function SeeLater({videos}) { + + return ( +
+

A regarder plus tard

+
+
+ {videos && videos.map((video, index) => ( + + ))} +
+
+
+ ) + +} \ No newline at end of file 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/components/TrendingVideos.jsx b/frontend/src/components/TrendingVideos.jsx index f0a4e1e..ff8f1d1 100644 --- a/frontend/src/components/TrendingVideos.jsx +++ b/frontend/src/components/TrendingVideos.jsx @@ -6,7 +6,7 @@ export default function TrendingVideos({ videos }) { return (

Tendances

-
+
{videos && videos.map((video, index) => ( ))} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 7899e9f..14b0083 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -5,7 +5,10 @@ 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'; +import SeeLater from "../components/SeeLater.jsx"; +import {getSeeLater} from "../services/playlist.service.js"; export default function Home() { const { isAuthenticated, user } = useAuth(); @@ -13,15 +16,27 @@ export default function Home() { const [loading, setLoading] = useState(true); const [topCreators, setTopCreators] = useState([]); const [trendingVideos, setTrendingVideos] = useState([]); + const [seeLaterVideos, setSeeLaterVideos] = useState([]); const [alerts, setAlerts] = useState([]); + const navigate = useNavigate(); + useEffect(() => { // Fetch recommendations, top creators, and trending videos const fetchData = async () => { - try { - setRecommendations(await getRecommendations(addAlert)); - } finally { - setLoading(false); + if (isAuthenticated) { + const token = localStorage.getItem('token'); + try { + setRecommendations(await getRecommendations(token, addAlert)); + } finally { + setLoading(false); + } + } else { + try { + setRecommendations(await getRecommendations(null, addAlert)); + } finally { + setLoading(false); + } } try { @@ -30,6 +45,21 @@ export default function Home() { setLoading(false); } + if (isAuthenticated) { + try { + const token = localStorage.getItem('token'); + setSeeLaterVideos(await getSeeLater(token, addAlert)); + } finally { + setLoading(false); + } + } else { + try { + setTopCreators(await getTopCreators(addAlert)); + } finally { + setLoading(false); + } + } + }; fetchData(); }, []); @@ -44,22 +74,22 @@ export default function Home() { }; return ( -
+
-
+
{/* Hero section */} -
- +
+ {isAuthenticated ? ( -

+

Bienvenue {user?.username} !

) : ( <> -

+

Regarder des vidéos comme jamais auparavant

@@ -82,7 +112,16 @@ export default function Home() { {/* Top Creators section */} - + + { + isAuthenticated ? ( + + ) : ( + + ) + } + + {/* Trending Videos section */} diff --git a/frontend/src/services/playlist.service.js b/frontend/src/services/playlist.service.js index e19e401..2393bf9 100644 --- a/frontend/src/services/playlist.service.js +++ b/frontend/src/services/playlist.service.js @@ -102,3 +102,20 @@ export async function deleteVideo(playlistId, videoId, token, addAlert) { addAlert('error', error.message); } } + +export async function getSeeLater(token, addAlert) { + try { + const response = await fetch(`/api/playlists/see-later`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + if (!response.ok) { + throw new Error('Failed to fetch see later playlist'); + } + const data = await response.json(); + return data; + } catch (error) { + addAlert('error', error.message); + } +} diff --git a/frontend/src/services/recommendation.service.js b/frontend/src/services/recommendation.service.js index 7935e43..a54d5c2 100644 --- a/frontend/src/services/recommendation.service.js +++ b/frontend/src/services/recommendation.service.js @@ -1,10 +1,36 @@ -export async function getRecommendations(addAlert) { +export async function getRecommendations(token, addAlert) { + + if (token) { + try { + const response = await fetch('/api/recommendations', { + method: 'GET', + headers: { + "Authorization": `Bearer ${token}`, + } + }); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching data:', error); + addAlert('error', 'Erreur lors du chargement des recommandations'); + } + } else { + try { + const response = await fetch('/api/recommendations'); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching data:', error); + addAlert('error', 'Erreur lors du chargement des recommandations'); + } + } + 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 +46,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