import { getClient } from "../utils/database.js"; import * as path from "node:path"; import * as fs from "node:fs"; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import jwt from "jsonwebtoken"; import { sendEmail } from "../utils/mail.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export async function upload(req, res) { // HANDLE VIDEO FILE const fileBuffer = req.file.buffer; let isGenerate = false; while (isGenerate === false) { const client = await getClient(); let letters = "0123456789ABCDEF"; let hex = ''; for (let i = 0; i < 16; i++) hex += letters[(Math.floor(Math.random() * 16))]; const query = `SELECT * FROM videos WHERE slug = $1`; const result = await client.query(query, [hex]); client.release(); if (result.rows.length === 0) { isGenerate = true; req.body.slug = hex; } } const finalName = req.body.slug + "." + req.file.originalname.split(".")[1]; const destinationPath = path.join(__dirname, "../uploads/videos/" + finalName) fs.writeFileSync(destinationPath, fileBuffer); const client = await getClient(); const video = { title: req.body.title, description: req.body.description, //TODO thumbnail file: "/api/media/video/" + finalName, channel: req.body.channel, slug: req.body.slug, format: req.file.originalname.split(".")[1], visibility: req.body.visibility, } // HANDLE VIDEO DETAILS const logger = req.body.logger; logger.write("try to upload video"); const releaseDate = new Date(Date.now()).toISOString(); const query = `INSERT INTO videos (title, thumbnail, description, channel, visibility, file, slug, format, release_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`; const idResult = await client.query(query, [video.title, 'null', video.description, video.channel, video.visibility, video.file, video.slug, video.format, releaseDate]); const id = idResult.rows[0].id; console.log(req.body.visibility, req.body.authorizedUsers) // HANDLE AUTHORIZED USERS if (req.body.visibility === "private" && req.body.authorizedUsers) { let authorizedUsers = req.body.authorizedUsers; // Parse if still a string (safety check) if (typeof authorizedUsers === 'string') { try { authorizedUsers = JSON.parse(authorizedUsers); } catch (error) { console.error("Failed to parse authorizedUsers:", error); authorizedUsers = []; } } if (Array.isArray(authorizedUsers) && authorizedUsers.length > 0) { for (let i = 0; i < authorizedUsers.length; i++) { const user = authorizedUsers[i]; const query = `INSERT INTO video_authorized_users (video_id, user_id) VALUES ($1, $2)`; // SEND EMAIL TO AUTHORIZED USER const emailQuery = `SELECT email FROM users WHERE id = $1`; const emailResult = await client.query(emailQuery, [user]); const email = emailResult.rows[0].email; const textMessage = `Vous êtes autorisé à visionner une vidéo privée. ${process.env.FRONTEND_URL}/videos/${id}`; const htmlMessage = ` Accès à une vidéo privée - Freetube

🔐 Vidéo privée partagée avec vous!

Bonjour! 👋

Vous avez été autorisé(e) à visionner une vidéo privée sur Freetube!

On vous a partagé une vidéo privée avec vous. Cliquez sur le bouton ci-dessous pour la regarder :

${video.title}

${video.description}

▶️ Regarder la vidéo

🔒 Cette vidéo est privée et n'est accessible qu'aux personnes autorisées.

Si vous pensez avoir reçu cet e-mail par erreur, vous pouvez l'ignorer.

© 2025 Freetube. Tous droits réservés.

`; sendEmail(email, "Invitation à visionner une vidéo privée", textMessage, htmlMessage); await client.query(query, [id, user]); } } } logger.write("successfully uploaded video", 200); await client.release() res.status(200).json({ "message": "Successfully uploaded video", "id": id }); } export async function uploadThumbnail(req, res) { const fileBuffer = req.file.buffer; const client = await getClient(); const videoId = req.body.video; const videoSlugQuery = `SELECT * from videos WHERE id = $1`; const result = await client.query(videoSlugQuery, [videoId]); const fileName = result.rows[0].slug + "." + req.file.originalname.split(".")[1]; const destinationPath = path.join(__dirname, "../uploads/thumbnails/", fileName); fs.writeFileSync(destinationPath, fileBuffer); const logger = req.body.logger; logger.action("try to add thumbnail to video " + req.body.video); const file = "/api/media/thumbnail/" + fileName; const updateQuery = `UPDATE videos SET thumbnail = $1 WHERE id = $2`; await client.query(updateQuery, [file, req.body.video]); logger.write("successfully uploaded thumbnail", 200); await client.release(); res.status(200).json({ "message": "Successfully uploaded thumbnail" }); } export async function getById(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to get video " + id); const client = await getClient(); const query = `SELECT * FROM videos WHERE id = $1`; const result = await client.query(query, [id]); const video = result.rows[0]; // GET VIEWS AND LIKES COUNT const viewsQuery = `SELECT COUNT(*) AS view_count FROM history WHERE video = $1`; const viewsResult = await client.query(viewsQuery, [id]); const likesQuery = `SELECT COUNT(*) AS like_count FROM likes WHERE video = $1`; const likesResult = await client.query(likesQuery, [id]); video.views = viewsResult.rows[0].view_count; video.likes = likesResult.rows[0].like_count; // GET COMMENTS const commentsQuery = `SELECT c.id, c.content, c.created_at, u.username, u.picture FROM comments c JOIN users u ON c.author = u.id WHERE c.video = $1 ORDER BY c.created_at DESC`; const commentsResult = await client.query(commentsQuery, [id]); video.comments = commentsResult.rows; // GET CREATOR const creatorQuery = `SELECT c.id, c.name, c.owner FROM channels c JOIN videos v ON c.id = v.channel WHERE v.id = $1`; const creatorResult = await client.query(creatorQuery, [id]); video.creator = creatorResult.rows[0]; // GET CREATOR PROFILE PICTURE const profilePictureQuery = `SELECT picture FROM users WHERE id = $1`; const profilePictureResult = await client.query(profilePictureQuery, [video.creator.owner]); video.creator.profile_picture = profilePictureResult.rows[0].picture; // GET CREATOR SUBSCRIBERS COUNT const subscribersQuery = `SELECT COUNT(*) AS subscriber_count FROM subscriptions WHERE channel = $1`; const subscribersResult = await client.query(subscribersQuery, [video.creator.id]); video.creator.subscribers = subscribersResult.rows[0].subscriber_count; // GET TAGS const tagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = $1`; const tagsResult = await client.query(tagsQuery, [id]); video.tags = tagsResult.rows.map(tag => tag.name); // GET AUTHORIZED USERS const authorizedUsersQuery = `SELECT u.id, u.username, u.picture FROM users u JOIN video_authorized_users vp ON u.id = vp.user_id WHERE vp.video_id = $1`; const authorizedUsersResult = await client.query(authorizedUsersQuery, [id]); video.authorizedUsers = authorizedUsersResult.rows; logger.write("successfully get video " + id, 200); client.release() res.status(200).json(video); } export async function getByChannel(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to get video from channel " + id); const client = await getClient(); const query = `SELECT * FROM videos WHERE channel = $1`; const result = await client.query(query, [id]); logger.write("successfully get video from channel " + id, 200); client.release() res.status(200).json(result.rows); } export async function update(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to update video " + id); const client = await getClient(); const query = `UPDATE videos SET title = $1, description = $2, visibility = $3 WHERE id = $4`; await client.query(query, [req.body.title, req.body.description, req.body.visibility, id]); const resultQuery = ` SELECT videos.id, videos.title, videos.thumbnail, videos.channel, videos.file, videos.description, videos.visibility, videos.release_date, COUNT(DISTINCT l.id) AS likes_count, COUNT(DISTINCT h.id) AS history_count, JSON_AGG( JSON_BUILD_OBJECT( 'id', c.id, 'content', c.content, 'username', u.username, 'video', c.video, 'created_at', c.created_at, 'picture', u.picture ) ) FILTER ( WHERE c.id IS NOT NULL ) AS comments, JSON_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags FROM public.videos LEFT JOIN public.likes l ON l.video = videos.id LEFT JOIN public.history h ON h.video = videos.id LEFT JOIN public.comments c ON c.video = videos.id LEFT JOIN public.video_tags vt ON vt.video = videos.id LEFT JOIN public.tags t ON vt.tag = t.id LEFT JOIN public.users u ON u.id = c.author WHERE videos.id = $1 GROUP BY public.videos.id `; const result = await client.query(resultQuery, [id]); logger.write("successfully updated video", 200); client.release() res.status(200).json(result.rows[0]); } export async function updateVideo(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to update video file " + id); const client = await getClient(); const videoQuery = `SELECT slug, format FROM videos WHERE id = $1`; const videoResult = await client.query(videoQuery, [id]); const video = videoResult.rows[0]; const slug = video.slug; const format = video.format; const pathToDelete = path.join(__dirname, "../uploads/videos/", slug + "." + format); fs.unlink(pathToDelete, (error) => { if (error) { logger.write(error, 500); client.release() res.status(500).json({ "message": "Failed to delete video" }); return } logger.action("successfully deleted video " + slug + "." + format); const fileBuffer = req.file.buffer; const finalName = slug + "." + format; const destinationPath = path.join(__dirname, "../uploads/videos/" + finalName) fs.writeFileSync(destinationPath, fileBuffer); logger.write("successfully updated video", 200); client.release() res.status(200).json({ "message": "Successfully updated video" }); }) } export async function del(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to delete video " + id); const client = await getClient(); const query = `SELECT slug, format, thumbnail FROM videos WHERE id = $1`; const result = await client.query(query, [id]); const video = result.rows[0]; const slug = video.slug; const format = video.format; const thumbnailFile = video.thumbnail.split("/")[4]; let pathToDelete = path.join(__dirname, "../uploads/videos/", slug + "." + format); fs.unlink(pathToDelete, (error) => { if (error) { logger.write(error, 500); client.release() res.status(500).json({ "message": "Failed to delete video" }); return } pathToDelete = path.join(__dirname, "../uploads/thumbnails/", thumbnailFile); fs.unlink(pathToDelete, async (error) => { if (error) { logger.write(error, 500); res.status(500).json({ "message": "Failed to delete video" }); return } const query = `DELETE FROM videos WHERE id = $1`; await client.query(query, [id]); logger.write("successfully deleted video", 200); client.release() res.status(200).json({ "message": "Successfully deleted video" }); }) }) } export async function toggleLike(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to toggle like on video " + id); const token = req.headers.authorization.split(" ")[1]; const claims = jwt.decode(token); const client = await getClient(); const userId = claims.id; const getLikeQuery = `SELECT * FROM likes WHERE owner = $1 AND video = $2`; const likeResult = await client.query(getLikeQuery, [userId, id]); if (likeResult.rows.length === 0) { const query = `INSERT INTO likes (video, owner) VALUES ($1, $2)`; await client.query(query, [id, userId]); // GET LIKES COUNT const likesCountQuery = `SELECT COUNT(*) AS like_count FROM likes WHERE video = $1`; const likesCountResult = await client.query(likesCountQuery, [id]); const likesCount = likesCountResult.rows[0].like_count; logger.write("no likes found adding likes for video " + id, 200); client.release(); res.status(200).json({ "message": "Successfully added like", "likes": likesCount }); } else { const query = `DELETE FROM likes WHERE owner = $1 AND video = $2`; await client.query(query, [userId, id]); // GET LIKES COUNT const likesCountQuery = `SELECT COUNT(*) AS like_count FROM likes WHERE video = $1`; const likesCountResult = await client.query(likesCountQuery, [id]); const likesCount = likesCountResult.rows[0].like_count; logger.write("likes found, removing like for video " + id, 200); client.release(); res.status(200).json({ "message": "Successfully removed like", "likes": likesCount }); } } export async function addTags(req, res) { const tags = req.body.tags; const videoId = req.params.id; const logger = req.body.logger; logger.action("try to add tags to video " + videoId); const client = await getClient(); // DECREASE USAGE COUNT FOR ALL TAGS const decreaseUsageCountQuery = `UPDATE tags SET usage_count = usage_count - 1 WHERE id IN (SELECT tag FROM video_tags WHERE video = $1)`; await client.query(decreaseUsageCountQuery, [videoId]); // DELETE ALL TAGS FOR VIDEO let deleteQuery = `DELETE FROM video_tags WHERE video = $1`; await client.query(deleteQuery, [videoId]); // INSERT NEW TAGS for (const tag of tags) { const tagQuery = `SELECT * FROM tags WHERE name = $1`; const tagResult = await client.query(tagQuery, [tag]); let id = null; if (tagResult.rows.length === 0) { const insertTagQuery = `INSERT INTO tags (name, usage_count) VALUES ($1, 1) RETURNING id`; const result = await client.query(insertTagQuery, [tag]); id = result.rows[0].id; } else { logger.write("Tag " + tag + " already exists for video " + videoId, 200); const getTagQuery = `SELECT usage_count FROM tags WHERE name = $1`; const getTagResult = await client.query(getTagQuery, [tag]); const usageCount = getTagResult.rows[0].usage_count + 1; const updateTagQuery = `UPDATE tags SET usage_count = $1 WHERE name = $2`; await client.query(updateTagQuery, [usageCount, tag]); id = tagResult.rows[0].id; } // INSERT INTO video_tags const insertVideoTagQuery = `INSERT INTO video_tags (tag, video) VALUES ($1, $2)`; await client.query(insertVideoTagQuery, [id, videoId]); } // GET UPDATED TAGS FOR VIDEO const updatedTagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = $1`; const updatedTagsResult = await client.query(updatedTagsQuery, [videoId]); const updatedTags = updatedTagsResult.rows; logger.write("successfully added tags to video " + videoId, 200); await client.release(); res.status(200).json({ "message": "Successfully added tags to video", "tags": updatedTags.map(tag => tag.name) }); } export async function getSimilarVideos(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to get similar videos for video " + id); const client = await getClient(); // Get tags for the video const tagsQuery = `SELECT t.name FROM tags t JOIN video_tags vt ON t.id = vt.tag WHERE vt.video = $1`; const tagsResult = await client.query(tagsQuery, [id]); const tags = tagsResult.rows.map(row => row.name); if (tags.length === 0) { logger.write("No tags found for video " + id, 404); res.status(404).json({ "message": "No similar videos found" }); return; } // Find similar videos based on tags which are not the same as the current video and limit to 10 results const similarVideosQuery = ` SELECT v.id, v.title, v.thumbnail, v.file, v.slug, v.format, v.release_date, c.name AS creator_name, c.id AS creator_id, c.owner AS creator_owner FROM videos v JOIN video_tags vt ON v.id = vt.video JOIN tags t ON vt.tag = t.id JOIN channels c ON v.channel = c.id WHERE t.name = ANY($1) AND v.id != $2 AND v.visibility = 'public' GROUP BY v.id, c.name, c.id LIMIT 10; `; const result = await client.query(similarVideosQuery, [tags, id]); for (let video of result.rows) { // Get views count for each video const viewsQuery = `SELECT COUNT(*) AS view_count FROM history WHERE video = $1`; const viewsResult = await client.query(viewsQuery, [video.id]); video.views = viewsResult.rows[0].view_count; // Get creator profile picture const profilePictureQuery = `SELECT picture FROM users WHERE id = $1`; const profilePictureResult = await client.query(profilePictureQuery, [video.creator_owner]); // Put creator info in video.creator video.creator = { id: video.creator_id, name: video.creator_name, profilePicture: profilePictureResult.rows[0].picture }; // Remove creator_id and creator_name from the video object delete video.creator_id; delete video.creator_name; } logger.write("successfully get similar videos for video " + id, 200); await client.release(); res.status(200).json(result.rows); } export async function getLikesPerDay(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to get likes per day"); const client = await getClient(); try { const response = {} const likeQuery = ` SELECT DATE(created_at) as date, COUNT(*) as count FROM likes WHERE video = $1 GROUP BY DATE(created_at) ORDER BY date DESC LIMIT 30 `; const viewQuery = ` SELECT DATE(viewed_at) as date, COUNT(*) as count FROM history WHERE video = $1 GROUP BY DATE(viewed_at) ORDER BY date DESC LIMIT 30 `; const resultViews = await client.query(viewQuery, [id]); response.views = resultViews.rows; const resultLikes = await client.query(likeQuery, [id]); response.likes = resultLikes.rows; logger.write("successfully retrieved likes per day", 200); res.status(200).json(response); } catch (error) { logger.write("Error retrieving likes per day: " + error.message, 500); res.status(500).json({ error: "Internal server error" }); } finally { await client.release(); } } export async function addViews(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to add views for video " + id); const token = req.headers.authorization.split(" ")[1]; const claims = jwt.decode(token); const userId = claims.id; // ADD VIEW TO HISTORY IF NOT EXISTS const client = await getClient(); const query = `SELECT * FROM history WHERE video = $1 AND user_id = $2`; const result = await client.query(query, [id, userId]); if (result.rows.length === 0) { const insertQuery = `INSERT INTO history (video, user_id) VALUES ($1, $2)`; await client.query(insertQuery, [id, userId]); } logger.write("successfully added views for video " + id, 200); await client.release(); res.status(200).json({ "message": "Successfully added views" }); } export async function updateAuthorizedUsers(req, res) { const id = req.params.id; const logger = req.body.logger; logger.action("try to update authorized users for video " + id); const { authorizedUsers } = req.body; const client = await getClient(); try { // Remove all existing authorized users const deleteQuery = `DELETE FROM video_authorized_users WHERE video_id = $1`; await client.query(deleteQuery, [id]); // Add new authorized users const insertQuery = `INSERT INTO video_authorized_users (video_id, user_id) VALUES ($1, $2)`; for (let i = 0; i < authorizedUsers.length; i++) { const user = authorizedUsers[i]; console.log(`INSERT INTO video_authorized_users (video_id, user_id) VALUES (${id}, ${user})`) await client.query(insertQuery, [id, user]); } logger.write("successfully updated authorized users for video " + id, 200); res.status(200).json({ "message": "Successfully updated authorized users" }); } catch (error) { logger.write("Error updating authorized users: " + error.message, 500); res.status(500).json({ error: "Internal server error" }); } finally { await client.release(); } }