@emgeebee/music_downcoder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +60 -0
  2. package/batch.sh +26 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +60 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config.d.ts +38 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +97 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/constants.d.ts +19 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +34 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/getMeta.d.ts +18 -0
  16. package/dist/getMeta.d.ts.map +1 -0
  17. package/dist/getMeta.js +100 -0
  18. package/dist/getMeta.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +10 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/paths.d.ts +11 -0
  24. package/dist/paths.d.ts.map +1 -0
  25. package/dist/paths.js +26 -0
  26. package/dist/paths.js.map +1 -0
  27. package/dist/processEncoder.d.ts +24 -0
  28. package/dist/processEncoder.d.ts.map +1 -0
  29. package/dist/processEncoder.js +238 -0
  30. package/dist/processEncoder.js.map +1 -0
  31. package/dist/prompts.d.ts +12 -0
  32. package/dist/prompts.d.ts.map +1 -0
  33. package/dist/prompts.js +19 -0
  34. package/dist/prompts.js.map +1 -0
  35. package/dist/run.d.ts +27 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +100 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/scripts.d.ts +13 -0
  40. package/dist/scripts.d.ts.map +1 -0
  41. package/dist/scripts.js +89 -0
  42. package/dist/scripts.js.map +1 -0
  43. package/dist/serve.d.ts +3 -0
  44. package/dist/serve.d.ts.map +1 -0
  45. package/dist/serve.js +137 -0
  46. package/dist/serve.js.map +1 -0
  47. package/dist/web/index.html +243 -0
  48. package/package.json +43 -0
package/dist/serve.js ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ import http from "http";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import minimist from "minimist";
7
+ import { getConfig } from "./config.js";
8
+ import { createAutoPrompts, clearCommandScripts, executeAllScripts, executeScript, initRuntime, listCommandScripts, runDowncoder, } from "./run.js";
9
+ import { describeScripts } from "./scripts.js";
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const args = minimist(process.argv.slice(2), {
12
+ string: ["H", "p", "c"],
13
+ alias: { H: "host", p: "port", c: "config" },
14
+ default: { H: "0.0.0.0", p: "3798" },
15
+ });
16
+ const configPath = args.config
17
+ ? path.resolve(args.config)
18
+ : undefined;
19
+ if (!configPath) {
20
+ console.error("serve requires -c /path/to/conf.json");
21
+ process.exit(1);
22
+ }
23
+ initRuntime(configPath);
24
+ const config = getConfig();
25
+ const prompts = createAutoPrompts(config);
26
+ const port = Number(args.port);
27
+ const host = args.host;
28
+ const toScriptResponse = (scriptPaths) => describeScripts(scriptPaths, config);
29
+ let runningJob = null;
30
+ let lastScripts = [];
31
+ const readBody = (req) => new Promise((resolve, reject) => {
32
+ let data = "";
33
+ req.on("data", (chunk) => {
34
+ data += chunk;
35
+ });
36
+ req.on("end", () => resolve(data));
37
+ req.on("error", reject);
38
+ });
39
+ const sendJson = (res, status, payload) => {
40
+ res.writeHead(status, { "Content-Type": "application/json" });
41
+ res.end(JSON.stringify(payload));
42
+ };
43
+ const htmlPath = path.join(__dirname, "web", "index.html");
44
+ const pageHtml = fs.readFileSync(htmlPath, "utf-8");
45
+ const server = http.createServer(async (req, res) => {
46
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
47
+ if (req.method === "GET" && url.pathname === "/") {
48
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
49
+ res.end(pageHtml);
50
+ return;
51
+ }
52
+ if (req.method === "GET" && url.pathname === "/api/config") {
53
+ sendJson(res, 200, {
54
+ startFolders: config.startFolders,
55
+ encoders: config.encoders,
56
+ });
57
+ return;
58
+ }
59
+ if (req.method === "GET" && url.pathname === "/api/scripts") {
60
+ lastScripts = listCommandScripts(config);
61
+ sendJson(res, 200, toScriptResponse(lastScripts));
62
+ return;
63
+ }
64
+ if (req.method === "GET" && url.pathname === "/api/status") {
65
+ sendJson(res, 200, { busy: runningJob !== null });
66
+ return;
67
+ }
68
+ if (req.method === "POST" && url.pathname === "/api/run") {
69
+ if (runningJob) {
70
+ sendJson(res, 409, { error: "A job is already running" });
71
+ return;
72
+ }
73
+ const body = JSON.parse(await readBody(req));
74
+ runningJob = runDowncoder({
75
+ startFolder: body.startFolder,
76
+ encoderIds: body.encoderIds,
77
+ filter: body.filter ?? "",
78
+ configPath,
79
+ prompts,
80
+ })
81
+ .then((result) => {
82
+ lastScripts = result.scripts;
83
+ return result;
84
+ })
85
+ .finally(() => {
86
+ runningJob = null;
87
+ });
88
+ const result = (await runningJob);
89
+ sendJson(res, 200, {
90
+ ...result,
91
+ scripts: toScriptResponse(result.scripts),
92
+ });
93
+ return;
94
+ }
95
+ if (req.method === "POST" && url.pathname === "/api/scripts/clear") {
96
+ if (runningJob) {
97
+ sendJson(res, 409, { error: "A job is already running" });
98
+ return;
99
+ }
100
+ const cleared = clearCommandScripts(config);
101
+ lastScripts = [];
102
+ sendJson(res, 200, { cleared });
103
+ return;
104
+ }
105
+ if (req.method === "POST" && url.pathname === "/api/scripts/run") {
106
+ if (runningJob) {
107
+ sendJson(res, 409, { error: "A job is already running" });
108
+ return;
109
+ }
110
+ const body = JSON.parse(await readBody(req));
111
+ runningJob = (async () => {
112
+ if (body.all) {
113
+ const scripts = listCommandScripts(config);
114
+ return executeAllScripts(scripts, config);
115
+ }
116
+ if (!body.path) {
117
+ throw new Error("path is required");
118
+ }
119
+ return executeScript(body.path, config);
120
+ })().finally(() => {
121
+ runningJob = null;
122
+ });
123
+ const result = await runningJob;
124
+ lastScripts = listCommandScripts(config);
125
+ sendJson(res, 200, {
126
+ ...result,
127
+ scripts: toScriptResponse(lastScripts),
128
+ });
129
+ return;
130
+ }
131
+ sendJson(res, 404, { error: "Not found" });
132
+ });
133
+ server.listen(port, host, () => {
134
+ console.log(`[music_downcoder] web UI at http://${host === "0.0.0.0" ? "localhost" : host}:${port}`);
135
+ console.log(`[music_downcoder] config: ${configPath}`);
136
+ });
137
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Music Downcoder</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: system-ui, sans-serif;
11
+ line-height: 1.4;
12
+ }
13
+ body {
14
+ margin: 0 auto;
15
+ max-width: 900px;
16
+ padding: 1.5rem;
17
+ }
18
+ h1, h2 {
19
+ margin-top: 0;
20
+ }
21
+ fieldset {
22
+ border: 1px solid #8884;
23
+ border-radius: 8px;
24
+ margin-bottom: 1rem;
25
+ padding: 1rem;
26
+ }
27
+ label {
28
+ display: block;
29
+ margin: 0.35rem 0;
30
+ }
31
+ button {
32
+ margin-right: 0.5rem;
33
+ margin-top: 0.5rem;
34
+ padding: 0.5rem 0.9rem;
35
+ }
36
+ pre {
37
+ background: #0001;
38
+ border-radius: 8px;
39
+ min-height: 8rem;
40
+ overflow: auto;
41
+ padding: 1rem;
42
+ white-space: pre-wrap;
43
+ }
44
+ .hidden {
45
+ display: none;
46
+ }
47
+ .script-row {
48
+ align-items: center;
49
+ display: flex;
50
+ gap: 0.75rem;
51
+ justify-content: space-between;
52
+ margin: 0.4rem 0;
53
+ }
54
+ .script-row code {
55
+ flex: 1;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ white-space: nowrap;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <h1>Music Downcoder</h1>
64
+ <p>Generate ffmpeg conversion scripts, then run them from the browser.</p>
65
+
66
+ <section id="setup-section">
67
+ <h2>1. Scan library</h2>
68
+ <form id="run-form">
69
+ <fieldset>
70
+ <legend>Source folder</legend>
71
+ <div id="start-folders"></div>
72
+ </fieldset>
73
+ <fieldset>
74
+ <legend>Encoders</legend>
75
+ <div id="encoders"></div>
76
+ </fieldset>
77
+ <label>
78
+ Artist filter (prefix)
79
+ <input id="filter" type="text" value="" />
80
+ </label>
81
+ <button type="submit" id="run-btn">Generate scripts</button>
82
+ </form>
83
+ </section>
84
+
85
+ <section id="scripts-section" class="hidden">
86
+ <h2>2. Run scripts</h2>
87
+ <div id="scripts-list"></div>
88
+ <button type="button" id="run-all-btn">Run all scripts</button>
89
+ <button type="button" id="refresh-btn">Refresh list</button>
90
+ <button type="button" id="clear-btn">Clear scripts</button>
91
+ </section>
92
+
93
+ <h2>Output</h2>
94
+ <pre id="log"></pre>
95
+
96
+ <script>
97
+ const logEl = document.getElementById("log");
98
+ const scriptsSection = document.getElementById("scripts-section");
99
+ const scriptsList = document.getElementById("scripts-list");
100
+ const startFoldersEl = document.getElementById("start-folders");
101
+ const encodersEl = document.getElementById("encoders");
102
+
103
+ const appendLog = (text) => {
104
+ logEl.textContent += text + "\n";
105
+ logEl.scrollTop = logEl.scrollHeight;
106
+ };
107
+
108
+ const setBusy = (busy) => {
109
+ document.getElementById("run-btn").disabled = busy;
110
+ document.getElementById("run-all-btn").disabled = busy;
111
+ document.getElementById("clear-btn").disabled = busy;
112
+ };
113
+
114
+ const renderScripts = (scripts) => {
115
+ scriptsList.innerHTML = "";
116
+ if (!scripts.length) {
117
+ scriptsList.textContent = "No scripts found in cmd folder.";
118
+ return;
119
+ }
120
+ for (const script of scripts) {
121
+ const row = document.createElement("div");
122
+ row.className = "script-row";
123
+ const code = document.createElement("code");
124
+ code.textContent = script.displayName;
125
+ code.title = script.name;
126
+ const btn = document.createElement("button");
127
+ btn.type = "button";
128
+ btn.textContent = "Run";
129
+ btn.addEventListener("click", async () => {
130
+ setBusy(true);
131
+ appendLog(`Running ${script.displayName}...`);
132
+ const res = await fetch("/api/scripts/run", {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ path: script.path }),
136
+ });
137
+ const data = await res.json();
138
+ appendLog(data.output || JSON.stringify(data));
139
+ if (data.scripts) {
140
+ renderScripts(data.scripts);
141
+ }
142
+ setBusy(false);
143
+ });
144
+ row.append(code, btn);
145
+ scriptsList.append(row);
146
+ }
147
+ };
148
+
149
+ const loadScripts = async () => {
150
+ const res = await fetch("/api/scripts");
151
+ const scripts = await res.json();
152
+ scriptsSection.classList.remove("hidden");
153
+ renderScripts(scripts);
154
+ };
155
+
156
+ const loadConfig = async () => {
157
+ const res = await fetch("/api/config");
158
+ const data = await res.json();
159
+ startFoldersEl.innerHTML = "";
160
+ encodersEl.innerHTML = "";
161
+
162
+ data.startFolders.forEach((folder, index) => {
163
+ const id = `start-${index}`;
164
+ const label = document.createElement("label");
165
+ label.innerHTML = `<input type="radio" name="startFolder" id="${id}" value="${folder.path}" ${index === 0 ? "checked" : ""} /> ${folder.name} <small>(${folder.path})</small>`;
166
+ startFoldersEl.append(label);
167
+ });
168
+
169
+ data.encoders.forEach((encoder, index) => {
170
+ const id = `encoder-${index}`;
171
+ const label = document.createElement("label");
172
+ label.innerHTML = `<input type="checkbox" name="encoder" id="${id}" value="${encoder.id}" ${encoder.default ? "checked" : ""} /> ${encoder.label}${encoder.description ? ` — ${encoder.description}` : ""}`;
173
+ encodersEl.append(label);
174
+ });
175
+ };
176
+
177
+ document.getElementById("run-form").addEventListener("submit", async (event) => {
178
+ event.preventDefault();
179
+ const startFolder = document.querySelector('input[name="startFolder"]:checked')?.value;
180
+ const encoderIds = [...document.querySelectorAll('input[name="encoder"]:checked')].map((el) => el.value);
181
+ const filter = document.getElementById("filter").value;
182
+
183
+ if (!startFolder || !encoderIds.length) {
184
+ appendLog("Select a source folder and at least one encoder.");
185
+ return;
186
+ }
187
+
188
+ setBusy(true);
189
+ appendLog("Scanning library and generating scripts...");
190
+ const res = await fetch("/api/run", {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({ startFolder, encoderIds, filter }),
194
+ });
195
+ const data = await res.json();
196
+ if (data.error) {
197
+ appendLog(data.error);
198
+ } else {
199
+ appendLog(`Done in ${data.elapsedSeconds}s. ${data.scripts.length} script(s) created.`);
200
+ renderScripts(data.scripts);
201
+ scriptsSection.classList.remove("hidden");
202
+ }
203
+ setBusy(false);
204
+ });
205
+
206
+ document.getElementById("run-all-btn").addEventListener("click", async () => {
207
+ setBusy(true);
208
+ appendLog("Running all scripts...");
209
+ const res = await fetch("/api/scripts/run", {
210
+ method: "POST",
211
+ headers: { "Content-Type": "application/json" },
212
+ body: JSON.stringify({ all: true }),
213
+ });
214
+ const data = await res.json();
215
+ appendLog(data.output || JSON.stringify(data));
216
+ if (data.scripts) {
217
+ renderScripts(data.scripts);
218
+ }
219
+ setBusy(false);
220
+ });
221
+
222
+ document.getElementById("refresh-btn").addEventListener("click", loadScripts);
223
+
224
+ document.getElementById("clear-btn").addEventListener("click", async () => {
225
+ if (!confirm("Delete all generated scripts in the cmd folder?")) {
226
+ return;
227
+ }
228
+ setBusy(true);
229
+ const res = await fetch("/api/scripts/clear", { method: "POST" });
230
+ const data = await res.json();
231
+ if (data.error) {
232
+ appendLog(data.error);
233
+ } else {
234
+ appendLog(`Cleared ${data.cleared} script(s).`);
235
+ renderScripts([]);
236
+ }
237
+ setBusy(false);
238
+ });
239
+
240
+ loadConfig().then(loadScripts);
241
+ </script>
242
+ </body>
243
+ </html>
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@emgeebee/music_downcoder",
3
+ "version": "1.0.0",
4
+ "description": "Scan music libraries and generate ffmpeg conversion scripts with a CLI or web UI",
5
+ "type": "module",
6
+ "bin": {
7
+ "music_downcoder": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "batch.sh"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && mkdir -p dist/web && cp src/web/index.html dist/web/index.html && npm run type-check",
16
+ "prepublishOnly": "npm run build",
17
+ "start": "tsx src/index.ts",
18
+ "serve": "tsx src/index.ts serve -c docker/conf.local.json -H 127.0.0.1 -p 3798",
19
+ "list": "find ./cmd/**/*.sh -type f 2>/dev/null | sort -nr",
20
+ "convert": "./batch.sh",
21
+ "type-check": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "@inquirer/prompts": "^7.6.0",
25
+ "fs-extra": "*",
26
+ "minimist": "^1.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/fs-extra": "^11.0.4",
30
+ "@types/minimist": "^1.2.5",
31
+ "@types/node": "^20.11.17",
32
+ "tsx": "^4.7.1",
33
+ "typescript": "^5.3.3"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/emgeebee/music-downcoder.git"
38
+ },
39
+ "license": "MIT",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }