This commit is contained in:
rgrgogu
2026-05-20 13:22:29 +08:00
parent 6907e9bb2d
commit 6a5145f553
26 changed files with 2615 additions and 221 deletions
+112
View File
@@ -0,0 +1,112 @@
// 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} hr`);
if (m) parts.push(`${m} min${m !== 1 ? "s" : ""}`);
if (!h && !m && s) parts.push(`${s} sec${s !== 1 ? "s" : ""}`);
return parts.join(" ");
}
module.exports = { estimateBlockDuration, recomputeDurations, formatDuration };