First commit
This commit is contained in:
parent
84a9b530d4
commit
0ea31a2e56
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ lib
|
||||
coverage
|
||||
@types
|
||||
|
||||
public/bundle.js
|
||||
/public/bundle.js.map
|
||||
|
14
captain-definition
Normal file
14
captain-definition
Normal 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
780
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -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
81
public/index.html
Normal 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
4
src/browser.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { encoders, parsers } from ".";
|
||||
|
||||
(window as any).parsers = parsers;
|
||||
(window as any).encoders = encoders;
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
71
src/index.ts
71
src/index.ts
@ -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 };
|
||||
|
@ -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/**"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user