First commit

This commit is contained in:
Kuba Orlik 2023-12-15 20:26:03 +01:00
parent 84a9b530d4
commit 0ea31a2e56
9 changed files with 948 additions and 30 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ lib
coverage
@types
public/bundle.js
/public/bundle.js.map

14
captain-definition Normal file
View File

@ -0,0 +1,14 @@
{
"schemaVersion": 2,
"dockerfileLines": [
"FROM node:20-alpine",
"RUN mkdir -p /usr/src/app",
"WORKDIR /usr/src/app",
"COPY ./ /usr/src/app",
"RUN npm install && npm cache clean --force && npm run build",
"ENV NODE_ENV production",
"ENV PORT 80",
"EXPOSE 80",
"CMD [ \"npm\", \"start\" ]"
]
}

780
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,13 @@
"main": "lib/index.js",
"scripts": {
"test": "mocha",
"build": "tsc",
"build": "esbuild src/browser.ts --bundle --minify --sourcemap --target=firefox120 --outfile=public/bundle.js",
"prepare": "npm run build",
"clean-coverage": "rm -rf coverage .nyc_output .xunit",
"coverage": "npm run clean-coverage && nyc mocha",
"test-reports": "npm run clean-coverage && nyc --reporter clover mocha --reporter xunit --reporter-option output=.xunit",
"coverage-html": "npm run test-reports && nyc report --reporter lcov && xdg-open coverage/lcov-report/index.html"
"coverage-html": "npm run test-reports && nyc report --reporter lcov && xdg-open coverage/lcov-report/index.html",
"start": "http-server public -p 8080"
},
"author": "",
"license": "ISC",
@ -19,6 +20,7 @@
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"esbuild": "^0.19.9",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
@ -31,5 +33,8 @@
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"types": "./@types/index.d.ts"
"types": "./@types/index.d.ts",
"dependencies": {
"http-server": "^14.1.1"
}
}

81
public/index.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<title>Podcast chapter converter</title>
<style>
#container {
display: flex;
align-items: center;
}
</style>
</head>
<body>
<script src="/bundle.js"></script>
<script>
function convert() {
output.value = window.encoders[to.value](
window.parsers[from.value](input.value)
);
}
function download(data, filename, type) {
var file = new Blob([data], { type: type });
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function download_output() {
download(
output.value,
"chapters." + to.value,
"application/json"
);
}
</script>
<h1>Convert podcast chapters</h1>
<div id="container">
<div>
<label>
From:
<select id="from">
<option value="audacity">Audacity labels export</option>
<option value="youtube">
Youtube plaintext timestamps
</option>
</select>
</label>
<div>
<textarea rows="40" cols="60" id="input"></textarea>
</div>
</div>
<div>
<button onClick="convert()">Convert→</button>
</div>
<div>
<label>
To:
<select id="to" onchange="convert()">
<option value="json">
JSON Chapters (for Castapod)
</option>
<option value="youtube">
Youtube plaintext timestamps
</option>
</select>
</label>
<button onclick="download_output()">download</button>
<div>
<textarea rows="40" cols="60" id="output"></textarea>
</div>
</div>
</div>
</body>
</html>

4
src/browser.ts Normal file
View File

@ -0,0 +1,4 @@
import { encoders, parsers } from ".";
(window as any).parsers = parsers;
(window as any).encoders = encoders;

View File

@ -1,11 +0,0 @@
import { Example } from "./index";
import * as assert from "assert";
describe("Example", () => {
describe("example", () => {
it("should equal 'example'", () => {
const example = new Example();
assert.equal(example.example(), "example");
});
});
});

View File

@ -1,5 +1,68 @@
export class Example {
example(): string {
return "example";
import fs from "fs";
type Timestamp = {
timestamp: number;
title: string;
};
export function parse_audacity(content: string): Timestamp[] {
const lines = content.split("\n");
return lines
.filter((line) => line.length > 4)
.map((line) => line.split("\t"))
.map(([start_s, _, title]) => ({
timestamp: parseFloat(start_s),
title,
}));
}
function parseTimestamp(s: string): number {
return s
.split(":")
.map((e, i, all) => Math.pow(60, all.length - i - 1) * parseInt(e))
.reduce((a, b) => a + b);
}
function toTimestamp(n: number): string {
return `${Math.floor(n / 60)
.toString()
.padStart(2, "0")}:${(n % 60).toString().padStart(2, "0")}`;
}
export function parse_youtube(content: string): Timestamp[] {
console.log(
content
.split("\n")
.filter((l) => l.length > 4)
.map((l) => l.match(/([0-9]{1,}:[0-9]{2})\s(.*)/))
);
return (
content
.split("\n")
.filter((l) => l.length > 4)
.map((l) => l.match(/([0-9]{1,}:[0-9]{2})\s(.*)/))
.filter((m) => m != null) as string[][]
).map(([_, time_s, title]) => ({
title,
timestamp: parseTimestamp(time_s),
}));
}
export function to_youtube(timestamps: Timestamp[]): string {
return timestamps
.map((ts) => `${toTimestamp(ts.timestamp)} ${ts.title}\n`)
.join("\n"); // yes, double newlines
}
export function to_json(timestamps: Timestamp[]): string {
return JSON.stringify({
version: "1.2",
chapters: timestamps.map((ts) => ({
startTime: ts.timestamp,
title: ts.title,
})),
});
}
export const parsers = { youtube: parse_youtube, audacity: parse_audacity };
export const encoders = { youtube: to_youtube, json: to_json };

View File

@ -8,7 +8,7 @@
"target": "ES6",
"declaration": true,
"esModuleInterop": true,
"lib": ["ES6", "ESNext"],
"lib": ["ES6", "ESNext", "DOM"],
"outDir": "lib",
"checkJs": true,
"allowJs": true,
@ -17,6 +17,6 @@
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*", ],
"include": ["src/**/*"],
"exclude": ["node_modules/**"]
}