Browse Source

Added gargantuesque trending query

features/trending
astria 4 months ago
parent
commit
b2e7f459bd
  1. 72
      backend/app/controllers/playlist.controller.js
  2. 131
      backend/app/controllers/recommendation.controller.js
  3. 6
      backend/app/routes/playlist.route.js
  4. 8
      backend/app/utils/database.js
  5. 282
      backend/logs/access.log
  6. 1
      backend/server.js
  7. 4
      create_db.sql
  8. 4
      db.sql
  9. 68
      default.conf
  10. 100
      deploy.sh
  11. 8
      docker-compose.yaml
  12. 2
      freetube.sh
  13. 220
      frontend/src/components/Navbar.jsx
  14. 4
      frontend/src/components/Recommendations.jsx
  15. 19
      frontend/src/components/SeeLater.jsx
  16. 2
      frontend/src/components/TrendingVideos.jsx
  17. 60
      frontend/src/pages/Home.jsx
  18. 17
      frontend/src/services/playlist.service.js
  19. 28
      frontend/src/services/recommendation.service.js

72
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" });
}
}

131
backend/app/controllers/recommendation.controller.js

@ -1,5 +1,5 @@
import {getClient} from "../utils/database.js";
import jwt from 'jsonwebtoken';
export async function getRecommendations(req, res) {
@ -68,23 +68,128 @@ export async function getRecommendations(req, res) {
} 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);
}
}

6
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;

8
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
})

282
backend/logs/access.log

@ -8524,3 +8524,285 @@
[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

1
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();

4
create_db.sql

@ -0,0 +1,4 @@
CREATE ROLE 'sacha' WITH PASSWORD 'sacha';
CREATE DATABASE 'sacha' OWNER 'sacha';

4
db.sql

@ -0,0 +1,4 @@
CREATE ROLE 'sacha' WITH PASSWORD 'sacha';
CREATE DATABASE 'sacha' OWNER 'sacha';

68
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;
}
}

100
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

8
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}

2
freetube.sh

@ -0,0 +1,2 @@
#!/bin/bash
node ./backend/server.js

220
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 (
<>
<nav className="flex justify-between items-center p-4 text-white absolute top-0 left-0 w-screen">
<div>
<h1 className="font-montserrat text-5xl font-black">
<a href="/">FreeTube</a>
</h1>
</div>
<div>
<ul className="flex items-center space-x-[44px] font-montserrat text-2xl font-black">
<li><a href="/">Accueil</a></li>
{isAuthenticated ? (
<>
<li><a href="/">Abonnements</a></li>
<li>
<a href="/profile" className="flex items-center space-x-4">
<span className="text-2xl">{user?.username}</span>
{user?.picture && (
<img
src={`${user.picture}`}
alt="Profile"
className="w-8 h-8 rounded-full object-cover"
/>
)}
</a>
</li>
<li>
<button
onClick={handleLogout}
className="bg-red-600 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer hover:bg-red-700"
>
Déconnexion
</button>
<nav className="flex justify-between items-center p-4 text-white top-0 left-0 w-screen z-50 relative">
<div>
<h1 className="font-montserrat text-4xl lg:text-5xl font-black">
<a href="/">FreeTube</a>
</h1>
</div>
<div className="hidden lg:block" >
<ul className="flex items-center space-x-[44px] font-montserrat text-2xl font-black">
<li><a href="/">Accueil</a></li>
{isAuthenticated ? (
<>
<li><a href="/">Abonnements</a></li>
<li>
<a href="/profile" className="flex items-center space-x-4">
<span className="text-2xl">{user?.username}</span>
{user?.picture && (
<img
src={`${user.picture}`}
alt="Profile"
className="w-8 h-8 rounded-full object-cover"
/>
)}
</a>
</li>
<li>
<button
onClick={handleLogout}
className="bg-red-600 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer hover:bg-red-700"
>
Déconnexion
</button>
</li>
</>
) : (
<>
<li><a href="/login">Se connecter</a></li>
<li className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</li>
</>
)}
{ !isSearchPage && (
<li className="bg-glass backdrop-blur-glass border-glass-full border rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer w-[600/1920] h-[50px] px-3 flex items-center justify-center">
<input
type="text"
name="search"
id="searchbar"
placeholder="Rechercher"
className="font-inter text-2xl font-normal focus:outline-none bg-transparent"
onKeyPress={(e) => handleKeypress(e)}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<circle cx="18.381" cy="13.619" r="12.119" stroke="white" strokeWidth="3"/>
<line x1="9.63207" y1="22.2035" x2="1.06064" y2="30.7749" stroke="white" strokeWidth="3"/>
</svg>
</li>
</>
) : (
<>
<li><a href="/login">Se connecter</a></li>
<li className="bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer">
<a href="/register">
<p>Créer un compte</p>
</a>
</li>
</>
)}
{ !isSearchPage && (
<li className="bg-glass backdrop-blur-glass border-glass-full border rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer w-[600/1920] h-[50px] px-3 flex items-center justify-center">
<input
type="text"
name="search"
id="searchbar"
placeholder="Rechercher"
className="font-inter text-2xl font-normal focus:outline-none bg-transparent"
onKeyPress={(e) => handleKeypress(e)}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<circle cx="18.381" cy="13.619" r="12.119" stroke="white" strokeWidth="3"/>
<line x1="9.63207" y1="22.2035" x2="1.06064" y2="30.7749" stroke="white" strokeWidth="3"/>
)}
</ul>
</div>
<div className="lg:hidden">
{/* Hamburger menu for mobile */}
<button
onClick={() => setIsNavbarOpen(!isNavbarOpen)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
</div>
</nav>
{/* Mobile menu overlay - moved outside of nav container */}
{isNavbarOpen && (
<div className="fixed inset-0 bg-primary z-50 lg:hidden w-screen h-screen">
<div className="flex justify-between items-center p-4">
<h1 className="font-montserrat text-4xl font-black text-white">
<a href="/">FreeTube</a>
</h1>
<button
onClick={() => setIsNavbarOpen(false)}
className="text-white"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</li>
)}
</button>
</div>
<div className="px-4 py-8">
<ul className="space-y-6 font-montserrat text-2xl font-black text-white">
<li><a href="/" onClick={() => setIsNavbarOpen(false)}>Accueil</a></li>
{isAuthenticated ? (
<>
<li><a href="/" onClick={() => setIsNavbarOpen(false)}>Abonnements</a></li>
<li>
<a href="/profile" className="flex items-center space-x-4" onClick={() => setIsNavbarOpen(false)}>
<span className="text-2xl">{user?.username}</span>
{user?.picture && (
<img
src={`${user.picture}`}
alt="Profile"
className="w-8 h-8 rounded-full object-cover"
/>
)}
</a>
</li>
<li>
<button
onClick={() => {
handleLogout();
setIsNavbarOpen(false);
}}
className="bg-red-600 p-3 rounded-sm text-white font-montserrat text-2xl font-black cursor-pointer hover:bg-red-700"
>
Déconnexion
</button>
</li>
</>
) : (
<>
<li><a href="/login" onClick={() => setIsNavbarOpen(false)}>Se connecter</a></li>
<li>
<a href="/register" onClick={() => setIsNavbarOpen(false)} className="block bg-primary p-3 rounded-sm text-white font-montserrat text-2xl font-black">
Créer un compte
</a>
</li>
</>
)}
{!isSearchPage && (
<li className="mt-8">
<div className="bg-glass backdrop-blur-glass border-glass-full border rounded-sm text-white font-montserrat text-2xl font-black p-3 flex items-center">
<input
type="text"
name="search"
id="searchbar-mobile"
placeholder="Rechercher"
className="font-inter text-2xl font-normal focus:outline-none bg-transparent flex-1"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleKeypress(e);
setIsNavbarOpen(false);
}
}}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<circle cx="18.381" cy="13.619" r="12.119" stroke="white" strokeWidth="3"/>
<line x1="9.63207" y1="22.2035" x2="1.06064" y2="30.7749" stroke="white" strokeWidth="3"/>
</svg>
</div>
</li>
)}
</ul>
</div>
</div>
)}
</ul>
</div>
</nav>
<AlertList alerts={allAlerts} onCloseAlert={handleCloseAlert} />
</>
)

4
frontend/src/components/Recommendations.jsx

@ -3,10 +3,10 @@ import VideoCard from "./VideoCard";
export default function Recommendations({videos}) {
console.log(videos);
return (
<div>
<div className="">
<h2 className="text-3xl font-bold mb-4 text-white">Recommendations</h2>
<div>
<div className="grid grid-cols-5 gap-8 mt-8">
<div className="grid grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => (
<VideoCard key={video.id || index} video={video} />
))}

19
frontend/src/components/SeeLater.jsx

@ -0,0 +1,19 @@
import VideoCard from "./VideoCard.jsx";
export default function SeeLater({videos}) {
return (
<div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">A regarder plus tard</h2>
<div>
<div className="grid grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => (
<VideoCard key={video.id || index} video={video} />
))}
</div>
</div>
</div>
)
}

2
frontend/src/components/TrendingVideos.jsx

@ -6,7 +6,7 @@ export default function TrendingVideos({ videos }) {
return (
<div className="mt-10">
<h2 className="text-3xl font-bold mb-4 text-white">Tendances</h2>
<div className="grid grid-cols-5 gap-8 mt-8">
<div className="grid grid-cols-5 gap-8 mt-2">
{videos && videos.map((video, index) => (
<VideoCard video={video} key={index} />
))}

60
frontend/src/pages/Home.jsx

@ -7,6 +7,8 @@ import TopCreators from "../components/TopCreators.jsx";
import TrendingVideos from "../components/TrendingVideos.jsx";
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();
@ -14,6 +16,7 @@ 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();
@ -21,10 +24,19 @@ export default function Home() {
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 {
@ -33,10 +45,19 @@ export default function Home() {
setLoading(false);
}
try {
setTopCreators(await getTopCreators(addAlert));
} finally {
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);
}
}
};
@ -53,22 +74,22 @@ export default function Home() {
};
return (
<div className="min-w-screen min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<div className=" lg:min-w-screen lg:min-h-screen bg-linear-to-br from-left-gradient to-right-gradient">
<Navbar isSearchPage={false} />
<main className="px-36">
<main className=" px-5 lg:px-36">
{/* Hero section */}
<div className="flex flex-col items-center w-full pt-[304px]">
<img src={HeroImage} alt="" className="w-1046/1920" />
<div className="flex flex-col items-center w-full pt-[128px] lg:pt-[304px]">
<img src={HeroImage} alt="" className=" w-1700/1920 lg:w-1046/1920" />
{isAuthenticated ? (
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
<h1 className="font-montserrat text-4xl lg:text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Bienvenue {user?.username} !
</h1>
) : (
<>
<h1 className="font-montserrat text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
<h1 className="font-montserrat text-4xl lg:text-8xl font-black w-1200/1920 text-center text-white -translate-y-1/2">
Regarder des vidéos comme jamais auparavant
</h1>
<div className="flex justify-center gap-28 -translate-y-[100px] mt-10">
@ -91,7 +112,16 @@ export default function Home() {
<Recommendations videos={recommendations} />
{/* Top Creators section */}
<TopCreators creators={topCreators} navigate={navigate} />
{
isAuthenticated ? (
<SeeLater videos={seeLaterVideos} />
) : (
<TopCreators creators={topCreators} navigate={navigate} />
)
}
{/* Trending Videos section */}
<TrendingVideos videos={trendingVideos} />

17
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);
}
}

28
frontend/src/services/recommendation.service.js

@ -1,6 +1,32 @@
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();

Loading…
Cancel
Save