const createPolicyFilter = (policy) => ({ _or: [ { policies: { policy: { name: { _eq: policy, }, }, }, }, { role: { name: { _eq: policy, }, }, }, ], }); export default { id: "sexy", handler: (router, context) => { const { services, getSchema } = context; const { ItemsService } = services; router.get("/stats", async (_req, res) => { const usersService = new ItemsService("directus_users", { schema: await getSchema(), }); const modelsCount = await usersService.readByQuery({ aggregate: { count: ["*"], }, filter: createPolicyFilter("Model"), }); const viewersCount = await usersService.readByQuery({ aggregate: { count: ["*"], }, filter: createPolicyFilter("Viewer"), }); const videosService = new ItemsService("sexy_videos", { schema: await getSchema(), }); const videosCount = await videosService.readByQuery({ aggregate: { count: ["*"], }, }); res.json({ models_count: modelsCount[0].count, viewers_count: viewersCount[0].count, videos_count: videosCount[0].count, }); }); // GET /sexy/models - Public endpoint to fetch models (bypasses permissions) router.get("/models", async (req, res) => { try { const { featured, limit } = req.query; const usersService = new ItemsService("directus_users", { schema: await getSchema(), accountability: null, }); const filter: any = createPolicyFilter("Model"); if (featured === "true") { filter._and = [filter, { featured: { _eq: true } }]; } const models = await usersService.readByQuery({ filter, fields: ["*", "photos.directus_files_id.*", "banner.*"], sort: ["-id"], limit: limit ? parseInt(limit as string) : -1, }); res.json(models); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to fetch models" }); } }); // GET /sexy/recordings - List user's recordings router.get("/recordings", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } const recordingsService = new ItemsService("sexy_recordings", { schema: await getSchema(), accountability, }); const { status, tags, linked_video, limit, page } = req.query; const filter: any = { user_created: { _eq: accountability.user, }, }; if (status) filter.status = { _eq: status }; if (tags) filter.tags = { _contains: tags }; if (linked_video) filter.linked_video = { _eq: linked_video }; const recordings = await recordingsService.readByQuery({ filter, limit: limit ? parseInt(limit as string) : 50, page: page ? parseInt(page as string) : 1, sort: ["-date_created"], }); res.json(recordings); }); // GET /sexy/recordings/:id - Get single recording router.get("/recordings/:id", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } const recordingsService = new ItemsService("sexy_recordings", { schema: await getSchema(), accountability, }); try { const recording = await recordingsService.readOne(req.params.id); // Check if user owns the recording or if it's public if ( recording.user_created !== accountability.user && !recording.public ) { return res.status(403).json({ error: "Forbidden" }); } res.json(recording); } catch (error) { res.status(404).json({ error: "Recording not found" }); } }); // POST /sexy/recordings - Create new recording router.post("/recordings", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } const recordingsService = new ItemsService("sexy_recordings", { schema: await getSchema(), accountability, }); const { title, description, duration, events, device_info, tags, linked_video, status } = req.body; // Validate required fields if (!title || !duration || !events || !device_info) { return res.status(400).json({ error: "Missing required fields: title, duration, events, device_info", }); } // Validate events structure if (!Array.isArray(events) || events.length === 0) { return res.status(400).json({ error: "Events must be a non-empty array" }); } // Generate slug from title const slug = title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); try { const recording = await recordingsService.createOne({ title, description, slug, duration, events, device_info, tags: tags || [], linked_video: linked_video || null, status: status || "draft", public: false, }); res.status(201).json(recording); } catch (error: any) { console.error("Failed to create recording:", error); res.status(500).json({ error: error.message || "Failed to create recording", details: error.toString() }); } }); // PATCH /sexy/recordings/:id - Update recording router.patch("/recordings/:id", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } const recordingsService = new ItemsService("sexy_recordings", { schema: await getSchema(), accountability, }); try { const existing = await recordingsService.readOne(req.params.id); // Only allow owner to update if (existing.user_created !== accountability.user) { return res.status(403).json({ error: "Forbidden" }); } const { title, description, tags, status, public: isPublic, linked_video } = req.body; const updates: any = {}; if (title !== undefined) { updates.title = title; updates.slug = title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); } if (description !== undefined) updates.description = description; if (tags !== undefined) updates.tags = tags; if (status !== undefined) updates.status = status; if (isPublic !== undefined) updates.public = isPublic; if (linked_video !== undefined) updates.linked_video = linked_video; const recording = await recordingsService.updateOne(req.params.id, updates); res.json(recording); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to update recording" }); } }); // DELETE /sexy/recordings/:id - Delete (archive) recording router.delete("/recordings/:id", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } const recordingsService = new ItemsService("sexy_recordings", { schema: await getSchema(), accountability, }); try { const existing = await recordingsService.readOne(req.params.id); // Only allow owner to delete if (existing.user_created !== accountability.user) { return res.status(403).json({ error: "Forbidden" }); } // Soft delete by setting status to archived await recordingsService.updateOne(req.params.id, { status: "archived", }); res.json({ success: true }); } catch (error: any) { 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" }); } }); // GET /sexy/analytics - Get analytics for the authenticated user's content router.get("/analytics", async (req, res) => { const accountability = req.accountability; if (!accountability?.user) { return res.status(401).json({ error: "Unauthorized" }); } try { const userId = accountability.user; // Get all videos by this user const videosService = new ItemsService("sexy_videos", { schema: await getSchema(), }); const videos = await videosService.readByQuery({ filter: { models: { directus_users_id: { _eq: userId, }, }, }, fields: ["id", "title", "slug", "likes_count", "plays_count", "upload_date"], limit: -1, }); if (videos.length === 0) { return res.json({ total_videos: 0, total_likes: 0, total_plays: 0, videos: [], }); } const videoIds = videos.map((v) => v.id); // Get play analytics const playsService = new ItemsService("sexy_video_plays", { schema: await getSchema(), }); const plays = await playsService.readByQuery({ filter: { video_id: { _in: videoIds, }, }, fields: ["video_id", "date_created", "duration_watched", "completed"], limit: -1, }); // Get like analytics const likesService = new ItemsService("sexy_video_likes", { schema: await getSchema(), }); const likes = await likesService.readByQuery({ filter: { video_id: { _in: videoIds, }, }, fields: ["video_id", "date_created"], limit: -1, }); // Calculate totals const totalLikes = videos.reduce((sum, v) => sum + (v.likes_count || 0), 0); const totalPlays = videos.reduce((sum, v) => sum + (v.plays_count || 0), 0); // Group plays by date for timeline const playsByDate = plays.reduce((acc, play) => { const date = new Date(play.date_created).toISOString().split("T")[0]; if (!acc[date]) acc[date] = 0; acc[date]++; return acc; }, {}); // Group likes by date for timeline const likesByDate = likes.reduce((acc, like) => { const date = new Date(like.date_created).toISOString().split("T")[0]; if (!acc[date]) acc[date] = 0; acc[date]++; return acc; }, {}); // Video-specific analytics const videoAnalytics = videos.map((video) => { const videoPlays = plays.filter((p) => p.video_id === video.id); const completedPlays = videoPlays.filter((p) => p.completed).length; const avgWatchTime = videoPlays.length > 0 ? videoPlays.reduce((sum, p) => sum + (p.duration_watched || 0), 0) / videoPlays.length : 0; return { id: video.id, title: video.title, slug: video.slug, upload_date: video.upload_date, likes: video.likes_count || 0, plays: video.plays_count || 0, completed_plays: completedPlays, completion_rate: video.plays_count ? (completedPlays / video.plays_count) * 100 : 0, avg_watch_time: Math.round(avgWatchTime), }; }); res.json({ total_videos: videos.length, total_likes: totalLikes, total_plays: totalPlays, plays_by_date: playsByDate, likes_by_date: likesByDate, videos: videoAnalytics, }); } catch (error: any) { console.error("Analytics error:", error); res.status(500).json({ error: error.message || "Failed to get analytics" }); } }); }, };