@gentbajko/slopify 3.8.1 → 3.8.3
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/edge/http/studio.js +6 -4
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.8.2.md +6 -0
- package/dist/patch-notes/3.8.3.md +5 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/rebuild/narration-retry.js +19 -2
- package/dist/slices/rebuild/recovery-repo.js +23 -2
- package/dist/slices/studio/releases.js +12 -2
- package/package.json +1 -1
package/dist/edge/http/studio.js
CHANGED
|
@@ -162,17 +162,19 @@ export function studioRoutes(deps) {
|
|
|
162
162
|
};
|
|
163
163
|
};
|
|
164
164
|
// A project ready to upload gets its release times from the posting plan when its upload is
|
|
165
|
-
// prepared (`releases.ts`)
|
|
165
|
+
// prepared (`releases.ts`).
|
|
166
|
+
// A long video already on YouTube keeps when it went up; its shorts still get times.
|
|
166
167
|
const planned = (projectId) => {
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const long = videoOf(deps.db, projectId, null);
|
|
169
|
+
const out = long?.uploadState === "done" ? long.recordedAt : undefined;
|
|
169
170
|
const result = uploadPack(deps, projectId);
|
|
170
|
-
if (!result.ok || result.pack.items[0]?.video == null)
|
|
171
|
+
if (!result.ok || (out === undefined && result.pack.items[0]?.video == null))
|
|
171
172
|
return;
|
|
172
173
|
planReleases(deps.db, readPlan(deps.db), {
|
|
173
174
|
id: projectId,
|
|
174
175
|
...whoOf(projectId, result.pack.series),
|
|
175
176
|
shorts: result.pack.items.filter((item) => item.kind === "short").length,
|
|
177
|
+
...(out === undefined ? {} : { longOut: out }),
|
|
176
178
|
}, deps.clock.now());
|
|
177
179
|
};
|
|
178
180
|
// Settings shows only the lines saved there; each schedule shows its own.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Slopify 3.8.2
|
|
2
|
+
|
|
3
|
+
Released 4 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Shorts of a video already on YouTube get release times.** A project whose long video went up before it had a release time left its shorts with none, so the Studio extension had no schedule to type in. Now the long video keeps the time it went up as its release, and its shorts take the release line's short times from the upload lead on (never due sooner than you could upload them).
|
|
6
|
+
- **A narration chunk recorded again by itself no longer gets stuck.** When the voice garbled words, Slopify promised to record the chunk again and carry on, but if the project's last other step was just finishing, the retry waited for a step that never came, and the video stayed failed. It now looks again every 15 seconds until the project is free.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Slopify 3.8.3
|
|
2
|
+
|
|
3
|
+
Released 4 October 2026.
|
|
4
|
+
|
|
5
|
+
- **A narration chunk Slopify records again by itself really does get recorded.** If its first try found the project busy, Slopify kept "busy" as that try's answer and gave it back every time after, even across restarts, so the video stayed failed and the update waited on it forever. A busy answer is no longer kept: the next look asks again and records the chunk. Retries stuck this way before the update start on their own once it is installed.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.8.3",
|
|
4
|
+
"title": "Slopify 3.8.3",
|
|
5
|
+
"version": "3.8.3",
|
|
6
|
+
"date": "2026-10-04"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.8.2",
|
|
10
|
+
"title": "Slopify 3.8.2",
|
|
11
|
+
"version": "3.8.2",
|
|
12
|
+
"date": "2026-10-04"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.8.1",
|
|
4
16
|
"title": "Slopify 3.8.1",
|
|
@@ -61,6 +61,13 @@ export function createNarrationRetries() {
|
|
|
61
61
|
continue;
|
|
62
62
|
running.add(id);
|
|
63
63
|
void recordAgain(deps, retry)
|
|
64
|
+
.then((waiting) => {
|
|
65
|
+
// The step that just finished may still count as running when it kicks this (the
|
|
66
|
+
// runner lets go of it after its `onFinished`), and nothing else finishes to kick
|
|
67
|
+
// again: look again shortly until the project is free.
|
|
68
|
+
if (waiting)
|
|
69
|
+
setTimeout(() => kick(retry.projectId), retryWaitMs).unref?.();
|
|
70
|
+
})
|
|
64
71
|
.catch((error) => {
|
|
65
72
|
deps.log.write("error", "narration.retry", {
|
|
66
73
|
projectId: retry.projectId,
|
|
@@ -86,11 +93,16 @@ export function createNarrationRetries() {
|
|
|
86
93
|
kick,
|
|
87
94
|
};
|
|
88
95
|
}
|
|
96
|
+
// How long a retry waiting for the project's other steps waits before it looks again.
|
|
97
|
+
export const retryWaitMs = 15_000;
|
|
98
|
+
// The waits already logged, so looking again doesn't log the same line every 15 seconds.
|
|
99
|
+
const told = new Set();
|
|
100
|
+
// True when the project's other steps are still going and the retry waits for them.
|
|
89
101
|
async function recordAgain(deps, retry) {
|
|
90
102
|
const base = currentRevisionId(deps.db, retry.projectId);
|
|
91
103
|
if (base === undefined) {
|
|
92
104
|
settle(deps.db, retry, "failed", "no project", deps.clock.now().toISOString());
|
|
93
|
-
return;
|
|
105
|
+
return false;
|
|
94
106
|
}
|
|
95
107
|
const result = await recoverProject(deps, retry.projectId, {
|
|
96
108
|
baseRevisionId: base,
|
|
@@ -106,6 +118,10 @@ async function recordAgain(deps, retry) {
|
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
120
|
else if (result.reason === "running") {
|
|
121
|
+
const key = retryKey(retry);
|
|
122
|
+
if (told.has(key))
|
|
123
|
+
return true;
|
|
124
|
+
told.add(key);
|
|
109
125
|
// Another step of the project is still going (its PDF, say): the retry waits for it and
|
|
110
126
|
// starts when that step finishes, which kicks this again (`onFinished`). Giving up here
|
|
111
127
|
// left the video failed with a message promising a retry that never came.
|
|
@@ -113,7 +129,7 @@ async function recordAgain(deps, retry) {
|
|
|
113
129
|
projectId: retry.projectId,
|
|
114
130
|
detail: `Recording ${retry.chunkKey} again once the project's running steps finish.`,
|
|
115
131
|
});
|
|
116
|
-
return;
|
|
132
|
+
return true;
|
|
117
133
|
}
|
|
118
134
|
else {
|
|
119
135
|
settle(deps.db, retry, "failed", refusal(result), now);
|
|
@@ -123,6 +139,7 @@ async function recordAgain(deps, retry) {
|
|
|
123
139
|
});
|
|
124
140
|
}
|
|
125
141
|
deps.emit(retry.projectId, { type: "project.updated", projectId: retry.projectId });
|
|
142
|
+
return false;
|
|
126
143
|
}
|
|
127
144
|
function refusal(result) {
|
|
128
145
|
const detail = result.fields?.map((field) => field.message).join(" ");
|
|
@@ -37,8 +37,16 @@ export function readRecovery(deps, projectId, input) {
|
|
|
37
37
|
const row = rowSchema.parse(raw);
|
|
38
38
|
if (row.request_hash !== requestHash("direct-recovery", input.baseRevisionId, input.action))
|
|
39
39
|
return { ok: false, reason: "idempotency-conflict" };
|
|
40
|
-
if (row.response_json !== null)
|
|
41
|
-
|
|
40
|
+
if (row.response_json !== null) {
|
|
41
|
+
const saved = recoveryResultSchema.parse(JSON.parse(row.response_json));
|
|
42
|
+
// "Busy" was the moment's answer, not the request's outcome: one kept from before this was
|
|
43
|
+
// forgotten is dropped, so the same request asks again instead of replaying it forever.
|
|
44
|
+
if (busy(saved)) {
|
|
45
|
+
forget(deps, projectId, input);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return saved;
|
|
49
|
+
}
|
|
42
50
|
return {
|
|
43
51
|
pending: true,
|
|
44
52
|
authority: row.authority_stamp,
|
|
@@ -57,11 +65,24 @@ export function reserveRecovery(deps, projectId, input, edit) {
|
|
|
57
65
|
}
|
|
58
66
|
export function rememberRecovery(deps, projectId, input, result) {
|
|
59
67
|
const response = recoveryResultSchema.parse(result);
|
|
68
|
+
// Refused because the project was busy: nothing was done, so the same request may come again
|
|
69
|
+
// (an automatic narration retry keeps its request id) and must be tried again, not replayed.
|
|
70
|
+
if (busy(response)) {
|
|
71
|
+
forget(deps, projectId, input);
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
60
74
|
deps.db
|
|
61
75
|
.prepare("UPDATE project_recovery_requests SET response_json=? WHERE project_id=? AND idempotency_key=?")
|
|
62
76
|
.run(JSON.stringify(response), projectId, input.idempotencyKey);
|
|
63
77
|
return response;
|
|
64
78
|
}
|
|
79
|
+
// Busy before anything was saved (a refusal after the revision was saved keeps its answer).
|
|
80
|
+
const busy = (result) => !result.ok && result.reason === "running" && result.intentRevisionId === undefined;
|
|
81
|
+
function forget(deps, projectId, input) {
|
|
82
|
+
deps.db
|
|
83
|
+
.prepare("DELETE FROM project_recovery_requests WHERE project_id=? AND idempotency_key=?")
|
|
84
|
+
.run(projectId, input.idempotencyKey);
|
|
85
|
+
}
|
|
65
86
|
// The head has unfinished stages but nothing that will move them on its own: no call in
|
|
66
87
|
// flight and no admitted, dispatchable work. A saved rerun or edit is admitted by no one
|
|
67
88
|
// until Resume, and work held by a pause, a restart or a failed sibling of its admission
|
|
@@ -84,6 +84,14 @@ export function freeSlots(db, plan, who, now, count, exceptProject) {
|
|
|
84
84
|
export function planReleases(db, plan, project, now) {
|
|
85
85
|
let releases = releasesOf(db, project.id);
|
|
86
86
|
let long = releases.find((release) => release.short === 0);
|
|
87
|
+
if (long === undefined && project.longOut !== undefined) {
|
|
88
|
+
const line = plan.rows.find((one) => one.schedule !== undefined && fits(one, project) && one.runDay === project.runDay) ?? plan.rows.find((one) => fits(one, project));
|
|
89
|
+
if (line === undefined)
|
|
90
|
+
return;
|
|
91
|
+
long = { short: 0, at: project.longOut, line: line.name, by: "plan" };
|
|
92
|
+
write(db, project.id, long, now);
|
|
93
|
+
releases = releasesOf(db, project.id);
|
|
94
|
+
}
|
|
87
95
|
if (long === undefined) {
|
|
88
96
|
// The line of the day its schedule ran comes first, then any line that takes it.
|
|
89
97
|
const slots = freeSlots(db, plan, project, now, 30, project.id);
|
|
@@ -108,10 +116,12 @@ export function planReleases(db, plan, project, now) {
|
|
|
108
116
|
if (release.at !== "")
|
|
109
117
|
taken.add(Math.floor(Date.parse(release.at) / hourMs));
|
|
110
118
|
const longAt = new Date(long.at);
|
|
119
|
+
// A short is never due before it could be uploaded in time.
|
|
120
|
+
const start = Math.max(longAt.getTime(), now.getTime() + readLeadHours(db) * hourMs);
|
|
111
121
|
const times = line.shorts
|
|
112
|
-
.flatMap((slot) => zonedTimes(zoneOf(plan, line), slot,
|
|
122
|
+
.flatMap((slot) => zonedTimes(zoneOf(plan, line), slot, new Date(start), new Date(start + 5 * 7 * 24 * hourMs)))
|
|
113
123
|
.toSorted((left, right) => left.getTime() - right.getTime());
|
|
114
|
-
let after =
|
|
124
|
+
let after = start;
|
|
115
125
|
for (let short = 1; short <= project.shorts; short += 1) {
|
|
116
126
|
const kept = releases.find((release) => release.short === short);
|
|
117
127
|
if (kept !== undefined) {
|