From 83fa2815ad523bf33d3252a48ac1273d6c454500 Mon Sep 17 00:00:00 2001 From: Rahul Vyas Date: Fri, 19 Sep 2025 03:04:19 +0530 Subject: [PATCH] feat: Implement playlist management features including create, update, delete, and video management feat: Enhance subscription functionality with toggle subscription and fetch subscriber details feat: Add tweet management capabilities including create, update, delete, and fetch tweets feat: Expand video management with features for publishing, updating, deleting, and fetching videos feat: Introduce email validation middleware using Abstract API fix: Add user validation methods in comment, playlist, tweet, and video models feat: Implement like management routes and controllers for videos, comments, and tweets feat: Enhance video routes with search, trending, random, and user-specific video fetching feat: Add cloudinary file deletion utility for managing video and thumbnail uploads --- .env.sample | 3 +- src/controllers/comment.controller.js | 90 ++++- src/controllers/dashboard.controller.js | 91 ++++- src/controllers/healthcheck.controller.js | 17 +- src/controllers/like.controller.js | 231 +++++++++-- src/controllers/playlist.controller.js | 208 ++++++++-- src/controllers/subscription.controller.js | 86 ++++- src/controllers/tweet.controller.js | 158 +++++++- src/controllers/video.controller.js | 379 +++++++++++++++++-- src/middlewares/emailValidator.middleware.js | 29 ++ src/models/comment.model.js | 4 +- src/models/like.model.js | 1 - src/models/playlist.model.js | 9 +- src/models/tweet.model.js | 8 +- src/models/user.model.js | 2 + src/models/video.model.js | 93 +++-- src/routes/like.routes.js | 22 +- src/routes/tweet.routes.js | 4 + src/routes/video.routes.js | 78 ++-- src/utils/cloudinary.js | 10 +- 20 files changed, 1275 insertions(+), 248 deletions(-) create mode 100644 src/middlewares/emailValidator.middleware.js diff --git a/.env.sample b/.env.sample index fec6db35..5d94b5f4 100644 --- a/.env.sample +++ b/.env.sample @@ -8,4 +8,5 @@ REFRESH_TOKEN_EXPIRY=10d CLOUDINARY_CLOUD_NAME= CLOUDINARY_API_KEY= -CLOUDINARY_API_SECRET= \ No newline at end of file +CLOUDINARY_API_SECRET= +ABSTRACT_API_KEY= \ No newline at end of file diff --git a/src/controllers/comment.controller.js b/src/controllers/comment.controller.js index 47b5a821..dce7f286 100644 --- a/src/controllers/comment.controller.js +++ b/src/controllers/comment.controller.js @@ -6,21 +6,107 @@ import {asyncHandler} from "../utils/asyncHandler.js" const getVideoComments = asyncHandler(async (req, res) => { //TODO: get all comments for a video - const {videoId} = req.params - const {page = 1, limit = 10} = req.query + const { videoId } = req.params; + const { page = 1, limit = 10 } = req.query; + if (!mongoose.Types.ObjectId.isValid(videoId)) + throw new ApiError(400, "Invalid videoId"); + + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + populate: { path: "owner", select: "username avatar" }, + }; + const result = await Comment.aggregatePaginate( + [ + { $match: { video: new mongoose.Types.ObjectId(videoId) } }, + { + $addFields: { + areYouOwner: { + $eq: ["$owner", new mongoose.Types.ObjectId(req.user._id)], + }, + }, + }, + ], + options + ); + res + .status(200) + .json(new ApiResponse(200, "Comments fetched successfully", result)); }) const addComment = asyncHandler(async (req, res) => { // TODO: add a comment to a video + + const content = req.body?.content; + const videoId = req.params?.videoId; + if (!content || !videoId) { + throw new ApiError(400, "Content and videoId are required"); + } + const userId = req.user._id; + if (!userId) throw new ApiError(401, "Unauthorized"); + if (!mongoose.Types.ObjectId.isValid(videoId)) + throw new ApiError(400, "Invalid videoId"); + + const comment = await Comment.create({ + content, + video: videoId, + owner: userId, + }); + + res + .status(201) + .json(new ApiResponse(201, "Comment added successfully", comment)); }) const updateComment = asyncHandler(async (req, res) => { // TODO: update a comment + const { commentId } = req.params; + const content = req.body?.content; + // console.log('this iscontent ',content,commentId); + + if (!content) throw new ApiError(400, "Content is required"); + if (!mongoose.Types.ObjectId.isValid(commentId)) + throw new ApiError(400, "Invalid commentId"); + + const userId = req.user._id; + if (!userId) + throw new ApiError(401, "Unauthorized or Token has been expired"); + const comment = await Comment.findById(commentId); + if (!comment) throw new ApiError(404, "Comment not found"); + const validUser = comment.validateUser(userId); + if (!validUser) + throw new ApiError(403, "You are not allowed to update this comment"); + comment.content = content; + await comment.save(); + res + .status(200) + .json(new ApiResponse(200, "Comment updated successfully", comment)); }) const deleteComment = asyncHandler(async (req, res) => { // TODO: delete a comment + const { commentId } = req.params; + if (!mongoose.Types.ObjectId.isValid(commentId)) + throw new ApiError(400, "Invalid commentId"); + const userId = req.user._id; + if (!userId) + throw new ApiError(401, "Unauthorized or Token has been expired"); + + const comment = await Comment.findById(commentId); + if (!comment) throw new ApiError(404, "Comment not found"); + + const validUser = comment.validateUser(userId); + if (!validUser) + throw new ApiError(403, "You are not allowed to delete this comment"); + + const deletedComment = await Comment.findByIdAndDelete(commentId); + res + .status(200) + .json( + new ApiResponse(200, "Comment deleted successfully", deletedComment) + ); }) export { diff --git a/src/controllers/dashboard.controller.js b/src/controllers/dashboard.controller.js index dd1748e4..1b8f52fb 100644 --- a/src/controllers/dashboard.controller.js +++ b/src/controllers/dashboard.controller.js @@ -1,20 +1,81 @@ -import mongoose from "mongoose" -import {Video} from "../models/video.model.js" -import {Subscription} from "../models/subscription.model.js" -import {Like} from "../models/like.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose from "mongoose"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import { Video } from "../models/video.model.js"; +import { Subscription } from "../models/subscription.model.js"; +import { Like } from "../models/like.model.js"; const getChannelStats = asyncHandler(async (req, res) => { - // TODO: Get the channel stats like total video views, total subscribers, total videos, total likes etc. -}) + // TODO: Get the channel stats like total video views, total subscribers, total videos, total likes etc. + const channelId = req.user._id; + + if (!channelId) { + throw new ApiError(401, "Not a valid channel"); + } + + const totalVideos = await Video.countDocuments({ owner: channelId }); + + const viewsAgg = await Video.aggregate([ + { $match: { owner: new mongoose.Types.ObjectId(channelId) } }, + { $group: { _id: null, totalViews: { $sum: "$views" } } }, + ]); + const totalViews = viewsAgg.length > 0 ? viewsAgg[0].totalViews : 0; + + const totalSubscribers = await Subscription.countDocuments({ + channel: channelId, + }); + + const likesAgg = await Like.aggregate([ + { + $lookup: { + from: "videos", + localField: "video", + foreignField: "_id", + as: "videoData", + }, + }, + { $unwind: "$videoData" }, + { $match: { "videoData.owner": new mongoose.Types.ObjectId(channelId) } } + ]); + + const totalLikes = likesAgg.length > 0 ? likesAgg.length : 0; + + return res.status(200).json( + new ApiResponse( + 200, + { + totalVideos, + totalViews, + totalSubscribers, + totalLikes, + }, + "Channel stats fetched successfully" + ) + ); +}); + const getChannelVideos = asyncHandler(async (req, res) => { - // TODO: Get all the videos uploaded by the channel -}) + // TODO: Get all the videos uploaded by the channel + const userId = req.user._id; + if (!mongoose.Types.ObjectId.isValid(userId)) { + throw new ApiError(400, "Invalid user ID"); + } + const { page = 1, limit = 10 } = req.query; + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + }; + const result = await Video.aggregatePaginate( + [{ $match: { owner: new mongoose.Types.ObjectId(userId) } }], + options + ); + if (!result.docs[0]) throw new ApiError(404, "No Video Found"); + return res + .status(200) + .json(new ApiResponse(200, "Videos fetched successfully", result)); +}); -export { - getChannelStats, - getChannelVideos - } \ No newline at end of file +export { getChannelStats, getChannelVideos }; diff --git a/src/controllers/healthcheck.controller.js b/src/controllers/healthcheck.controller.js index ed5921de..05a11214 100644 --- a/src/controllers/healthcheck.controller.js +++ b/src/controllers/healthcheck.controller.js @@ -1,13 +1,10 @@ -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const healthcheck = asyncHandler(async (req, res) => { - //TODO: build a healthcheck response that simply returns the OK status as json with a message -}) + //TODO: build a healthcheck response that simply returns the OK status as json with a message + return res.status(200).json(new ApiResponse("OK", null, "Server is healty")); +}); -export { - healthcheck - } - \ No newline at end of file +export { healthcheck }; diff --git a/src/controllers/like.controller.js b/src/controllers/like.controller.js index de76ee15..4a77c919 100644 --- a/src/controllers/like.controller.js +++ b/src/controllers/like.controller.js @@ -1,33 +1,220 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Like} from "../models/like.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose, { isValidObjectId } from "mongoose"; +import { Like } from "../models/like.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import { Video } from "../models/video.model.js"; +import { Comment } from "../models/comment.model.js"; +import { Tweet } from "../models/tweet.model.js"; const toggleVideoLike = asyncHandler(async (req, res) => { - const {videoId} = req.params - //TODO: toggle like on video -}) + //TODO: toggle like on video + const { videoId } = req.params; + if (!videoId) throw new ApiError(400, "videoId is required"); + const userId = req.user._id; + if (!userId) throw new ApiError(401, "Unauthorized"); + if (!mongoose.Types.ObjectId.isValid(videoId)) + throw new ApiError(400, "Invalid videoId"); + + const isValidId = await Video.findById(videoId); + if (!isValidId) { + throw new ApiError(404, "No video found to this ID"); + } + + const existingLike = await Like.findOne({ + video: videoId, + likedBy: userId, + }); + console.log("this is video like", existingLike); + + if (existingLike) { + await Like.findByIdAndDelete(existingLike._id); + return res + .status(200) + .json(new ApiResponse(200, "Video unliked successfully")); + } else { + const newLike = await Like.create({ video: videoId, likedBy: userId }); + return res + .status(201) + .json(new ApiResponse(201, "Video liked successfully", newLike)); + } +}); const toggleCommentLike = asyncHandler(async (req, res) => { - const {commentId} = req.params - //TODO: toggle like on comment + //TODO: toggle like on comment + const { commentId } = req.params; + if (!commentId) throw new ApiError(400, "commentId is required"); + const userId = req.user._id; + + if (!userId) throw new ApiError(401, "Unauthorized"); + + if (!mongoose.Types.ObjectId.isValid(commentId)) + throw new ApiError(400, "Invalid commentId"); -}) + const isValidId = await Comment.findById(commentId); + if (!isValidId) { + throw new ApiError(404, "No comment found to this ID"); + } + + const existingLike = await Like.findOne({ + comment: commentId, + likedBy: userId, + }); + if (existingLike) { + await Like.findByIdAndDelete(existingLike._id); + return res + .status(200) + .json(new ApiResponse(200, "Comment unliked successfully")); + } else { + const newLike = await Like.create({ + comment: commentId, + likedBy: userId, + }); + return res + .status(201) + .json(new ApiResponse(201, "Comment liked successfully", newLike)); + } +}); const toggleTweetLike = asyncHandler(async (req, res) => { - const {tweetId} = req.params - //TODO: toggle like on tweet -} -) + //TODO: toggle like on tweet + const { tweetId } = req.params; + if (!tweetId) throw new ApiError(400, "tweetId is required"); + const userId = req.user._id; + if (!userId) throw new ApiError(401, "Unauthorized"); + if (!mongoose.Types.ObjectId.isValid(tweetId)) + throw new ApiError(400, "Invalid tweetId"); + + const isValidId = await Tweet.findById(tweetId); + if (!isValidId) { + throw new ApiError(404, "No tweet found to this ID"); + } + + const existingLike = await Like.findOne({ + tweet: tweetId, + likedBy: userId, + }); + console.log("this is existing user ", existingLike); + + if (existingLike) { + await Like.findByIdAndDelete(existingLike._id); + return res + .status(200) + .json(new ApiResponse(200, "Tweet unliked successfully")); + } else { + const newLike = await Like.create({ tweet: tweetId, likedBy: userId }); + return res + .status(201) + .json(new ApiResponse(201, "Tweet liked successfully", newLike)); + } +}); const getLikedVideos = asyncHandler(async (req, res) => { - //TODO: get all liked videos -}) + //TODO: get all liked videos + const { userId } = req.user._id; + const { page = 1, limit = 10 } = req.query; + if (!userId) { + throw new ApiError(401, "not a valid user"); + } + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + }; + const result = await Like.aggregate( + [ + { + $match: { likedBy: new mongoose.Types.ObjectId(userId) }, + $lookup: { + from: "videos", + localField: "video", + foreignField: "_id", + as: "video", + }, + }, + { + $project: { + _id: 0, + "video._id": 1, + "video.title": 1, + "video.thumbnail": 1, + "video.duration": 1, + "video.views": 1, + createdAt: 1, + }, + }, + ], + options + ); + + if (result.length == 0) { + return res + .status(200) + .json(new ApiResponse(200, null, "no video is liked by you")); + } + return res + .status(200) + .json(new ApiResponse(200, result, "videos fetched successfully")); +}); + +const getLikesByVideo = asyncHandler(async (req, res) => { + const { videoId } = req.params; + if (!videoId) throw new ApiError(400, "videoId is required"); + + if (!mongoose.Types.ObjectId.isValid(videoId)) + throw new ApiError(400, "Invalid videoId"); + const likes = await Like.find({ video: videoId }); + console.log("this i s video", likes); + + if (!likes) { + throw new ApiError(404, "No likes found for this video"); + } + res.status(200).json( + new ApiResponse(200, "Likes fetched successfully", { + likes, + totalLikes: likes.length, + }) + ); +}); + +const getLikesByComment = asyncHandler(async (req, res) => { + const { commentId } = req.params; + if (!commentId) throw new ApiError(400, "commentId is required"); + + const likes = await Like.find({ comment: commentId }); + if (!likes) { + throw new ApiError(404, "No likes found for this comment"); + } + res.status(200).json( + new ApiResponse(200, "Likes fetched successfully", { + likes, + totalLikes: likes.length, + }) + ); +}); + +const getLikesByTweet = asyncHandler(async (req, res) => { + const { tweetId } = req.params; + if (!tweetId) throw new ApiError(400, "tweetId is required"); + + const likes = await Like.find({ tweet: tweetId }); + if (!likes) { + throw new ApiError(404, "No likes found for this tweet"); + } + res.status(200).json( + new ApiResponse(200, "Likes fetched successfully", { + likes, + totalLikes: likes.length, + }) + ); +}); export { - toggleCommentLike, - toggleTweetLike, - toggleVideoLike, - getLikedVideos -} \ No newline at end of file + toggleCommentLike, + toggleTweetLike, + toggleVideoLike, + getLikedVideos, + getLikesByComment, + getLikesByTweet, + getLikesByVideo, +}; diff --git a/src/controllers/playlist.controller.js b/src/controllers/playlist.controller.js index 18d8c2dd..7cdd3750 100644 --- a/src/controllers/playlist.controller.js +++ b/src/controllers/playlist.controller.js @@ -1,53 +1,191 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Playlist} from "../models/playlist.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { Playlist } from "../models/playlist.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import { Video } from "../models/video.model.js"; const createPlaylist = asyncHandler(async (req, res) => { - const {name, description} = req.body + //TODO: create playlist + const name = req.body?.name; + const description = req.body?.description; + const videosId = req.body?.videosId; + const owner = req.user._id; + if (!name) { + throw new ApiError(400, "Playlist name is required"); + } + const newPlaylist = new Playlist({ + name, + description, + videos: videosId || [], + owner, + }); + await newPlaylist.save(); + + if (!newPlaylist) { + throw new ApiError(500, "Failed to create playlist"); + } - //TODO: create playlist -}) + return res + .status(201) + .json(new ApiResponse(201, "Playlist created successfully", newPlaylist)); +}); const getUserPlaylists = asyncHandler(async (req, res) => { - const {userId} = req.params - //TODO: get user playlists -}) + //TODO: get user playlists + const userId = req.user._id || req.params?.userId; + const playlists = await Playlist.find({ owner: userId }) + .populate("videos") + .sort({ createdAt: -1 }) + .select("-__v -owner"); + return res + .status(200) + .json( + new ApiResponse(200, "User playlists fetched successfully", playlists) + ); +}); const getPlaylistById = asyncHandler(async (req, res) => { - const {playlistId} = req.params - //TODO: get playlist by id -}) + //TODO: get playlist by id + const { playlistId } = req.params; + if (!mongoose.Types.ObjectId.isValid(playlistId)) { + throw new ApiError(400, "Invalid playlistId"); + } + const playlist = await Playlist.findById(playlistId) + .populate("videos") + .select("-__v -owner"); + if (!playlist) { + throw new ApiError(404, "Playlist not found"); + } + return res + .status(200) + .json( + new ApiResponse(200, "Playlist details fetched successfully", playlist) + ); +}); const addVideoToPlaylist = asyncHandler(async (req, res) => { - const {playlistId, videoId} = req.params -}) + const playlistId = req.params?.playlistId; + const videoId = req.params?.videoId; + const userId = req.user._id; + + if ( + !mongoose.Types.ObjectId.isValid(playlistId) || + !mongoose.Types.ObjectId.isValid(videoId) + ) { + throw new ApiError(400, "Invalid playlistId or videoId"); + } + const isValidId = await Video.findById(videoId); + if (!isValidId) { + throw new ApiError(404, "this is not a valid Video ID"); + } + + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(404, "Playlist not found"); + } + const result = playlist.videos.filter((vidId) => vidId.toString() === videoId.toString()); + console.log("this is result ", result); + if (result.length !== 0) { + return res + .status(201) + .json( + new ApiResponse(201, "Video already available to playlist", playlist) + ); + } + const validateOwner = playlist.validateOwner(userId); + if (!validateOwner) { + throw new ApiError(403, "You are not authorized to modify this playlist"); + } + playlist.videos.push(videoId); + await playlist.save(); + return res + .status(200) + .json( + new ApiResponse(200, "Video added to playlist successfully", playlist) + ); +}); const removeVideoFromPlaylist = asyncHandler(async (req, res) => { - const {playlistId, videoId} = req.params - // TODO: remove video from playlist + // TODO: remove video from playlist + const { playlistId } = req.params; + const { videoId } = req.params; + const userId = req.user._id; -}) + if ( + !mongoose.Types.ObjectId.isValid(playlistId) || + !mongoose.Types.ObjectId.isValid(videoId) + ) { + throw new ApiError(400, "Invalid playlistId or videoId"); + } + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(404, "Playlist not found"); + } + const validateOwner = playlist.validateOwner(userId); + if (!validateOwner) { + throw new ApiError(403, "You are not authorized to modify this playlist"); + } + playlist.videos = playlist.videos.filter((vId) => vId.toString() !== videoId); + await playlist.save(); + return res + .status(200) + .json( + new ApiResponse(200, "Video removed from playlist successfully", playlist) + ); +}); const deletePlaylist = asyncHandler(async (req, res) => { - const {playlistId} = req.params - // TODO: delete playlist -}) + // TODO: delete playlist + const { playlistId } = req.params; + const userId = req.user._id; + if (!mongoose.Types.ObjectId.isValid(playlistId)) { + throw new ApiError(400, "Invalid playlistId"); + } + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(404, "Playlist not found"); + } + const validateOwner = playlist.validateOwner(userId); + if (!validateOwner) { + throw new ApiError(403, "You are not authorized to delete this playlist"); + } + const deletedPlaylist = await Playlist.findByIdAndDelete(playlistId); + return res + .status(200) + .json( + new ApiResponse(200, "Playlist deleted successfully", deletedPlaylist) + ); +}); const updatePlaylist = asyncHandler(async (req, res) => { - const {playlistId} = req.params - const {name, description} = req.body - //TODO: update playlist -}) + const { playlistId } = req.params; + const { name, description } = req.body; + //TODO: update playlist + const userId = req.user._id; + if (!mongoose.Types.ObjectId.isValid(playlistId)) { + throw new ApiError(400, "Invalid playlistId"); + } + const playlist = await Playlist.findById(playlistId); + if (!playlist) throw new ApiError(404, "No playlist found with this id"); + const validateOwner = playlist.validateOwner(userId); + if (!validateOwner) { + throw new ApiError(403, "You are not authorized to update this playlist"); + } + playlist.name = name || playlist.name; + playlist.description = description || playlist.description; + await playlist.save(); + return res + .status(200) + .json(new ApiResponse(200, "Playlist updated successfully", playlist)); +}); export { - createPlaylist, - getUserPlaylists, - getPlaylistById, - addVideoToPlaylist, - removeVideoFromPlaylist, - deletePlaylist, - updatePlaylist -} + createPlaylist, + addVideoToPlaylist, + getUserPlaylists, + getPlaylistById, + removeVideoFromPlaylist, + deletePlaylist, + updatePlaylist, +}; diff --git a/src/controllers/subscription.controller.js b/src/controllers/subscription.controller.js index c89d9956..99b3d615 100644 --- a/src/controllers/subscription.controller.js +++ b/src/controllers/subscription.controller.js @@ -1,28 +1,72 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {User} from "../models/user.model.js" -import { Subscription } from "../models/subscription.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { User } from "../models/user.model.js"; +import { Subscription } from "../models/subscription.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const toggleSubscription = asyncHandler(async (req, res) => { - const {channelId} = req.params - // TODO: toggle subscription -}) + // TODO: toggle subscription + const { channelId } = req.params; + const subscriberId = req.user._id; + if (!mongoose.Types.ObjectId.isValid(channelId)) { + throw new ApiError(400, "Invalid channel ID"); + } + const existingSubscription = await Subscription.findOne({ + subscriber: subscriberId, + channel: channelId, + }); + if (existingSubscription) { + const unsubscribed = await Subscription.findByIdAndDelete( + existingSubscription._id + ); + + if (!unsubscribed) { + throw new ApiError(501, "failed to delete the object"); + // throw new ApiError(501,"failed to unsubscribe the channel"); + } + return res + .status(200) + .json(new ApiResponse(200, unsubscribed, "unsubscribed successfully")); + } + const subscription = new Subscription({ + subscriber: subscriberId, + channel: channelId, + }); + await subscription.save(); + res.status(201).json( + new ApiResponse(201, "Subscribed successfully", { + subscription, + length: subscription.length, + }) + ); +}); -// controller to return subscriber list of a channel const getUserChannelSubscribers = asyncHandler(async (req, res) => { - const {channelId} = req.params -}) + const { subscriberId } = req.params; + if (!mongoose.Types.ObjectId.isValid(subscriberId)) { + throw new ApiError(400, "Invalid channel ID"); + } + const subscribers = await Subscription.find({ + channel: subscriberId, + }).populate("subscriber", "username email avatar"); + res + .status(200) + .json( + new ApiResponse(200, subscribers, "Subscribers fetched successfully") + ); +}); -// controller to return channel list to which user has subscribed const getSubscribedChannels = asyncHandler(async (req, res) => { - const { subscriberId } = req.params -}) + const subscriberId = req.user._id; + const subscriptions = await Subscription.find({ + subscriber: subscriberId, + }).populate("channel", "username email avatar"); + res + .status(200) + .json( + new ApiResponse(200, "Subscriptions fetched successfully", subscriptions) + ); +}); -export { - toggleSubscription, - getUserChannelSubscribers, - getSubscribedChannels -} \ No newline at end of file +export { toggleSubscription, getUserChannelSubscribers, getSubscribedChannels }; diff --git a/src/controllers/tweet.controller.js b/src/controllers/tweet.controller.js index 21b001d4..60ff208a 100644 --- a/src/controllers/tweet.controller.js +++ b/src/controllers/tweet.controller.js @@ -1,29 +1,145 @@ -import mongoose, { isValidObjectId } from "mongoose" -import {Tweet} from "../models/tweet.model.js" -import {User} from "../models/user.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose, { isValidObjectId } from "mongoose"; +import { Tweet } from "../models/tweet.model.js"; +import { User } from "../models/user.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const createTweet = asyncHandler(async (req, res) => { - //TODO: create tweet -}) + //TODO: create tweet + const content = req.body?.content; + if (!content) { + throw new ApiError(400, "Content is required"); + } + if (content.length > 280) { + throw new ApiError(400, "Content exceeds maximum length of 280 characters"); + } + const userId = req.user._id; + + const tweet = new Tweet({ content, owner: userId }); + await tweet.save(); + res + .status(201) + .json(new ApiResponse(201, "Tweet created successfully", tweet)); +}); const getUserTweets = asyncHandler(async (req, res) => { - // TODO: get user tweets -}) + // TODO: get user tweets + const { userId } = req.params; + if (!mongoose.Types.ObjectId.isValid(userId)) { + throw new ApiError(400, "Invalid tweet ID"); + } + const tweet = await Tweet.aggregatePaginate([ + { + $match: { owner: new mongoose.Types.ObjectId(userId) }, + }, + ]); + if (!tweet.docs[0]) { + throw new ApiError(404, "no tweets found"); + } + res + .status(200) + .json(new ApiResponse(200, "Tweet fetched successfully", tweet)); +}); + +export const getTweetById = asyncHandler(async (req, res) => { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new ApiError(400, "Invalid tweet ID"); + } + console.log('id',new mongoose.Types.ObjectId(id)); + + // const tweet = await Tweet.aggregatePaginate( + // [ + // { + // $match: { _id: new mongoose.Types.ObjectId(id) }, + // }, + // ], + // { populate: { path: "owner", select: "username avatar" } } + // ); + // if (!tweet.docs[0]) { + // throw new ApiError(404, "Tweet not found"); + // } + + const tweet = await Tweet.findById(id); + // console.log('tweet',tweet.docs); + + res + .status(200) + .json(new ApiResponse(200, tweet, "Tweet fetched successfully")); +}); const updateTweet = asyncHandler(async (req, res) => { - //TODO: update tweet -}) + //TODO: update tweet + const { tweetId } = req.params; + const userId = req.user._id; + const content = req.body?.content; + if (!mongoose.Types.ObjectId.isValid(tweetId)) { + throw new ApiError(400, "Invalid tweet ID"); + } + if (!content) { + throw new ApiError(400, "Content is required"); + } + if (content.length > 280) { + throw new ApiError(400, "Content exceeds maximum length of 280 characters"); + } + const tweet = await Tweet.findById(tweetId); + + if (!tweet) { + throw new ApiError(404, "Tweet not found"); + } + if (!tweet.validateUser(userId)) { + throw new ApiError(403, "You are not authorized to update this tweet"); + } + if (tweet.content === content) + throw new ApiError(409, "This content is already present"); + tweet.content = content; + await tweet.save(); + res + .status(200) + .json(new ApiResponse(200, "Tweet updated successfully", tweet)); +}); const deleteTweet = asyncHandler(async (req, res) => { - //TODO: delete tweet -}) - -export { - createTweet, - getUserTweets, - updateTweet, - deleteTweet -} + //TODO: delete tweet + const { tweetId } = req.params; + const userId = req.user._id; + if (!mongoose.Types.ObjectId.isValid(tweetId)) { + throw new ApiError(400, "Invalid tweet ID"); + } + const tweet = await Tweet.findById(tweetId); + if (!tweet) { + throw new ApiError(404, "Tweet not found"); + } + if (!tweet.validateUser(userId)) { + throw new ApiError(403, "You are not authorized to delete this tweet"); + } + const deletedTweet = await Tweet.findByIdAndDelete(tweetId); + res + .status(200) + .json(new ApiResponse(200, "Tweet deleted successfully", deletedTweet)); +}); + +const getTweets = asyncHandler(async (req, res) => { + const { page = 1, limit = 10 } = req.query; + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + populate: { path: "owner", select: "username avatar" }, + }; + const tweets = await Tweet.aggregatePaginate( + [ + { + $match: {}, + }, + ], + options + ); + + res + .status(200) + .json(new ApiResponse(200, "Tweets fetched successfully", tweets)); +}); + +export { createTweet, getUserTweets, updateTweet, deleteTweet, getTweets }; diff --git a/src/controllers/video.controller.js b/src/controllers/video.controller.js index 78a52627..44cf25d1 100644 --- a/src/controllers/video.controller.js +++ b/src/controllers/video.controller.js @@ -1,47 +1,366 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Video} from "../models/video.model.js" -import {User} from "../models/user.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" -import {uploadOnCloudinary} from "../utils/cloudinary.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { Video } from "../models/video.model.js"; +import { User } from "../models/user.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import { uploadOnCloudinary, DeleteFile } from "../utils/cloudinary.js"; +import { Subscription} from "../models/subscription.model.js" const getAllVideos = asyncHandler(async (req, res) => { - const { page = 1, limit = 10, query, sortBy, sortType, userId } = req.query - //TODO: get all videos based on query, sort, pagination -}) + // const { page = 1, limit = 10, query, sortBy, sortType, userId } = req.query + //TODO: get all videos based on query, sort, pagination + const { page = 1, limit = 10 } = req.query; + // console.log('paeg and limit ',page,limit); + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + populate: { path: "owner", select: "username email avatar" }, + }; + console.log("option", options); + + const result = await Video.aggregatePaginate( + [ + { + $match: { isPublished: true }, + }, + ], + options + ); + return res + .status(200) + .json(new ApiResponse(200, "Videos fetched successfully", result)); +}); const publishAVideo = asyncHandler(async (req, res) => { - const { title, description} = req.body - // TODO: get video, upload to cloudinary, create video -}) + // const { title, description} = req.body + // TODO: get video, upload to cloudinary, create video + const title = req.body?.title; + const description = req.body?.description; + const duration = parseFloat(req.body?.duration); + const videoFile = req.files?.videoFile?.[0]?.path; + const thumbnail = req.files?.thumbnail?.[0]?.path; + + console.log( + "this areall fields", + title, + description, + duration, + videoFile, + thumbnail + ); + + if (!videoFile || !thumbnail || !title || !description || !duration) { + throw new ApiError(400, "All fields are required"); + } + const userId = req.user._id; + const videoCloudinaryData = await uploadOnCloudinary(videoFile); + const thumbnailCloudinaryData = await uploadOnCloudinary(thumbnail); + const videoFilePublicId = videoCloudinaryData.public_id; + const thumbnailPublicId = thumbnailCloudinaryData.public_id; + + const videoLink = videoCloudinaryData.url; + const thumbnailLink = thumbnailCloudinaryData.url; + + const newVideo = new Video({ + videoFile: videoLink, + thumbnail: thumbnailLink, + title, + description, + duration, + owner: userId, + videoFilePublicId, + thumbnailPublicId, + }); + await newVideo.save(); + if (!newVideo) { + throw new ApiError(500, "Failed to create video"); + } + return res + .status(201) + .json(new ApiResponse(201, "Video created successfully", newVideo)); +}); + +export const incrementVideoViews = asyncHandler(async (req, res, next) => { + const videoId = req.params.videoId; + console.log('videoid',videoId); + + if (!mongoose.Types.ObjectId.isValid(videoId)) { + throw new ApiError(400, "Invalid video ID"); + } + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + await video.incrementViews(); + next(); +}); const getVideoById = asyncHandler(async (req, res) => { - const { videoId } = req.params - //TODO: get video by id -}) + // const { videoId } = req.params + //TODO: get video by id + const videoId = req.params.videoId; + if (!mongoose.Types.ObjectId.isValid(videoId)) { + throw new ApiError(400, "Invalid video ID"); + } + const video = await Video.findById(videoId).populate( + "owner", + "username email avatar" + ); + if (!video) { + throw new ApiError(404, "Video not found"); + } + return res + .status(200) + .json(new ApiResponse(200, "Video fetched successfully", video)); +}); const updateVideo = asyncHandler(async (req, res) => { - const { videoId } = req.params +// const { videoId } = req.params; //TODO: update video details like title, description, thumbnail + const videoId = req.params?.videoId; + const title = req.body?.title; + const description = req.body?.description; + const isPublished = req.body?.isPublished; + const duration = parseFloat(req.body?.duration); + const videoFile = req.files?.videoFile?.[0]?.path; + const thumbnail = req.files?.thumbnail?.[0]?.path; + + if (!videoFile || !duration) { + throw new ApiError(404, "All mendetary fields are required"); + } + + if (!mongoose.Types.ObjectId.isValid(videoId)) { + throw new ApiError(400, "Invalid video ID"); + } + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + if (!video.validateUser(req.user._id)) { + throw new ApiError(403, "You are not authorized to update this video"); + } + + // if (!videoFile || !thumbnail) + // throw new ApiError(500, "Error in uploading File"); + if (title) video.title = title; + if (description) video.description = description; + if (!isNaN(duration)) video.duration = duration; + if (videoFile) { + const videoCloudinaryData = await uploadOnCloudinary(videoFile); + if (!videoCloudinaryData) { + throw new ApiError(500, "error in uploading file on cloudinary"); + } + const videoLink = videoCloudinaryData.url; + const videoFilePublicId = videoCloudinaryData.public_id; + const deletedFile = await DeleteFile(video.videoFilePublicId); + // console.log('this deleted the video',deletedFile); -}) + if (!deletedFile) { + throw new ApiError(500, "error in updating file on cloudinary"); + } + video.videoFilePublicId = videoFilePublicId; + video.videoFile = videoLink; + } + if (thumbnail) { + const thumbnailCloudinaryData = await uploadOnCloudinary(thumbnail); + if (!thumbnailCloudinaryData) { + throw new ApiError(500, "error in uploading file on cloudinary"); + } + const thumbnailLink = thumbnailCloudinaryData.url; + const thumbnailPublicId = thumbnailCloudinaryData.public_id; + const deletedFile = await DeleteFile(video.thumbnailPublicId); + if (!deletedFile) { + throw new ApiError(500, "error in updating file on cloudinary"); + } + video.thumbnailPublicId = thumbnailPublicId; + video.thumbnail = thumbnailLink; + } + if (typeof isPublished === "boolean") video.isPublished = isPublished; + await video.save(); + return res + .status(200) + .json(new ApiResponse(200, "Video updated successfully", video)); +}); const deleteVideo = asyncHandler(async (req, res) => { - const { videoId } = req.params +// const { videoId } = req.params; //TODO: delete video -}) + const videoId = req.params.videoId; + if (!mongoose.Types.ObjectId.isValid(videoId)) { + throw new ApiError(400, "Invalid video ID"); + } + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + if (!video.validateUser(req.user._id)) { + throw new ApiError(403, "You are not authorized to delete this video"); + } + //delete from Cloudinary + const deletedVideoFile = await DeleteFile(video.videoFilePublicId); + const deletedThumbnailFile = await DeleteFile(video.thumbnailPublicId); + if (!deletedThumbnailFile || !deletedVideoFile) { + throw new ApiError(501, "Failed to delete video"); + } + + const deletedVideo = await Video.findByIdAndDelete(videoId); + return res + .status(200) + .json(new ApiResponse(200, "Video deleted successfully", deletedVideo)); +}); const togglePublishStatus = asyncHandler(async (req, res) => { - const { videoId } = req.params -}) + // const { videoId } = req.params; + const videoId = req.params.videoId; + if (!mongoose.Types.ObjectId.isValid(videoId)) { + throw new ApiError(400, "Invalid video ID"); + } + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + console.log("user", req.user._id); + if (!video.validateUser(req.user._id)) { + throw new ApiError(403, "You are not authorized to update this video"); + } + video.isPublished = !video.isPublished; + await video.save(); + return res + .status(200) + .json(new ApiResponse(200, "Video publish status toggled", video)); +}); + +export const getRandomVideos = asyncHandler(async (req, res) => { + const { limit = 10 } = req.query; + const videos = await Video.aggregate([ + { $match: { isPublished: true } }, + { $sample: { size: parseInt(limit, 10) } }, + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "owner", + }, + }, + ]); + return res + .status(200) + .json(new ApiResponse(200, "Random videos fetched successfully", videos)); +}); + +export const getSubscribedVideos = asyncHandler(async (req, res) => { + const userId = req.user._id; + const { page = 1, limit = 10 } = req.query; + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + populate: { path: "owner", select: "username email avatar" }, + }; + + const result = await Subscription.aggregate( + [ + { $match: { subscriber: userId } }, + { + $lookup: { + from: "videos", + localField: "channel", + foreignField: "owner", + as: "videos", + pipeline: [ + { $match: { isPublished: true } }, + { $sort: { createdAt: -1 } }, + ], + }, + }, + ], + options + ); + + return res + .status(200) + .json( + new ApiResponse(200, "Subscribed videos fetched successfully", result) + ); +}); + +export const getTrendingVideos = asyncHandler(async (req, res) => { + const { page = 1, limit = 10 } = req.query; + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { views: -1 }, + populate: { path: "owner", select: "username email avatar" }, + }; + const result = await Video.aggregatePaginate( + [ + { + $match: { isPublished: true }, + }, + ], + options + ); + return res + .status(200) + .json(new ApiResponse(200, "Trending videos fetched successfully", result)); +}); + +export const searchVideos = asyncHandler(async (req, res) => { + const { query, page = 1, limit = 10 } = req.query; + if (!query) { + throw new ApiError(400, "Search query is required"); + } + // console.log('query',query); + + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + populate: { path: "owner", select: "username email avatar" }, + }; + const result = await Video.aggregatePaginate( + [ + { + $match: { $text: { $search: query }, isPublished: true }, + }, + ], + options + ); + if (!result.docs[0]) throw new ApiError(404, "No Matches Found "); + return res + .status(200) + .json(new ApiResponse(200, "Videos fetched successfully", result)); +}); + +export const getVideosByUser = asyncHandler(async (req, res) => { + const userId = req.params.userId; + if (!mongoose.Types.ObjectId.isValid(userId)) { + throw new ApiError(400, "Invalid user ID"); + } + const { page = 1, limit = 10 } = req.query; + const options = { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + sort: { createdAt: -1 }, + }; + const result = await Video.aggregatePaginate( + [{ $match: { owner: new mongoose.Types.ObjectId(userId) } }], + options + ); + if (!result.docs[0]) throw new ApiError(404, "No Video Found"); + return res + .status(200) + .json(new ApiResponse(200, "Videos fetched successfully", result)); +}); export { - getAllVideos, - publishAVideo, - getVideoById, - updateVideo, - deleteVideo, - togglePublishStatus -} + getAllVideos, + publishAVideo, + getVideoById, + updateVideo, + deleteVideo, + togglePublishStatus, +}; diff --git a/src/middlewares/emailValidator.middleware.js b/src/middlewares/emailValidator.middleware.js new file mode 100644 index 00000000..7f1e241f --- /dev/null +++ b/src/middlewares/emailValidator.middleware.js @@ -0,0 +1,29 @@ +import axios from "axios"; +import { ApiError } from "../utils/ApiError.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import dotenv from "dotenv"; +dotenv.config(); + +const ABSTRACT_API_KEY = process.env.ABSTRACT_API_KEY; + +//middleware to validate email using Abstract API +export const emailValidator = asyncHandler(async (req, res, next) => { + // console.log("Inside emailValidator middleware", req.body); + + const { email } = req.body; + if (!email) { + throw new ApiError(400, "Email is required"); + } + //call Abstract API to validate email + const response = await axios.get( + `https://emailreputation.abstractapi.com/v1/?api_key=${ABSTRACT_API_KEY}&email=${email}` + ); + + const data = response.data; + // console.log("Email validation response:", data); + + if (data.email_deliverability.status !== "DELIVERABLE") { + throw new ApiError(400, "Invalid email address"); + } + next(); +}); diff --git a/src/models/comment.model.js b/src/models/comment.model.js index f6b16875..66091465 100644 --- a/src/models/comment.model.js +++ b/src/models/comment.model.js @@ -23,5 +23,7 @@ const commentSchema = new Schema( commentSchema.plugin(mongooseAggregatePaginate) - +commentSchema.methods.validateUser = function (userId) { + return this.owner.toString() === userId.toString(); +}; export const Comment = mongoose.model("Comment", commentSchema) \ No newline at end of file diff --git a/src/models/like.model.js b/src/models/like.model.js index 963e127a..16a944a3 100644 --- a/src/models/like.model.js +++ b/src/models/like.model.js @@ -1,6 +1,5 @@ import mongoose, {Schema} from "mongoose"; - const likeSchema = new Schema({ video: { type: Schema.Types.ObjectId, diff --git a/src/models/playlist.model.js b/src/models/playlist.model.js index 8bb94eeb..0dacc9a9 100644 --- a/src/models/playlist.model.js +++ b/src/models/playlist.model.js @@ -1,4 +1,5 @@ -import mongoose, {Schema} from "mongoose"; +import mongoose, { Schema } from "mongoose"; +import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; const playlistSchema = new Schema({ name: { @@ -21,6 +22,8 @@ const playlistSchema = new Schema({ }, }, {timestamps: true}) - - +playlistSchema.plugin(mongooseAggregatePaginate); +playlistSchema.methods.validateOwner = function (userId) { + return this.owner.toString() === userId.toString(); +}; export const Playlist = mongoose.model("Playlist", playlistSchema) \ No newline at end of file diff --git a/src/models/tweet.model.js b/src/models/tweet.model.js index a3f7d1f6..06433128 100644 --- a/src/models/tweet.model.js +++ b/src/models/tweet.model.js @@ -1,4 +1,5 @@ -import mongoose, {Schema} from "mongoose"; +import mongoose, { Schema } from "mongoose"; +import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; const tweetSchema = new Schema({ content: { @@ -11,5 +12,8 @@ const tweetSchema = new Schema({ } }, {timestamps: true}) - +tweetSchema.plugin(mongooseAggregatePaginate); +tweetSchema.methods.validateUser = function (userId) { + return this.owner.toString() === userId.toString(); +}; export const Tweet = mongoose.model("Tweet", tweetSchema) \ No newline at end of file diff --git a/src/models/user.model.js b/src/models/user.model.js index 06f044ff..48742c39 100644 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -1,6 +1,8 @@ import mongoose, {Schema} from "mongoose"; import jwt from "jsonwebtoken" import bcrypt from "bcrypt" +import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; + const userSchema = new Schema( { diff --git a/src/models/video.model.js b/src/models/video.model.js index 0c1d305c..92ceb6cc 100644 --- a/src/models/video.model.js +++ b/src/models/video.model.js @@ -1,47 +1,56 @@ -import mongoose, {Schema} from "mongoose"; +import mongoose, { Schema } from "mongoose"; import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; const videoSchema = new Schema( - { - videoFile: { - type: String, //cloudinary url - required: true - }, - thumbnail: { - type: String, //cloudinary url - required: true - }, - title: { - type: String, - required: true - }, - description: { - type: String, - required: true - }, - duration: { - type: Number, - required: true - }, - views: { - type: Number, - default: 0 - }, - isPublished: { - type: Boolean, - default: true - }, - owner: { - type: Schema.Types.ObjectId, - ref: "User" - } + { + videoFile: { + type: String, //cloudinary url + required: true, + }, + thumbnail: { + type: String, //cloudinary url + required: true, + }, + title: { + type: String, + required: true, + }, + description: { + type: String, + required: true, + }, + duration: { + type: Number, + required: true, + }, + views: { + type: Number, + default: 0, + }, + isPublished: { + type: Boolean, + default: true, + }, + owner: { + type: Schema.Types.ObjectId, + ref: "User", + }, + videoFilePublicId: String, + thumbnailPublicId:String, + }, + { + timestamps: true, + } +); - }, - { - timestamps: true - } -) +videoSchema.index({ title: "text", description: "text" }); +videoSchema.methods.incrementViews = function () { + this.views += 1; + return this.save(); +}; +videoSchema.methods.validateUser = function (userId) { + return this.owner.toString() === userId.toString(); +}; +videoSchema.plugin(mongooseAggregatePaginate); -videoSchema.plugin(mongooseAggregatePaginate) - -export const Video = mongoose.model("Video", videoSchema) \ No newline at end of file +export const Video = mongoose.model("Video", videoSchema); diff --git a/src/routes/like.routes.js b/src/routes/like.routes.js index dc92ac14..4b4505c7 100644 --- a/src/routes/like.routes.js +++ b/src/routes/like.routes.js @@ -1,11 +1,14 @@ -import { Router } from 'express'; +import { Router } from "express"; import { - getLikedVideos, - toggleCommentLike, - toggleVideoLike, - toggleTweetLike, -} from "../controllers/like.controller.js" -import {verifyJWT} from "../middlewares/auth.middleware.js" + getLikedVideos, + toggleCommentLike, + toggleVideoLike, + toggleTweetLike, + getLikesByVideo, + getLikesByComment, + getLikesByTweet, +} from "../controllers/like.controller.js"; +import { verifyJWT } from "../middlewares/auth.middleware.js"; const router = Router(); router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file @@ -14,5 +17,8 @@ router.route("/toggle/v/:videoId").post(toggleVideoLike); router.route("/toggle/c/:commentId").post(toggleCommentLike); router.route("/toggle/t/:tweetId").post(toggleTweetLike); router.route("/videos").get(getLikedVideos); +router.get("/video/:videoId", getLikesByVideo); +router.get("/comment/:commentId", getLikesByComment); +router.get("/tweet/:tweetId", getLikesByTweet); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/tweet.routes.js b/src/routes/tweet.routes.js index c81e84a7..68e9f36e 100644 --- a/src/routes/tweet.routes.js +++ b/src/routes/tweet.routes.js @@ -4,6 +4,8 @@ import { deleteTweet, getUserTweets, updateTweet, + getTweetById, + getTweets } from "../controllers/tweet.controller.js" import {verifyJWT} from "../middlewares/auth.middleware.js" @@ -13,5 +15,7 @@ router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file router.route("/").post(createTweet); router.route("/user/:userId").get(getUserTweets); router.route("/:tweetId").patch(updateTweet).delete(deleteTweet); +router.get("/", getTweets); +router.get("/:id", getTweetById); export default router \ No newline at end of file diff --git a/src/routes/video.routes.js b/src/routes/video.routes.js index 8bd263f0..a11a4459 100644 --- a/src/routes/video.routes.js +++ b/src/routes/video.routes.js @@ -1,42 +1,54 @@ -import { Router } from 'express'; +import { Router } from "express"; import { - deleteVideo, - getAllVideos, - getVideoById, - publishAVideo, - togglePublishStatus, - updateVideo, -} from "../controllers/video.controller.js" -import {verifyJWT} from "../middlewares/auth.middleware.js" -import {upload} from "../middlewares/multer.middleware.js" + deleteVideo, + getAllVideos, + getVideoById, + publishAVideo, + togglePublishStatus, + updateVideo, + incrementVideoViews, + searchVideos, + getVideosByUser, + getRandomVideos, + getSubscribedVideos, + getTrendingVideos, +} from "../controllers/video.controller.js"; +import { verifyJWT } from "../middlewares/auth.middleware.js"; +import { upload } from "../middlewares/multer.middleware.js"; const router = Router(); router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file router - .route("/") - .get(getAllVideos) - .post( - upload.fields([ - { - name: "videoFile", - maxCount: 1, - }, - { - name: "thumbnail", - maxCount: 1, - }, - - ]), - publishAVideo - ); + .route("/") + .get(getAllVideos) + .post( + upload.fields([ + { + name: "videoFile", + maxCount: 1, + }, + { + name: "thumbnail", + maxCount: 1, + }, + ]), + publishAVideo + ); -router - .route("/:videoId") - .get(getVideoById) - .delete(deleteVideo) - .patch(upload.single("thumbnail"), updateVideo); +router.route("/search").get(searchVideos); router.route("/toggle/publish/:videoId").patch(togglePublishStatus); - -export default router \ No newline at end of file +router.route("/random").get(getRandomVideos); +router.route("/trending").get(getTrendingVideos); +router.route("/subscriptions").get(getSubscribedVideos); +router.route("/user/:userId").get(getVideosByUser); +router + .route("/:videoId") + .get(incrementVideoViews, getVideoById) + .delete(deleteVideo) + .patch( + upload.fields([{ name: "videoFile" }, { name: "thumbnail" }]), + updateVideo + ); +export default router; diff --git a/src/utils/cloudinary.js b/src/utils/cloudinary.js index 7b67fdc6..672d4315 100644 --- a/src/utils/cloudinary.js +++ b/src/utils/cloudinary.js @@ -26,6 +26,14 @@ const uploadOnCloudinary = async (localFilePath) => { } } - +export const DeleteFile = async (publicId) => { + try { + const result = await cloudinary.uploader.destroy(publicId); + return result; + } catch (error) { + console.error("src :: utils :: Cloudinary :: DeleteFile", error); + throw error; + } +}; export {uploadOnCloudinary} \ No newline at end of file