// utils/duration.util.js const AVG_WORDS_PER_MINUTE = 200; function stripHtml(html) { // Simple regex strip — no extra dependency needed return (html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); } /** * Estimate seconds for a single lesson block * Block shape: { type: 'text'|'image'|'video', content, word_count, video_duration_seconds } */ function estimateBlockDuration(block) { const videoDuration = Number(block.video_duration_seconds) || 0; // Derive word count from HTML content on the fly const getWordCount = (html) => { const text = stripHtml(html); return text ? text.split(" ").filter(Boolean).length : 0; }; const readingSecs = (html) => Math.ceil((getWordCount(html) / AVG_WORDS_PER_MINUTE) * 60); switch (block.type) { case "text": return readingSecs(block.content?.body); case "image": return 60; case "video": return videoDuration; case "text-image": case "text_image": return readingSecs(block.content?.body) + 60; case "text-video": case "text_video": return readingSecs(block.content?.body) + videoDuration; default: return 0; } } /** * Recompute and persist duration_seconds up the chain: * blocks → lesson → unit → course */ async function recomputeDurations(lessonId) { const Lesson = require("../models/courses/lessons.mdl"); const Unit = require("../models/courses/units.mdl"); const { Course } = require("../models/courses/courses.mdl"); const LessonPage = require("../models/courses/lesson_page.mdl"); // 1. Lesson duration from blocks const page = await LessonPage.findOne({ where: { lesson_id: lessonId } }); const lessonSecs = (page?.blocks ?? []).reduce( (sum, block) => sum + estimateBlockDuration(block), 0 ); // Guard against NaN before DB write const safeLessonSecs = isNaN(lessonSecs) ? 0 : lessonSecs; await Lesson.update({ duration_seconds: safeLessonSecs }, { where: { lesson_id: lessonId } }); // 2. Unit duration — sum of its lessons const lesson = await Lesson.findOne({ where: { lesson_id: lessonId } }); const [unitResult] = await Lesson.sequelize.query(` SELECT COALESCE(SUM(duration_seconds), 0) AS total FROM lessons WHERE unit_id = :unitId AND "deletedAt" IS NULL `, { replacements: { unitId: lesson.unit_id }, type: Lesson.sequelize.QueryTypes.SELECT }); await Unit.update( { duration_seconds: unitResult.total }, { where: { unit_id: lesson.unit_id } } ); // 3. Course duration — sum of its units const unit = await Unit.findOne({ where: { unit_id: lesson.unit_id } }); const [courseResult] = await Lesson.sequelize.query(` SELECT COALESCE(SUM(duration_seconds), 0) AS total FROM units WHERE course_id = :courseId AND "deletedAt" IS NULL `, { replacements: { courseId: unit.course_id }, type: Lesson.sequelize.QueryTypes.SELECT }); await Course.update( { duration_seconds: courseResult.total }, { where: { course_id: unit.course_id } } ); } /** * Format seconds → human readable: "1 hr 10 mins", "45 mins", "30 secs" */ function formatDuration(seconds) { if (!seconds) return "0 mins"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; const parts = []; if (h) parts.push(`${h} hour${h !== 1 ? "s" : ""}`); if (m) parts.push(`${m} minute${m !== 1 ? "s" : ""}`); if (!h && !m && s) parts.push(`${s} second${s !== 1 ? "s" : ""}`); return parts.join(" "); } module.exports = { estimateBlockDuration, recomputeDurations, formatDuration };