@wji.ai/cli 0.9.2 → 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 +132 -35
- package/package.json +1 -1
- package/skill/backreel/SKILL.md +29 -15
- package/skill/backreel/reference/crowdfunding.md +61 -30
- package/skill/backreel/reference/episodes.md +9 -5
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));
|
|
@@ -6955,7 +6963,11 @@ function enquiryCommand(globals2) {
|
|
|
6955
6963
|
|
|
6956
6964
|
// src/commands/backreel-fund.ts
|
|
6957
6965
|
import { Command as Command22 } from "commander";
|
|
6958
|
-
var
|
|
6966
|
+
var CASH_CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
|
|
6967
|
+
var CURRENCIES = ["credits", ...CASH_CURRENCIES];
|
|
6968
|
+
var MODES = ["credits", "cash", "both"];
|
|
6969
|
+
var hasCredits = (mode) => mode === "credits" || mode === "both";
|
|
6970
|
+
var fmt = (n) => Math.round(Number(n) || 0).toLocaleString("en-US");
|
|
6959
6971
|
var MAX_TIERS = 10;
|
|
6960
6972
|
var num = (value, flag, min = 0) => {
|
|
6961
6973
|
const n = Number(value);
|
|
@@ -6975,6 +6987,10 @@ function itemIndex(n, count, what) {
|
|
|
6975
6987
|
}
|
|
6976
6988
|
function campaignDto(c) {
|
|
6977
6989
|
return {
|
|
6990
|
+
// fundingMode / currency / creditGoal: the server keeps the stored value when a field is left out
|
|
6991
|
+
fundingMode: c?.fundingMode ?? void 0,
|
|
6992
|
+
currency: c?.fundingMode && c.fundingMode !== "credits" ? c?.currency ?? void 0 : void 0,
|
|
6993
|
+
creditGoal: c?.creditGoal ?? void 0,
|
|
6978
6994
|
goalAmount: c?.goalAmount ?? 0,
|
|
6979
6995
|
offlineRaisedAmount: c?.offlineRaisedAmount ?? 0,
|
|
6980
6996
|
deadline: c?.deadline ?? null,
|
|
@@ -7004,22 +7020,40 @@ var put = (client, id, body) => client.put(`${POSTS}/list/${id}`, body);
|
|
|
7004
7020
|
function printFund(post) {
|
|
7005
7021
|
const c = post.campaign;
|
|
7006
7022
|
const d = post.donation || {};
|
|
7007
|
-
const
|
|
7023
|
+
const mode = c?.fundingMode || "cash";
|
|
7024
|
+
const cur = (c?.currency || d.currency !== "credits" && d.currency || "hkd").toUpperCase();
|
|
7008
7025
|
if (c) {
|
|
7009
|
-
const
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7026
|
+
const rows = [["funding", mode]];
|
|
7027
|
+
if (hasCredits(mode)) {
|
|
7028
|
+
const pct = c.creditGoal ? ` (${Math.round((c.creditsRaised || 0) / c.creditGoal * 100)}%)` : "";
|
|
7029
|
+
rows.push(
|
|
7030
|
+
["credits goal", `${fmt(c.creditGoal)} credits`],
|
|
7031
|
+
["credits raised", `${fmt(c.creditsRaised)} credits${pct} from ${c.creditBackerCount ?? 0} backer(s)`],
|
|
7032
|
+
["outcome", c.outcome === "funded" ? color.green("funded") : c.outcome === "failed" ? color.red("failed \u2014 refunded") : "raising"]
|
|
7033
|
+
);
|
|
7034
|
+
}
|
|
7035
|
+
if (mode !== "credits") {
|
|
7036
|
+
const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
|
|
7037
|
+
rows.push(
|
|
7038
|
+
["cash goal", `${c.goalAmount} ${cur}`],
|
|
7039
|
+
["cash raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`]
|
|
7040
|
+
);
|
|
7041
|
+
}
|
|
7042
|
+
if (c.production) {
|
|
7043
|
+
rows.push([
|
|
7044
|
+
"production",
|
|
7045
|
+
`${c.production.status} \u2014 ${c.production.milestonesApproved}/${c.production.milestonesTotal} milestones (contract: \`wji br contract get ${c.production.contractJobId}\`)`
|
|
7046
|
+
]);
|
|
7047
|
+
}
|
|
7048
|
+
rows.push(["supporters", c.supporterCount], ["deadline", c.deadline || color.dim("none")]);
|
|
7049
|
+
detail(rows);
|
|
7016
7050
|
process.stdout.write("\n");
|
|
7017
7051
|
} else {
|
|
7018
7052
|
info(color.dim(`${post.kind} post \u2014 no campaign (goal / offline supporters are crowdfunding-only); donations still work`));
|
|
7019
7053
|
}
|
|
7020
7054
|
detail([
|
|
7021
7055
|
["donations", d.enabled ? color.green("on") : "off"],
|
|
7022
|
-
["currency", (d.currency || "hkd").toUpperCase()],
|
|
7056
|
+
["currency", d.currency === "credits" ? "WJI credits" : (d.currency || "hkd").toUpperCase()],
|
|
7023
7057
|
["minimum", d.minAmount ?? 0]
|
|
7024
7058
|
]);
|
|
7025
7059
|
if ((d.tiers || []).length) {
|
|
@@ -7048,13 +7082,17 @@ function fundCommand(globals2) {
|
|
|
7048
7082
|
const post = await loadPost(client, postId);
|
|
7049
7083
|
render({ campaign: post.campaign, donation: post.donation, kind: post.kind }, () => printFund(post));
|
|
7050
7084
|
});
|
|
7051
|
-
fund.command("set <postId>").description("
|
|
7085
|
+
fund.command("set <postId>").description("Funding type, goals, deadline and donation switches; only what you pass changes").option("--mode <mode>", `${MODES.join(" | ")} \u2014 credits: any creator, all-or-nothing at the deadline; cash / both need Cash fundraiser verification`).option("--credit-goal <credits>", "credits goal (credits / both campaigns)").option("--goal <amount>", "cash goal (cash / both campaigns)").option("--cash-currency <code>", `${CASH_CURRENCIES.join(" | ")} \u2014 currency of the cash goal`).option("--offline-raised <amount>", "money raised outside Backreel, added to the progress bar").option("--deadline <date>", 'ISO date (e.g. 2026-12-31); "none" clears \u2014 donations stop after it').option("--donations <on|off>", "accept support (credits pledges, or legacy Stripe donations)").option("--currency <code>", `${CURRENCIES.join(" | ")} \u2014 credits = WJI credits; can't leave credits once someone pledged`).option("--min <amount>", "minimum donation").action(async (postId, opts) => {
|
|
7052
7086
|
const client = backreelClient(globals2);
|
|
7053
7087
|
const post = await loadPost(client, postId);
|
|
7054
7088
|
const body = {};
|
|
7055
|
-
|
|
7089
|
+
const campaignFlags = [opts.goal, opts.offlineRaised, opts.deadline, opts.mode, opts.creditGoal, opts.cashCurrency];
|
|
7090
|
+
if (campaignFlags.some((v) => v !== void 0)) {
|
|
7056
7091
|
requireCampaign(post);
|
|
7057
7092
|
const campaign = campaignDto(post.campaign);
|
|
7093
|
+
if (opts.mode !== void 0) campaign.fundingMode = oneOf2(String(opts.mode).toLowerCase(), MODES, "--mode");
|
|
7094
|
+
if (opts.creditGoal !== void 0) campaign.creditGoal = Math.round(num(opts.creditGoal, "--credit-goal", 1));
|
|
7095
|
+
if (opts.cashCurrency !== void 0) campaign.currency = oneOf2(String(opts.cashCurrency).toLowerCase(), CASH_CURRENCIES, "--cash-currency");
|
|
7058
7096
|
if (opts.goal !== void 0) campaign.goalAmount = num(opts.goal, "--goal");
|
|
7059
7097
|
if (opts.offlineRaised !== void 0) campaign.offlineRaisedAmount = num(opts.offlineRaised, "--offline-raised");
|
|
7060
7098
|
if (opts.deadline !== void 0) {
|
|
@@ -7066,9 +7104,12 @@ function fundCommand(globals2) {
|
|
|
7066
7104
|
}
|
|
7067
7105
|
}
|
|
7068
7106
|
body.campaign = campaign;
|
|
7107
|
+
if (hasCredits(campaign.fundingMode) && opts.currency === void 0 && opts.donations === void 0) {
|
|
7108
|
+
body.donation = { ...donationDto(post.donation), enabled: true, currency: "credits" };
|
|
7109
|
+
}
|
|
7069
7110
|
}
|
|
7070
7111
|
if (opts.donations !== void 0 || opts.currency !== void 0 || opts.min !== void 0) {
|
|
7071
|
-
const donation = donationDto(post.donation);
|
|
7112
|
+
const donation = body.donation || donationDto(post.donation);
|
|
7072
7113
|
if (opts.donations !== void 0) donation.enabled = oneOf2(String(opts.donations).toLowerCase(), ["on", "off"], "--donations") === "on";
|
|
7073
7114
|
if (opts.currency !== void 0) donation.currency = oneOf2(String(opts.currency).toLowerCase(), CURRENCIES, "--currency");
|
|
7074
7115
|
if (opts.min !== void 0) donation.minAmount = Math.round(num(opts.min, "--min"));
|
|
@@ -7185,6 +7226,46 @@ function fundCommand(globals2) {
|
|
|
7185
7226
|
if ((r.payouts || []).length) info(color.dim(`${r.payouts.length} payout(s) \u2014 payouts and refunds are handled by Backreel staff`));
|
|
7186
7227
|
});
|
|
7187
7228
|
});
|
|
7229
|
+
fund.command("pledges <postId>").description("Credits backers of your post (anonymous backers stay hidden)").option("--limit <n>", "how many (\u2264200)", "100").action(async (postId, opts) => {
|
|
7230
|
+
const client = backreelClient(globals2);
|
|
7231
|
+
const r = await client.get(`/backreel/pledge/post/${postId}/manage`, { limit: Number(opts.limit) });
|
|
7232
|
+
render(r, () => {
|
|
7233
|
+
detail([
|
|
7234
|
+
["credits", fmt(r.stats?.credits ?? 0)],
|
|
7235
|
+
["backers", r.stats?.backerCount ?? 0],
|
|
7236
|
+
["pledges", r.stats?.pledgeCount ?? 0]
|
|
7237
|
+
]);
|
|
7238
|
+
process.stdout.write("\n");
|
|
7239
|
+
table(r.data || [], [
|
|
7240
|
+
{ header: "when", value: (p) => relativeTime(p.createdAt) },
|
|
7241
|
+
{ header: "backer", value: (p) => p.isAnonymous ? color.dim("anonymous") : truncate(p.backer?.name, 20) },
|
|
7242
|
+
{ header: "credits", value: (p) => fmt(p.credits) },
|
|
7243
|
+
{ header: "tier", value: (p) => truncate(p.tier?.title, 16) },
|
|
7244
|
+
{ header: "status", value: (p) => p.status },
|
|
7245
|
+
{ header: "message", value: (p) => truncate(p.message, 30) }
|
|
7246
|
+
]);
|
|
7247
|
+
});
|
|
7248
|
+
});
|
|
7249
|
+
fund.command("back <postId> <credits>").description("Back someone's post with your WJI credits \u2014 campaigns hold them until the deadline (refunded if the goal is missed); showcase works send them to the creator").option("--tier <n>", "pick tier n (see `fund show`)").option("-m, --message <text>", "a note to the creator (\u2264500)").option("--anonymous", "hide your name on the supporter wall").option("-y, --yes", "skip confirmation").action(async (postId, credits, opts) => {
|
|
7250
|
+
const client = backreelClient(globals2);
|
|
7251
|
+
const post = await client.get(`${POSTS}/list/${postId}`);
|
|
7252
|
+
if (post.donation?.currency !== "credits" || !post.donation?.enabled) {
|
|
7253
|
+
throw new Error(`"${post.title}" doesn't take credits support`);
|
|
7254
|
+
}
|
|
7255
|
+
const tiers = post.donation.tiers || [];
|
|
7256
|
+
const tier2 = opts.tier ? tiers[itemIndex(opts.tier, tiers.length, "Tier")] : null;
|
|
7257
|
+
const amount = Math.round(num(credits, "<credits>", 1));
|
|
7258
|
+
const where = post.kind === "crowdfunding" ? "held by the campaign until its deadline" : "sent to the creator";
|
|
7259
|
+
if (!await confirm(`Back "${post.title}" with ${fmt(amount)} credits (${where})?`, opts.yes)) return info("Aborted");
|
|
7260
|
+
const pledge = await client.post("/backreel/pledge", {
|
|
7261
|
+
post: postId,
|
|
7262
|
+
credits: amount,
|
|
7263
|
+
tierId: tier2?.id,
|
|
7264
|
+
message: opts.message,
|
|
7265
|
+
isAnonymous: !!opts.anonymous
|
|
7266
|
+
});
|
|
7267
|
+
render(pledge, () => success(`Backed "${post.title}" with ${fmt(pledge.credits)} credits \u2014 ${pledge.status}`));
|
|
7268
|
+
});
|
|
7188
7269
|
return fund;
|
|
7189
7270
|
}
|
|
7190
7271
|
|
|
@@ -7211,12 +7292,12 @@ function parseDay(value, flag) {
|
|
|
7211
7292
|
return d.toISOString();
|
|
7212
7293
|
}
|
|
7213
7294
|
var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7214
|
-
var
|
|
7295
|
+
var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7215
7296
|
function budgetText(j) {
|
|
7216
7297
|
if (j.budgetNegotiable && j.budgetMin == null && j.budgetMax == null) return "negotiable";
|
|
7217
|
-
const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${
|
|
7298
|
+
const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt2(n)}`;
|
|
7218
7299
|
const cash = j.budgetMin != null && j.budgetMax != null && j.budgetMin !== j.budgetMax ? `${cur(j.budgetMin)}\u2013${cur(j.budgetMax)}` : j.budgetMax != null || j.budgetMin != null ? cur(j.budgetMax ?? j.budgetMin) : "";
|
|
7219
|
-
return [cash, j.budgetCredits ? `${
|
|
7300
|
+
return [cash, j.budgetCredits ? `${fmt2(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
|
|
7220
7301
|
}
|
|
7221
7302
|
function jobOptions2(cmd) {
|
|
7222
7303
|
return cmd.option("--category <c>", `one of ${CATEGORIES2.join(", ")}`).option("--brief <text|@file>", "what you need (max 5000)").option("--deliverables <text|@file>", "what gets delivered (max 2000)").option("--budget <min[-max]>", 'cash budget in --currency, e.g. 8000 or 5000-10000; "none" clears').option("--currency <code>", "currency of --budget (converted to USD by the server)", "USD").option("--negotiable", "budget is negotiable").option("--credits <n>", "WJI credits budget on top of cash; 0 clears").option("--duration <len>", "running time: 90, 3m, 1h30m, 2:30").option("--resolution <r>", `delivery resolution: ${RESOLUTIONS.join(", ")}`).option("--upscale", "upscaling to the resolution is acceptable").option("--no-upscale", "must be generated at the resolution").option("--model <name...>", "required WJI models (max 10)").option("--deadline <date>", "bid deadline, YYYY-MM-DD (\u2264 90 days)").option("--delivery <date>", 'delivery date, YYYY-MM-DD; "none" clears').option("--location <text>", "location, if any").option("--link <url...>", "reference links (max 5)").option("--verified-only", "only verified creators can bid").option("--open-to-all", "anyone can bid (undo --verified-only)").option("--org <orgId>", 'post as one of your organizations; "none" = personal');
|
|
@@ -7294,8 +7375,8 @@ function printBids(rows) {
|
|
|
7294
7375
|
{ header: "job", value: (b) => truncate(b.job?.title, 24) },
|
|
7295
7376
|
{ header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
|
|
7296
7377
|
{ header: "status", value: (b) => b.status },
|
|
7297
|
-
{ header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${
|
|
7298
|
-
{ header: "credits", value: (b) =>
|
|
7378
|
+
{ header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt2(b.price)}` },
|
|
7379
|
+
{ header: "credits", value: (b) => fmt2(b.creditsPrice) },
|
|
7299
7380
|
{ header: "days", value: (b) => b.timelineDays ?? "" },
|
|
7300
7381
|
{ header: "pitch", value: (b) => truncate(b.pitch, 30) }
|
|
7301
7382
|
]);
|
|
@@ -7463,7 +7544,7 @@ var KIND_NAMES = {
|
|
|
7463
7544
|
draft: "Draft"
|
|
7464
7545
|
};
|
|
7465
7546
|
var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7466
|
-
var
|
|
7547
|
+
var fmt3 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7467
7548
|
var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
|
|
7468
7549
|
async function loadContract(client, ref) {
|
|
7469
7550
|
try {
|
|
@@ -7506,12 +7587,12 @@ function printContract(c) {
|
|
|
7506
7587
|
["you are", c.mySide],
|
|
7507
7588
|
["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
|
|
7508
7589
|
["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
|
|
7509
|
-
["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${
|
|
7590
|
+
["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt3(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
|
|
7510
7591
|
["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
|
|
7511
7592
|
["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
|
|
7512
7593
|
[
|
|
7513
7594
|
"project credits",
|
|
7514
|
-
e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${
|
|
7595
|
+
e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt3(e.settledCreator)} to creator, ${fmt3(e.settledPoster)} to poster` : `${fmt3(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt3(e.topupReturned)} top-up returned` : ""}` : `${fmt3(Math.max(0, e.left))} left of ${fmt3(e.budget ?? e.funded)} (used ${fmt3(e.spent)}${e.toppedUp ? `, creator topped up ${fmt3(e.toppedUp)}` : ""}; overdraft ${fmt3(e.overdraft)})` : void 0
|
|
7515
7596
|
],
|
|
7516
7597
|
["workspace", e?.workspaceId],
|
|
7517
7598
|
["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
|
|
@@ -7524,7 +7605,7 @@ function printContract(c) {
|
|
|
7524
7605
|
{ header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
|
|
7525
7606
|
{ header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
|
|
7526
7607
|
{ header: "cash", value: (m) => usd2(m.cashUsd) },
|
|
7527
|
-
...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) =>
|
|
7608
|
+
...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt3(m.credits) }] : [],
|
|
7528
7609
|
{ header: "status", value: (m) => m.status },
|
|
7529
7610
|
{ header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
|
|
7530
7611
|
{
|
|
@@ -7535,14 +7616,14 @@ function printContract(c) {
|
|
|
7535
7616
|
const pending = (c.changeRequests || []).find((r) => r.status === "pending");
|
|
7536
7617
|
if (pending) {
|
|
7537
7618
|
info("");
|
|
7538
|
-
info(`${color.yellow("Change request waiting")} ${pending.id}: +${
|
|
7619
|
+
info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt3(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
|
|
7539
7620
|
}
|
|
7540
7621
|
const d = c.dispute;
|
|
7541
7622
|
if (d && d.status !== "withdrawn") {
|
|
7542
7623
|
info("");
|
|
7543
7624
|
info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
|
|
7544
7625
|
for (const st of d.statements || []) info(color.dim(` ${st.side} \xB7 ${relativeTime(st.at)}: ${truncate(st.text, 80)}`));
|
|
7545
|
-
if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${
|
|
7626
|
+
if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt3(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
|
|
7546
7627
|
}
|
|
7547
7628
|
}
|
|
7548
7629
|
function parsePayment(spec, perMilestone) {
|
|
@@ -7645,9 +7726,9 @@ function contractCommand(globals2) {
|
|
|
7645
7726
|
const client = backreelClient(globals2);
|
|
7646
7727
|
const c = await loadContract(client, ref);
|
|
7647
7728
|
if (c.myCredits != null && c.myCredits < c.credits) {
|
|
7648
|
-
throw new Error(`You have ${
|
|
7729
|
+
throw new Error(`You have ${fmt3(c.myCredits)} general credits; this contract needs ${fmt3(c.credits)}. Top up on WJI first.`);
|
|
7649
7730
|
}
|
|
7650
|
-
if (!await confirm(`Add ${
|
|
7731
|
+
if (!await confirm(`Add ${fmt3(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
|
|
7651
7732
|
await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
|
|
7652
7733
|
});
|
|
7653
7734
|
ct.command("submit <contract> <payment>").description("Creator: submit a payment (milestone) for review").option("--note <text|@file>", "what you are delivering").option("--link <url...>", "links to the delivery (max 5)").action(async (ref, n, opts) => {
|
|
@@ -7695,7 +7776,7 @@ function contractCommand(globals2) {
|
|
|
7695
7776
|
ct.command("topup <contract> <credits>").description("Creator: add your own credits to the project (unused ones come back to you)").action(async (ref, credits) => {
|
|
7696
7777
|
const n = Math.floor(Number(credits));
|
|
7697
7778
|
if (!(n > 0)) throw new Error("credits must be a positive number");
|
|
7698
|
-
await run(ref, "/topup", { credits: n }, (u) => `Topped up ${
|
|
7779
|
+
await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt3(n)} \u2014 ${fmt3(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
|
|
7699
7780
|
});
|
|
7700
7781
|
const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
|
|
7701
7782
|
change.command("request <contract>", { isDefault: true }).description("Creator: propose extra credits and/or cash").option("--credits <n>", "extra credits", "0").option("--cash <n>", "extra cash in USD").option("--reason <r>", `${CR_REASONS.join(", ")}`, "scope").option("--payment <n>", "the payment it relates to (for extra revisions)").requiredOption("--note <text|@file>", "what changed").action(async (ref, opts) => {
|
|
@@ -7848,6 +7929,7 @@ function backreelCommand(globals2) {
|
|
|
7848
7929
|
{ header: "publish", value: (p2) => p2.publishStatus },
|
|
7849
7930
|
{ header: "project", value: (p2) => p2.projectStatus },
|
|
7850
7931
|
{ header: "credits", value: (p2) => (p2.credits || []).length },
|
|
7932
|
+
{ header: "interested", value: (p2) => p2.interestCount ?? 0 },
|
|
7851
7933
|
{ header: "updated", value: (p2) => relativeTime(p2.updatedAt) }
|
|
7852
7934
|
]);
|
|
7853
7935
|
const p = r?.pagination;
|
|
@@ -7863,7 +7945,7 @@ function backreelCommand(globals2) {
|
|
|
7863
7945
|
br.command("create <title>").description("Create a post (always a draft until `backreel publish`)")
|
|
7864
7946
|
).action(async (title, opts) => {
|
|
7865
7947
|
const client = backreelClient(globals2);
|
|
7866
|
-
const body = { kind: "showcase", description: "", ...await postBody(opts), title };
|
|
7948
|
+
const body = { kind: "showcase", description: "", ...await postBody(opts, { client, title }), title };
|
|
7867
7949
|
const created = await client.post(`${POSTS}/create`, body);
|
|
7868
7950
|
render(created, (p) => success(`Created draft "${p.title}" \u2014 ${p.id}`));
|
|
7869
7951
|
});
|
|
@@ -7871,7 +7953,7 @@ function backreelCommand(globals2) {
|
|
|
7871
7953
|
br.command("set <postId>").description("Update a post; only the fields you pass change").option("--title <text>", "title")
|
|
7872
7954
|
).action(async (id, opts) => {
|
|
7873
7955
|
const client = backreelClient(globals2);
|
|
7874
|
-
const body = await postBody(opts);
|
|
7956
|
+
const body = await postBody(opts, { client, postId: id, title: opts.title });
|
|
7875
7957
|
if (opts.title) body.title = opts.title;
|
|
7876
7958
|
if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field flag");
|
|
7877
7959
|
const updated = await client.put(`${POSTS}/list/${id}`, body);
|
|
@@ -8069,9 +8151,20 @@ function backreelCommand(globals2) {
|
|
|
8069
8151
|
return br;
|
|
8070
8152
|
}
|
|
8071
8153
|
function postOptions(cmd) {
|
|
8072
|
-
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 };
|
|
8073
8166
|
}
|
|
8074
|
-
async function postBody(opts) {
|
|
8167
|
+
async function postBody(opts, ctx = {}) {
|
|
8075
8168
|
const body = { ...await readJson(opts.body) || {} };
|
|
8076
8169
|
if (opts.kind) body.kind = oneOf2(opts.kind, KINDS, "--kind");
|
|
8077
8170
|
if (opts.logline !== void 0) {
|
|
@@ -8081,12 +8174,13 @@ async function postBody(opts) {
|
|
|
8081
8174
|
}
|
|
8082
8175
|
if (opts.category) body.category = opts.category;
|
|
8083
8176
|
if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
|
|
8177
|
+
if (opts.onSlate !== void 0) body.showOnSlate = oneOf2(opts.onSlate, ["on", "off"], "--on-slate") === "on";
|
|
8084
8178
|
if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
|
|
8085
8179
|
if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
|
|
8086
8180
|
if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
|
|
8087
8181
|
if (opts.cover) body.cover = opts.cover;
|
|
8088
8182
|
if (opts.coverPortrait) body.coverPortrait = opts.coverPortrait;
|
|
8089
|
-
if (opts.trailer) body
|
|
8183
|
+
if (opts.trailer) Object.assign(body, await trailerFields(String(opts.trailer), ctx));
|
|
8090
8184
|
if (opts.media) {
|
|
8091
8185
|
const media = opts.media.filter((m) => m !== "none");
|
|
8092
8186
|
if (media.length > MEDIA_MAX) throw new Error(`--media takes at most ${MEDIA_MAX} items (the web's LookBook limit)`);
|
|
@@ -8123,10 +8217,13 @@ function printPost(p) {
|
|
|
8123
8217
|
["media", (p.assets || []).length ? `${p.assets.length} item(s)` : void 0],
|
|
8124
8218
|
["episodes", (p.episodes || []).length ? `${p.episodes.length} (\`backreel episode list\`)` : void 0],
|
|
8125
8219
|
["campaign", p.campaign ? `${p.campaign.raisedAmount}/${p.campaign.goalAmount} ${(p.campaign.currency || "").toUpperCase()}` : void 0],
|
|
8220
|
+
["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
|
|
8221
|
+
["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
|
|
8126
8222
|
["donations", p.donation?.enabled ? "on" : void 0],
|
|
8127
|
-
["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],
|
|
8128
8224
|
["creator", p.creator?.name],
|
|
8129
8225
|
["likes", p.likeCount],
|
|
8226
|
+
["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
|
|
8130
8227
|
["updated", relativeTime(p.updatedAt)]
|
|
8131
8228
|
]);
|
|
8132
8229
|
process.stdout.write("\n");
|
package/package.json
CHANGED
package/skill/backreel/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: backreel
|
|
3
|
-
description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign
|
|
3
|
+
description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign in WJI credits (all-or-nothing, tiers) or, for verified fundraisers, cash with offline supporters, back a post with credits, read backers and donation records, answer enquiries (查詢 / 合作邀請 / 投資 / 授權) — reply, accept or decline, grant partner access — write blog posts (文章 / 作品動態) on a profile or under a project, and work the Job Plaza (接單廣場) — post a job, place a sealed bid, award it, then run the contract: payment plan, project credits, milestones, top-ups, change requests and disputes. Use whenever a task mentions Backreel, a Backreel post / work / reel, its episodes, trailer, cover or poster sizes, crowdfunding / 眾籌 / 捐款, supporters, enquiries / 查詢 on a creator's work, a Backreel blog post, project update or behind-the-scenes note, or a Backreel job, bid, quote, contract / 合約, milestone or dispute.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Backreel
|
|
@@ -26,11 +26,12 @@ 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
|
-
| campaign +
|
|
32
|
+
| campaign + support | funding type (credits / cash / both), credits goal, cash goal, deadline, tiers, offline supporters | `br fund …` |
|
|
33
33
|
| enquiries | messages from people who want to work with / invest in / license the work | `br enquiry …` |
|
|
34
|
+
| where it lists | showcase → the Slate (`/slate`); crowdfunding → the Crowdfunding page (`/crowdfunding`, filterable by Raising / Ended), and also the Slate when the creator opts in | `br set --on-slate on` |
|
|
34
35
|
|
|
35
36
|
Viewer tiers, lowest to highest: `public` (anyone) < `member` (signed in) <
|
|
36
37
|
`supporter` (has donated) < `partner` (granted by the creator or a verified
|
|
@@ -111,24 +112,37 @@ transcoding". Numbers on the command line are 1-based positions. Read
|
|
|
111
112
|
[reference/episodes.md](reference/episodes.md) for the trailer, the legacy
|
|
112
113
|
asset path, tier strategy, subtitles and failure handling.
|
|
113
114
|
|
|
114
|
-
## 4. Crowdfunding and
|
|
115
|
+
## 4. Crowdfunding and credits support
|
|
115
116
|
|
|
116
117
|
```bash
|
|
117
|
-
wji br create "下一部長片" --kind crowdfunding -d @pitch.md
|
|
118
|
-
wji br fund set <postId> --goal
|
|
119
|
-
|
|
120
|
-
wji br fund
|
|
118
|
+
wji br create "下一部長片" --kind crowdfunding -d @pitch.md # new campaigns default to credits
|
|
119
|
+
wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
|
|
120
|
+
wji br fund tier add <postId> --title "早鳥" --amount 2000 --perks "片尾鳴謝" --limit 300
|
|
121
|
+
wji br fund show <postId> # funding type, progress, outcome, tiers
|
|
122
|
+
wji br fund pledges <postId> # credits backers
|
|
123
|
+
wji br fund back <postId> 500 --tier 1 -y # back someone else's post with your credits
|
|
124
|
+
|
|
125
|
+
# cash — only for publishers with Cash fundraiser verification
|
|
126
|
+
wji br fund set <postId> --mode both --goal 500000 --cash-currency hkd
|
|
121
127
|
wji br fund supporter add <postId> --name "ABC Films" --amount 50000
|
|
122
128
|
wji br fund set <postId> --offline-raised 50000 # money raised outside Backreel
|
|
123
|
-
wji br fund show <postId> # progress, settings, tiers
|
|
124
|
-
wji br fund donations <postId> # online donations, balance, payouts
|
|
125
129
|
```
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
Support is in **WJI credits**, from the backer's wallet — no card. On a
|
|
132
|
+
crowdfunding post the credits are held until the deadline: **all or
|
|
133
|
+
nothing** — goal missed → every backer refunded; goal reached → the campaign
|
|
134
|
+
is `funded` and becomes a production contract released by milestone. On a
|
|
135
|
+
showcase post credits go straight to the creator. Funding type: `credits`
|
|
136
|
+
(any creator), `cash` or `both` (need Cash fundraiser verification for the
|
|
137
|
+
person or organization publishing). Read
|
|
138
|
+
[reference/crowdfunding.md](reference/crowdfunding.md) for the rules and tier
|
|
139
|
+
design.
|
|
140
|
+
|
|
141
|
+
Members can also mark any published work **Interested** (有興趣). It is a
|
|
142
|
+
public count (`interested` in `backreel list` / `backreel get`); only the
|
|
143
|
+
creator sees who, on the site's Manage → Supporters tab. Interested members get
|
|
144
|
+
an in-app notice when a new episode goes live or a credits campaign ends. A
|
|
145
|
+
creator cannot mark their own work, and the CLI cannot set it.
|
|
132
146
|
|
|
133
147
|
## 5. Enquiries
|
|
134
148
|
|
|
@@ -1,35 +1,65 @@
|
|
|
1
|
-
# Crowdfunding,
|
|
1
|
+
# Crowdfunding, credits support and supporters
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Funding types
|
|
4
4
|
|
|
5
|
-
| |
|
|
5
|
+
| | Credits | Cash |
|
|
6
6
|
| --- | --- | --- |
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
7
|
+
| who can run it | any creator | publishers (person or organization) with **Cash fundraiser** verification |
|
|
8
|
+
| goal | `--credit-goal` (credits) | `--goal` + `--cash-currency` |
|
|
9
|
+
| how backers give | pledge WJI credits on the site (`br fund back`) — held until the deadline | commitments arranged with Backreel, money paid outside the site, recorded as `--offline-raised` / `fund supporter` |
|
|
10
|
+
| at the deadline | **all or nothing**: goal reached → `funded` (becomes a production contract, credits released by milestone); missed → `failed`, every backer refunded automatically | nothing automatic |
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
`--mode both` runs both at once with two progress bars. New campaigns default
|
|
13
|
+
to credits. Posts made before funding types existed read as `cash`.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Credits support is also how showcase works take support: credits go straight
|
|
16
|
+
to the creator (no deadline, nothing held). Legacy Stripe donations stay in
|
|
17
|
+
`fund donations` but new support is credits.
|
|
18
|
+
|
|
19
|
+
## Setting up a credits campaign
|
|
15
20
|
|
|
16
21
|
```bash
|
|
17
22
|
wji br create "下一部長片" --kind crowdfunding -d @pitch.md --logline "…"
|
|
18
|
-
wji br fund set <postId> --goal
|
|
19
|
-
wji br fund
|
|
20
|
-
wji br fund tier add <postId> --title "
|
|
21
|
-
wji br fund tier add <postId> --title "製片人" --amount 20000 --perks "聯合製片人名銜、探班一次" --limit 5
|
|
23
|
+
wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
|
|
24
|
+
wji br fund tier add <postId> --title "早鳥支持" --amount 2000 --perks "片尾鳴謝 + 數碼海報" --limit 300
|
|
25
|
+
wji br fund tier add <postId> --title "製片人" --amount 200000 --perks "聯合製片人名銜、探班一次" --limit 5
|
|
22
26
|
wji br check <postId>
|
|
23
27
|
wji br publish <postId> # ask first
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
30
|
+
- `--mode credits` (or `both`) switches credits support on by itself.
|
|
31
|
+
- A credits campaign needs a goal ≥ 1 and a **deadline in the future** to
|
|
32
|
+
publish; it settles when the deadline passes (checked every 10 minutes).
|
|
33
|
+
- **Funded → production contract.** The moment a credits campaign is funded,
|
|
34
|
+
Backreel turns it into a job contract (`wji br contract get <jobId>`, the
|
|
35
|
+
id is in `br fund show`): credits only, released milestone by milestone,
|
|
36
|
+
milestones must add up to exactly the credits raised. The creator sends the
|
|
37
|
+
plan (`br contract plan`), a Backreel admin accepts it on the backers'
|
|
38
|
+
behalf, the project workspace opens with the first milestone's credits, and
|
|
39
|
+
each later milestone unlocks when Backreel approves the previous one. If
|
|
40
|
+
the contract is cancelled, unused credits go back to the backers pro rata;
|
|
41
|
+
if it completes, what's left is the creator's.
|
|
42
|
+
- Once anyone has pledged, the campaign can't drop credits, the post can't be
|
|
43
|
+
deleted or switched to showcase, and support can't go back to cash. After it
|
|
44
|
+
settles the credits goal is fixed.
|
|
45
|
+
- Backers can't take a pledge back; Backreel staff can refund one.
|
|
46
|
+
- Where it lists: every published crowdfunding post is on the Crowdfunding
|
|
47
|
+
page (`/crowdfunding`, filterable by Raising / Ended). It is **not** on the
|
|
48
|
+
Slate unless the creator opts in: `wji br set <postId> --on-slate on`.
|
|
29
49
|
- Amounts are whole numbers. Every tier must be ≥ the minimum (and ≥ 1);
|
|
30
50
|
≤10 tiers; `--limit` caps the slots (`--limit 0` on `tier set` = unlimited).
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
|
|
52
|
+
## Cash campaigns (verified fundraisers only)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
wji br fund set <postId> --mode cash --goal 500000 --cash-currency hkd --deadline 2026-12-31
|
|
56
|
+
wji br fund supporter add <postId> --name "ABC Films" --amount 50000
|
|
57
|
+
wji br fund set <postId> --offline-raised 50000
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without Cash fundraiser verification the server refuses `cash` / `both`;
|
|
61
|
+
apply on the Backreel profile (Settings → verification). A post that already
|
|
62
|
+
had cash keeps it even if the verification later lapses.
|
|
33
63
|
|
|
34
64
|
## Designing tiers
|
|
35
65
|
|
|
@@ -58,25 +88,26 @@ Adding a supporter does **not** change the raised amount — update
|
|
|
58
88
|
`--offline-raised` yourself (keep the two consistent). Avatars are square
|
|
59
89
|
images (`br image logo.png --slot photo`).
|
|
60
90
|
|
|
61
|
-
##
|
|
91
|
+
## Backers and donation records
|
|
62
92
|
|
|
63
93
|
```bash
|
|
64
|
-
wji br fund
|
|
94
|
+
wji br fund pledges <postId> # credits backers: time, backer, credits, tier, status, message
|
|
95
|
+
wji br fund donations <postId> # legacy Stripe donations, balance, payouts
|
|
65
96
|
```
|
|
66
97
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
do it.
|
|
98
|
+
Pledge status: `held` (campaign running, or funded and waiting for its
|
|
99
|
+
contract), `released` (a showcase tip, already the creator's), `refunded`,
|
|
100
|
+
`transferred` (moved into the production contract). Anonymous backers stay
|
|
101
|
+
hidden even from the creator. Legacy donations: refunds and payouts are
|
|
102
|
+
handled by Backreel staff — the CLI cannot do it.
|
|
73
103
|
|
|
74
|
-
## Showcase posts
|
|
104
|
+
## Showcase posts take credits support too
|
|
75
105
|
|
|
76
|
-
A showcase post (episodes) can
|
|
77
|
-
to later episodes — there is
|
|
106
|
+
A showcase post (episodes) can take credits support to sell `supporter`
|
|
107
|
+
access to later episodes — there is no goal or progress bar, and the credits
|
|
108
|
+
go straight to the creator:
|
|
78
109
|
|
|
79
110
|
```bash
|
|
80
|
-
wji br fund set <postId> --donations on --currency
|
|
111
|
+
wji br fund set <postId> --donations on --currency credits --min 100
|
|
81
112
|
wji br episode tiers <postId> --public-to 2 --rest supporter
|
|
82
113
|
```
|
|
@@ -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
|
|