youtube-knowledge-mcp 2.2.0 → 3.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.
- package/README.md +62 -18
- package/dist/harvest-schemas.d.ts +75 -0
- package/dist/harvest-schemas.d.ts.map +1 -0
- package/dist/harvest-schemas.js +106 -0
- package/dist/harvest-schemas.js.map +1 -0
- package/dist/registry/harvest-tools.d.ts +10 -0
- package/dist/registry/harvest-tools.d.ts.map +1 -0
- package/dist/registry/harvest-tools.js +109 -0
- package/dist/registry/harvest-tools.js.map +1 -0
- package/dist/registry/health-tools.d.ts.map +1 -1
- package/dist/registry/health-tools.js +18 -0
- package/dist/registry/health-tools.js.map +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/video-tools.js +1 -1
- package/dist/registry/video-tools.js.map +1 -1
- package/dist/schemas.d.ts +239 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +56 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-schemas.d.ts +28 -28
- package/dist/tools/ask-brain.js +1 -1
- package/dist/tools/ask-brain.js.map +1 -1
- package/dist/tools/brain-info.js +1 -1
- package/dist/tools/brain-info.js.map +1 -1
- package/dist/tools/check-health.d.ts +59 -0
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +85 -6
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/fetch-channel-thumbnails.d.ts +8 -8
- package/dist/tools/fetch-videos.d.ts +15 -4
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +63 -22
- package/dist/tools/fetch-videos.js.map +1 -1
- package/dist/tools/get-comments.d.ts +238 -6
- package/dist/tools/get-comments.d.ts.map +1 -1
- package/dist/tools/get-comments.js +85 -21
- package/dist/tools/get-comments.js.map +1 -1
- package/dist/tools/get-coverage.d.ts +122 -0
- package/dist/tools/get-coverage.d.ts.map +1 -0
- package/dist/tools/get-coverage.js +105 -0
- package/dist/tools/get-coverage.js.map +1 -0
- package/dist/tools/harvest-channel.d.ts +92 -0
- package/dist/tools/harvest-channel.d.ts.map +1 -0
- package/dist/tools/harvest-channel.js +62 -0
- package/dist/tools/harvest-channel.js.map +1 -0
- package/dist/tools/harvest-comments.d.ts +81 -0
- package/dist/tools/harvest-comments.d.ts.map +1 -0
- package/dist/tools/harvest-comments.js +115 -0
- package/dist/tools/harvest-comments.js.map +1 -0
- package/dist/tools/library.js +1 -1
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/list-channel-thumbnails.d.ts +10 -10
- package/dist/tools/list-channel-thumbnails.js +1 -1
- package/dist/tools/list-channel-thumbnails.js.map +1 -1
- package/dist/tools/list-library.js +1 -1
- package/dist/tools/list-library.js.map +1 -1
- package/dist/tools/prune-harvest.d.ts +59 -0
- package/dist/tools/prune-harvest.d.ts.map +1 -0
- package/dist/tools/prune-harvest.js +108 -0
- package/dist/tools/prune-harvest.js.map +1 -0
- package/dist/tools/query-comments.d.ts +125 -0
- package/dist/tools/query-comments.d.ts.map +1 -0
- package/dist/tools/query-comments.js +106 -0
- package/dist/tools/query-comments.js.map +1 -0
- package/dist/tools/query-videos.d.ts +118 -0
- package/dist/tools/query-videos.d.ts.map +1 -0
- package/dist/tools/query-videos.js +97 -0
- package/dist/tools/query-videos.js.map +1 -0
- package/dist/tools/repair-store.d.ts +34 -0
- package/dist/tools/repair-store.d.ts.map +1 -0
- package/dist/tools/repair-store.js +58 -0
- package/dist/tools/repair-store.js.map +1 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/search-channels.js +5 -1
- package/dist/tools/search-channels.js.map +1 -1
- package/dist/tools/search-videos.js +1 -1
- package/dist/tools/search-videos.js.map +1 -1
- package/dist/utils/comment-store.d.ts +71 -0
- package/dist/utils/comment-store.d.ts.map +1 -0
- package/dist/utils/comment-store.js +129 -0
- package/dist/utils/comment-store.js.map +1 -0
- package/dist/utils/comment-threads.d.ts +51 -0
- package/dist/utils/comment-threads.d.ts.map +1 -0
- package/dist/utils/comment-threads.js +76 -0
- package/dist/utils/comment-threads.js.map +1 -0
- package/dist/utils/coverage-text.d.ts +3 -0
- package/dist/utils/coverage-text.d.ts.map +1 -0
- package/dist/utils/coverage-text.js +62 -0
- package/dist/utils/coverage-text.js.map +1 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.d.ts.map +1 -0
- package/dist/utils/coverage.js +142 -0
- package/dist/utils/coverage.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +9 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/format.d.ts +17 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +1 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/harvest-catalog.d.ts +44 -0
- package/dist/utils/harvest-catalog.d.ts.map +1 -0
- package/dist/utils/harvest-catalog.js +164 -0
- package/dist/utils/harvest-catalog.js.map +1 -0
- package/dist/utils/harvest-lock.d.ts +3 -0
- package/dist/utils/harvest-lock.d.ts.map +1 -0
- package/dist/utils/harvest-lock.js +26 -0
- package/dist/utils/harvest-lock.js.map +1 -0
- package/dist/utils/harvest-receipts.d.ts +14 -0
- package/dist/utils/harvest-receipts.d.ts.map +1 -0
- package/dist/utils/harvest-receipts.js +110 -0
- package/dist/utils/harvest-receipts.js.map +1 -0
- package/dist/utils/listing-cursor.d.ts +31 -0
- package/dist/utils/listing-cursor.d.ts.map +1 -0
- package/dist/utils/listing-cursor.js +39 -0
- package/dist/utils/listing-cursor.js.map +1 -0
- package/dist/utils/pot-preflight.d.ts +53 -0
- package/dist/utils/pot-preflight.d.ts.map +1 -0
- package/dist/utils/pot-preflight.js +146 -0
- package/dist/utils/pot-preflight.js.map +1 -0
- package/dist/utils/sleep.d.ts +9 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +25 -0
- package/dist/utils/sleep.js.map +1 -0
- package/dist/utils/store-health.d.ts +37 -0
- package/dist/utils/store-health.d.ts.map +1 -0
- package/dist/utils/store-health.js +92 -0
- package/dist/utils/store-health.js.map +1 -0
- package/dist/utils/store-migrations.d.ts +33 -0
- package/dist/utils/store-migrations.d.ts.map +1 -0
- package/dist/utils/store-migrations.js +41 -0
- package/dist/utils/store-migrations.js.map +1 -0
- package/dist/utils/store-paths.d.ts +33 -0
- package/dist/utils/store-paths.d.ts.map +1 -0
- package/dist/utils/store-paths.js +48 -0
- package/dist/utils/store-paths.js.map +1 -0
- package/dist/utils/store-rows.d.ts +22 -0
- package/dist/utils/store-rows.d.ts.map +1 -0
- package/dist/utils/store-rows.js +62 -0
- package/dist/utils/store-rows.js.map +1 -0
- package/dist/utils/store-schema.d.ts +16 -0
- package/dist/utils/store-schema.d.ts.map +1 -0
- package/dist/utils/store-schema.js +168 -0
- package/dist/utils/store-schema.js.map +1 -0
- package/dist/utils/store.d.ts +34 -0
- package/dist/utils/store.d.ts.map +1 -0
- package/dist/utils/store.js +157 -0
- package/dist/utils/store.js.map +1 -0
- package/dist/utils/video-stats-cache.d.ts +16 -0
- package/dist/utils/video-stats-cache.d.ts.map +1 -0
- package/dist/utils/video-stats-cache.js +81 -0
- package/dist/utils/video-stats-cache.js.map +1 -0
- package/dist/utils/video-stats.d.ts +186 -0
- package/dist/utils/video-stats.d.ts.map +1 -0
- package/dist/utils/video-stats.js +168 -0
- package/dist/utils/video-stats.js.map +1 -0
- package/dist/utils/video-store.d.ts +69 -0
- package/dist/utils/video-store.d.ts.map +1 -0
- package/dist/utils/video-store.js +77 -0
- package/dist/utils/video-store.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +9 -2
- package/dist/utils/youtube-channel.d.ts.map +1 -1
- package/dist/utils/youtube-channel.js +33 -3
- package/dist/utils/youtube-channel.js.map +1 -1
- package/dist/utils/youtube-schemas.d.ts +98 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -1
- package/dist/utils/youtube-schemas.js +30 -0
- package/dist/utils/youtube-schemas.js.map +1 -1
- package/dist/utils/youtube-video.d.ts +22 -1
- package/dist/utils/youtube-video.d.ts.map +1 -1
- package/dist/utils/youtube-video.js +40 -28
- package/dist/utils/youtube-video.js.map +1 -1
- package/dist/utils/ytdlp-limiter.d.ts +3 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -1
- package/dist/utils/ytdlp-limiter.js +19 -3
- package/dist/utils/ytdlp-limiter.js.map +1 -1
- package/dist/utils/ytdlp-pacer.d.ts +64 -0
- package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
- package/dist/utils/ytdlp-pacer.js +165 -0
- package/dist/utils/ytdlp-pacer.js.map +1 -0
- package/dist/utils/ytdlp-stream.d.ts +32 -0
- package/dist/utils/ytdlp-stream.d.ts.map +1 -0
- package/dist/utils/ytdlp-stream.js +61 -0
- package/dist/utils/ytdlp-stream.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +10 -0
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +36 -25
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { coverageSchema, harvestScopeSchema } from '../harvest-schemas.js';
|
|
3
|
+
import { assertCoverageConsistent } from './coverage.js';
|
|
4
|
+
import { queryRow, queryRows, sqliteBoolean } from './store-rows.js';
|
|
5
|
+
/**
|
|
6
|
+
* Persisting completeness receipts.
|
|
7
|
+
*
|
|
8
|
+
* Written in the same transaction as the rows they describe. That is the whole
|
|
9
|
+
* reason this store is a database: there is no interval in which a receipt can
|
|
10
|
+
* claim something the store does not hold, so the reconciliation dance that
|
|
11
|
+
* `brain-build.ts` performs between its manifest and its chunks has no
|
|
12
|
+
* analogue here.
|
|
13
|
+
*/
|
|
14
|
+
const receiptRowSchema = z.object({
|
|
15
|
+
scope: harvestScopeSchema,
|
|
16
|
+
target_id: z.string(),
|
|
17
|
+
state: z.string(),
|
|
18
|
+
reason: z.string(),
|
|
19
|
+
have: z.number(),
|
|
20
|
+
expected: z.number().nullable(),
|
|
21
|
+
expected_source: z.string().nullable(),
|
|
22
|
+
source: z.string(),
|
|
23
|
+
limit_applied: z.number().nullable(),
|
|
24
|
+
sort_applied: z.string().nullable(),
|
|
25
|
+
ran_to_exhaustion: sqliteBoolean.nullable(),
|
|
26
|
+
resume_token: z.string().nullable(),
|
|
27
|
+
started_at: z.number(),
|
|
28
|
+
finished_at: z.number().nullable(),
|
|
29
|
+
attempts: z.number(),
|
|
30
|
+
error_code: z.string().nullable(),
|
|
31
|
+
note: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
const UPSERT = `
|
|
34
|
+
INSERT INTO harvest_receipt (
|
|
35
|
+
scope, target_id, state, reason, have, expected, expected_source, source,
|
|
36
|
+
limit_applied, sort_applied, ran_to_exhaustion, resume_token,
|
|
37
|
+
started_at, finished_at, attempts, error_code, note
|
|
38
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
39
|
+
ON CONFLICT(scope, target_id) DO UPDATE SET
|
|
40
|
+
state = excluded.state, reason = excluded.reason, have = excluded.have,
|
|
41
|
+
expected = excluded.expected, expected_source = excluded.expected_source,
|
|
42
|
+
source = excluded.source, limit_applied = excluded.limit_applied,
|
|
43
|
+
sort_applied = excluded.sort_applied, ran_to_exhaustion = excluded.ran_to_exhaustion,
|
|
44
|
+
resume_token = excluded.resume_token, finished_at = excluded.finished_at,
|
|
45
|
+
attempts = harvest_receipt.attempts + 1, error_code = excluded.error_code,
|
|
46
|
+
note = excluded.note`;
|
|
47
|
+
/** `complete` is the receipt's own word; the rest describe how it ended. */
|
|
48
|
+
function stateOf(coverage) {
|
|
49
|
+
if (coverage.complete)
|
|
50
|
+
return 'complete';
|
|
51
|
+
if (coverage.reason === 'SOURCE_REFUSED' || coverage.reason === 'TIMEOUT')
|
|
52
|
+
return 'failed';
|
|
53
|
+
if (coverage.reason === 'NOT_ATTEMPTED')
|
|
54
|
+
return 'running';
|
|
55
|
+
return 'partial';
|
|
56
|
+
}
|
|
57
|
+
function toRow(coverage) {
|
|
58
|
+
// Re-checked on the way to disk as well as on the way out of coverageOf: a
|
|
59
|
+
// receipt that reaches storage wrong is one that will be believed later.
|
|
60
|
+
assertCoverageConsistent(coverage);
|
|
61
|
+
return coverage;
|
|
62
|
+
}
|
|
63
|
+
export function saveReceipt(database, coverage, options = {}) {
|
|
64
|
+
const checked = toRow(coverage);
|
|
65
|
+
const finishedAt = Date.parse(checked.harvestedAt);
|
|
66
|
+
database
|
|
67
|
+
.prepare(UPSERT)
|
|
68
|
+
.run(checked.scope, checked.targetId, stateOf(checked), checked.reason, checked.have, checked.expected ?? null, checked.expectedSource ?? null, checked.source, checked.limitApplied ?? null, checked.sortApplied ?? null, checked.ranToExhaustion === undefined ? null : Number(checked.ranToExhaustion), checked.resumeToken ?? null, options.startedAt ?? finishedAt, finishedAt, 0, options.errorCode ?? null, checked.note ?? null);
|
|
69
|
+
database
|
|
70
|
+
.prepare('INSERT INTO harvest_event (scope, target_id, at, state, delta, note) VALUES (?,?,?,?,?,?)')
|
|
71
|
+
.run(checked.scope, checked.targetId, finishedAt, stateOf(checked), checked.have, checked.reason);
|
|
72
|
+
}
|
|
73
|
+
function fromRow(row) {
|
|
74
|
+
const expected = row.expected;
|
|
75
|
+
const expectedSource = row.expected_source;
|
|
76
|
+
return coverageSchema.parse({
|
|
77
|
+
scope: row.scope,
|
|
78
|
+
targetId: row.target_id,
|
|
79
|
+
complete: row.state === 'complete',
|
|
80
|
+
reason: row.reason,
|
|
81
|
+
have: row.have,
|
|
82
|
+
...(expected === null || expectedSource === null ? {} : { expected, expectedSource }),
|
|
83
|
+
...(row.ran_to_exhaustion === null ? {} : { ranToExhaustion: row.ran_to_exhaustion }),
|
|
84
|
+
source: row.source,
|
|
85
|
+
harvestedAt: new Date(row.finished_at ?? row.started_at).toISOString(),
|
|
86
|
+
...(row.limit_applied === null ? {} : { limitApplied: row.limit_applied }),
|
|
87
|
+
...(row.sort_applied === null ? {} : { sortApplied: row.sort_applied }),
|
|
88
|
+
...(row.resume_token === null ? {} : { resumeToken: row.resume_token }),
|
|
89
|
+
...(row.note === null ? {} : { note: row.note }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function readReceipt(database, scope, targetId) {
|
|
93
|
+
const row = queryRow(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? AND target_id = ?'), receiptRowSchema, scope, targetId);
|
|
94
|
+
return row === undefined ? undefined : fromRow(row);
|
|
95
|
+
}
|
|
96
|
+
export function listReceipts(database, options = {}) {
|
|
97
|
+
const limit = options.limit ?? 50;
|
|
98
|
+
const offset = options.offset ?? 0;
|
|
99
|
+
const rows = options.scope === undefined
|
|
100
|
+
? queryRows(database.prepare('SELECT * FROM harvest_receipt ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, limit, offset)
|
|
101
|
+
: queryRows(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, options.scope, limit, offset);
|
|
102
|
+
return rows.map(fromRow);
|
|
103
|
+
}
|
|
104
|
+
export function countReceipts(database, scope) {
|
|
105
|
+
const row = scope === undefined
|
|
106
|
+
? queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt'), z.object({ n: z.number() }))
|
|
107
|
+
: queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt WHERE scope = ?'), z.object({ n: z.number() }), scope);
|
|
108
|
+
return row?.n ?? 0;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=harvest-receipts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harvest-receipts.js","sourceRoot":"","sources":["../../src/utils/harvest-receipts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The cursor `fetch_videos` hands back.
|
|
4
|
+
*
|
|
5
|
+
* YouTube's listings are continuation-paginated, so `--playlist-items 101:120`
|
|
6
|
+
* still walks from the start: paging server-side is O(n) per page and O(n^2)
|
|
7
|
+
* overall. It is also not a cursor in any useful sense — a new upload shifts
|
|
8
|
+
* every index by one, so an offset silently re-reads or skips at the seam.
|
|
9
|
+
*
|
|
10
|
+
* So the cursor carries the next index *and* the id of the last item already
|
|
11
|
+
* returned. If that id reappears on the next page the listing moved under us,
|
|
12
|
+
* and the duplicate is dropped rather than handed over twice.
|
|
13
|
+
*/
|
|
14
|
+
declare const cursorSchema: z.ZodObject<{
|
|
15
|
+
v: z.ZodLiteral<1>;
|
|
16
|
+
start: z.ZodNumber;
|
|
17
|
+
anchor: z.ZodString;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
anchor: string;
|
|
20
|
+
start: number;
|
|
21
|
+
v: 1;
|
|
22
|
+
}, {
|
|
23
|
+
anchor: string;
|
|
24
|
+
start: number;
|
|
25
|
+
v: 1;
|
|
26
|
+
}>;
|
|
27
|
+
export type ListingCursor = z.infer<typeof cursorSchema>;
|
|
28
|
+
export declare function encodeCursor(cursor: ListingCursor): string;
|
|
29
|
+
export declare function decodeCursor(token: string): ListingCursor;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=listing-cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-cursor.d.ts","sourceRoot":"","sources":["../../src/utils/listing-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,YAAY;;;;;;;;;;;;EAIhB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEzD,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAezD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { YouTubeError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The cursor `fetch_videos` hands back.
|
|
5
|
+
*
|
|
6
|
+
* YouTube's listings are continuation-paginated, so `--playlist-items 101:120`
|
|
7
|
+
* still walks from the start: paging server-side is O(n) per page and O(n^2)
|
|
8
|
+
* overall. It is also not a cursor in any useful sense — a new upload shifts
|
|
9
|
+
* every index by one, so an offset silently re-reads or skips at the seam.
|
|
10
|
+
*
|
|
11
|
+
* So the cursor carries the next index *and* the id of the last item already
|
|
12
|
+
* returned. If that id reappears on the next page the listing moved under us,
|
|
13
|
+
* and the duplicate is dropped rather than handed over twice.
|
|
14
|
+
*/
|
|
15
|
+
const cursorSchema = z.object({
|
|
16
|
+
v: z.literal(1),
|
|
17
|
+
start: z.number().int().min(1),
|
|
18
|
+
anchor: z.string(),
|
|
19
|
+
});
|
|
20
|
+
export function encodeCursor(cursor) {
|
|
21
|
+
return Buffer.from(JSON.stringify(cursor), 'utf8').toString('base64url');
|
|
22
|
+
}
|
|
23
|
+
export function decodeCursor(token) {
|
|
24
|
+
const parsed = (() => {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
const result = cursorSchema.safeParse(parsed);
|
|
33
|
+
if (result.success)
|
|
34
|
+
return result.data;
|
|
35
|
+
throw new YouTubeError('INVALID_INPUT', 'That cursor is not one this server issued.', {
|
|
36
|
+
nextStep: 'Omit `cursor` to start from the first page.',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=listing-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-cursor.js","sourceRoot":"","sources":["../../src/utils/listing-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;GAWG;AAEH,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACf,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAIH,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,MAAM,GAAG,CAAC,GAAY,EAAE;QAC5B,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAEvC,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,4CAA4C,EAAE;QACpF,QAAQ,EAAE,6CAA6C;KACxD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session preflight: what yt-dlp can prove about itself before we ask YouTube
|
|
3
|
+
* anything.
|
|
4
|
+
*
|
|
5
|
+
* Measured on 2026-09-06: with no PO token provider installed, every *player*
|
|
6
|
+
* request from this host failed with "Sign in to confirm you're not a bot",
|
|
7
|
+
* while flat listings were never affected. Rotating `player_client` across six
|
|
8
|
+
* clients and waiting out a 15-minute cooldown both failed. The cause was not
|
|
9
|
+
* rate limiting and not the address alone — `yt-dlp -v` reported
|
|
10
|
+
* `PO Token Providers: none`, so every player request went out without the
|
|
11
|
+
* integrity token a browser attaches.
|
|
12
|
+
*
|
|
13
|
+
* That diagnosis is only useful if the server can see it, so this module reads
|
|
14
|
+
* it out of yt-dlp's own debug output rather than guessing. It is deliberately
|
|
15
|
+
* provider-agnostic: it reports what yt-dlp says it has, and never looks for a
|
|
16
|
+
* particular plugin by name.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProviderStatus {
|
|
19
|
+
name: string;
|
|
20
|
+
available: boolean;
|
|
21
|
+
/** Flags yt-dlp printed in parentheses, e.g. "external". */
|
|
22
|
+
flags: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface SessionReport {
|
|
25
|
+
/** False when yt-dlp reports no usable PO token provider. */
|
|
26
|
+
potAvailable: boolean;
|
|
27
|
+
potProviders: ProviderStatus[];
|
|
28
|
+
jsRuntimes: ProviderStatus[];
|
|
29
|
+
impersonateTargets: string[];
|
|
30
|
+
/** Set when the probe itself could not run; the report is then unknown, not empty. */
|
|
31
|
+
probeFailed?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Split on commas that separate entries, not the ones inside a parenthesised
|
|
35
|
+
* flag list. yt-dlp prints `bgutil:http-1.3.2 (external, unavailable)` — a
|
|
36
|
+
* plain `split(',')` tears that entry in half and loses the flag that matters.
|
|
37
|
+
*/
|
|
38
|
+
export declare function splitTopLevel(value: string): string[];
|
|
39
|
+
/** Parses one `Providers: ...` payload. `none` is an empty list, not a provider. */
|
|
40
|
+
export declare function parseProviderList(payload: string): ProviderStatus[];
|
|
41
|
+
/**
|
|
42
|
+
* Reads `yt-dlp --list-impersonate-targets`, keeping only rows that are
|
|
43
|
+
* actually usable. Every target is unavailable without curl_cffi, and passing
|
|
44
|
+
* `--impersonate` to a build that lacks it fails every spawn — so an
|
|
45
|
+
* unavailable target must be visible here rather than discovered per request.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseImpersonateTargets(stdout: string): string[];
|
|
48
|
+
export declare function runSessionPreflight(options?: {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
}): Promise<SessionReport>;
|
|
51
|
+
/** Test seam — the report is cached for the process lifetime in normal use. */
|
|
52
|
+
export declare function resetSessionPreflightCache(): void;
|
|
53
|
+
//# sourceMappingURL=pot-preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pot-preflight.d.ts","sourceRoot":"","sources":["../../src/utils/pot-preflight.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAyBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBrD;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAenE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBhE;AAqBD,wBAAsB,mBAAmB,CACvC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,OAAO,CAAC,aAAa,CAAC,CAwCxB;AAED,+EAA+E;AAC/E,wBAAgB,0BAA0B,IAAI,IAAI,CAGjD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
3
|
+
const FAILED_REPORT_TTL_MS = 60_000;
|
|
4
|
+
/**
|
|
5
|
+
* The `[youtube] [pot]` and `[youtube] [jsc]` debug lines are emitted only when
|
|
6
|
+
* the *video* extractor initialises. Measured 2026-09-06: `--version`,
|
|
7
|
+
* `ytsearch0:` and a `--flat-playlist` channel walk all produce neither line —
|
|
8
|
+
* the listing plane never touches either subsystem.
|
|
9
|
+
*
|
|
10
|
+
* So the probe uses a syntactically valid but nonexistent video id. yt-dlp
|
|
11
|
+
* initialises the extractor, prints both lines, then fails the lookup. That
|
|
12
|
+
* failure is expected and ignored: it costs one request, depends on no real
|
|
13
|
+
* video continuing to exist, and needs no fixture to be kept up to date.
|
|
14
|
+
*/
|
|
15
|
+
const PROBE_VIDEO_URL = 'https://www.youtube.com/watch?v=00000000000';
|
|
16
|
+
const PROBE_ARGS = ['-v', '--simulate', '--skip-download', PROBE_VIDEO_URL];
|
|
17
|
+
const POT_LINE = /PO Token Providers:\s*(.+)$/;
|
|
18
|
+
const JSC_LINE = /JS Challenge Providers:\s*(.+)$/;
|
|
19
|
+
let cached;
|
|
20
|
+
let cachedAt = 0;
|
|
21
|
+
/**
|
|
22
|
+
* Split on commas that separate entries, not the ones inside a parenthesised
|
|
23
|
+
* flag list. yt-dlp prints `bgutil:http-1.3.2 (external, unavailable)` — a
|
|
24
|
+
* plain `split(',')` tears that entry in half and loses the flag that matters.
|
|
25
|
+
*/
|
|
26
|
+
export function splitTopLevel(value) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
31
|
+
const char = value[i];
|
|
32
|
+
if (char === '(')
|
|
33
|
+
depth += 1;
|
|
34
|
+
else if (char === ')')
|
|
35
|
+
depth = Math.max(0, depth - 1);
|
|
36
|
+
else if (char === ',' && depth === 0) {
|
|
37
|
+
parts.push(value.slice(start, i));
|
|
38
|
+
start = i + 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
parts.push(value.slice(start));
|
|
42
|
+
return parts.map((part) => part.trim()).filter((part) => part.length > 0);
|
|
43
|
+
}
|
|
44
|
+
/** Parses one `Providers: ...` payload. `none` is an empty list, not a provider. */
|
|
45
|
+
export function parseProviderList(payload) {
|
|
46
|
+
const trimmed = payload.trim();
|
|
47
|
+
if (trimmed === '' || trimmed.toLowerCase() === 'none')
|
|
48
|
+
return [];
|
|
49
|
+
return splitTopLevel(trimmed).map((entry) => {
|
|
50
|
+
const match = /^(.*?)\s*\((.*)\)$/.exec(entry);
|
|
51
|
+
if (match === null)
|
|
52
|
+
return { name: entry, available: true, flags: [] };
|
|
53
|
+
const flags = splitTopLevel(match[2] ?? '').map((flag) => flag.toLowerCase());
|
|
54
|
+
return {
|
|
55
|
+
name: (match[1] ?? '').trim(),
|
|
56
|
+
available: !flags.includes('unavailable'),
|
|
57
|
+
flags,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads `yt-dlp --list-impersonate-targets`, keeping only rows that are
|
|
63
|
+
* actually usable. Every target is unavailable without curl_cffi, and passing
|
|
64
|
+
* `--impersonate` to a build that lacks it fails every spawn — so an
|
|
65
|
+
* unavailable target must be visible here rather than discovered per request.
|
|
66
|
+
*/
|
|
67
|
+
export function parseImpersonateTargets(stdout) {
|
|
68
|
+
const targets = [];
|
|
69
|
+
for (const raw of stdout.split('\n')) {
|
|
70
|
+
const line = raw.trim();
|
|
71
|
+
if (line === '' || line.startsWith('[') || line.startsWith('-'))
|
|
72
|
+
continue;
|
|
73
|
+
if (line.toLowerCase().startsWith('client'))
|
|
74
|
+
continue;
|
|
75
|
+
if (line.toLowerCase().includes('(unavailable)'))
|
|
76
|
+
continue;
|
|
77
|
+
const columns = line.split(/\s{2,}/).filter((column) => column.length > 0);
|
|
78
|
+
const client = columns[0]?.trim();
|
|
79
|
+
if (client === undefined || client === '')
|
|
80
|
+
continue;
|
|
81
|
+
const os = columns[1]?.trim();
|
|
82
|
+
targets.push(os === undefined || os === '' || os === '-' ? client : `${client}:${os}`);
|
|
83
|
+
}
|
|
84
|
+
return targets;
|
|
85
|
+
}
|
|
86
|
+
async function probe(args) {
|
|
87
|
+
try {
|
|
88
|
+
// yt-dlp writes debug lines to stderr and exits non-zero on some probes;
|
|
89
|
+
// `reject: false` keeps both streams instead of throwing them away.
|
|
90
|
+
const result = await execa('yt-dlp', args, { timeout: PROBE_TIMEOUT_MS, reject: false });
|
|
91
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function readProviders(output, pattern) {
|
|
98
|
+
for (const line of output.split('\n')) {
|
|
99
|
+
const match = pattern.exec(line);
|
|
100
|
+
if (match !== null)
|
|
101
|
+
return parseProviderList(match[1] ?? '');
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
export async function runSessionPreflight(options = {}) {
|
|
106
|
+
// Mirrors runPreflight: a good answer holds for the process, a bad one gets a
|
|
107
|
+
// short life so one slow spawn does not pin the diagnosis for good.
|
|
108
|
+
if (cached !== undefined && !options.force) {
|
|
109
|
+
if (cached.probeFailed === undefined || Date.now() - cachedAt < FAILED_REPORT_TTL_MS) {
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const [session, impersonate] = await Promise.all([
|
|
114
|
+
probe(PROBE_ARGS),
|
|
115
|
+
probe(['--list-impersonate-targets']),
|
|
116
|
+
]);
|
|
117
|
+
if (session === undefined) {
|
|
118
|
+
cached = {
|
|
119
|
+
potAvailable: false,
|
|
120
|
+
potProviders: [],
|
|
121
|
+
jsRuntimes: [],
|
|
122
|
+
impersonateTargets: [],
|
|
123
|
+
probeFailed: 'yt-dlp could not be probed for session capabilities.',
|
|
124
|
+
};
|
|
125
|
+
cachedAt = Date.now();
|
|
126
|
+
return cached;
|
|
127
|
+
}
|
|
128
|
+
const output = `${session.stderr}\n${session.stdout}`;
|
|
129
|
+
const potProviders = readProviders(output, POT_LINE);
|
|
130
|
+
cached = {
|
|
131
|
+
potAvailable: potProviders.some((provider) => provider.available),
|
|
132
|
+
potProviders,
|
|
133
|
+
jsRuntimes: readProviders(output, JSC_LINE),
|
|
134
|
+
impersonateTargets: impersonate === undefined
|
|
135
|
+
? []
|
|
136
|
+
: parseImpersonateTargets(`${impersonate.stdout}\n${impersonate.stderr}`),
|
|
137
|
+
};
|
|
138
|
+
cachedAt = Date.now();
|
|
139
|
+
return cached;
|
|
140
|
+
}
|
|
141
|
+
/** Test seam — the report is cached for the process lifetime in normal use. */
|
|
142
|
+
export function resetSessionPreflightCache() {
|
|
143
|
+
cached = undefined;
|
|
144
|
+
cachedAt = 0;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=pot-preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pot-preflight.js","sourceRoot":"","sources":["../../src/utils/pot-preflight.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cancellable pause.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the retry loop and the pacer so a cooling-down server still
|
|
5
|
+
* answers a client that gives up: an uncancellable sleep is how a cooldown
|
|
6
|
+
* turns into a wedge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=sleep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB3E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { YouTubeError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* A cancellable pause.
|
|
4
|
+
*
|
|
5
|
+
* Shared by the retry loop and the pacer so a cooling-down server still
|
|
6
|
+
* answers a client that gives up: an uncancellable sleep is how a cooldown
|
|
7
|
+
* turns into a wedge.
|
|
8
|
+
*/
|
|
9
|
+
export async function sleep(ms, signal) {
|
|
10
|
+
if (signal?.aborted === true) {
|
|
11
|
+
throw new YouTubeError('CANCELLED', 'The request was cancelled.');
|
|
12
|
+
}
|
|
13
|
+
await new Promise((resolve, reject) => {
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
signal?.removeEventListener('abort', onAbort);
|
|
16
|
+
resolve();
|
|
17
|
+
}, ms);
|
|
18
|
+
const onAbort = () => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
reject(new YouTubeError('CANCELLED', 'The request was cancelled.'));
|
|
21
|
+
};
|
|
22
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=sleep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IAC1D,IAAI,MAAM,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,YAAY,CAAC,WAAW,EAAE,4BAA4B,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,YAAY,CAAC,WAAW,EAAE,4BAA4B,CAAC,CAAC,CAAC;QACtE,CAAC,CAAC;QACF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What can be said about the store without harvesting anything.
|
|
3
|
+
*
|
|
4
|
+
* Integrity is checked here and nowhere else on the hot path: `quick_check` is
|
|
5
|
+
* bounded and catches the realistic failures, while the full `integrity_check`
|
|
6
|
+
* is O(database) and would add seconds to every server start on a store that
|
|
7
|
+
* is measured in gigabytes.
|
|
8
|
+
*/
|
|
9
|
+
export interface StoreHealth {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
path: string;
|
|
12
|
+
exists: boolean;
|
|
13
|
+
storeVersion?: number;
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
walBytes: number;
|
|
16
|
+
integrity: 'ok' | 'failed' | 'unchecked';
|
|
17
|
+
channels: number;
|
|
18
|
+
videos: number;
|
|
19
|
+
comments: number;
|
|
20
|
+
receipts: {
|
|
21
|
+
complete: number;
|
|
22
|
+
partial: number;
|
|
23
|
+
running: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
};
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function readStoreHealth(): Promise<StoreHealth>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the store should make an overall health report say "not ready".
|
|
31
|
+
*
|
|
32
|
+
* A missing store must not: a fresh install has none, and check_health's job
|
|
33
|
+
* is diagnosing the external binaries. A store that fails its integrity check,
|
|
34
|
+
* or claims a version this build does not understand, must.
|
|
35
|
+
*/
|
|
36
|
+
export declare function storeFailsHealth(health: StoreHealth): boolean;
|
|
37
|
+
//# sourceMappingURL=store-health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-health.d.ts","sourceRoot":"","sources":["../../src/utils/store-health.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiBD,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC,CA0D5D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAG7D"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { countRows, queryRow } from './store-rows.js';
|
|
4
|
+
import { getStore, readUserVersion } from './store.js';
|
|
5
|
+
import { storeDatabasePath } from './store-paths.js';
|
|
6
|
+
import { STORE_VERSION } from './store-schema.js';
|
|
7
|
+
const quickCheckSchema = z.object({ quick_check: z.string() }).passthrough();
|
|
8
|
+
const stateCountSchema = z.object({ state: z.string(), n: z.number() }).passthrough();
|
|
9
|
+
async function sizeOf(path) {
|
|
10
|
+
try {
|
|
11
|
+
return (await stat(path)).size;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function emptyReceipts() {
|
|
18
|
+
return { complete: 0, partial: 0, running: 0, failed: 0 };
|
|
19
|
+
}
|
|
20
|
+
export async function readStoreHealth() {
|
|
21
|
+
const path = storeDatabasePath();
|
|
22
|
+
const [sizeBytes, walBytes] = await Promise.all([sizeOf(path), sizeOf(`${path}-wal`)]);
|
|
23
|
+
const base = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
path,
|
|
26
|
+
exists: sizeBytes > 0,
|
|
27
|
+
sizeBytes,
|
|
28
|
+
// A -wal file that never shrinks means a reader is pinning it, which is
|
|
29
|
+
// worth seeing before it becomes a disk-space surprise.
|
|
30
|
+
walBytes,
|
|
31
|
+
integrity: 'unchecked',
|
|
32
|
+
channels: 0,
|
|
33
|
+
videos: 0,
|
|
34
|
+
comments: 0,
|
|
35
|
+
receipts: emptyReceipts(),
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const store = await getStore();
|
|
39
|
+
const quick = queryRow(store.prepare('PRAGMA quick_check'), quickCheckSchema);
|
|
40
|
+
const receipts = emptyReceipts();
|
|
41
|
+
for (const row of store
|
|
42
|
+
.prepare('SELECT state, COUNT(*) AS n FROM harvest_receipt GROUP BY state')
|
|
43
|
+
.all()) {
|
|
44
|
+
const parsed = stateCountSchema.safeParse(row);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
continue;
|
|
47
|
+
// A `state` this build does not know about is counted nowhere rather
|
|
48
|
+
// than crashing: the enum is allowed to grow ahead of this reader.
|
|
49
|
+
const { state, n } = parsed.data;
|
|
50
|
+
if (state === 'complete')
|
|
51
|
+
receipts.complete = n;
|
|
52
|
+
else if (state === 'partial')
|
|
53
|
+
receipts.partial = n;
|
|
54
|
+
else if (state === 'running')
|
|
55
|
+
receipts.running = n;
|
|
56
|
+
else if (state === 'failed')
|
|
57
|
+
receipts.failed = n;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
storeVersion: readUserVersion(store),
|
|
62
|
+
integrity: quick?.quick_check === 'ok' ? 'ok' : 'failed',
|
|
63
|
+
channels: countRows(store.prepare('SELECT COUNT(*) FROM channel')),
|
|
64
|
+
videos: countRows(store.prepare('SELECT COUNT(*) FROM video')),
|
|
65
|
+
comments: countRows(store.prepare('SELECT COUNT(*) FROM comment')),
|
|
66
|
+
receipts,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// A store that cannot be opened is a reportable condition, not a reason
|
|
71
|
+
// for check_health itself to fail: the binaries it exists to diagnose are
|
|
72
|
+
// unaffected.
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
enabled: false,
|
|
76
|
+
error: error instanceof Error ? error.message : String(error),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether the store should make an overall health report say "not ready".
|
|
82
|
+
*
|
|
83
|
+
* A missing store must not: a fresh install has none, and check_health's job
|
|
84
|
+
* is diagnosing the external binaries. A store that fails its integrity check,
|
|
85
|
+
* or claims a version this build does not understand, must.
|
|
86
|
+
*/
|
|
87
|
+
export function storeFailsHealth(health) {
|
|
88
|
+
if (health.integrity === 'failed')
|
|
89
|
+
return true;
|
|
90
|
+
return health.storeVersion !== undefined && health.storeVersion > STORE_VERSION;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=store-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-health.js","sourceRoot":"","sources":["../../src/utils/store-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* Ordered schema migrations for the harvested store.
|
|
4
|
+
*
|
|
5
|
+
* `PRAGMA user_version` carries the store's version, deliberately independent
|
|
6
|
+
* of the server's semver: the schema changes far less often than the code, and
|
|
7
|
+
* tying them together would force an empty migration on every release.
|
|
8
|
+
*
|
|
9
|
+
* Version 0 means "no store yet". Creating the tables is `STORE_SCHEMA`'s job,
|
|
10
|
+
* not a migration's, so the first run only stamps the version.
|
|
11
|
+
*/
|
|
12
|
+
export interface StoreMigration {
|
|
13
|
+
/** The version this migration produces. */
|
|
14
|
+
to: number;
|
|
15
|
+
apply: (database: DatabaseSync) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Empty on purpose at v1: `STORE_SCHEMA` is `CREATE TABLE IF NOT EXISTS`
|
|
19
|
+
* throughout, so a fresh database and an existing v1 database converge without
|
|
20
|
+
* one. The list exists so the second schema change is a one-line addition
|
|
21
|
+
* rather than a retrofit.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STORE_MIGRATIONS: readonly StoreMigration[];
|
|
24
|
+
/**
|
|
25
|
+
* Brings a database up to STORE_VERSION.
|
|
26
|
+
*
|
|
27
|
+
* Each migration runs inside its own transaction so a failure leaves the
|
|
28
|
+
* store at the last version that fully applied, rather than half-way through
|
|
29
|
+
* one. A newer-than-known version is rejected before this is reached, in
|
|
30
|
+
* `store.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runStoreMigrations(database: DatabaseSync, fromVersion: number): number;
|
|
33
|
+
//# sourceMappingURL=store-migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-migrations.d.ts","sourceRoot":"","sources":["../../src/utils/store-migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAAO,CAAC;AAE9D;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAwBtF"}
|