Skip to content

Commit

Permalink
init testing
Browse files Browse the repository at this point in the history
add title
add artist
  • Loading branch information
DrSkunk committed Oct 19, 2021
1 parent 24d3ef6 commit b5f13c3
Show file tree
Hide file tree
Showing 14 changed files with 4,443 additions and 4,185 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,7 @@ dist
examples/*

# build folder
build/*
build/*

# example folder
example/*
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Options:
--cover Path to an image file to be used as cover art, will be
automatically resized to 600x600 [string]
--overwrite Remove existing tags and overwrite them [boolean]
--title Add title [string]
--artist Add artist [string]
```

## Build as standalone
Expand Down
151 changes: 27 additions & 124 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#!/usr/bin/env node
const fs = require("fs");
const parse = require("csv-parse/lib/sync");
const NodeID3 = require("node-id3");
const getMP3Duration = require("get-mp3-duration");
const sharp = require("sharp");
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");

const { createTags } = require("./lib/createTags");

// Source for chaptering info: https://auphonic.com/blog/2013/07/03/chapter-marks-and-enhanced-podcasts/

async function main() {
const { markers, mp3, cover, overwrite } = yargs(hideBin(process.argv))
const { markers, mp3, cover, title, artist, overwrite } = yargs(
hideBin(process.argv)
)
.option("markers", {
type: "string",
description: "Path to Adobe Audition Markers",
Expand All @@ -28,139 +29,41 @@ async function main() {
type: "boolean",
description: "Remove existing tags and overwrite them",
})
.option("title", {
type: "string",
description: "Add title",
})
.option("artist", {
type: "string",
description: "Add artist",
})
.demandOption(["markers", "mp3"])
.parse();

const inputMp3 = mp3;
const inputCsv = fs
.readFileSync(markers, "utf8")
.replace(/[^\x00-\x7F]/g, ""); // remove all non-ascii characters, unfortunately including emojis 😢

const currentTags = await NodeID3.read(inputMp3);
const duration = getDuration(inputMp3);

if (!overwrite && (currentTags.chapter || currentTags.tableOfContents)) {
throw new Error("File already has chapter markers");
}
const mp3Buffer = await fs.promises.readFile(mp3);
const markersText = fs.readFileSync(markers, "utf8");

const chapters = parse(inputCsv, {
columns: true,
delimiter: "\t",
skip_empty_lines: true,
const totalTags = await createTags({
mp3Buffer,
overwrite,
markersText,
title,
artist,
cover,
});
console.info(`Read ${chapters.length} chapters from csv`);

if (formatStartTime(parseTime(chapters[0].Start)) > 0) {
console.info("Adding intro chapter");
const intro = {
Name: "Intro",
Start: "0:00.000",
};
chapters.unshift(intro);
}

const chapterTag = chapters
.map((record, i) => ({
elementID: `chap${i}`,
startTimeMs: formatStartTime(parseTime(record.Start)),
tags: {
title: record.Name,
},
}))
.map((record, i, records) => ({
...record,
endTimeMs:
i === records.length - 1 ? duration : records[i + 1].startTimeMs,
}));

const comment = chapters
.map(
(record, i) => `${formatTime(parseTime(record.Start))}: ${record.Name}`
)
.join("\n");
console.info("Writing the following chapter tags:");
console.info(comment);

const totalTags = {
chapter: chapterTag,
comment: {
language: "nld",
text: comment,
},
unsynchronisedLyrics: {
language: "nld",
text: comment,
},
tableOfContents: [
{
elementID: "toc1",
isOrdered: true,
elements: chapterTag.map(({ elementID }) => elementID),
},
],
};

console.info(`Writing ${chapterTag.length} chapters to mp3`);

if (cover) {
console.info("Adding cover image");
totalTags.image = {
mime: "image/png",
type: {
id: 3,
name: "front cover",
},
imageBuffer: await sharp(cover).resize(600, 600).png().toBuffer(),
};
}

let success;
if (overwrite) {
success = NodeID3.write(totalTags, inputMp3);
success = NodeID3.write(totalTags, mp3Buffer);
} else {
success = NodeID3.update(totalTags, inputMp3);
success = NodeID3.update(totalTags, mp3Buffer);
}

const outputName = `${mp3.replace(/\.mp3$/, "")}-chapters.mp3`;
console.log(`Writing to ${outputName}`);
if (!success) {
throw new Error("Failed to write ID3 tags");
}
await fs.promises.writeFile(outputName, mp3Buffer);
console.info("Successfully wrote ID3 tags");
}
main().catch((err) => console.error(err));

function parseTime(time) {
const times = time.split(":");
let hours = 0;
let minutes = 0;
// only minutes, seconds and milliseconds
if (times.length === 2) {
minutes = parseInt(times[0]);
} else if (times.length === 3) {
// hours, minutes, seconds and milliseconds
hours = parseInt(times[0]);
minutes = parseInt(times[1]);
} else {
throw new Error("Invalid time format");
}
const [s, ms] = times[times.length - 1].split(".");
const seconds = parseInt(s);
const milliseconds = parseInt(ms);

return { hours, minutes, seconds, milliseconds };
}

function formatStartTime({ hours, minutes, seconds, milliseconds }) {
return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds;
}

function formatTime({ hours, minutes, seconds }) {
const h = hours.toString().padStart(2, "0");
const m = minutes.toString().padStart(2, "0");
const s = seconds.toString().padStart(2, "0");
return `${h}:${m}:${s}`;
}

function getDuration(filePath) {
const buffer = fs.readFileSync(filePath);
return getMP3Duration(buffer);
}
66 changes: 66 additions & 0 deletions lib/createTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const NodeID3 = require("node-id3");
const sharp = require("sharp");
const getMP3Duration = require("get-mp3-duration");
const { parseMarkers } = require("./parseMarkers");

async function createTags({
mp3Buffer,
overwrite,
markersText,
title,
artist,
cover,
}) {
const currentTags = await NodeID3.read(mp3Buffer);
const duration = getMP3Duration(mp3Buffer);

if (!overwrite && (currentTags.chapter || currentTags.tableOfContents)) {
throw new Error("File already has chapter markers");
}
const { chapterTag, comment, tableOfContentsElements } = parseMarkers(
markersText,
duration
);

const totalTags = {
chapter: chapterTag,
comment: {
language: "nld",
text: comment,
},
unsynchronisedLyrics: {
language: "nld",
text: comment,
},
tableOfContents: [
{
elementID: "toc1",
isOrdered: true,
elements: tableOfContentsElements,
},
],
};

if (title) {
totalTags.title = title;
}
if (artist) {
totalTags.artist = artist;
}

console.info(`Writing ${chapterTag.length} chapters to mp3`);

if (cover) {
console.info("Adding cover image");
totalTags.image = {
mime: "image/png",
type: {
id: 3,
name: "front cover",
},
imageBuffer: await sharp(cover).resize(600, 600).png().toBuffer(),
};
}
return totalTags;
}
exports.createTags = createTags;
4 changes: 4 additions & 0 deletions lib/formatStartTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function formatStartTime({ hours, minutes, seconds, milliseconds }) {
return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds;
}
exports.formatStartTime = formatStartTime;
7 changes: 7 additions & 0 deletions lib/formatTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function formatTime({ hours, minutes, seconds }) {
const h = hours.toString().padStart(2, "0");
const m = minutes.toString().padStart(2, "0");
const s = seconds.toString().padStart(2, "0");
return `${h}:${m}:${s}`;
}
exports.formatTime = formatTime;
49 changes: 49 additions & 0 deletions lib/parseMarkers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const parse = require("csv-parse/lib/sync");
const { formatTime } = require("./formatTime");
const { formatStartTime } = require("./formatStartTime");
const { parseTime } = require("./parseTime");

function parseMarkers(inputCsv, duration) {
// remove all non-ascii characters, unfortunately including emojis 😢
const cleanCsv = inputCsv.replace(/[^\x00-\x7F]/g, "");

const chapters = parse(cleanCsv, {
columns: true,
delimiter: "\t",
skip_empty_lines: true,
});
console.info(`Read ${chapters.length} chapters from csv`);

if (formatStartTime(parseTime(chapters[0].Start)) > 0) {
console.info("Adding intro chapter");
const intro = {
Name: "Intro",
Start: "0:00.000",
};
chapters.unshift(intro);
}

const chapterTag = chapters
.map((record, i) => ({
elementID: `chap${i}`,
startTimeMs: formatStartTime(parseTime(record.Start)),
tags: {
title: record.Name,
},
}))
.map((record, i, records) => ({
...record,
endTimeMs:
i === records.length - 1 ? duration : records[i + 1].startTimeMs,
}));

const comment = chapters
.map((record) => `${formatTime(parseTime(record.Start))}: ${record.Name}`)
.join("\n");
console.info("Writing the following chapter tags:");
console.info(comment);

const tableOfContentsElements = chapterTag.map(({ elementID }) => elementID);
return { chapterTag, comment, tableOfContentsElements };
}
exports.parseMarkers = parseMarkers;
29 changes: 29 additions & 0 deletions lib/parseTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const timeError = new Error("Invalid time format");
function parseTime(time) {
const times = time.split(":");
let hours = 0;
let minutes = 0;
// only minutes, seconds and milliseconds
if (times.length === 2) {
minutes = parseInt(times[0]);
} else if (times.length === 3) {
// hours, minutes, seconds and milliseconds
hours = parseInt(times[0]);
minutes = parseInt(times[1]);
} else {
throw timeError;
}
const [s, ms] = times[times.length - 1].split(".");
const seconds = parseInt(s);
const milliseconds = parseInt(ms);

const result = { hours, minutes, seconds, milliseconds };
Object.values(result).forEach((value) => {
if (isNaN(value)) {
throw timeError;
}
});

return { hours, minutes, seconds, milliseconds };
}
exports.parseTime = parseTime;
Loading

0 comments on commit b5f13c3

Please sign in to comment.