feat: add video likes and plays tracking system
Backend (Database & API): - Add likes_count, plays_count, views_count to sexy_videos table - Create sexy_video_likes junction table for user-video likes - Create sexy_video_plays table for analytics tracking - Add POST /sexy/videos/:id/like endpoint - Add DELETE /sexy/videos/:id/like endpoint - Add GET /sexy/videos/:id/like-status endpoint - Add POST /sexy/videos/:id/play endpoint - Add PATCH /sexy/videos/:id/play/:playId endpoint Frontend (Types & Services): - Update Video interface with counter fields - Add VideoLikeStatus, VideoLikeResponse, VideoPlayResponse types - Add likeVideo() service function - Add unlikeVideo() service function - Add getVideoLikeStatus() service function - Add recordVideoPlay() service function - Add updateVideoPlay() service function Next: Implement UI components for like button and play count display
This commit is contained in:
@@ -249,5 +249,177 @@ export default {
|
||||
res.status(500).json({ error: error.message || "Failed to delete recording" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /sexy/videos/:id/like - Like a video
|
||||
router.post("/videos/:id/like", async (req, res) => {
|
||||
const accountability = req.accountability;
|
||||
if (!accountability?.user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const videoId = req.params.id;
|
||||
const userId = accountability.user;
|
||||
|
||||
try {
|
||||
const likesService = new ItemsService("sexy_video_likes", {
|
||||
schema: await getSchema(),
|
||||
accountability,
|
||||
});
|
||||
|
||||
// Check if already liked
|
||||
const existing = await likesService.readByQuery({
|
||||
filter: { video_id: videoId, user_id: userId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({ error: "Already liked" });
|
||||
}
|
||||
|
||||
// Create like
|
||||
await likesService.createOne({
|
||||
video_id: videoId,
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
// Increment likes_count
|
||||
const videosService = new ItemsService("sexy_videos", {
|
||||
schema: await getSchema(),
|
||||
});
|
||||
const video = await videosService.readOne(videoId);
|
||||
await videosService.updateOne(videoId, {
|
||||
likes_count: (video.likes_count || 0) + 1,
|
||||
});
|
||||
|
||||
res.json({ liked: true, likes_count: (video.likes_count || 0) + 1 });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message || "Failed to like video" });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /sexy/videos/:id/like - Unlike a video
|
||||
router.delete("/videos/:id/like", async (req, res) => {
|
||||
const accountability = req.accountability;
|
||||
if (!accountability?.user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const videoId = req.params.id;
|
||||
const userId = accountability.user;
|
||||
|
||||
try {
|
||||
const likesService = new ItemsService("sexy_video_likes", {
|
||||
schema: await getSchema(),
|
||||
accountability,
|
||||
});
|
||||
|
||||
// Find and delete like
|
||||
const existing = await likesService.readByQuery({
|
||||
filter: { video_id: videoId, user_id: userId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (existing.length === 0) {
|
||||
return res.status(400).json({ error: "Not liked" });
|
||||
}
|
||||
|
||||
await likesService.deleteOne(existing[0].id);
|
||||
|
||||
// Decrement likes_count
|
||||
const videosService = new ItemsService("sexy_videos", {
|
||||
schema: await getSchema(),
|
||||
});
|
||||
const video = await videosService.readOne(videoId);
|
||||
await videosService.updateOne(videoId, {
|
||||
likes_count: Math.max((video.likes_count || 0) - 1, 0),
|
||||
});
|
||||
|
||||
res.json({ liked: false, likes_count: Math.max((video.likes_count || 0) - 1, 0) });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message || "Failed to unlike video" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /sexy/videos/:id/like-status - Get like status for a video
|
||||
router.get("/videos/:id/like-status", async (req, res) => {
|
||||
const accountability = req.accountability;
|
||||
if (!accountability?.user) {
|
||||
return res.json({ liked: false });
|
||||
}
|
||||
|
||||
const videoId = req.params.id;
|
||||
const userId = accountability.user;
|
||||
|
||||
try {
|
||||
const likesService = new ItemsService("sexy_video_likes", {
|
||||
schema: await getSchema(),
|
||||
accountability,
|
||||
});
|
||||
|
||||
const existing = await likesService.readByQuery({
|
||||
filter: { video_id: videoId, user_id: userId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
res.json({ liked: existing.length > 0 });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message || "Failed to get like status" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /sexy/videos/:id/play - Record a video play
|
||||
router.post("/videos/:id/play", async (req, res) => {
|
||||
const accountability = req.accountability;
|
||||
const videoId = req.params.id;
|
||||
const { session_id } = req.body;
|
||||
|
||||
try {
|
||||
const playsService = new ItemsService("sexy_video_plays", {
|
||||
schema: await getSchema(),
|
||||
});
|
||||
|
||||
const videosService = new ItemsService("sexy_videos", {
|
||||
schema: await getSchema(),
|
||||
});
|
||||
|
||||
// Record play
|
||||
const play = await playsService.createOne({
|
||||
video_id: videoId,
|
||||
user_id: accountability?.user || null,
|
||||
session_id: session_id || null,
|
||||
});
|
||||
|
||||
// Increment plays_count
|
||||
const video = await videosService.readOne(videoId);
|
||||
await videosService.updateOne(videoId, {
|
||||
plays_count: (video.plays_count || 0) + 1,
|
||||
});
|
||||
|
||||
res.json({ success: true, play_id: play, plays_count: (video.plays_count || 0) + 1 });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message || "Failed to record play" });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /sexy/videos/:id/play/:playId - Update play progress
|
||||
router.patch("/videos/:id/play/:playId", async (req, res) => {
|
||||
const { playId } = req.params;
|
||||
const { duration_watched, completed } = req.body;
|
||||
|
||||
try {
|
||||
const playsService = new ItemsService("sexy_video_plays", {
|
||||
schema: await getSchema(),
|
||||
});
|
||||
|
||||
await playsService.updateOne(playId, {
|
||||
duration_watched,
|
||||
completed,
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message || "Failed to update play" });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
readComments,
|
||||
aggregate,
|
||||
} from "@directus/sdk";
|
||||
import type { Article, Model, Recording, Stats, User, Video } from "$lib/types";
|
||||
import type { Article, Model, Recording, Stats, User, Video, VideoLikeStatus, VideoLikeResponse, VideoPlayResponse } from "$lib/types";
|
||||
import { PUBLIC_URL } from "$env/static/public";
|
||||
import { logger } from "$lib/logger";
|
||||
|
||||
@@ -630,3 +630,95 @@ export async function getRecording(id: string, fetch?: typeof globalThis.fetch)
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export async function likeVideo(videoId: string) {
|
||||
return loggedApiCall(
|
||||
"likeVideo",
|
||||
async () => {
|
||||
const directus = getDirectusInstance(fetch);
|
||||
return directus.request<VideoLikeResponse>(
|
||||
customEndpoint({
|
||||
method: "POST",
|
||||
path: `/sexy/videos/${videoId}/like`,
|
||||
})
|
||||
);
|
||||
},
|
||||
{ videoId }
|
||||
);
|
||||
}
|
||||
|
||||
export async function unlikeVideo(videoId: string) {
|
||||
return loggedApiCall(
|
||||
"unlikeVideo",
|
||||
async () => {
|
||||
const directus = getDirectusInstance(fetch);
|
||||
return directus.request<VideoLikeResponse>(
|
||||
customEndpoint({
|
||||
method: "DELETE",
|
||||
path: `/sexy/videos/${videoId}/like`,
|
||||
})
|
||||
);
|
||||
},
|
||||
{ videoId }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getVideoLikeStatus(videoId: string, fetch?: typeof globalThis.fetch) {
|
||||
return loggedApiCall(
|
||||
"getVideoLikeStatus",
|
||||
async () => {
|
||||
const directus = getDirectusInstance(fetch);
|
||||
return directus.request<VideoLikeStatus>(
|
||||
customEndpoint({
|
||||
method: "GET",
|
||||
path: `/sexy/videos/${videoId}/like-status`,
|
||||
})
|
||||
);
|
||||
},
|
||||
{ videoId }
|
||||
);
|
||||
}
|
||||
|
||||
export async function recordVideoPlay(videoId: string, sessionId?: string) {
|
||||
return loggedApiCall(
|
||||
"recordVideoPlay",
|
||||
async () => {
|
||||
const directus = getDirectusInstance(fetch);
|
||||
return directus.request<VideoPlayResponse>(
|
||||
customEndpoint({
|
||||
method: "POST",
|
||||
path: `/sexy/videos/${videoId}/play`,
|
||||
body: JSON.stringify({ session_id: sessionId }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
},
|
||||
{ videoId }
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateVideoPlay(
|
||||
videoId: string,
|
||||
playId: string,
|
||||
durationWatched: number,
|
||||
completed: boolean
|
||||
) {
|
||||
return loggedApiCall(
|
||||
"updateVideoPlay",
|
||||
async () => {
|
||||
const directus = getDirectusInstance(fetch);
|
||||
return directus.request(
|
||||
customEndpoint({
|
||||
method: "PATCH",
|
||||
path: `/sexy/videos/${videoId}/play/${playId}`,
|
||||
body: JSON.stringify({
|
||||
duration_watched: durationWatched,
|
||||
completed,
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
},
|
||||
{ videoId, playId, durationWatched, completed }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ export interface Video {
|
||||
upload_date: Date;
|
||||
premium?: boolean;
|
||||
featured?: boolean;
|
||||
likes_count?: number;
|
||||
plays_count?: number;
|
||||
views_count?: number;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
@@ -155,3 +158,25 @@ export interface Recording {
|
||||
featured?: boolean;
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
export interface VideoLikeStatus {
|
||||
liked: boolean;
|
||||
}
|
||||
|
||||
export interface VideoPlayRecord {
|
||||
id: string;
|
||||
video_id: string;
|
||||
duration_watched?: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface VideoLikeResponse {
|
||||
liked: boolean;
|
||||
likes_count: number;
|
||||
}
|
||||
|
||||
export interface VideoPlayResponse {
|
||||
success: boolean;
|
||||
play_id: string;
|
||||
plays_count: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user