mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
112 lines
3.5 KiB
JavaScript
112 lines
3.5 KiB
JavaScript
// 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 }; |