@wji.ai/cli 0.9.4 → 0.9.5
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/dist/index.js
CHANGED
|
@@ -6106,6 +6106,7 @@ function printChanges2(changes, warnings = []) {
|
|
|
6106
6106
|
}
|
|
6107
6107
|
|
|
6108
6108
|
// src/commands/backreel.ts
|
|
6109
|
+
import { stat as stat6 } from "fs/promises";
|
|
6109
6110
|
import { Command as Command26 } from "commander";
|
|
6110
6111
|
|
|
6111
6112
|
// src/lib/backreel.ts
|
|
@@ -6382,11 +6383,18 @@ async function resolveSource(client, opts, postId, file) {
|
|
|
6382
6383
|
const asset = await client.upload("/assets/v2/upload", file, {});
|
|
6383
6384
|
return { video: asset.id };
|
|
6384
6385
|
}
|
|
6386
|
+
return { vodVid: await uploadVideoToVod(client, file, { postId, title: opts.title }) };
|
|
6387
|
+
}
|
|
6388
|
+
async function uploadVideoToVod(client, file, opts = {}) {
|
|
6389
|
+
const ext = extname2(file).toLowerCase();
|
|
6390
|
+
if (!VIDEO_EXTS.includes(ext)) throw new Error(`${basename6(file)}: videos must be ${VIDEO_EXTS.join(" / ")}`);
|
|
6391
|
+
const size = (await stat5(file)).size;
|
|
6392
|
+
if (size > VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 500 MB`);
|
|
6385
6393
|
const token = await client.post("/backreel/vod/upload-token");
|
|
6386
6394
|
const vid = await uploadToVod(token, file, (step) => info(color.dim(`VOD: ${step}\u2026`)));
|
|
6387
|
-
const claimed = await client.post("/backreel/vod/claim", { vid, postId, title: opts.title?.slice(0, 128) });
|
|
6395
|
+
const claimed = await client.post("/backreel/vod/claim", { vid, postId: opts.postId ?? void 0, title: opts.title?.slice(0, 128) });
|
|
6388
6396
|
info(color.dim(`VOD ${vid}: ${claimed?.status ?? "claimed"}`));
|
|
6389
|
-
return
|
|
6397
|
+
return vid;
|
|
6390
6398
|
}
|
|
6391
6399
|
function printEpisodes(post) {
|
|
6392
6400
|
const rows = [...post.episodes || []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -7937,7 +7945,7 @@ function backreelCommand(globals2) {
|
|
|
7937
7945
|
br.command("create <title>").description("Create a post (always a draft until `backreel publish`)")
|
|
7938
7946
|
).action(async (title, opts) => {
|
|
7939
7947
|
const client = backreelClient(globals2);
|
|
7940
|
-
const body = { kind: "showcase", description: "", ...await postBody(opts), title };
|
|
7948
|
+
const body = { kind: "showcase", description: "", ...await postBody(opts, { client, title }), title };
|
|
7941
7949
|
const created = await client.post(`${POSTS}/create`, body);
|
|
7942
7950
|
render(created, (p) => success(`Created draft "${p.title}" \u2014 ${p.id}`));
|
|
7943
7951
|
});
|
|
@@ -7945,7 +7953,7 @@ function backreelCommand(globals2) {
|
|
|
7945
7953
|
br.command("set <postId>").description("Update a post; only the fields you pass change").option("--title <text>", "title")
|
|
7946
7954
|
).action(async (id, opts) => {
|
|
7947
7955
|
const client = backreelClient(globals2);
|
|
7948
|
-
const body = await postBody(opts);
|
|
7956
|
+
const body = await postBody(opts, { client, postId: id, title: opts.title });
|
|
7949
7957
|
if (opts.title) body.title = opts.title;
|
|
7950
7958
|
if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field flag");
|
|
7951
7959
|
const updated = await client.put(`${POSTS}/list/${id}`, body);
|
|
@@ -8143,9 +8151,20 @@ function backreelCommand(globals2) {
|
|
|
8143
8151
|
return br;
|
|
8144
8152
|
}
|
|
8145
8153
|
function postOptions(cmd) {
|
|
8146
|
-
return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video
|
|
8154
|
+
return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <file|vid|assetId|none>", "trailer: a local video file is uploaded to VOD (\u2264500 MB, any length); vod:<Vid> reuses an upload; a WJI asset id is the old way; none clears").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--on-slate <on|off>", "crowdfunding only: also list it on the Slate, not just the Crowdfunding page (default off)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
|
|
8155
|
+
}
|
|
8156
|
+
async function trailerFields(value, ctx) {
|
|
8157
|
+
if (value === "none") return { trailer: null, trailerVod: null };
|
|
8158
|
+
if (value.startsWith("vod:")) return { trailerVod: { vid: value.slice(4) } };
|
|
8159
|
+
const isFile = await stat6(value).then((s) => s.isFile()).catch(() => false);
|
|
8160
|
+
if (isFile) {
|
|
8161
|
+
if (!ctx.client) throw new Error("--trailer <file> needs a signed-in client");
|
|
8162
|
+
const vid = await uploadVideoToVod(ctx.client, value, { postId: ctx.postId, title: `${ctx.title || "trailer"} \xB7 trailer` });
|
|
8163
|
+
return { trailerVod: { vid } };
|
|
8164
|
+
}
|
|
8165
|
+
return { trailer: value };
|
|
8147
8166
|
}
|
|
8148
|
-
async function postBody(opts) {
|
|
8167
|
+
async function postBody(opts, ctx = {}) {
|
|
8149
8168
|
const body = { ...await readJson(opts.body) || {} };
|
|
8150
8169
|
if (opts.kind) body.kind = oneOf2(opts.kind, KINDS, "--kind");
|
|
8151
8170
|
if (opts.logline !== void 0) {
|
|
@@ -8161,7 +8180,7 @@ async function postBody(opts) {
|
|
|
8161
8180
|
if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
|
|
8162
8181
|
if (opts.cover) body.cover = opts.cover;
|
|
8163
8182
|
if (opts.coverPortrait) body.coverPortrait = opts.coverPortrait;
|
|
8164
|
-
if (opts.trailer) body
|
|
8183
|
+
if (opts.trailer) Object.assign(body, await trailerFields(String(opts.trailer), ctx));
|
|
8165
8184
|
if (opts.media) {
|
|
8166
8185
|
const media = opts.media.filter((m) => m !== "none");
|
|
8167
8186
|
if (media.length > MEDIA_MAX) throw new Error(`--media takes at most ${MEDIA_MAX} items (the web's LookBook limit)`);
|
|
@@ -8201,7 +8220,7 @@ function printPost(p) {
|
|
|
8201
8220
|
["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
|
|
8202
8221
|
["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
|
|
8203
8222
|
["donations", p.donation?.enabled ? "on" : void 0],
|
|
8204
|
-
["trailer", p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
|
|
8223
|
+
["trailer", p.trailerVod?.vid ? `vod ${p.trailerVod.vid} \xB7 ${p.trailerVod.status}${p.trailerDurationSec ? ` (${Math.round(p.trailerDurationSec)}s)` : ""}` : p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
|
|
8205
8224
|
["creator", p.creator?.name],
|
|
8206
8225
|
["likes", p.likeCount],
|
|
8207
8226
|
["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
|
package/package.json
CHANGED
package/skill/backreel/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ wji br list # your posts, drafts included
|
|
|
26
26
|
| kind | `showcase` (episodes, trailer) or `crowdfunding` (goal, supporters) — switching wipes the other side's data | `br create --kind`, `br set --kind` |
|
|
27
27
|
| title / logline / description | logline = one public line ≤200 chars; description = HTML or plain text | `br set --logline`, `-d @file.md` |
|
|
28
28
|
| images | 16:9 cover, 9:16 portrait, LookBook gallery (≤9), credit photos | `br image <file> --slot … --post` |
|
|
29
|
-
| trailer | a video
|
|
29
|
+
| trailer | a video on VOD, ≤500 MB, any length | `br set --trailer <file>` |
|
|
30
30
|
| episodes | ≤30, each with its own viewer tier, hosted on VOD | `br episode …` |
|
|
31
31
|
| credits | cast / crew / creative, ≤30, may link a WJI user or an NPC | `br credit …` |
|
|
32
32
|
| campaign + support | funding type (credits / cash / both), credits goal, cash goal, deadline, tiers, offline supporters | `br fund …` |
|
|
@@ -8,18 +8,22 @@
|
|
|
8
8
|
| count | one | ≤30 |
|
|
9
9
|
| length | no limit | no limit |
|
|
10
10
|
| size | ≤500 MB | ≤500 MB |
|
|
11
|
-
| storage |
|
|
11
|
+
| storage | BytePlus VOD (signed URL per play, ~5 min); older trailers may be a WJI asset | BytePlus VOD (signed URL per play, ~5 min) |
|
|
12
12
|
| gated | no — always public | per episode: public / member / supporter / partner |
|
|
13
13
|
|
|
14
14
|
### Trailer
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
wji
|
|
18
|
-
wji br set <postId> --trailer
|
|
17
|
+
wji br set <postId> --trailer trailer.mp4 # uploads to VOD, ≤500 MB, any length
|
|
18
|
+
wji br set <postId> --trailer vod:<Vid> # reuse a video already in VOD
|
|
19
|
+
wji br set <postId> --trailer none # remove it
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
The file goes straight to Backreel's VOD space (the same flow as episodes), so
|
|
23
|
+
viewers get adaptive streaming and the quality menu. There is no duration cap;
|
|
24
|
+
only files over 500 MB are refused. It plays after transcoding finishes,
|
|
25
|
+
usually within a minute or two (`br get` shows `processing` → `ready`).
|
|
26
|
+
Passing a WJI asset id still works but is the old way.
|
|
23
27
|
|
|
24
28
|
## Uploading episodes
|
|
25
29
|
|