Skip to main content

Validate YouTube Chapter Timestamps with Node.js

Updated 5 min readMarkdown

A shot list can change every few seconds, but a chapter list works best when a viewer can use it to jump to a new task. This local Node.js tool checks chapter starts against the runtime and prints description-ready lines. It takes a small JSON file, makes no network requests, and leaves the video untouched.

1. Mark changes in the viewer’s task

Open the final export in your editor and note where a viewer can start a new task. For a tutorial, good candidates might be seeing the finished result, setting up the example, and checking the export. Use the final video's clock. If the shot list was timed against an excerpt, first translate its times back to the full video and spot-check the boundaries.

YouTube’s manual chapter rules call for a zero start, at least three starts in increasing order, and a minimum of ten seconds per chapter. For the final chapter, subtract its start from the video duration. For every other chapter, use the next chapter’s start. The script keeps your input order so it can flag a misplaced entry instead of sorting it silently.

2. Save starts as whole seconds

This example uses a fictional 96-second video. Save the data as chapter-input.json. The script is scoped to exports shorter than one hour. Use whole-second starts and round a fractional total runtime down for this check, so the final chapter does not get credit for a partial second.

{
  "duration": 96,
  "chapters": [
    { "start": 0, "title": "See the finished result" },
    { "start": 18, "title": "Set up the example" },
    { "start": 61, "title": "Check the final export" }
  ]
}

3. Validate the intervals

Save this function as chapters.mjs. It accepts titles on one line, keeps non-English text, and reports chapter numbers starting at one. It computes the duration of the last chapter with the same minimum as the others.

export function buildChapters(duration, chapters) {
  if (!Number.isInteger(duration) || duration < 30 || duration >= 3600) {
    throw new Error("Use a whole-second duration from 30 to 3599");
  }
  if (!Array.isArray(chapters) || chapters.length < 3) {
    throw new Error("Provide at least three chapters");
  }
  const rows = Array.from(chapters, (row, i) => {
    const { start, title } = row ?? {};
    if (!Number.isInteger(start) || start < 0 || start >= duration) {
      throw new Error(`Chapter ${i + 1}: start must be within the video`);
    }
    if (typeof title !== "string" || !title.trim() ||
        /[\p{Cc}\u2028\u2029]/u.test(title)) {
      throw new Error(`Chapter ${i + 1}: use a single-line title`);
    }
    return { start, title: title.trim() };
  });
  if (rows[0].start !== 0) throw new Error("Start the first chapter at zero");

  return rows.map(({ start, title }, i) => {
    const end = rows[i + 1]?.start ?? duration;
    if (end <= start) {
      throw new Error(`Chapter ${i + 1}: starts must increase`);
    }
    if (end - start < 10) {
      throw new Error(`Chapter ${i + 1}: only ${end - start} seconds`);
    }
    const two = (n) => String(n).padStart(2, "0");
    return `${two(Math.floor(start / 60))}:${two(start % 60)} ${title}`;
  }).join("\n");
}

4. Run the formatter locally

Save this wrapper as format-chapters.mjs and run node format-chapters.mjs chapter-input.json. The successful example prints 00:00 See the finished result, 00:18 Set up the example, and 01:01 Check the final export on separate lines.

import { readFile } from "node:fs/promises";
import { buildChapters } from "./chapters.mjs";

try {
  const file = process.argv[2];
  if (!file) throw new Error("Usage: node format-chapters.mjs input.json");
  const { duration, chapters } = JSON.parse(await readFile(file, "utf8"));
  console.log(buildChapters(duration, chapters));
} catch (error) {
  console.error(error.message);
  process.exitCode = 2;
}

If you move the last start to 87, the video ends nine seconds later. The command prints the error to stderr and exits with code 2. Merge that section or choose an earlier start that still matches what the viewer will see.

5. Check boundary cases

Save this file as test.mjs and run node test.mjs. The first case verifies that exactly ten seconds is accepted for every chapter. Other cases check duplicate starts, descending order, a start at the video end, and a title that would break the output line.

import assert from "node:assert/strict";
import { buildChapters } from "./chapters.mjs";

const rows = (...starts) => starts.map((start, i) => ({
  start, title: `Part ${i + 1}`,
}));
assert.equal(buildChapters(30, rows(0, 10, 20)),
  "00:00 Part 1\n00:10 Part 2\n00:20 Part 3");
assert.match(buildChapters(96, rows(0, 18, 61)), /01:01 Part 3$/);
assert.throws(() => buildChapters(96, rows(0, 18, 87)), /only 9 seconds/);
assert.throws(() => buildChapters(96, rows(0, 9, 40)), /only 9 seconds/);
assert.throws(() => buildChapters(96, rows(0, 18, 18)), /must increase/);
assert.throws(() => buildChapters(96, rows(0, 50, 18)), /must increase/);
assert.throws(() => buildChapters(96, rows(1, 18, 61)), /at zero/);
assert.throws(() => buildChapters(96, rows(0, 18)), /at least three/);
assert.throws(() => buildChapters(96, rows(0, 18, 96)), /within the video/);
assert.throws(() => buildChapters(96, rows(0, 18.5, 61)), /within the video/);
assert.throws(() => buildChapters(96, [...rows(0, 18),
  { start: 61, title: "Heading\nExtra line" }]), /single-line/);
assert.throws(() => buildChapters(3600, rows(0, 18, 61)), /duration/);
console.log("12 checks passed");

6. Review the description against the final cut

Use the printed lines in the description of a video you manage. Before saving, jump to each start in the final export and check whether the title describes what begins there. If the file is recut, update its duration and chapter starts together.

The example and assertions were checked with synthetic input in Node.js 24.20.0. No video was uploaded, and chapter rendering and channel eligibility were outside the test.

AI assistance was used to draft and edit this article.

Source workflow. YouTube Video to Prompt: Shot-by-Shot Extraction Guide

Platform reference. YouTube Help: Video Chapters

2 views

About the author

  • The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/

No comments yet. Be the first to comment.

More from this blog