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,16 @@
|
|
|
1
|
+
import { type VideoStats } from './video-stats.js';
|
|
2
|
+
export interface StatsResult {
|
|
3
|
+
stats: VideoStats;
|
|
4
|
+
/** True when served from disk rather than refetched. */
|
|
5
|
+
cached: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A video's stats, from disk when a fresh copy is there.
|
|
9
|
+
*
|
|
10
|
+
* A cache write that fails is not allowed to fail the read: the caller already
|
|
11
|
+
* has the answer, and a full disk should cost speed, not correctness.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readVideoStats(urlOrId: string, options?: {
|
|
14
|
+
refresh?: boolean;
|
|
15
|
+
}, now?: number): Promise<StatsResult>;
|
|
16
|
+
//# sourceMappingURL=video-stats-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stats-cache.d.ts","sourceRoot":"","sources":["../../src/utils/video-stats-cache.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAC;AAqD1B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,CAAC;IAClB,wDAAwD;IACxD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,EACnC,GAAG,SAAa,GACf,OAAO,CAAC,WAAW,CAAC,CAyBtB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { dataDir, ensurePrivateDir } from './paths.js';
|
|
5
|
+
import { readJsonFile, writeJsonAtomic } from './json-file.js';
|
|
6
|
+
import { envInt } from './env.js';
|
|
7
|
+
import { assertVideoId } from './validate.js';
|
|
8
|
+
import { extractVideoId } from './youtube-url.js';
|
|
9
|
+
import { fetchVideoStatsRow, toVideoStats, videoStatsRowSchema, } from './video-stats.js';
|
|
10
|
+
/**
|
|
11
|
+
* Keeping one video's stats on disk.
|
|
12
|
+
*
|
|
13
|
+
* Every per-video read goes through here, so get_video_info, get_chapters, a
|
|
14
|
+
* caption inventory and a catalogue detail pass share a single request rather
|
|
15
|
+
* than making one each. Requests are what gets a client throttled, so this is
|
|
16
|
+
* the largest reduction available.
|
|
17
|
+
*
|
|
18
|
+
* The cached document is the yt-dlp row, not the derived object: the row is
|
|
19
|
+
* what has a schema, and re-deriving on read means a cached video and a fresh
|
|
20
|
+
* one always come back through the same code path. Shaped after
|
|
21
|
+
* transcript-cache.ts — sharded so no directory grows to 100,000 entries,
|
|
22
|
+
* validated on read, and anything unreadable treated as absent.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Resolved per call, not once at import.
|
|
26
|
+
*
|
|
27
|
+
* A module-level dataDir() reads the home directory the instant the module
|
|
28
|
+
* loads, which is before a test can point HOME anywhere — and it means the
|
|
29
|
+
* path is fixed for the process. store-paths.ts uses functions for the same
|
|
30
|
+
* reason.
|
|
31
|
+
*/
|
|
32
|
+
function cacheDir() {
|
|
33
|
+
return dataDir('stats');
|
|
34
|
+
}
|
|
35
|
+
/** Counts move constantly, so a day is already generous. */
|
|
36
|
+
const VIDEO_STATS_TTL_MS = envInt('YOUTUBE_MCP_STATS_TTL_MS', 86_400_000, { min: 0 });
|
|
37
|
+
/** Bumped when the stored shape changes, so old files are refetched, not misread. */
|
|
38
|
+
const CACHE_VERSION = 1;
|
|
39
|
+
const cachedStatsSchema = z.object({
|
|
40
|
+
version: z.literal(CACHE_VERSION),
|
|
41
|
+
fetchedAt: z.number(),
|
|
42
|
+
row: videoStatsRowSchema,
|
|
43
|
+
});
|
|
44
|
+
function shardDir(videoId) {
|
|
45
|
+
return join(cacheDir(), assertVideoId(videoId).slice(0, 2));
|
|
46
|
+
}
|
|
47
|
+
function cachePath(videoId) {
|
|
48
|
+
return join(shardDir(videoId), `${assertVideoId(videoId)}.json`);
|
|
49
|
+
}
|
|
50
|
+
function isFresh(fetchedAt, now) {
|
|
51
|
+
return VIDEO_STATS_TTL_MS === 0 || now - fetchedAt < VIDEO_STATS_TTL_MS;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A video's stats, from disk when a fresh copy is there.
|
|
55
|
+
*
|
|
56
|
+
* A cache write that fails is not allowed to fail the read: the caller already
|
|
57
|
+
* has the answer, and a full disk should cost speed, not correctness.
|
|
58
|
+
*/
|
|
59
|
+
export async function readVideoStats(urlOrId, options = {}, now = Date.now()) {
|
|
60
|
+
// The id comes from the URL, not from a fetch: checking the cache after
|
|
61
|
+
// fetching would be no cache at all.
|
|
62
|
+
const videoId = extractVideoId(urlOrId);
|
|
63
|
+
if (options.refresh !== true) {
|
|
64
|
+
const path = cachePath(videoId);
|
|
65
|
+
if (existsSync(path)) {
|
|
66
|
+
const parsed = await readJsonFile(path, cachedStatsSchema);
|
|
67
|
+
if (parsed !== undefined && isFresh(parsed.fetchedAt, now)) {
|
|
68
|
+
return { stats: toVideoStats(parsed.row), cached: true };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const row = await fetchVideoStatsRow(urlOrId);
|
|
73
|
+
await ensurePrivateDir(shardDir(videoId));
|
|
74
|
+
await writeJsonAtomic(cachePath(videoId), {
|
|
75
|
+
version: CACHE_VERSION,
|
|
76
|
+
fetchedAt: now,
|
|
77
|
+
row,
|
|
78
|
+
}).catch(() => undefined);
|
|
79
|
+
return { stats: toVideoStats(row), cached: false };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=video-stats-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stats-cache.js","sourceRoot":"","sources":["../../src/utils/video-stats-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* One full read of a video, serving every caller that needs part of it.
|
|
4
|
+
*
|
|
5
|
+
* `getVideoInfo` ran a narrow `--print` and `getVideoDetails` ran a full `-j`
|
|
6
|
+
* and discarded all but three fields, so a single video cost two spawns for
|
|
7
|
+
* data one already had. Requests to YouTube are the thing that gets a client
|
|
8
|
+
* throttled, so halving them is the cheapest reliability win available.
|
|
9
|
+
*
|
|
10
|
+
* What is NOT kept matters as much as what is. Measured on a 637KB payload:
|
|
11
|
+
* `automatic_captions` is 460,965 bytes (72%) of URLs that expire, `formats`
|
|
12
|
+
* another 113,601 (18%), and `heatmap` 6,589 — larger than the whole rest of
|
|
13
|
+
* the record. Stripped, a video is 4,785 bytes. Storing the raw object would
|
|
14
|
+
* cost 6.4GB per 10,000 videos instead of 48MB.
|
|
15
|
+
*/
|
|
16
|
+
/** A stored snapshot must stay small enough that a catalogue is not mostly URLs. */
|
|
17
|
+
export declare const MAX_DETAIL_JSON_BYTES = 32768;
|
|
18
|
+
/** Keys dropped before storage: expiring URLs and bulk we can re-fetch. */
|
|
19
|
+
export declare const STRIPPED_KEYS: readonly ["formats", "requested_formats", "automatic_captions", "subtitles", "thumbnails", "comments", "heatmap"];
|
|
20
|
+
export declare const videoStatsRowSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
23
|
+
channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
+
channel_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
+
channel_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
26
|
+
channel_follower_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
27
|
+
upload_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
28
|
+
duration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
29
|
+
view_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
30
|
+
like_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
31
|
+
comment_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
32
|
+
concurrent_view_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
33
|
+
categories: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
34
|
+
tags: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
35
|
+
age_limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
36
|
+
availability: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
live_status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
+
was_live: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
39
|
+
is_live: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
40
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
|
+
thumbnail: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
+
chapters: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
43
|
+
title: z.ZodString;
|
|
44
|
+
start_time: z.ZodNumber;
|
|
45
|
+
end_time: z.ZodNumber;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
title: string;
|
|
48
|
+
start_time: number;
|
|
49
|
+
end_time: number;
|
|
50
|
+
}, {
|
|
51
|
+
title: string;
|
|
52
|
+
start_time: number;
|
|
53
|
+
end_time: number;
|
|
54
|
+
}>, "many">>>;
|
|
55
|
+
heatmap: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
56
|
+
start_time: z.ZodNumber;
|
|
57
|
+
end_time: z.ZodNumber;
|
|
58
|
+
value: z.ZodNumber;
|
|
59
|
+
}, "strip", z.ZodTypeAny, {
|
|
60
|
+
value: number;
|
|
61
|
+
start_time: number;
|
|
62
|
+
end_time: number;
|
|
63
|
+
}, {
|
|
64
|
+
value: number;
|
|
65
|
+
start_time: number;
|
|
66
|
+
end_time: number;
|
|
67
|
+
}>, "many">>>;
|
|
68
|
+
automatic_captions: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
69
|
+
subtitles: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
70
|
+
}, "strip", z.ZodTypeAny, {
|
|
71
|
+
id: string;
|
|
72
|
+
title?: string | null | undefined;
|
|
73
|
+
channel?: string | null | undefined;
|
|
74
|
+
description?: string | null | undefined;
|
|
75
|
+
tags?: string[] | null | undefined;
|
|
76
|
+
duration?: number | null | undefined;
|
|
77
|
+
upload_date?: string | null | undefined;
|
|
78
|
+
thumbnail?: string | null | undefined;
|
|
79
|
+
view_count?: number | null | undefined;
|
|
80
|
+
like_count?: number | null | undefined;
|
|
81
|
+
comment_count?: number | null | undefined;
|
|
82
|
+
availability?: string | null | undefined;
|
|
83
|
+
live_status?: string | null | undefined;
|
|
84
|
+
chapters?: {
|
|
85
|
+
title: string;
|
|
86
|
+
start_time: number;
|
|
87
|
+
end_time: number;
|
|
88
|
+
}[] | null | undefined;
|
|
89
|
+
subtitles?: Record<string, unknown> | null | undefined;
|
|
90
|
+
automatic_captions?: Record<string, unknown> | null | undefined;
|
|
91
|
+
channel_id?: string | null | undefined;
|
|
92
|
+
channel_url?: string | null | undefined;
|
|
93
|
+
channel_follower_count?: number | null | undefined;
|
|
94
|
+
heatmap?: {
|
|
95
|
+
value: number;
|
|
96
|
+
start_time: number;
|
|
97
|
+
end_time: number;
|
|
98
|
+
}[] | null | undefined;
|
|
99
|
+
concurrent_view_count?: number | null | undefined;
|
|
100
|
+
categories?: string[] | null | undefined;
|
|
101
|
+
age_limit?: number | null | undefined;
|
|
102
|
+
was_live?: boolean | null | undefined;
|
|
103
|
+
is_live?: boolean | null | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
id: string;
|
|
106
|
+
title?: string | null | undefined;
|
|
107
|
+
channel?: string | null | undefined;
|
|
108
|
+
description?: string | null | undefined;
|
|
109
|
+
tags?: string[] | null | undefined;
|
|
110
|
+
duration?: number | null | undefined;
|
|
111
|
+
upload_date?: string | null | undefined;
|
|
112
|
+
thumbnail?: string | null | undefined;
|
|
113
|
+
view_count?: number | null | undefined;
|
|
114
|
+
like_count?: number | null | undefined;
|
|
115
|
+
comment_count?: number | null | undefined;
|
|
116
|
+
availability?: string | null | undefined;
|
|
117
|
+
live_status?: string | null | undefined;
|
|
118
|
+
chapters?: {
|
|
119
|
+
title: string;
|
|
120
|
+
start_time: number;
|
|
121
|
+
end_time: number;
|
|
122
|
+
}[] | null | undefined;
|
|
123
|
+
subtitles?: Record<string, unknown> | null | undefined;
|
|
124
|
+
automatic_captions?: Record<string, unknown> | null | undefined;
|
|
125
|
+
channel_id?: string | null | undefined;
|
|
126
|
+
channel_url?: string | null | undefined;
|
|
127
|
+
channel_follower_count?: number | null | undefined;
|
|
128
|
+
heatmap?: {
|
|
129
|
+
value: number;
|
|
130
|
+
start_time: number;
|
|
131
|
+
end_time: number;
|
|
132
|
+
}[] | null | undefined;
|
|
133
|
+
concurrent_view_count?: number | null | undefined;
|
|
134
|
+
categories?: string[] | null | undefined;
|
|
135
|
+
age_limit?: number | null | undefined;
|
|
136
|
+
was_live?: boolean | null | undefined;
|
|
137
|
+
is_live?: boolean | null | undefined;
|
|
138
|
+
}>;
|
|
139
|
+
export interface VideoStats {
|
|
140
|
+
id: string;
|
|
141
|
+
title: string;
|
|
142
|
+
url: string;
|
|
143
|
+
channel: string;
|
|
144
|
+
channelId?: string;
|
|
145
|
+
channelFollowerCount?: number;
|
|
146
|
+
uploadDate: string;
|
|
147
|
+
durationSeconds: number;
|
|
148
|
+
durationFormatted: string;
|
|
149
|
+
viewCount?: number;
|
|
150
|
+
likeCount?: number;
|
|
151
|
+
/** YouTube's own count. Trustworthy here because this read has no --write-comments. */
|
|
152
|
+
commentCount?: number;
|
|
153
|
+
categories: string[];
|
|
154
|
+
tags: string[];
|
|
155
|
+
ageLimit: number;
|
|
156
|
+
availability?: string;
|
|
157
|
+
liveStatus?: string;
|
|
158
|
+
wasLive: boolean;
|
|
159
|
+
chapters: {
|
|
160
|
+
title: string;
|
|
161
|
+
startTime: number;
|
|
162
|
+
endTime: number;
|
|
163
|
+
}[];
|
|
164
|
+
/** Language tags only — never the URL arrays, which are 72% of the payload. */
|
|
165
|
+
captions: {
|
|
166
|
+
manual: string[];
|
|
167
|
+
automatic: string[];
|
|
168
|
+
};
|
|
169
|
+
hasHeatmap: boolean;
|
|
170
|
+
heatmap?: {
|
|
171
|
+
startSeconds: number;
|
|
172
|
+
endSeconds: number;
|
|
173
|
+
value: number;
|
|
174
|
+
}[];
|
|
175
|
+
descriptionChars: number;
|
|
176
|
+
description: string;
|
|
177
|
+
thumbnailUrl?: string;
|
|
178
|
+
}
|
|
179
|
+
export declare const CATALOG_DESCRIPTION_PREVIEW_CHARS = 500;
|
|
180
|
+
/** Drops the bulk keys, so what is stored is the record rather than its attachments. */
|
|
181
|
+
export declare function stripForStorage(raw: Record<string, unknown>): Record<string, unknown>;
|
|
182
|
+
export declare function toVideoStats(row: z.infer<typeof videoStatsRowSchema>): VideoStats;
|
|
183
|
+
/** One `-j` read, serving info, details, chapters, captions and the heatmap. */
|
|
184
|
+
export declare function fetchVideoStatsRow(urlOrId: string): Promise<z.infer<typeof videoStatsRowSchema>>;
|
|
185
|
+
export declare function getVideoStats(urlOrId: string): Promise<VideoStats>;
|
|
186
|
+
//# sourceMappingURL=video-stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stats.d.ts","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,2EAA2E;AAC3E,eAAO,MAAM,aAAa,mHAQhB,CAAC;AAQX,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4B9B,CAAC;AAEH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,+EAA+E;IAC/E,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcrF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,GAAG,UAAU,CA0EjF;AAED,gFAAgF;AAChF,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAS9C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAExE"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { TIMEOUTS, parseYtDlpJson, runYtDlp } from './ytdlp.js';
|
|
3
|
+
import { extractVideoId, formatDuration, watchUrl } from './youtube-url.js';
|
|
4
|
+
import { classifyPlayability } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* One full read of a video, serving every caller that needs part of it.
|
|
7
|
+
*
|
|
8
|
+
* `getVideoInfo` ran a narrow `--print` and `getVideoDetails` ran a full `-j`
|
|
9
|
+
* and discarded all but three fields, so a single video cost two spawns for
|
|
10
|
+
* data one already had. Requests to YouTube are the thing that gets a client
|
|
11
|
+
* throttled, so halving them is the cheapest reliability win available.
|
|
12
|
+
*
|
|
13
|
+
* What is NOT kept matters as much as what is. Measured on a 637KB payload:
|
|
14
|
+
* `automatic_captions` is 460,965 bytes (72%) of URLs that expire, `formats`
|
|
15
|
+
* another 113,601 (18%), and `heatmap` 6,589 — larger than the whole rest of
|
|
16
|
+
* the record. Stripped, a video is 4,785 bytes. Storing the raw object would
|
|
17
|
+
* cost 6.4GB per 10,000 videos instead of 48MB.
|
|
18
|
+
*/
|
|
19
|
+
/** A stored snapshot must stay small enough that a catalogue is not mostly URLs. */
|
|
20
|
+
export const MAX_DETAIL_JSON_BYTES = 32_768;
|
|
21
|
+
/** Keys dropped before storage: expiring URLs and bulk we can re-fetch. */
|
|
22
|
+
export const STRIPPED_KEYS = [
|
|
23
|
+
'formats',
|
|
24
|
+
'requested_formats',
|
|
25
|
+
'automatic_captions',
|
|
26
|
+
'subtitles',
|
|
27
|
+
'thumbnails',
|
|
28
|
+
'comments',
|
|
29
|
+
'heatmap',
|
|
30
|
+
];
|
|
31
|
+
const heatmapEntrySchema = z.object({
|
|
32
|
+
start_time: z.number(),
|
|
33
|
+
end_time: z.number(),
|
|
34
|
+
value: z.number(),
|
|
35
|
+
});
|
|
36
|
+
export const videoStatsRowSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
title: z.string().nullish(),
|
|
39
|
+
channel: z.string().nullish(),
|
|
40
|
+
channel_id: z.string().nullish(),
|
|
41
|
+
channel_url: z.string().nullish(),
|
|
42
|
+
channel_follower_count: z.number().nullish(),
|
|
43
|
+
upload_date: z.string().nullish(),
|
|
44
|
+
duration: z.number().nullish(),
|
|
45
|
+
view_count: z.number().nullish(),
|
|
46
|
+
like_count: z.number().nullish(),
|
|
47
|
+
comment_count: z.number().nullish(),
|
|
48
|
+
concurrent_view_count: z.number().nullish(),
|
|
49
|
+
categories: z.array(z.string()).nullish(),
|
|
50
|
+
tags: z.array(z.string()).nullish(),
|
|
51
|
+
age_limit: z.number().nullish(),
|
|
52
|
+
availability: z.string().nullish(),
|
|
53
|
+
live_status: z.string().nullish(),
|
|
54
|
+
was_live: z.boolean().nullish(),
|
|
55
|
+
is_live: z.boolean().nullish(),
|
|
56
|
+
description: z.string().nullish(),
|
|
57
|
+
thumbnail: z.string().nullish(),
|
|
58
|
+
chapters: z
|
|
59
|
+
.array(z.object({ title: z.string(), start_time: z.number(), end_time: z.number() }))
|
|
60
|
+
.nullish(),
|
|
61
|
+
heatmap: z.array(heatmapEntrySchema).nullish(),
|
|
62
|
+
automatic_captions: z.record(z.string(), z.unknown()).nullish(),
|
|
63
|
+
subtitles: z.record(z.string(), z.unknown()).nullish(),
|
|
64
|
+
});
|
|
65
|
+
export const CATALOG_DESCRIPTION_PREVIEW_CHARS = 500;
|
|
66
|
+
/** Drops the bulk keys, so what is stored is the record rather than its attachments. */
|
|
67
|
+
export function stripForStorage(raw) {
|
|
68
|
+
const kept = {};
|
|
69
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
70
|
+
if (STRIPPED_KEYS.some((stripped) => stripped === key))
|
|
71
|
+
continue;
|
|
72
|
+
kept[key] = value;
|
|
73
|
+
}
|
|
74
|
+
const description = kept.description;
|
|
75
|
+
if (typeof description === 'string' && description.length > CATALOG_DESCRIPTION_PREVIEW_CHARS) {
|
|
76
|
+
kept.description = description.slice(0, CATALOG_DESCRIPTION_PREVIEW_CHARS);
|
|
77
|
+
kept.description_truncated = true;
|
|
78
|
+
}
|
|
79
|
+
return kept;
|
|
80
|
+
}
|
|
81
|
+
export function toVideoStats(row) {
|
|
82
|
+
// classifyPlayability RETURNS the error rather than throwing it, so ignoring
|
|
83
|
+
// the result would hand back ordinary-looking stats for a private or
|
|
84
|
+
// members-only video.
|
|
85
|
+
const unplayable = classifyPlayability({
|
|
86
|
+
availability: row.availability ?? undefined,
|
|
87
|
+
liveStatus: row.live_status ?? undefined,
|
|
88
|
+
});
|
|
89
|
+
if (unplayable !== undefined)
|
|
90
|
+
throw unplayable;
|
|
91
|
+
const duration = row.duration ?? 0;
|
|
92
|
+
const heatmap = row.heatmap ?? undefined;
|
|
93
|
+
const description = row.description ?? '';
|
|
94
|
+
return {
|
|
95
|
+
id: row.id,
|
|
96
|
+
title: row.title ?? 'Unknown',
|
|
97
|
+
url: watchUrl(row.id),
|
|
98
|
+
channel: row.channel ?? 'Unknown',
|
|
99
|
+
...(row.channel_id === null || row.channel_id === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { channelId: row.channel_id }),
|
|
102
|
+
...(row.channel_follower_count === null || row.channel_follower_count === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { channelFollowerCount: row.channel_follower_count }),
|
|
105
|
+
uploadDate: row.upload_date ?? '',
|
|
106
|
+
durationSeconds: duration,
|
|
107
|
+
durationFormatted: formatDuration(Math.floor(duration)),
|
|
108
|
+
...(row.view_count === null || row.view_count === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { viewCount: row.view_count }),
|
|
111
|
+
...(row.like_count === null || row.like_count === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { likeCount: row.like_count }),
|
|
114
|
+
...(row.comment_count === null || row.comment_count === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { commentCount: row.comment_count }),
|
|
117
|
+
categories: row.categories ?? [],
|
|
118
|
+
tags: row.tags ?? [],
|
|
119
|
+
ageLimit: row.age_limit ?? 0,
|
|
120
|
+
...(row.availability === null || row.availability === undefined
|
|
121
|
+
? {}
|
|
122
|
+
: { availability: row.availability }),
|
|
123
|
+
...(row.live_status === null || row.live_status === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { liveStatus: row.live_status }),
|
|
126
|
+
wasLive: row.was_live ?? false,
|
|
127
|
+
chapters: (row.chapters ?? []).map((chapter) => ({
|
|
128
|
+
title: chapter.title,
|
|
129
|
+
startTime: chapter.start_time,
|
|
130
|
+
endTime: chapter.end_time,
|
|
131
|
+
})),
|
|
132
|
+
// Keys only: the values are arrays of expiring URLs, and the inventory is
|
|
133
|
+
// what a caller actually asks about.
|
|
134
|
+
captions: {
|
|
135
|
+
manual: Object.keys(row.subtitles ?? {}),
|
|
136
|
+
automatic: Object.keys(row.automatic_captions ?? {}),
|
|
137
|
+
},
|
|
138
|
+
hasHeatmap: heatmap !== undefined && heatmap.length > 0,
|
|
139
|
+
...(heatmap === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: {
|
|
142
|
+
heatmap: heatmap.map((entry) => ({
|
|
143
|
+
startSeconds: entry.start_time,
|
|
144
|
+
endSeconds: entry.end_time,
|
|
145
|
+
value: entry.value,
|
|
146
|
+
})),
|
|
147
|
+
}),
|
|
148
|
+
descriptionChars: description.length,
|
|
149
|
+
description,
|
|
150
|
+
...(row.thumbnail === null || row.thumbnail === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { thumbnailUrl: row.thumbnail }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** One `-j` read, serving info, details, chapters, captions and the heatmap. */
|
|
156
|
+
export async function fetchVideoStatsRow(urlOrId) {
|
|
157
|
+
const videoId = extractVideoId(urlOrId);
|
|
158
|
+
const stdout = await runYtDlp(['-j', '--skip-download'], {
|
|
159
|
+
label: 'get_video_stats',
|
|
160
|
+
timeoutMs: TIMEOUTS.metadata,
|
|
161
|
+
target: watchUrl(videoId),
|
|
162
|
+
});
|
|
163
|
+
return parseYtDlpJson(stdout, videoStatsRowSchema, 'video stats');
|
|
164
|
+
}
|
|
165
|
+
export async function getVideoStats(urlOrId) {
|
|
166
|
+
return toVideoStats(await fetchVideoStatsRow(urlOrId));
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=video-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stats.js","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Reading the catalogued videos back.
|
|
5
|
+
*
|
|
6
|
+
* `upload_date` is NULL, never '', for a video only ever seen in a flat
|
|
7
|
+
* listing — flat entries carry no publication date. A date filter therefore
|
|
8
|
+
* excludes those rows rather than silently treating them as ancient.
|
|
9
|
+
*/
|
|
10
|
+
declare const videoRowSchema: z.ZodObject<{
|
|
11
|
+
video_id: z.ZodString;
|
|
12
|
+
channel_id: z.ZodNullable<z.ZodString>;
|
|
13
|
+
title: z.ZodString;
|
|
14
|
+
duration_s: z.ZodNullable<z.ZodNumber>;
|
|
15
|
+
upload_date: z.ZodNullable<z.ZodString>;
|
|
16
|
+
view_count: z.ZodNullable<z.ZodNumber>;
|
|
17
|
+
comment_count: z.ZodNullable<z.ZodNumber>;
|
|
18
|
+
live_status: z.ZodNullable<z.ZodString>;
|
|
19
|
+
thumbnail_url: z.ZodNullable<z.ZodString>;
|
|
20
|
+
tab: z.ZodNullable<z.ZodString>;
|
|
21
|
+
also_in_tabs: z.ZodString;
|
|
22
|
+
detail_at: z.ZodNullable<z.ZodNumber>;
|
|
23
|
+
stored_comments: z.ZodNumber;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
title: string;
|
|
26
|
+
upload_date: string | null;
|
|
27
|
+
view_count: number | null;
|
|
28
|
+
comment_count: number | null;
|
|
29
|
+
live_status: string | null;
|
|
30
|
+
channel_id: string | null;
|
|
31
|
+
tab: string | null;
|
|
32
|
+
video_id: string;
|
|
33
|
+
duration_s: number | null;
|
|
34
|
+
thumbnail_url: string | null;
|
|
35
|
+
also_in_tabs: string;
|
|
36
|
+
detail_at: number | null;
|
|
37
|
+
stored_comments: number;
|
|
38
|
+
}, {
|
|
39
|
+
title: string;
|
|
40
|
+
upload_date: string | null;
|
|
41
|
+
view_count: number | null;
|
|
42
|
+
comment_count: number | null;
|
|
43
|
+
live_status: string | null;
|
|
44
|
+
channel_id: string | null;
|
|
45
|
+
tab: string | null;
|
|
46
|
+
video_id: string;
|
|
47
|
+
duration_s: number | null;
|
|
48
|
+
thumbnail_url: string | null;
|
|
49
|
+
also_in_tabs: string;
|
|
50
|
+
detail_at: number | null;
|
|
51
|
+
stored_comments: number;
|
|
52
|
+
}>;
|
|
53
|
+
export type StoredVideo = z.infer<typeof videoRowSchema>;
|
|
54
|
+
export interface VideoQuery {
|
|
55
|
+
channelId?: string;
|
|
56
|
+
match?: string;
|
|
57
|
+
since?: string;
|
|
58
|
+
until?: string;
|
|
59
|
+
minDurationSeconds?: number;
|
|
60
|
+
detailedOnly?: boolean;
|
|
61
|
+
withComments?: boolean;
|
|
62
|
+
order?: 'newest' | 'oldest' | 'views' | 'comments' | 'catalog';
|
|
63
|
+
limit?: number;
|
|
64
|
+
offset?: number;
|
|
65
|
+
}
|
|
66
|
+
export declare function queryVideos(database: DatabaseSync, query: VideoQuery): StoredVideo[];
|
|
67
|
+
export declare function countVideos(database: DatabaseSync, query: VideoQuery): number;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=video-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-store.d.ts","sourceRoot":"","sources":["../../src/utils/video-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AAEH,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAclB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiDD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,WAAW,EAAE,CAYpF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAQ7E"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { queryRows } from './store-rows.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reading the catalogued videos back.
|
|
5
|
+
*
|
|
6
|
+
* `upload_date` is NULL, never '', for a video only ever seen in a flat
|
|
7
|
+
* listing — flat entries carry no publication date. A date filter therefore
|
|
8
|
+
* excludes those rows rather than silently treating them as ancient.
|
|
9
|
+
*/
|
|
10
|
+
const videoRowSchema = z.object({
|
|
11
|
+
video_id: z.string(),
|
|
12
|
+
channel_id: z.string().nullable(),
|
|
13
|
+
title: z.string(),
|
|
14
|
+
duration_s: z.number().nullable(),
|
|
15
|
+
upload_date: z.string().nullable(),
|
|
16
|
+
view_count: z.number().nullable(),
|
|
17
|
+
comment_count: z.number().nullable(),
|
|
18
|
+
live_status: z.string().nullable(),
|
|
19
|
+
thumbnail_url: z.string().nullable(),
|
|
20
|
+
tab: z.string().nullable(),
|
|
21
|
+
also_in_tabs: z.string(),
|
|
22
|
+
detail_at: z.number().nullable(),
|
|
23
|
+
stored_comments: z.number(),
|
|
24
|
+
});
|
|
25
|
+
function buildWhere(query) {
|
|
26
|
+
const clauses = [];
|
|
27
|
+
const params = [];
|
|
28
|
+
if (query.channelId !== undefined) {
|
|
29
|
+
clauses.push('v.channel_id = ?');
|
|
30
|
+
params.push(query.channelId);
|
|
31
|
+
}
|
|
32
|
+
if (query.match !== undefined && query.match !== '') {
|
|
33
|
+
clauses.push('lower(v.title) LIKE ?');
|
|
34
|
+
params.push(`%${query.match.toLowerCase()}%`);
|
|
35
|
+
}
|
|
36
|
+
if (query.since !== undefined) {
|
|
37
|
+
clauses.push('v.upload_date IS NOT NULL AND v.upload_date >= ?');
|
|
38
|
+
params.push(query.since.replace(/-/g, ''));
|
|
39
|
+
}
|
|
40
|
+
if (query.until !== undefined) {
|
|
41
|
+
clauses.push('v.upload_date IS NOT NULL AND v.upload_date <= ?');
|
|
42
|
+
params.push(query.until.replace(/-/g, ''));
|
|
43
|
+
}
|
|
44
|
+
if (query.minDurationSeconds !== undefined && query.minDurationSeconds > 0) {
|
|
45
|
+
clauses.push('v.duration_s >= ?');
|
|
46
|
+
params.push(query.minDurationSeconds);
|
|
47
|
+
}
|
|
48
|
+
if (query.detailedOnly === true)
|
|
49
|
+
clauses.push('v.detail_at IS NOT NULL');
|
|
50
|
+
if (query.withComments === true) {
|
|
51
|
+
clauses.push('EXISTS (SELECT 1 FROM comment c WHERE c.video_id = v.video_id)');
|
|
52
|
+
}
|
|
53
|
+
return { sql: clauses.length === 0 ? '' : ` WHERE ${clauses.join(' AND ')}`, params };
|
|
54
|
+
}
|
|
55
|
+
const ORDER = {
|
|
56
|
+
// NULLS LAST so an un-dated video sorts as unknown rather than oldest.
|
|
57
|
+
newest: 'v.upload_date DESC NULLS LAST, v.video_id',
|
|
58
|
+
oldest: 'v.upload_date ASC NULLS LAST, v.video_id',
|
|
59
|
+
views: 'v.view_count DESC NULLS LAST, v.video_id',
|
|
60
|
+
comments: 'stored_comments DESC, v.video_id',
|
|
61
|
+
catalog: 'v.catalog_rank ASC NULLS LAST, v.video_id',
|
|
62
|
+
};
|
|
63
|
+
const SELECT = `
|
|
64
|
+
SELECT v.video_id, v.channel_id, v.title, v.duration_s, v.upload_date, v.view_count,
|
|
65
|
+
v.comment_count, v.live_status, v.thumbnail_url, v.tab, v.also_in_tabs, v.detail_at,
|
|
66
|
+
(SELECT COUNT(*) FROM comment c WHERE c.video_id = v.video_id) AS stored_comments
|
|
67
|
+
FROM video v`;
|
|
68
|
+
export function queryVideos(database, query) {
|
|
69
|
+
const where = buildWhere(query);
|
|
70
|
+
return queryRows(database.prepare(`${SELECT}${where.sql} ORDER BY ${ORDER[query.order ?? 'newest']} LIMIT ? OFFSET ?`), videoRowSchema, ...where.params, query.limit ?? 25, query.offset ?? 0);
|
|
71
|
+
}
|
|
72
|
+
export function countVideos(database, query) {
|
|
73
|
+
const where = buildWhere(query);
|
|
74
|
+
const rows = queryRows(database.prepare(`SELECT COUNT(*) AS n FROM video v${where.sql}`), z.object({ n: z.number() }), ...where.params);
|
|
75
|
+
return rows[0]?.n ?? 0;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=video-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-store.js","sourceRoot":"","sources":["../../src/utils/video-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type ListedImage } from './flat-listing.js';
|
|
2
|
-
/** Listings and metadata for channels and playlists. */
|
|
3
2
|
export interface VideoListItem {
|
|
4
3
|
id: string;
|
|
5
4
|
title: string;
|
|
@@ -38,7 +37,15 @@ export interface PlaylistInfo {
|
|
|
38
37
|
url: string;
|
|
39
38
|
description: string;
|
|
40
39
|
}
|
|
41
|
-
export declare function listVideos(urlOrChannel: string, limit?: number): Promise<VideoListItem[]>;
|
|
40
|
+
export declare function listVideos(urlOrChannel: string, limit?: number, start?: number): Promise<VideoListItem[]>;
|
|
41
|
+
/**
|
|
42
|
+
* How many videos a listing says it has, when it says anything.
|
|
43
|
+
*
|
|
44
|
+
* `playlist_count` is populated for playlists and routinely null for channel
|
|
45
|
+
* tabs, so this returns undefined rather than a guess. One extra cheap call —
|
|
46
|
+
* the same `--playlist-items 0` metadata read getPlaylistInfo already does.
|
|
47
|
+
*/
|
|
48
|
+
export declare function playlistTotal(urlOrChannel: string): Promise<number | undefined>;
|
|
42
49
|
export declare function getPlaylistInfo(playlistUrl: string): Promise<PlaylistInfo>;
|
|
43
50
|
export declare function getChannelInfo(channel: string): Promise<ChannelInfo>;
|
|
44
51
|
//# sourceMappingURL=youtube-channel.d.ts.map
|