libroadcast-cli 2.4.2 → 2.6.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/.dockerignore +5 -0
- package/README.md +11 -7
- package/dist/cmd/push.js +7 -67
- package/dist/cmd/pushFilterID.js +35 -80
- package/dist/utils/help.js +2 -1
- package/dist/utils/pushTools.js +76 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,6 @@ npm install -g libroadcast-cli
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
```bash
|
|
10
|
-
libroadcast login
|
|
11
|
-
|
|
12
|
-
libroadcast <command> [options]
|
|
13
|
-
```
|
|
14
|
-
|
|
15
9
|
```bash
|
|
16
10
|
Usage: <command> [options]
|
|
17
11
|
|
|
@@ -82,11 +76,12 @@ Commands:
|
|
|
82
76
|
Options:
|
|
83
77
|
--loop <intervalInSeconds> Continuously push the PGN file at the specified interval in seconds.
|
|
84
78
|
|
|
85
|
-
pushFilterID <roundId> <PGNFromPathOrUrl> <FideIds...> [--loop <intervalInSeconds>]
|
|
79
|
+
pushFilterID <roundId> <PGNFromPathOrUrl> <FideIds...> [--loop <intervalInSeconds>] [--firstOngoing]
|
|
86
80
|
Upload a PGN file from a local path or URL to the specified broadcast round, filtering games by FIDE ID.
|
|
87
81
|
Note: The PGN file must be accessible from the provided path or URL.
|
|
88
82
|
Options:
|
|
89
83
|
--loop <intervalInSeconds> Continuously push the PGN file at the specified interval in seconds.
|
|
84
|
+
--firstOngoing Push the first ongoing games in each round.
|
|
90
85
|
|
|
91
86
|
|
|
92
87
|
Examples:
|
|
@@ -117,3 +112,12 @@ Examples:
|
|
|
117
112
|
# Push a PGN file from URL filtering by FIDE IDs in loop mode every 120 seconds
|
|
118
113
|
$ pushFilterID round456 https://example.com/games.pgn 12345 67890 --loop 120
|
|
119
114
|
```
|
|
115
|
+
|
|
116
|
+
### Test Docker Build Locally
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker build -t broadcast-cli .
|
|
120
|
+
|
|
121
|
+
docker run --rm -it broadcast-cli bash
|
|
122
|
+
> libroadcast
|
|
123
|
+
```
|
package/dist/cmd/push.js
CHANGED
|
@@ -1,67 +1,16 @@
|
|
|
1
1
|
import { exit } from "node:process";
|
|
2
|
-
import {
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { client, msgCommonErrorHelp, sleep, checkTokenScopes, packageJson, } from "../utils/commandHandler.js";
|
|
2
|
+
import { msgCommonErrorHelp, sleep, checkTokenScopes, } from "../utils/commandHandler.js";
|
|
5
3
|
import { getBroadcastRound } from "../utils/getInfoBroadcast.js";
|
|
6
4
|
import cl from "../utils/colors.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const res = await client
|
|
10
|
-
.POST("/api/broadcast/round/{broadcastRoundId}/push", {
|
|
11
|
-
params: {
|
|
12
|
-
path: { broadcastRoundId: round.id },
|
|
13
|
-
},
|
|
14
|
-
body: pgn,
|
|
15
|
-
bodySerializer: (body) => body,
|
|
16
|
-
})
|
|
17
|
-
.then((response) => response.data);
|
|
18
|
-
console.log(cl.green(`✓ Successfully pushed PGN for round ${cl.whiteBold(round.id)}.`));
|
|
19
|
-
console.table(res?.games.map((game, i) => {
|
|
20
|
-
return {
|
|
21
|
-
"Game #": i + 1,
|
|
22
|
-
"White Player": game.tags["White"] || "Unknown",
|
|
23
|
-
"Black Player": game.tags["Black"] || "Unknown",
|
|
24
|
-
Result: game.tags["Result"] || "Unknown",
|
|
25
|
-
"Ply Count": game.moves || "Unknown",
|
|
26
|
-
Error: game.error || "None",
|
|
27
|
-
};
|
|
28
|
-
}));
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
console.error(cl.red(`Error pushing PGN for round ${cl.whiteBold(round.id)}:`), error);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const readPGNFromURL = async (pgnURL) => {
|
|
35
|
-
if (pgnURL.startsWith("http://") || pgnURL.startsWith("https://")) {
|
|
36
|
-
const response = await fetch(pgnURL, {
|
|
37
|
-
method: "GET",
|
|
38
|
-
headers: {
|
|
39
|
-
"User-Agent": packageJson.name + "/" + packageJson.version,
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
console.error(cl.red(`Failed to fetch PGN from URL: ${response.statusText}`));
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
const pgnText = await response.text();
|
|
47
|
-
return pgnText;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const resolvedPath = path.resolve(pgnURL);
|
|
51
|
-
const stats = await readFile(resolvedPath, { encoding: "utf-8" }).catch((err) => {
|
|
52
|
-
console.error(cl.red(`Failed to read PGN file: ${err.message}`));
|
|
53
|
-
return undefined;
|
|
54
|
-
});
|
|
55
|
-
if (!stats)
|
|
56
|
-
return undefined;
|
|
57
|
-
return stats.toString();
|
|
58
|
-
}
|
|
59
|
-
};
|
|
5
|
+
import { pushPGN, readPGNFromURL, loopChecker } from "../utils/pushTools.js";
|
|
6
|
+
let lastPGN = "";
|
|
60
7
|
const loop = async (roundInfo, pgnPath, loopTimer) => {
|
|
61
8
|
while (true) {
|
|
62
9
|
const pgnContent = await readPGNFromURL(pgnPath);
|
|
63
|
-
if (pgnContent)
|
|
10
|
+
if (pgnContent && pgnContent !== lastPGN) {
|
|
64
11
|
await pushPGN(roundInfo, pgnContent);
|
|
12
|
+
lastPGN = pgnContent;
|
|
13
|
+
}
|
|
65
14
|
await sleep(loopTimer * 1000);
|
|
66
15
|
}
|
|
67
16
|
};
|
|
@@ -77,16 +26,7 @@ export const pushCommand = async (args) => {
|
|
|
77
26
|
console.error(cl.red("Round not found."));
|
|
78
27
|
exit(1);
|
|
79
28
|
}
|
|
80
|
-
const
|
|
81
|
-
let loopTimer = undefined;
|
|
82
|
-
if (loopArgIndex !== -1 && loopArgIndex + 1 < args.length) {
|
|
83
|
-
const loopTimerStr = args[loopArgIndex + 1];
|
|
84
|
-
loopTimer = parseInt(loopTimerStr, 10);
|
|
85
|
-
if (isNaN(loopTimer) || loopTimer <= 0) {
|
|
86
|
-
console.error(cl.red("Loop timer must be a positive integer."));
|
|
87
|
-
exit(1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
29
|
+
const loopTimer = loopChecker(args);
|
|
90
30
|
if (loopTimer) {
|
|
91
31
|
console.log(cl.green(`Starting loop to push PGN every ${cl.whiteBold(loopTimer.toString())} seconds...`));
|
|
92
32
|
console.log(cl.blue("Press Ctrl+C to stop."));
|
package/dist/cmd/pushFilterID.js
CHANGED
|
@@ -1,40 +1,12 @@
|
|
|
1
1
|
import { exit } from "node:process";
|
|
2
|
-
import {
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { client, msgCommonErrorHelp, sleep, checkTokenScopes, packageJson, } from "../utils/commandHandler.js";
|
|
2
|
+
import { msgCommonErrorHelp, sleep, checkTokenScopes, } from "../utils/commandHandler.js";
|
|
5
3
|
import { parsePgn, makePgn } from "chessops/pgn";
|
|
6
4
|
import { getBroadcastRound } from "../utils/getInfoBroadcast.js";
|
|
7
5
|
import cl from "../utils/colors.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const res = await client
|
|
11
|
-
.POST("/api/broadcast/round/{broadcastRoundId}/push", {
|
|
12
|
-
params: {
|
|
13
|
-
path: { broadcastRoundId: round.id },
|
|
14
|
-
},
|
|
15
|
-
body: pgn,
|
|
16
|
-
bodySerializer: (body) => body,
|
|
17
|
-
})
|
|
18
|
-
.then((response) => response.data);
|
|
19
|
-
console.log(cl.green(`✓ Successfully pushed PGN for round ${cl.whiteBold(round.id)}.`));
|
|
20
|
-
console.table(res?.games.map((game, i) => {
|
|
21
|
-
return {
|
|
22
|
-
"Game #": i + 1,
|
|
23
|
-
"White Player": game.tags["White"] || "Unknown",
|
|
24
|
-
"Black Player": game.tags["Black"] || "Unknown",
|
|
25
|
-
Result: game.tags["Result"] || "Unknown",
|
|
26
|
-
"Ply Count": game.moves ?? "Unknown",
|
|
27
|
-
Error: game.error || "None",
|
|
28
|
-
};
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
console.error(cl.red(`Error pushing PGN for round ${cl.whiteBold(round.id)}:`), error);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const filterPgnByIds = (pgn, filterIds) => {
|
|
6
|
+
import { loopChecker, pushPGN, readPGNFromURL } from "../utils/pushTools.js";
|
|
7
|
+
const filterPgnByIds = (pgn, filterIds, firstOngoing) => {
|
|
36
8
|
const parsed = parsePgn(pgn);
|
|
37
|
-
|
|
9
|
+
let filteredGames = parsed.filter((game) => {
|
|
38
10
|
const whiteFideId = game.headers.get("WhiteFideId");
|
|
39
11
|
const blackFideId = game.headers.get("BlackFideId");
|
|
40
12
|
if (filterIds.length > 1) {
|
|
@@ -54,46 +26,32 @@ const filterPgnByIds = (pgn, filterIds) => {
|
|
|
54
26
|
filterIds.includes(parseInt(blackFideId || "", 10)));
|
|
55
27
|
}
|
|
56
28
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const response = await fetch(pgnURL, {
|
|
62
|
-
method: "GET",
|
|
63
|
-
headers: {
|
|
64
|
-
"User-Agent": packageJson.name + "/" + packageJson.version,
|
|
65
|
-
},
|
|
29
|
+
if (firstOngoing) {
|
|
30
|
+
const ongoingGames = filteredGames.filter((game) => {
|
|
31
|
+
const result = game.headers.get("Result");
|
|
32
|
+
return !result || result === "*";
|
|
66
33
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
const pgnText = await response.text();
|
|
72
|
-
const filteredPgn = filterPgnByIds(pgnText, filterIds);
|
|
73
|
-
if (!filteredPgn) {
|
|
74
|
-
console.error(cl.red(`No games found matching the provided filter IDs.`));
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
return filteredPgn;
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const resolvedPath = path.resolve(pgnURL);
|
|
81
|
-
const stats = await readFile(resolvedPath, { encoding: "utf-8" }).catch((err) => {
|
|
82
|
-
console.error(cl.red(`Failed to read PGN file: ${err.message}`));
|
|
83
|
-
return undefined;
|
|
34
|
+
const gamesFinished = filteredGames.filter((game) => {
|
|
35
|
+
const result = game.headers.get("Result");
|
|
36
|
+
return result && result !== "*";
|
|
84
37
|
});
|
|
85
|
-
|
|
86
|
-
return undefined;
|
|
87
|
-
return stats.toString();
|
|
38
|
+
filteredGames = [...ongoingGames, ...gamesFinished];
|
|
88
39
|
}
|
|
40
|
+
return filteredGames.map((game) => makePgn(game)).join("\n\n");
|
|
89
41
|
};
|
|
90
42
|
let lastPGN = "";
|
|
91
|
-
const loop = async (roundInfo, pgnPath, loopTimer, filterIds) => {
|
|
43
|
+
const loop = async (roundInfo, pgnPath, loopTimer, filterIds, firstOngoing) => {
|
|
92
44
|
while (true) {
|
|
93
|
-
const pgnContent = await readPGNFromURL(pgnPath
|
|
94
|
-
if (pgnContent
|
|
95
|
-
|
|
96
|
-
|
|
45
|
+
const pgnContent = await readPGNFromURL(pgnPath);
|
|
46
|
+
if (!pgnContent) {
|
|
47
|
+
console.error(cl.red(`Failed to read PGN content. Retrying in ${loopTimer} seconds...`));
|
|
48
|
+
await sleep(loopTimer * 1000);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const filteredPgn = filterPgnByIds(pgnContent, filterIds, firstOngoing);
|
|
52
|
+
if (filteredPgn && filteredPgn !== lastPGN) {
|
|
53
|
+
await pushPGN(roundInfo, filteredPgn);
|
|
54
|
+
lastPGN = filteredPgn;
|
|
97
55
|
}
|
|
98
56
|
await sleep(loopTimer * 1000);
|
|
99
57
|
}
|
|
@@ -114,24 +72,21 @@ export const pushFilterIDCommand = async (args) => {
|
|
|
114
72
|
console.error(cl.red("Round not found."));
|
|
115
73
|
exit(1);
|
|
116
74
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
if (loopArgIndex !== -1 && loopArgIndex + 1 < args.length) {
|
|
120
|
-
const loopTimerStr = args[loopArgIndex + 1];
|
|
121
|
-
loopTimer = parseInt(loopTimerStr, 10);
|
|
122
|
-
if (isNaN(loopTimer) || loopTimer <= 0) {
|
|
123
|
-
console.error(cl.red("Loop timer must be a positive integer."));
|
|
124
|
-
exit(1);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
75
|
+
const firstOngoing = args.includes("--firstOngoing");
|
|
76
|
+
const loopTimer = loopChecker(args);
|
|
127
77
|
if (loopTimer) {
|
|
128
78
|
console.log(cl.green(`Starting loop to push PGN every ${cl.whiteBold(loopTimer.toString())} seconds...`));
|
|
129
79
|
console.log(cl.blue("Press Ctrl+C to stop."));
|
|
130
|
-
await loop(roundInfo, pgnPath, loopTimer, filterIds);
|
|
80
|
+
await loop(roundInfo, pgnPath, loopTimer, filterIds, firstOngoing);
|
|
131
81
|
}
|
|
132
82
|
else {
|
|
133
|
-
const pgnContent = await readPGNFromURL(pgnPath
|
|
134
|
-
if (pgnContent)
|
|
135
|
-
|
|
83
|
+
const pgnContent = await readPGNFromURL(pgnPath);
|
|
84
|
+
if (!pgnContent) {
|
|
85
|
+
console.error(cl.red(`Failed to read PGN content.`));
|
|
86
|
+
exit(1);
|
|
87
|
+
}
|
|
88
|
+
const filteredPgn = filterPgnByIds(pgnContent, filterIds, firstOngoing);
|
|
89
|
+
if (filteredPgn)
|
|
90
|
+
await pushPGN(roundInfo, filteredPgn);
|
|
136
91
|
}
|
|
137
92
|
};
|
package/dist/utils/help.js
CHANGED
|
@@ -76,11 +76,12 @@ const helpPush = [
|
|
|
76
76
|
` --loop <intervalInSeconds> ${cl.gray("Continuously push the PGN file at the specified interval in seconds.")}`,
|
|
77
77
|
].join("\n");
|
|
78
78
|
const helpPushFilterID = [
|
|
79
|
-
` ${cl.underItalic("pushFilterID <roundId> <PGNFromPathOrUrl> <FideIds...> [--loop <intervalInSeconds>]")}`,
|
|
79
|
+
` ${cl.underItalic("pushFilterID <roundId> <PGNFromPathOrUrl> <FideIds...> [--loop <intervalInSeconds>] [--firstOngoing]")}`,
|
|
80
80
|
` ${cl.gray("Upload a PGN file from a local path or URL to the specified broadcast round, filtering games by FIDE ID.")}`,
|
|
81
81
|
` ${cl.bold("Note:")} ${cl.gray("The PGN file must be accessible from the provided path or URL.")}`,
|
|
82
82
|
` ${cl.bold("Options:")}`,
|
|
83
83
|
` --loop <intervalInSeconds> ${cl.gray("Continuously push the PGN file at the specified interval in seconds.")}`,
|
|
84
|
+
` --firstOngoing ${cl.gray("Push the first ongoing games in each round.")}`,
|
|
84
85
|
].join("\n");
|
|
85
86
|
const msg = [
|
|
86
87
|
`${cl.boldYellow("Usage:")} ${cl.underItalic("<command> [options]")}`,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { client, packageJson } from "./commandHandler.js";
|
|
2
|
+
import cl from "./colors.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { exit } from "node:process";
|
|
6
|
+
export const pushPGN = async (round, pgn) => {
|
|
7
|
+
try {
|
|
8
|
+
const res = await client
|
|
9
|
+
.POST("/api/broadcast/round/{broadcastRoundId}/push", {
|
|
10
|
+
params: {
|
|
11
|
+
path: { broadcastRoundId: round.id },
|
|
12
|
+
},
|
|
13
|
+
body: pgn,
|
|
14
|
+
bodySerializer: (body) => body,
|
|
15
|
+
})
|
|
16
|
+
.then((response) => response.data);
|
|
17
|
+
console.log(cl.green(`✓ Successfully pushed PGN for round ${cl.whiteBold(round.id)}.`));
|
|
18
|
+
console.table(res?.games
|
|
19
|
+
.map((game, i) => {
|
|
20
|
+
return {
|
|
21
|
+
id: i + 1,
|
|
22
|
+
"White Player": game.tags["White"] || "Unknown",
|
|
23
|
+
"Black Player": game.tags["Black"] || "Unknown",
|
|
24
|
+
Result: game.tags["Result"] || "Unknown",
|
|
25
|
+
"Ply Count": game.moves ?? "Unknown",
|
|
26
|
+
Error: game.error || "None",
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.reduce((acc, { id, ...rest }) => {
|
|
30
|
+
acc[id] = rest;
|
|
31
|
+
return acc;
|
|
32
|
+
}, {}));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(cl.red(`Error pushing PGN for round ${cl.whiteBold(round.id)}:`), error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const readPGNFromURL = async (pgnURL) => {
|
|
39
|
+
if (pgnURL.startsWith("http://") || pgnURL.startsWith("https://")) {
|
|
40
|
+
const response = await fetch(pgnURL, {
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers: {
|
|
43
|
+
"User-Agent": packageJson.name + "/" + packageJson.version,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
console.error(cl.red(`Failed to fetch PGN from URL: ${response.statusText}`));
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const pgnText = await response.text();
|
|
51
|
+
return pgnText;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const resolvedPath = path.resolve(pgnURL);
|
|
55
|
+
const stats = await readFile(resolvedPath, { encoding: "utf-8" }).catch((err) => {
|
|
56
|
+
console.error(cl.red(`Failed to read PGN file: ${err.message}`));
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
if (!stats)
|
|
60
|
+
return undefined;
|
|
61
|
+
return stats.toString();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export const loopChecker = (args) => {
|
|
65
|
+
const loopArgIndex = args.findIndex((arg) => arg === "--loop");
|
|
66
|
+
let loopTimer = undefined;
|
|
67
|
+
if (loopArgIndex !== -1 && loopArgIndex + 1 < args.length) {
|
|
68
|
+
const loopTimerStr = args[loopArgIndex + 1];
|
|
69
|
+
loopTimer = parseInt(loopTimerStr, 10);
|
|
70
|
+
if (isNaN(loopTimer) || loopTimer <= 0) {
|
|
71
|
+
console.error(cl.red("Loop timer must be a positive integer."));
|
|
72
|
+
exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return loopTimer;
|
|
76
|
+
};
|