Skip to content

Commit a1e09e2

Browse files
authored
Merge pull request #1 from TorBox-App/v1
V1
2 parents 40921a3 + 32d29f8 commit a1e09e2

13 files changed

Lines changed: 1355 additions & 1 deletion

File tree

.bunli/commands.gen.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// This file was automatically generated by Bunli.
2+
// You should NOT make any changes in this file as it will be overwritten.
3+
4+
import type { Command, CLI, GeneratedOptionMeta, RegisteredCommands, CommandOptions, GeneratedCommandMeta } from '@bunli/core'
5+
import { createGeneratedHelpers, registerGeneratedStore } from '@bunli/core'
6+
7+
import Test from '../src/commands/test.js'
8+
9+
// Narrow list of command names to avoid typeof-cycles in types
10+
const names = ['test'] as const
11+
type GeneratedNames = typeof names[number]
12+
13+
const modules: Record<GeneratedNames, Command<any>> = {
14+
'test': Test
15+
} as const
16+
17+
const metadata: Record<GeneratedNames, GeneratedCommandMeta> = {
18+
'test': {
19+
name: 'test',
20+
description: 'Tests a video link and simulates start, seek and buffering.',
21+
options: {
22+
'link': { type: 'z.url.optional', required: false, hasDefault: false, description: 'Link to test', short: 'l', schema: {"type":"zod","method":"optional","args":[]}, validator: '(val) => true' }
23+
},
24+
path: './src/commands/test'
25+
}
26+
} as const
27+
28+
export const generated = registerGeneratedStore(createGeneratedHelpers(modules, metadata))
29+
30+
export const commands = generated.commands
31+
export const commandMeta = generated.metadata
32+
33+
export interface GeneratedCLI {
34+
register(cli?: CLI<any>): GeneratedCLI
35+
list(): Array<{
36+
name: GeneratedNames
37+
command: (typeof modules)[GeneratedNames]
38+
metadata: (typeof metadata)[GeneratedNames]
39+
}>
40+
get<Name extends GeneratedNames>(name: Name): (typeof modules)[Name]
41+
getMetadata<Name extends GeneratedNames>(name: Name): (typeof metadata)[Name]
42+
getFlags<Name extends keyof RegisteredCommands & string>(name: Name): CommandOptions<Name>
43+
getFlagsMeta<Name extends GeneratedNames>(name: Name): Record<string, GeneratedOptionMeta>
44+
withCLI(cli: CLI<any>): { execute(name: string, options: unknown): Promise<void> }
45+
}
46+
47+
export const cli: GeneratedCLI = {
48+
register: (cliInstance?: CLI<any>) => { generated.register(cliInstance); return cli },
49+
list: () => generated.list(),
50+
get: <Name extends GeneratedNames>(name: Name) => generated.get(name),
51+
getMetadata: <Name extends GeneratedNames>(name: Name) => generated.getMetadata(name),
52+
getFlags: <Name extends keyof RegisteredCommands & string>(name: Name) => generated.getFlags(name) as CommandOptions<Name>,
53+
getFlagsMeta: <Name extends GeneratedNames>(name: Name) => generated.getFlagsMeta(name),
54+
withCLI: (cliInstance) => generated.withCLI(cliInstance)
55+
}
56+
57+
// Enhanced helper functions
58+
export const listCommands = () => generated.list().map(c => c.name)
59+
export const getCommandApi = <Name extends GeneratedNames>(name: Name) => generated.getMetadata(name)
60+
export const getTypedFlags = <Name extends GeneratedNames>(name: Name) => generated.getFlags(name) as CommandOptions<Name>
61+
export const validateCommand = <Name extends GeneratedNames>(name: Name, flags: Record<string, unknown>) => generated.validateCommand(name, flags)
62+
export const findCommandByName = <Name extends GeneratedNames>(name: Name) => generated.findByName(name)
63+
export const findCommandsByDescription = (searchTerm: string) => generated.findByDescription(searchTerm)
64+
export const getCommandNames = () => generated.getCommandNames()
65+
66+
// Auto-register on import for zero-config usage
67+
export default cli
68+
69+
// Ensure module augmentation happens on import
70+
declare module '@bunli/core' {
71+
// Precise key mapping without typeof cycles
72+
interface RegisteredCommands extends Record<GeneratedNames, Command<any>> {}
73+
}

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,41 @@
11
# video-link-debugger
2-
CLI debugging application which tests any video link, and simulates start, seek, and buffer times for comparison or racing. Open source for transparency.
2+
3+
A CLI built with Bunli
4+
5+
## Installation
6+
7+
```bash
8+
bun install
9+
```
10+
11+
## Development
12+
13+
```bash
14+
bun dev -- [command]
15+
```
16+
17+
## Building
18+
19+
```bash
20+
bun run build
21+
```
22+
23+
## Testing
24+
25+
```bash
26+
bun test
27+
```
28+
29+
## Usage
30+
31+
```bash
32+
video-link-debugger hello --name World
33+
```
34+
35+
## Commands
36+
37+
- `hello` - A simple greeting command
38+
39+
## License
40+
41+
MIT

bun.lock

Lines changed: 418 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunli.config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { defineConfig } from "@bunli/core";
2+
3+
export default defineConfig({
4+
name: "video-link-debugger",
5+
version: "0.1.0",
6+
description: "A CLI built with Bunli",
7+
8+
commands: {
9+
directory: "./src/commands",
10+
},
11+
12+
build: {
13+
entry: "./src/index.ts",
14+
outdir: "./dist",
15+
targets: ["native"],
16+
minify: true,
17+
sourcemap: true,
18+
compress: false,
19+
},
20+
21+
dev: {
22+
watch: true,
23+
inspect: true,
24+
},
25+
26+
test: {
27+
pattern: ["**/*.test.ts", "**/*.spec.ts"],
28+
coverage: true,
29+
watch: false,
30+
},
31+
32+
plugins: [],
33+
});

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "video-link-debugger",
3+
"version": "0.1.0",
4+
"description": "A CLI built with Bunli",
5+
"author": "",
6+
"bin": {
7+
"video-link-debugger": "./dist/index.js"
8+
},
9+
"type": "module",
10+
"scripts": {
11+
"postinstall": "bunli generate",
12+
"dev": "bun run src/index.ts",
13+
"build": "bunli build",
14+
"test": "bun test",
15+
"typecheck": "tsgo --noEmit"
16+
},
17+
"dependencies": {
18+
"@bunli/core": "latest",
19+
"@opentui/core": "^0.2.6"
20+
},
21+
"devDependencies": {
22+
"@bunli/test": "latest",
23+
"@tsconfig/bun": "1.0.10",
24+
"@types/bun": "latest",
25+
"bunli": "^0.9.1",
26+
"typescript": "^5.0.0"
27+
},
28+
"bunli": {
29+
"entry": "./src/index.ts",
30+
"outDir": "./dist"
31+
}
32+
}

src/commands/test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { defineCommand, option } from "@bunli/core";
2+
import { z } from "zod";
3+
import {
4+
createCliRenderer,
5+
BoxRenderable,
6+
TextRenderable,
7+
type CliRenderer,
8+
} from "@opentui/core";
9+
import { getLinkInformation } from "../functions/linkValidation";
10+
import {
11+
getLinkTimings,
12+
type TimingPhase,
13+
type DownloadProgress,
14+
type DownloadResult,
15+
SeekRandomMultipleTimes,
16+
downloadFull,
17+
} from "../functions/downloadFunctions";
18+
import {
19+
PHASES,
20+
renderTable,
21+
renderMultiTable,
22+
linkInfoRows,
23+
timingRows,
24+
seekRows,
25+
downloadRows,
26+
formatBytes,
27+
formatSpeed,
28+
} from "../library/tables";
29+
30+
const MULTI_CONNECTIONS = 4;
31+
const BAR_WIDTH = 40;
32+
33+
function makeProgressBox(renderer: CliRenderer, title: string) {
34+
const box = new BoxRenderable(renderer, {
35+
borderStyle: "rounded",
36+
padding: 1,
37+
title,
38+
});
39+
const bar = new TextRenderable(renderer, { content: "", fg: "#888888" });
40+
const stats = new TextRenderable(renderer, { content: "", fg: "#888888" });
41+
box.add(bar);
42+
box.add(stats);
43+
44+
const update = (state: DownloadProgress) => {
45+
if (state.totalBytes !== null && state.totalBytes > 0) {
46+
const pct = Math.min(1, state.bytes / state.totalBytes);
47+
const filled = Math.floor(pct * BAR_WIDTH);
48+
bar.content = `${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)} ${(pct * 100).toFixed(1)}%`;
49+
} else {
50+
bar.content = "░".repeat(BAR_WIDTH);
51+
}
52+
const total = state.totalBytes !== null ? ` / ${formatBytes(state.totalBytes)}` : "";
53+
stats.content = `${formatBytes(state.bytes)}${total} · ${formatSpeed(state.bytesPerSecond)}`;
54+
};
55+
56+
const finish = (color: string) => {
57+
bar.fg = color;
58+
stats.fg = color;
59+
};
60+
61+
return { box, update, finish };
62+
}
63+
64+
export default defineCommand({
65+
name: "test" as const,
66+
description: "Tests a video link and simulates start, seek and buffering.",
67+
options: {
68+
link: option(z.url().optional(), { description: "Link to test", short: "l" }),
69+
},
70+
handler: async ({ flags, positional }) => {
71+
const link = z.url().parse(positional[0] ?? flags.link);
72+
73+
const linkInfo = await getLinkInformation(link);
74+
75+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
76+
77+
const progressBox = new BoxRenderable(renderer, {
78+
borderStyle: "rounded",
79+
padding: 1,
80+
title: "Measuring",
81+
});
82+
const rows = new Map<TimingPhase, TextRenderable>();
83+
for (const { key, label } of PHASES) {
84+
const row = new TextRenderable(renderer, {
85+
content: `⋯ ${label}`,
86+
fg: "#888888",
87+
});
88+
rows.set(key, row);
89+
progressBox.add(row);
90+
}
91+
renderer.root.add(progressBox);
92+
93+
const results = new Map<TimingPhase, number>();
94+
await getLinkTimings(link, undefined, (phase, ms) => {
95+
results.set(phase, ms);
96+
const row = rows.get(phase);
97+
if (!row) return;
98+
const label = PHASES.find((p) => p.key === phase)?.label ?? phase;
99+
row.content = `✓ ${label.padEnd(16)} ${ms.toFixed(2)} ms`;
100+
row.fg = "#00d787";
101+
});
102+
103+
const seekResults = await SeekRandomMultipleTimes(linkInfo, link, 5);
104+
105+
const single = makeProgressBox(renderer, "Downloading (single connection)");
106+
renderer.root.add(single.box);
107+
const singleResult = await downloadFull(link, {
108+
connections: 1,
109+
size: linkInfo.size ?? undefined,
110+
onProgress: single.update,
111+
});
112+
single.finish(singleResult ? "#00d787" : "#ff5f5f");
113+
114+
let multiResult: DownloadResult | null = null;
115+
const canMulti = !!linkInfo.size && linkInfo.acceptsRanges;
116+
if (canMulti) {
117+
const multi = makeProgressBox(
118+
renderer,
119+
`Downloading (${MULTI_CONNECTIONS} connections)`,
120+
);
121+
renderer.root.add(multi.box);
122+
multiResult = await downloadFull(link, {
123+
connections: MULTI_CONNECTIONS,
124+
size: linkInfo.size ?? undefined,
125+
onProgress: multi.update,
126+
});
127+
multi.finish(multiResult ? "#00d787" : "#ff5f5f");
128+
}
129+
130+
renderer.destroy();
131+
132+
console.log(renderTable("Link Information", linkInfoRows(linkInfo)));
133+
console.log(renderTable("Network Timings", timingRows(results)));
134+
console.log(
135+
renderMultiTable(
136+
"Seek Results",
137+
["#", "Status", "TTFB", "Receive", "Total"],
138+
seekRows(seekResults),
139+
),
140+
);
141+
console.log(
142+
renderMultiTable(
143+
"Download Comparison",
144+
["Mode", "Conns", "Time", "Bytes", "Speed"],
145+
downloadRows([
146+
{ label: "Single", result: singleResult },
147+
...(canMulti
148+
? [{ label: `Multi`, result: multiResult }]
149+
: []),
150+
]),
151+
),
152+
);
153+
},
154+
});

0 commit comments

Comments
 (0)