@archhaze24/korg 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/bin/korg.js +99 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kirill Belolipetsky
|
|
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,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# korg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Kirby's music organizer. Turns a FLAC library tagged with MusicBrainz Picard into a small Opus library.
|
|
4
|
+
|
|
5
|
+
One `korg run` does four steps:
|
|
6
|
+
|
|
7
|
+
1. **covers**: resize and clean `cover.jpg`
|
|
8
|
+
2. **lyrics**: download synced lyrics from [LRCLIB](https://lrclib.net) as `.lrc` files
|
|
9
|
+
3. **encode**: FLAC to Opus (160 kbit/s VBR), MP3 is copied, covers, `.lrc` and booklets are copied along
|
|
10
|
+
4. **gain**: ReplayGain for both libraries
|
|
11
|
+
|
|
12
|
+
Works on Linux, macOS and Windows.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
brew install archhaze24/tap/korg # macOS, Linux
|
|
18
|
+
scoop bucket add archhaze24 https://github.com/archhaze24/scoop-bucket && scoop install korg # Windows
|
|
19
|
+
npx @archhaze24/korg --help # any, via npm
|
|
20
|
+
cargo install korg-cli # from source, Rust 1.89+
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Prebuilt archives are on the [releases page](https://github.com/archhaze24/korg/releases).
|
|
24
|
+
|
|
25
|
+
korg calls two external tools, so install them too (Homebrew installs them with the formula):
|
|
26
|
+
|
|
27
|
+
- [`opusenc`](https://opus-codec.org/downloads/) (opus-tools)
|
|
28
|
+
- [`rsgain`](https://github.com/complexlogic/rsgain) with a preset named `opus`
|
|
29
|
+
|
|
30
|
+
The gain step runs `rsgain easy -p opus`, so create `opus.ini` in rsgain's presets folder:
|
|
31
|
+
`~/.config/rsgain/presets/` (Linux), `~/Library/rsgain/presets/` (macOS), `%USERPROFILE%\.rsgain\presets\` (Windows).
|
|
32
|
+
This is enough to get R128 tags in Opus files:
|
|
33
|
+
|
|
34
|
+
```ini
|
|
35
|
+
[Opus]
|
|
36
|
+
OpusMode=r
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
rsgain ignores `[Opus]` in a folder that mixes Opus and MP3 and writes plain ReplayGain tags there. Use
|
|
40
|
+
`[Global]` instead of `[Opus]` if you want R128 everywhere.
|
|
41
|
+
|
|
42
|
+
`korg doctor` checks the tools and the preset.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
korg run # ./toencode -> ./encoded
|
|
48
|
+
korg run -s ~/Music/toencode -d ~/Music/encoded
|
|
49
|
+
korg run --skip lyrics,gain # leave steps out
|
|
50
|
+
korg run --dry-run # show what would happen
|
|
51
|
+
korg covers | lyrics | encode | gain # one step only
|
|
52
|
+
korg restore-names # undo renames after a crash
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Source and destination can also come from `KORG_SOURCE` and `KORG_DEST`. `-j` sets parallel encodes per album
|
|
56
|
+
(default: CPU cores minus 2), `-v` prints every checked file.
|
|
57
|
+
|
|
58
|
+
Don't run two instances on the same folders, and don't edit the source in Picard meanwhile.
|
|
59
|
+
|
|
60
|
+
## What the steps do
|
|
61
|
+
|
|
62
|
+
**covers.** `cover.jpg`, `cover.jpeg`, `front.jpg` and `folder.jpg` (any case) become `cover.jpg`. A side over 2000 px
|
|
63
|
+
or an EXIF rotation means resize and re-encode (quality 90, progressive); a heavy file is re-encoded only if that
|
|
64
|
+
saves 10% or more. Everything else just loses its metadata. Several candidates in one folder: reported, skipped.
|
|
65
|
+
|
|
66
|
+
**lyrics.** For each FLAC/MP3, LRCLIB is asked by title, artist, album and duration, then by title and artist only.
|
|
67
|
+
A match needs the same title and artist (ignoring case and punctuation) and a duration within 2 s; the closest
|
|
68
|
+
duration wins, the album only breaks ties. Only lyrics with timestamps are saved. An existing `.lrc` with
|
|
69
|
+
timestamps is kept, one without is deleted and searched again. To search again, delete the `.lrc`.
|
|
70
|
+
|
|
71
|
+
**encode.** Albums run one after another, tracks inside an album in parallel. Tags and artwork are kept; ReplayGain
|
|
72
|
+
and encoder tags are dropped, and the FLAC is never modified. A duration that differs by more than 0.1 s is an
|
|
73
|
+
error (`--allow-duration-mismatch` makes it a warning). An album is skipped if its folder already exists in the
|
|
74
|
+
destination, nothing else is checked: delete the folder to redo it. Covers, `.lrc` and `booklet.pdf` are copied for
|
|
75
|
+
every album, skipped ones too.
|
|
76
|
+
|
|
77
|
+
**gain.** `rsgain easy -m MAX <source>`, then `rsgain easy -m MAX -p opus <dest>`. Both always run.
|
|
78
|
+
|
|
79
|
+
## Long paths
|
|
80
|
+
|
|
81
|
+
If `opusenc` says a path was not found, the track is copied to a short temporary path and encoded there. Such
|
|
82
|
+
albums are renamed to short names for the rsgain run and renamed back. The renames are journaled; Ctrl+C and
|
|
83
|
+
errors restore them. After a crash the next run refuses to start until `korg restore-names`.
|
|
84
|
+
|
|
85
|
+
## Exit codes
|
|
86
|
+
|
|
87
|
+
`0` ok, `1` some files failed (see `<dest>/korg-report.json`), `2` bad arguments or setup, `130` interrupted.
|
|
88
|
+
Output goes to stderr; `NO_COLOR` is respected.
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`tests/e2e.rs` needs `opusenc`, `rsgain`, `flac` and `lame` in `PATH` and skips itself without them.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/bin/korg.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher: runs the native binary shipped by the matching platform package.
|
|
3
|
+
// build.mjs copies this script to bin/korg.js of the launcher package.
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
|
|
10
|
+
const NAME = "korg";
|
|
11
|
+
|
|
12
|
+
// Candidate packages per platform, in order of preference. The musl build is fully static,
|
|
13
|
+
// so on Linux (x64 and arm64) it is also a valid fallback when the glibc package was not installed.
|
|
14
|
+
const PLATFORMS = {
|
|
15
|
+
"linux-x64": ["@archhaze24/korg-linux-x64", "@archhaze24/korg-linux-x64-musl"],
|
|
16
|
+
"linux-arm64": ["@archhaze24/korg-linux-arm64", "@archhaze24/korg-linux-arm64-musl"],
|
|
17
|
+
"darwin-x64": ["@archhaze24/korg-darwin-x64"],
|
|
18
|
+
"darwin-arm64": ["@archhaze24/korg-darwin-arm64"],
|
|
19
|
+
"win32-x64": ["@archhaze24/korg-win32-x64"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// True on a musl-based Linux (Alpine etc.): glibc reports its version, musl does not.
|
|
23
|
+
function isMusl() {
|
|
24
|
+
if (process.platform !== "linux") return false;
|
|
25
|
+
try {
|
|
26
|
+
const report = process.report.getReport();
|
|
27
|
+
const header = typeof report === "string" ? JSON.parse(report).header : report.header;
|
|
28
|
+
if (header && "glibcVersionRuntime" in header) return !header.glibcVersionRuntime;
|
|
29
|
+
} catch {
|
|
30
|
+
// fall through to the file check
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return require("node:fs").readdirSync("/lib").some((name) => name.startsWith("ld-musl-"));
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function binaryPath() {
|
|
40
|
+
const key = `${process.platform}-${process.arch}`;
|
|
41
|
+
const candidates = PLATFORMS[key];
|
|
42
|
+
if (!candidates) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`${NAME} has no prebuilt binary for ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}. ` +
|
|
45
|
+
"Build from source instead: cargo install korg-cli",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const ordered = key.startsWith("linux-") && isMusl() ? [...candidates].reverse() : candidates;
|
|
49
|
+
const exe = process.platform === "win32" ? `${NAME}.exe` : NAME;
|
|
50
|
+
for (const pkg of ordered) {
|
|
51
|
+
try {
|
|
52
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
53
|
+
return path.join(path.dirname(manifest), "bin", exe);
|
|
54
|
+
} catch {
|
|
55
|
+
// not installed, try the next candidate
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error(
|
|
59
|
+
`The platform package ${ordered[0]} is not installed (npm skipped the optional dependency). ` +
|
|
60
|
+
"Reinstall without --omit=optional, or run: cargo install korg-cli",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let binary;
|
|
65
|
+
try {
|
|
66
|
+
binary = binaryPath();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`${NAME}: ${error.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
73
|
+
|
|
74
|
+
// Forward termination signals so the native process does not outlive the launcher: korg
|
|
75
|
+
// restores renamed album folders when it is interrupted, so it must get the chance to run
|
|
76
|
+
// its cleanup. Windows only supports SIGINT/SIGBREAK; the others are skipped.
|
|
77
|
+
const FORWARDED = process.platform === "win32" ? ["SIGINT", "SIGBREAK"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
78
|
+
// On a POSIX terminal Ctrl-C already reaches the child through the tty (same foreground process
|
|
79
|
+
// group), so SIGINT is only swallowed here (the handler keeps the launcher alive until the child
|
|
80
|
+
// exits) and not forwarded a second time.
|
|
81
|
+
const TTY_SIGINT = process.platform !== "win32" && process.stdin.isTTY === true;
|
|
82
|
+
for (const signal of FORWARDED) {
|
|
83
|
+
process.on(signal, () => {
|
|
84
|
+
if (signal === "SIGINT" && TTY_SIGINT) return;
|
|
85
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
child.on("error", (error) => {
|
|
90
|
+
console.error(`${NAME}: could not run ${binary}: ${error.message}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
child.on("exit", (code, signal) => {
|
|
95
|
+
if (signal) {
|
|
96
|
+
process.exit(128 + (os.constants.signals[signal] ?? 0));
|
|
97
|
+
}
|
|
98
|
+
process.exit(code ?? 1);
|
|
99
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archhaze24/korg",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kirby's music organizer: covers, synced lyrics, FLAC to Opus, ReplayGain",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"music",
|
|
7
|
+
"flac",
|
|
8
|
+
"opus",
|
|
9
|
+
"replaygain",
|
|
10
|
+
"lrc",
|
|
11
|
+
"lrclib",
|
|
12
|
+
"picard"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "archhaze24",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/archhaze24/korg.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/archhaze24/korg#readme",
|
|
21
|
+
"bin": {
|
|
22
|
+
"korg": "bin/korg.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"@archhaze24/korg-linux-x64": "0.1.0",
|
|
34
|
+
"@archhaze24/korg-linux-x64-musl": "0.1.0",
|
|
35
|
+
"@archhaze24/korg-linux-arm64": "0.1.0",
|
|
36
|
+
"@archhaze24/korg-linux-arm64-musl": "0.1.0",
|
|
37
|
+
"@archhaze24/korg-darwin-x64": "0.1.0",
|
|
38
|
+
"@archhaze24/korg-darwin-arm64": "0.1.0",
|
|
39
|
+
"@archhaze24/korg-win32-x64": "0.1.0"
|
|
40
|
+
}
|
|
41
|
+
}
|