argonuts 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/bin/argonuts.js +365 -0
- package/package.json +22 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the Argonuts Core contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# argonuts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your media library from the terminal. `argonuts` keeps a list of your media folders, scans them for
|
|
4
|
+
videos and pictures, and keeps their thumbnails ready. It is the terminal companion of Argonuts Core.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx argonuts@latest library add ~/Movies
|
|
8
|
+
npx argonuts@latest scan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
`argonuts` runs on Node.js 20 or later, which brings the `npx` and `npm` commands with it. If
|
|
14
|
+
`npx --version` already prints a version, skip to step 3.
|
|
15
|
+
|
|
16
|
+
### Windows (x64 and ARM)
|
|
17
|
+
|
|
18
|
+
1. Install Node.js. In Command Prompt or PowerShell:
|
|
19
|
+
|
|
20
|
+
```bat
|
|
21
|
+
winget install --exact --id OpenJS.NodeJS.LTS --source winget
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--source winget` takes Node.js from winget's own catalog and skips the store source (`msstore`),
|
|
25
|
+
which fails on some networks. Or use the Windows installer from the Node.js website.
|
|
26
|
+
|
|
27
|
+
2. Close the terminal and open a new one, so it finds the new commands. Check that it worked:
|
|
28
|
+
|
|
29
|
+
```bat
|
|
30
|
+
node --version
|
|
31
|
+
npx --version
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. Try argonuts, in Command Prompt:
|
|
35
|
+
|
|
36
|
+
```bat
|
|
37
|
+
npx argonuts@latest --version
|
|
38
|
+
npx argonuts@latest library add "%USERPROFILE%\Videos"
|
|
39
|
+
npx argonuts@latest scan
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
In PowerShell, write the folder as `"$HOME\Videos"`. If PowerShell answers _running scripts is
|
|
43
|
+
disabled on this system_, type `npx.cmd` instead of `npx` (and `argonuts.cmd` instead of
|
|
44
|
+
`argonuts`), or use Command Prompt. There is no need to change the execution policy.
|
|
45
|
+
|
|
46
|
+
### macOS (Apple Silicon and Intel)
|
|
47
|
+
|
|
48
|
+
1. Install Node.js with Homebrew (`brew install node`), or use the macOS installer from the Node.js
|
|
49
|
+
website.
|
|
50
|
+
2. Open a new Terminal window and check: `node --version` and `npx --version`.
|
|
51
|
+
3. Try argonuts:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npx argonuts@latest --version
|
|
55
|
+
npx argonuts@latest library add ~/Movies
|
|
56
|
+
npx argonuts@latest scan
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Keep it installed
|
|
60
|
+
|
|
61
|
+
`npx` downloads argonuts the first time and reuses it afterwards. To type just `argonuts`, install it
|
|
62
|
+
once:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm install --global argonuts
|
|
66
|
+
argonuts scan
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Update it later with the same command.
|
|
70
|
+
|
|
71
|
+
### When something goes wrong
|
|
72
|
+
|
|
73
|
+
| What you see | What to do |
|
|
74
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `'npx' is not recognized` or `command not found: npx` | Node.js isn't installed, or the terminal was opened before it was. Do step 1, then open a new terminal |
|
|
76
|
+
| winget: `Failed when searching source: msstore` | The store source isn't reachable from this network (often a certificate error). Add `--source winget`, as in step 1 |
|
|
77
|
+
| PowerShell: `running scripts is disabled on this system` | Use `npx.cmd` / `argonuts.cmd`, or Command Prompt |
|
|
78
|
+
| `argonuts has no build for … yet` | Your computer isn't supported yet: Linux comes next |
|
|
79
|
+
| `@argonuts/cli-… is not installed` | npm skipped the part made for your computer. Run the command again; if you set npm to leave out optional packages (`omit=optional`), remove that setting |
|
|
80
|
+
| Windows blocks `argonuts-cli` | Version 0.1's programs aren't signed yet. On Windows 11 with Smart App Control turned on, they can't run; signed builds are planned |
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
| Command | What it does |
|
|
85
|
+
| ---------------------------------- | ------------------------------------------------------------------------ |
|
|
86
|
+
| `argonuts library` | Your library's folders and their last scans |
|
|
87
|
+
| `argonuts library add <folder>...` | Add folders to your library |
|
|
88
|
+
| `argonuts library remove <folder>` | Remove a folder; its last scan stays on disk |
|
|
89
|
+
| `argonuts scan [<folder>...]` | Scan every library folder, or the folders named, which join your library |
|
|
90
|
+
| `argonuts scan --cached-only` | Use earlier thumbnails only and run no ffmpeg |
|
|
91
|
+
| `argonuts --version` | The versions of argonuts and argonuts-cli |
|
|
92
|
+
|
|
93
|
+
Ctrl-C stops a scan; a second Ctrl-C stops it at once. A scan reads your files and never changes
|
|
94
|
+
them.
|
|
95
|
+
|
|
96
|
+
## Where your library is kept
|
|
97
|
+
|
|
98
|
+
In the core's data folder, shared with the Argonuts desktop app:
|
|
99
|
+
|
|
100
|
+
- macOS: `~/Library/Application Support/Argonuts`
|
|
101
|
+
- Windows: `%LOCALAPPDATA%\Argonuts`
|
|
102
|
+
|
|
103
|
+
Set `ARGONUTS_DATA_DIR` to keep it somewhere else.
|
|
104
|
+
|
|
105
|
+
## Computers it runs on
|
|
106
|
+
|
|
107
|
+
macOS on Apple Silicon and Intel, and Windows on x64 and on ARM, with Node.js 20 or later. Linux
|
|
108
|
+
comes next.
|
|
109
|
+
|
|
110
|
+
On Windows on ARM, argonuts uses the x64 build for now, which Windows 11 runs through its built-in
|
|
111
|
+
x64 emulation. A native ARM build will replace it.
|
|
112
|
+
|
|
113
|
+
The work is done by `argonuts-cli`, the core's own program, which npm installs with argonuts for your
|
|
114
|
+
computer only, together with the `ffmpeg` and `ffprobe` it uses for thumbnails.
|
|
115
|
+
|
|
116
|
+
## Licenses
|
|
117
|
+
|
|
118
|
+
`argonuts` and `argonuts-cli` are MIT licensed. `ffmpeg` and `ffprobe` are FFmpeg, under the GNU
|
|
119
|
+
LGPL 2.1 or later; the platform package that carries them includes its notice and license.
|
package/bin/argonuts.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/core.ts
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { accessSync, chmodSync, constants, existsSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
var PLATFORMS = {
|
|
8
|
+
"darwin-arm64": "macOS on Apple Silicon",
|
|
9
|
+
"darwin-x64": "macOS on Intel",
|
|
10
|
+
"win32-x64": "Windows on x64",
|
|
11
|
+
"win32-arm64": "Windows on ARM"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class CompanionError extends Error {
|
|
15
|
+
exitCode;
|
|
16
|
+
constructor(message, exitCode = 1) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function platformKey() {
|
|
22
|
+
return `${process.platform}-${process.arch}`;
|
|
23
|
+
}
|
|
24
|
+
function corePath() {
|
|
25
|
+
const given = process.env.ARGONUTS_CLI;
|
|
26
|
+
if (given) {
|
|
27
|
+
if (!existsSync(given))
|
|
28
|
+
throw new CompanionError(`ARGONUTS_CLI is set to ${given}, which does not exist.`);
|
|
29
|
+
return given;
|
|
30
|
+
}
|
|
31
|
+
const key = platformKey();
|
|
32
|
+
if (!(key in PLATFORMS)) {
|
|
33
|
+
throw new CompanionError(`argonuts has no build for ${key} yet. It runs on ${Object.values(PLATFORMS).join(", ")}.`);
|
|
34
|
+
}
|
|
35
|
+
const name = `@argonuts/cli-${key}`;
|
|
36
|
+
let manifest;
|
|
37
|
+
try {
|
|
38
|
+
manifest = createRequire(import.meta.url).resolve(`${name}/package.json`);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new CompanionError(`${name} is not installed. Reinstall argonuts: npm installs it for this computer.`);
|
|
41
|
+
}
|
|
42
|
+
const binary = path.join(path.dirname(manifest), "bin", key.startsWith("win32") ? "argonuts-cli.exe" : "argonuts-cli");
|
|
43
|
+
if (!existsSync(binary))
|
|
44
|
+
throw new CompanionError(`${binary} is missing. Reinstall argonuts.`);
|
|
45
|
+
makeRunnable(binary);
|
|
46
|
+
return binary;
|
|
47
|
+
}
|
|
48
|
+
function makeRunnable(binary) {
|
|
49
|
+
if (process.platform === "win32")
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
accessSync(binary, constants.X_OK);
|
|
53
|
+
} catch {
|
|
54
|
+
try {
|
|
55
|
+
chmodSync(binary, 493);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function startCore(args) {
|
|
60
|
+
return spawn(corePath(), args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
61
|
+
}
|
|
62
|
+
function captureCore(args) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn(corePath(), args, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
65
|
+
let stdout = "";
|
|
66
|
+
let stderr = "";
|
|
67
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
|
|
68
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
69
|
+
child.on("error", reject);
|
|
70
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function coreJson(args) {
|
|
74
|
+
const { code, stdout, stderr } = await captureCore(args);
|
|
75
|
+
if (code !== 0)
|
|
76
|
+
throw new CompanionError(refusal(stderr) || `argonuts-cli stopped with exit code ${code}.`, code ?? 1);
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(stdout);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new CompanionError(`argonuts-cli answered with something that is not JSON: ${stdout.slice(0, 200)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function refusal(stderr) {
|
|
84
|
+
return stderr.trim().replace(/^Error:\s*/, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/library.ts
|
|
88
|
+
import path3 from "node:path";
|
|
89
|
+
|
|
90
|
+
// src/show.ts
|
|
91
|
+
import os from "node:os";
|
|
92
|
+
import path2 from "node:path";
|
|
93
|
+
function count(value) {
|
|
94
|
+
return Math.trunc(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
95
|
+
}
|
|
96
|
+
function bytes(value) {
|
|
97
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
98
|
+
let scaled = value;
|
|
99
|
+
let unit = 0;
|
|
100
|
+
while (scaled >= 1000 && unit < units.length - 1) {
|
|
101
|
+
scaled /= 1000;
|
|
102
|
+
unit += 1;
|
|
103
|
+
}
|
|
104
|
+
return unit === 0 ? `${value} B` : `${scaled.toFixed(1)} ${units[unit]}`;
|
|
105
|
+
}
|
|
106
|
+
function plural(value, word) {
|
|
107
|
+
return value === 1 ? `1 ${word}` : `${count(value)} ${word}s`;
|
|
108
|
+
}
|
|
109
|
+
function when(rfc3339) {
|
|
110
|
+
const time = new Date(rfc3339);
|
|
111
|
+
if (Number.isNaN(time.getTime()))
|
|
112
|
+
return rfc3339;
|
|
113
|
+
return `${time.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
114
|
+
}
|
|
115
|
+
function shown(folder) {
|
|
116
|
+
const home = os.homedir();
|
|
117
|
+
if (!home)
|
|
118
|
+
return folder;
|
|
119
|
+
if (folder === home)
|
|
120
|
+
return "~";
|
|
121
|
+
if (folder.startsWith(home + path2.sep))
|
|
122
|
+
return `~${folder.slice(home.length)}`;
|
|
123
|
+
return folder;
|
|
124
|
+
}
|
|
125
|
+
function fit(line, columns = process.stderr.columns) {
|
|
126
|
+
if (!columns || line.length < columns)
|
|
127
|
+
return line;
|
|
128
|
+
return `${line.slice(0, Math.max(0, columns - 2))}…`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/library.ts
|
|
132
|
+
async function libraryCommand(args) {
|
|
133
|
+
const [action = "list", ...rest] = args;
|
|
134
|
+
let library;
|
|
135
|
+
switch (action) {
|
|
136
|
+
case "list":
|
|
137
|
+
if (rest.length > 0)
|
|
138
|
+
throw new CompanionError("`argonuts library list` takes no folders.", 2);
|
|
139
|
+
library = await coreJson(["library", "list"]);
|
|
140
|
+
break;
|
|
141
|
+
case "add":
|
|
142
|
+
if (rest.length === 0)
|
|
143
|
+
throw new CompanionError("Name a folder: argonuts library add <folder>", 2);
|
|
144
|
+
library = await coreJson(["library", "add", ...rest.map((folder) => path3.resolve(folder))]);
|
|
145
|
+
break;
|
|
146
|
+
case "remove":
|
|
147
|
+
if (rest.length !== 1)
|
|
148
|
+
throw new CompanionError("Name one folder: argonuts library remove <folder>", 2);
|
|
149
|
+
library = await coreJson(["library", "remove", path3.resolve(rest[0])]);
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
throw new CompanionError(`There is no \`argonuts library ${action}\`. Use list, add or remove.`, 2);
|
|
153
|
+
}
|
|
154
|
+
printLibrary(library);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
function printLibrary({ folders }) {
|
|
158
|
+
if (folders.length === 0) {
|
|
159
|
+
console.log("Your library has no folders yet. Add one: argonuts library add <folder>");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const scans = folders.flatMap((entry) => entry.last_scan ? [entry.last_scan] : []);
|
|
163
|
+
const files = scans.reduce((sum, scan) => sum + scan.files, 0);
|
|
164
|
+
const size = scans.reduce((sum, scan) => sum + scan.total_bytes, 0);
|
|
165
|
+
const width = Math.max(...folders.map((entry) => shown(entry.folder).length));
|
|
166
|
+
console.log(`Your library: ${plural(folders.length, "folder")} · ${count(files)} files · ${bytes(size)}`);
|
|
167
|
+
for (const entry of folders) {
|
|
168
|
+
const scan = entry.last_scan;
|
|
169
|
+
const state = scan ? `${plural(scan.files, "file")} · ${bytes(scan.total_bytes)} · scanned ${when(scan.scanned_at)}` : "not scanned yet";
|
|
170
|
+
const connected = entry.present ? "" : " · ⚠ not connected";
|
|
171
|
+
console.log(` ${shown(entry.folder).padEnd(width)} ${state}${connected}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/scan.ts
|
|
176
|
+
import { statSync } from "node:fs";
|
|
177
|
+
import path4 from "node:path";
|
|
178
|
+
import { createInterface } from "node:readline";
|
|
179
|
+
var EXIT_CANCELLED = 130;
|
|
180
|
+
async function scanCommand(args) {
|
|
181
|
+
const named = [];
|
|
182
|
+
let cachedOnly = false;
|
|
183
|
+
for (const arg of args) {
|
|
184
|
+
if (arg === "--cached-only")
|
|
185
|
+
cachedOnly = true;
|
|
186
|
+
else if (arg.startsWith("-"))
|
|
187
|
+
throw new CompanionError(`\`argonuts scan\` has no option ${arg}.`, 2);
|
|
188
|
+
else
|
|
189
|
+
named.push(path4.resolve(arg));
|
|
190
|
+
}
|
|
191
|
+
const library = await coreJson(named.length > 0 ? ["library", "add", ...named] : ["library", "list"]);
|
|
192
|
+
const targets = named.length > 0 ? named.map((folder) => ({ folder, present: isFolder(folder) })) : library.folders;
|
|
193
|
+
if (targets.length === 0) {
|
|
194
|
+
console.log("Your library has no folders yet. Add one: argonuts library add <folder>");
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
let failed = false;
|
|
198
|
+
for (const { folder, present } of targets) {
|
|
199
|
+
if (!present) {
|
|
200
|
+
console.log(`⚠ ${shown(folder)} is not connected; skipped. Its last scan stays.`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const snapshot = await scanFolder(folder, cachedOnly);
|
|
204
|
+
printSummary(snapshot);
|
|
205
|
+
if (snapshot.phase === "cancelled") {
|
|
206
|
+
console.log("Stopped.");
|
|
207
|
+
return EXIT_CANCELLED;
|
|
208
|
+
}
|
|
209
|
+
failed ||= snapshot.phase === "failed";
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
printLibrary(await coreJson(["library", "list"]));
|
|
213
|
+
return failed ? 1 : 0;
|
|
214
|
+
}
|
|
215
|
+
function isFolder(folder) {
|
|
216
|
+
try {
|
|
217
|
+
return statSync(folder).isDirectory();
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function scanFolder(folder, cachedOnly) {
|
|
223
|
+
const label = shown(folder);
|
|
224
|
+
const redraw = process.stderr.isTTY === true;
|
|
225
|
+
const draw = (text) => redraw && process.stderr.write(`\r\x1B[2K${fit(`${label} · ${text}`)}`);
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const child = startCore([
|
|
228
|
+
"--stop-on-stdin-close",
|
|
229
|
+
"scan",
|
|
230
|
+
folder,
|
|
231
|
+
"--json",
|
|
232
|
+
...cachedOnly ? ["--cached-only"] : []
|
|
233
|
+
]);
|
|
234
|
+
let last = null;
|
|
235
|
+
let finished = false;
|
|
236
|
+
let interrupts = 0;
|
|
237
|
+
let stderr = "";
|
|
238
|
+
const onInterrupt = () => {
|
|
239
|
+
interrupts += 1;
|
|
240
|
+
if (interrupts === 1) {
|
|
241
|
+
child.stdin.write(`cancel
|
|
242
|
+
`);
|
|
243
|
+
draw("stopping…");
|
|
244
|
+
} else {
|
|
245
|
+
child.kill();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
process.on("SIGINT", onInterrupt);
|
|
249
|
+
child.stdin.on("error", () => {});
|
|
250
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
251
|
+
createInterface({ input: child.stdout }).on("line", (raw) => {
|
|
252
|
+
let line;
|
|
253
|
+
try {
|
|
254
|
+
line = JSON.parse(raw);
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
last = line.snapshot;
|
|
259
|
+
if (line.event === "done")
|
|
260
|
+
finished = true;
|
|
261
|
+
else
|
|
262
|
+
draw(progressLine(line.snapshot));
|
|
263
|
+
});
|
|
264
|
+
child.on("error", (error) => {
|
|
265
|
+
process.off("SIGINT", onInterrupt);
|
|
266
|
+
reject(error);
|
|
267
|
+
});
|
|
268
|
+
child.on("close", (code) => {
|
|
269
|
+
process.off("SIGINT", onInterrupt);
|
|
270
|
+
child.stdin.end();
|
|
271
|
+
if (redraw)
|
|
272
|
+
process.stderr.write("\r\x1B[2K");
|
|
273
|
+
if (!last) {
|
|
274
|
+
reject(new CompanionError(refusal(stderr) || `The scan of ${label} stopped with exit code ${code}.`, code ?? 1));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!finished)
|
|
278
|
+
last.phase = interrupts > 0 ? "cancelled" : "failed";
|
|
279
|
+
resolve(last);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function progressLine(snapshot) {
|
|
284
|
+
if (snapshot.phase === "discovering")
|
|
285
|
+
return `Looking for media… ${count(snapshot.discovered_files)} files`;
|
|
286
|
+
return `Scanning ${count(snapshot.processed_files)}/${count(snapshot.discovered_files)} files · ` + `${count(snapshot.new_files)} new · ${count(snapshot.cached_files)} from cache · ` + `${count(snapshot.failed_files)} failed`;
|
|
287
|
+
}
|
|
288
|
+
function printSummary(snapshot) {
|
|
289
|
+
const verdict = { finished: "Scanned", cancelled: "Stopped", failed: "Failed" }[snapshot.phase] ?? "Failed";
|
|
290
|
+
console.log(`${verdict} ${shown(snapshot.root)}`);
|
|
291
|
+
console.log(` ${count(snapshot.discovered_files)} files (${count(snapshot.video_files)} videos, ` + `${count(snapshot.image_files)} pictures), ${count(snapshot.new_files)} new, ` + `${count(snapshot.cached_files)} from cache, ${count(snapshot.failed_files)} failed`);
|
|
292
|
+
console.log(` ${count(snapshot.thumbnails_generated)} thumbnails made`);
|
|
293
|
+
if (snapshot.unreadable_dirs > 0)
|
|
294
|
+
console.log(` ${count(snapshot.unreadable_dirs)} folders could not be read`);
|
|
295
|
+
for (const failure of snapshot.failures)
|
|
296
|
+
console.error(` ✗ ${failure.relative_path}: ${failure.error}`);
|
|
297
|
+
if (snapshot.error)
|
|
298
|
+
console.error(`✗ ${snapshot.error}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/main.ts
|
|
302
|
+
var VERSION = "0.1.1";
|
|
303
|
+
var HELP = `argonuts ${VERSION}: your media library, from the terminal.
|
|
304
|
+
|
|
305
|
+
Usage:
|
|
306
|
+
argonuts library Your library's folders and their last scans
|
|
307
|
+
argonuts library add <folder>... Add folders to your library
|
|
308
|
+
argonuts library remove <folder> Remove a folder; its last scan stays on disk
|
|
309
|
+
argonuts scan [<folder>...] Scan every library folder, or the folders named,
|
|
310
|
+
which join your library
|
|
311
|
+
--cached-only Use earlier thumbnails only and run no ffmpeg
|
|
312
|
+
argonuts --version The versions of argonuts and argonuts-cli
|
|
313
|
+
|
|
314
|
+
Ctrl-C stops a scan; a second Ctrl-C stops it at once.
|
|
315
|
+
Your library is kept in the core's data folder, shared with the desktop app.
|
|
316
|
+
Set ARGONUTS_DATA_DIR to keep it somewhere else.
|
|
317
|
+
`;
|
|
318
|
+
async function main(argv) {
|
|
319
|
+
const [command, ...rest] = argv;
|
|
320
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
321
|
+
process.stdout.write(HELP);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
switch (command) {
|
|
325
|
+
case undefined:
|
|
326
|
+
case "help":
|
|
327
|
+
case "--help":
|
|
328
|
+
case "-h":
|
|
329
|
+
process.stdout.write(HELP);
|
|
330
|
+
return 0;
|
|
331
|
+
case "version":
|
|
332
|
+
case "--version":
|
|
333
|
+
case "-v":
|
|
334
|
+
return printVersion();
|
|
335
|
+
case "library":
|
|
336
|
+
return libraryCommand(rest);
|
|
337
|
+
case "scan":
|
|
338
|
+
return scanCommand(rest);
|
|
339
|
+
default:
|
|
340
|
+
process.stderr.write(`✗ There is no \`argonuts ${command}\`.
|
|
341
|
+
|
|
342
|
+
${HELP}`);
|
|
343
|
+
return 2;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function printVersion() {
|
|
347
|
+
console.log(`argonuts ${VERSION}`);
|
|
348
|
+
try {
|
|
349
|
+
const { code, stdout, stderr } = await captureCore(["--version"]);
|
|
350
|
+
if (code !== 0)
|
|
351
|
+
throw new CompanionError(refusal(stderr) || `argonuts-cli stopped with exit code ${code}.`);
|
|
352
|
+
console.log(stdout.trim());
|
|
353
|
+
return 0;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
main(process.argv.slice(2)).then((code) => {
|
|
360
|
+
process.exitCode = code;
|
|
361
|
+
}, (error) => {
|
|
362
|
+
process.stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}
|
|
363
|
+
`);
|
|
364
|
+
process.exitCode = error instanceof CompanionError ? error.exitCode : 1;
|
|
365
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argonuts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Your media library from the terminal: add folders, scan them and keep their thumbnails ready.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"argonuts": "bin/argonuts.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"optionalDependencies": {
|
|
19
|
+
"@argonuts/cli-darwin-arm64": "0.1.1",
|
|
20
|
+
"@argonuts/cli-darwin-x64": "0.1.1",
|
|
21
|
+
"@argonuts/cli-win32-x64": "0.1.1",
|
|
22
|
+
"@argonuts/cli-win32-arm64": "0.1.1"
|
|
23
|
+
}
|
|
24
|
+
}
|