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" });
|
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,
|
readComments,
|
||||||
aggregate,
|
aggregate,
|
||||||
} from "@directus/sdk";
|
} 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 { PUBLIC_URL } from "$env/static/public";
|
||||||
import { logger } from "$lib/logger";
|
import { logger } from "$lib/logger";
|
||||||
|
|
||||||
@@ -630,3 +630,95 @@ export async function getRecording(id: string, fetch?: typeof globalThis.fetch)
|
|||||||
{ id },
|
{ 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;
|
upload_date: Date;
|
||||||
premium?: boolean;
|
premium?: boolean;
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
|
likes_count?: number;
|
||||||
|
plays_count?: number;
|
||||||
|
views_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
@@ -155,3 +158,25 @@ export interface Recording {
|
|||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
public?: 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