funoteka 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/.env.example +90 -0
- package/CHANGELOG.md +104 -0
- package/DEPLOY.md +375 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/deploy/answers.schema.json +151 -0
- package/deploy/build.mjs +88 -0
- package/deploy/check-publishable.mjs +246 -0
- package/deploy/smoke.mjs +681 -0
- package/deploy/systemd/funoteka.env.example +41 -0
- package/deploy/systemd/funoteka.service +77 -0
- package/deploy/windows/funoteka-service.xml +58 -0
- package/deploy/windows/install-service.ps1 +158 -0
- package/dist/annotation/store.js +67 -0
- package/dist/api/admin-export.js +227 -0
- package/dist/api/admin-guard.js +181 -0
- package/dist/api/admin-library.js +113 -0
- package/dist/api/admin.js +1162 -0
- package/dist/api/annotation.js +193 -0
- package/dist/api/artistinfo.js +140 -0
- package/dist/api/audit.js +32 -0
- package/dist/api/auth.js +145 -0
- package/dist/api/bookmark.js +108 -0
- package/dist/api/browse.js +1241 -0
- package/dist/api/config-file.js +202 -0
- package/dist/api/config.js +285 -0
- package/dist/api/cover.js +304 -0
- package/dist/api/download.js +112 -0
- package/dist/api/envelope.js +162 -0
- package/dist/api/extensions.js +43 -0
- package/dist/api/genre.js +81 -0
- package/dist/api/health.js +35 -0
- package/dist/api/history.js +374 -0
- package/dist/api/idempotency.js +66 -0
- package/dist/api/keys.js +90 -0
- package/dist/api/log-file.js +65 -0
- package/dist/api/meta.js +1410 -0
- package/dist/api/playlist.js +311 -0
- package/dist/api/rescan.js +196 -0
- package/dist/api/roots.js +161 -0
- package/dist/api/router.js +133 -0
- package/dist/api/scanner.js +163 -0
- package/dist/api/search.js +82 -0
- package/dist/api/server.js +643 -0
- package/dist/api/settings.js +67 -0
- package/dist/api/stream.js +442 -0
- package/dist/api/stubs.js +183 -0
- package/dist/api/transcode.js +678 -0
- package/dist/api/user.js +73 -0
- package/dist/api/virtual.js +362 -0
- package/dist/api/visibility.js +21 -0
- package/dist/artist/apply.js +752 -0
- package/dist/artist/credit.js +95 -0
- package/dist/artist/folder.js +65 -0
- package/dist/artist/name.js +205 -0
- package/dist/artist/nfo.js +101 -0
- package/dist/artist/translit.js +79 -0
- package/dist/bookmark/store.js +54 -0
- package/dist/classify/classify.js +405 -0
- package/dist/classify/collision-name.js +109 -0
- package/dist/classify/folder-name.js +466 -0
- package/dist/classify/roles.js +277 -0
- package/dist/classify/shelf-name.js +178 -0
- package/dist/classify/tree.js +43 -0
- package/dist/cli/args.js +169 -0
- package/dist/cli/daemon.js +192 -0
- package/dist/cli/entry.js +25 -0
- package/dist/cli/keys.js +52 -0
- package/dist/cli.js +638 -0
- package/dist/cover/pick.js +58 -0
- package/dist/cover/picture.js +111 -0
- package/dist/cue/engine.js +759 -0
- package/dist/cue/match.js +142 -0
- package/dist/cue/parse.js +182 -0
- package/dist/cue/plan.js +286 -0
- package/dist/cue/track-name.js +120 -0
- package/dist/db/index.js +163 -0
- package/dist/db/issue.js +73 -0
- package/dist/db/ledger.js +84 -0
- package/dist/db/migrations/001_init.sql +196 -0
- package/dist/db/migrations/002_cue_track_flexible.sql +35 -0
- package/dist/db/migrations/003_cue_metadata.sql +14 -0
- package/dist/db/migrations/004_cue_performer.sql +12 -0
- package/dist/db/migrations/005_incremental.sql +23 -0
- package/dist/db/migrations/006_tags.sql +57 -0
- package/dist/db/migrations/007_artist_credit.sql +35 -0
- package/dist/db/migrations/008_issue_stage.sql +39 -0
- package/dist/db/migrations/009_cue_title.sql +20 -0
- package/dist/db/migrations/010_duration_source.sql +31 -0
- package/dist/db/migrations/011_track_title_source.sql +28 -0
- package/dist/db/migrations/012_file_container.sql +25 -0
- package/dist/db/migrations/013_cover_art.sql +28 -0
- package/dist/db/migrations/014_album_year.sql +17 -0
- package/dist/db/migrations/015_probe_method.sql +14 -0
- package/dist/db/migrations/016_tags_method.sql +20 -0
- package/dist/db/migrations/017_cover_indirect.sql +23 -0
- package/dist/db/migrations/018_release_year.sql +14 -0
- package/dist/db/migrations/019_file_tag_name_file_value.sql +20 -0
- package/dist/db/migrations/020_analyze_after_index.sql +16 -0
- package/dist/db/migrations/021_cue_index_files.sql +15 -0
- package/dist/db/migrations/022_sidecar_text.sql +19 -0
- package/dist/db/migrations/023_track_credit.sql +23 -0
- package/dist/db/migrations/024_playlists.sql +63 -0
- package/dist/db/migrations/025_playlist_sequence.sql +27 -0
- package/dist/db/migrations/026_playlist_source_file.sql +19 -0
- package/dist/db/migrations/027_lookup_indexes.sql +30 -0
- package/dist/db/migrations/028_annotations.sql +47 -0
- package/dist/db/migrations/029_scan_state_root_run.sql +28 -0
- package/dist/db/migrations/030_history.sql +108 -0
- package/dist/db/migrations/031_queue_position_and_plays.sql +46 -0
- package/dist/db/migrations/032_bookmarks.sql +27 -0
- package/dist/db/migrations/033_bookmark_order.sql +14 -0
- package/dist/db/migrations/034_junk.sql +33 -0
- package/dist/db/migrations/035_api_key.sql +38 -0
- package/dist/db/migrations/036_file_tag_first.sql +88 -0
- package/dist/db/migrations/037_admin_idempotency.sql +31 -0
- package/dist/db/migrations/038_cue_audio_index.sql +24 -0
- package/dist/db/sweep.js +109 -0
- package/dist/history/store.js +151 -0
- package/dist/inventory/inventory.js +294 -0
- package/dist/junk/marks.js +143 -0
- package/dist/junk/rule.js +93 -0
- package/dist/mcp/client.js +48 -0
- package/dist/mcp/server.js +175 -0
- package/dist/mcp/tools.js +348 -0
- package/dist/playlist/files.js +116 -0
- package/dist/playlist/import.js +260 -0
- package/dist/playlist/store.js +240 -0
- package/dist/probe/ffprobe.js +179 -0
- package/dist/run.js +120 -0
- package/dist/scan/kinds.js +49 -0
- package/dist/scan/scan.js +503 -0
- package/dist/scan/schedule.js +132 -0
- package/dist/scan/settle.js +172 -0
- package/dist/scan/walk.js +132 -0
- package/dist/search/index.js +41 -0
- package/dist/search/query.js +28 -0
- package/dist/stream/flac.js +541 -0
- package/dist/stream/mpeg.js +179 -0
- package/dist/stream/recode.js +393 -0
- package/dist/stream/rewrite.js +163 -0
- package/dist/stream/segment.js +624 -0
- package/dist/tags/apply.js +703 -0
- package/dist/tags/encode.js +164 -0
- package/dist/tags/first.js +130 -0
- package/dist/tags/flac.js +196 -0
- package/dist/tags/genres.js +123 -0
- package/dist/tags/id3v1.js +209 -0
- package/dist/tags/id3v2.js +754 -0
- package/dist/tags/mp4.js +534 -0
- package/dist/tags/mpeg.js +355 -0
- package/dist/tags/ogg.js +463 -0
- package/dist/tags/picture.js +71 -0
- package/dist/tags/read.js +269 -0
- package/dist/tags/types.js +51 -0
- package/dist/tags/vorbis-comment.js +155 -0
- package/dist/text/encoding.js +384 -0
- package/dist/text/site-name.js +37 -0
- package/dist/util/child.js +21 -0
- package/dist/util/names.js +97 -0
- package/funoteka.json.example +27 -0
- package/package.json +55 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
/** CRC-16 as section 9.3 defines it: `x^16 + x^15 + x^2 + 1`, no reflection. */
|
|
3
|
+
const CRC_TABLE = (() => {
|
|
4
|
+
const table = new Uint16Array(256);
|
|
5
|
+
for (let index = 0; index < 256; index += 1) {
|
|
6
|
+
let crc = index << 8;
|
|
7
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
8
|
+
crc = (crc & 0x8000) !== 0 ? ((crc << 1) ^ 0x8005) & 0xffff : (crc << 1) & 0xffff;
|
|
9
|
+
}
|
|
10
|
+
table[index] = crc;
|
|
11
|
+
}
|
|
12
|
+
return table;
|
|
13
|
+
})();
|
|
14
|
+
/** The CRC-16 of `bytes[from, to)`, carried on from `crc`. */
|
|
15
|
+
function crc16(crc, bytes, from = 0, to = bytes.length) {
|
|
16
|
+
let running = crc;
|
|
17
|
+
for (let at = from; at < to; at += 1) {
|
|
18
|
+
running = (((running << 8) & 0xffff) ^ CRC_TABLE[((running >> 8) ^ bytes[at]) & 0xff]) & 0xffff;
|
|
19
|
+
}
|
|
20
|
+
return running;
|
|
21
|
+
}
|
|
22
|
+
/** Two bytes of CRC-16, most significant first, as a frame ends with them. */
|
|
23
|
+
function footer(crc) {
|
|
24
|
+
const bytes = Buffer.alloc(2);
|
|
25
|
+
bytes.writeUInt16BE(crc);
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
/** What a set of reframed frames adds to, or takes from, the range they sit in. */
|
|
29
|
+
export function restatedSize(frames) {
|
|
30
|
+
return frames.reduce((total, frame) => total + frame.header.bytes.length - frame.header.length, 0);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A stream that writes what it is given, restating the frames it names.
|
|
34
|
+
*
|
|
35
|
+
* Each named frame comes out as its new header, its body unchanged, and a footer
|
|
36
|
+
* computed over the two. Bytes outside those frames are passed through as the
|
|
37
|
+
* file holds them — the frames of a track that needed no restating keep the
|
|
38
|
+
* footers they came with.
|
|
39
|
+
*
|
|
40
|
+
* The frames must be in order and each must be ahead of the stream when it
|
|
41
|
+
* arrives. One that has already been passed is refused rather than skipped: it
|
|
42
|
+
* would mean the walk and the stream disagree about where the audio is, and the
|
|
43
|
+
* bytes that followed would be served as a track numbered from the wrong place.
|
|
44
|
+
*/
|
|
45
|
+
export function restating(frames, base, window) {
|
|
46
|
+
/** The next frame to restate, and where in the file the input has arrived. */
|
|
47
|
+
let next = 0;
|
|
48
|
+
let at = base;
|
|
49
|
+
/** Bytes of the file still to drop: the header that was, or the footer that was. */
|
|
50
|
+
let dropping = 0;
|
|
51
|
+
/** Bytes of the frame being written still to come, its footer included. */
|
|
52
|
+
let left = 0;
|
|
53
|
+
/** The CRC-16 of that frame over everything written of it so far. */
|
|
54
|
+
let crc = 0;
|
|
55
|
+
// What the window still asks for: bytes to pass over before writing, and how
|
|
56
|
+
// many are left to write. The CRC is carried over the bytes passed over too —
|
|
57
|
+
// a frame's footer covers the whole frame, so the part of it a seek landed in
|
|
58
|
+
// the middle of still has to be counted.
|
|
59
|
+
let skip = window?.skip ?? 0;
|
|
60
|
+
let limit = window?.limit ?? Number.POSITIVE_INFINITY;
|
|
61
|
+
let finished = false;
|
|
62
|
+
return new Transform({
|
|
63
|
+
transform(chunk, _encoding, done) {
|
|
64
|
+
if (finished) {
|
|
65
|
+
done();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const out = [];
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
/** Write `bytes`, less whatever the window still wants passed over. */
|
|
71
|
+
const write = (bytes) => {
|
|
72
|
+
if (finished || bytes.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
let piece = bytes;
|
|
75
|
+
if (skip > 0) {
|
|
76
|
+
const passed = Math.min(skip, piece.length);
|
|
77
|
+
skip -= passed;
|
|
78
|
+
piece = piece.subarray(passed);
|
|
79
|
+
if (piece.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (piece.length >= limit) {
|
|
83
|
+
out.push(piece.subarray(0, limit));
|
|
84
|
+
limit = 0;
|
|
85
|
+
finished = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
out.push(piece);
|
|
89
|
+
limit -= piece.length;
|
|
90
|
+
};
|
|
91
|
+
while (cursor < chunk.length) {
|
|
92
|
+
if (dropping > 0) {
|
|
93
|
+
const dropped = Math.min(dropping, chunk.length - cursor);
|
|
94
|
+
cursor += dropped;
|
|
95
|
+
dropping -= dropped;
|
|
96
|
+
at += dropped;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Inside a frame: its body, and then the two bytes that end it.
|
|
100
|
+
if (left > 0) {
|
|
101
|
+
if (left > 2) {
|
|
102
|
+
const until = Math.min(chunk.length, cursor + left - 2);
|
|
103
|
+
// The CRC is only worth taking for a frame that goes out. One that
|
|
104
|
+
// ends before the window opens is passed over whole, and a footer
|
|
105
|
+
// nobody will read is a byte-at-a-time cost on a seek: this is what
|
|
106
|
+
// keeps a range near the end of a long track from walking the whole
|
|
107
|
+
// of it through the polynomial.
|
|
108
|
+
if (skip < left)
|
|
109
|
+
crc = crc16(crc, chunk, cursor, until);
|
|
110
|
+
write(chunk.subarray(cursor, until));
|
|
111
|
+
left -= until - cursor;
|
|
112
|
+
at += until - cursor;
|
|
113
|
+
cursor = until;
|
|
114
|
+
if (finished)
|
|
115
|
+
break;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
write(footer(crc));
|
|
119
|
+
dropping = 2;
|
|
120
|
+
left = 0;
|
|
121
|
+
crc = 0;
|
|
122
|
+
if (finished)
|
|
123
|
+
break;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const frame = next < frames.length ? frames[next] : undefined;
|
|
127
|
+
if (frame === undefined) {
|
|
128
|
+
write(chunk.subarray(cursor));
|
|
129
|
+
at += chunk.length - cursor;
|
|
130
|
+
cursor = chunk.length;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (frame.header.at < at) {
|
|
134
|
+
done(new Error(`a frame at ${frame.header.at} was passed at position ${at}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Not there yet: bytes outside a restated frame go out as they are.
|
|
138
|
+
if (frame.header.at > at) {
|
|
139
|
+
const until = Math.min(chunk.length, cursor + frame.header.at - at);
|
|
140
|
+
write(chunk.subarray(cursor, until));
|
|
141
|
+
at += until - cursor;
|
|
142
|
+
cursor = until;
|
|
143
|
+
if (finished)
|
|
144
|
+
break;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
write(frame.header.bytes);
|
|
148
|
+
crc = crc16(0, frame.header.bytes);
|
|
149
|
+
dropping = frame.header.length;
|
|
150
|
+
left = frame.to - frame.from - frame.header.length;
|
|
151
|
+
next += 1;
|
|
152
|
+
if (finished)
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
done(null, out.length === 0 ? undefined : out.length === 1 ? out[0] : Buffer.concat(out));
|
|
156
|
+
// Nothing more will be written, so the answer ends here rather than at the
|
|
157
|
+
// end of the file: a seek near the end of a three-hundred-megabyte image
|
|
158
|
+
// would otherwise read all of it to throw away.
|
|
159
|
+
if (finished)
|
|
160
|
+
this.push(null);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|