@gentbajko/slopify 3.8.0 → 3.8.2
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.1.md +7 -0
- package/dist/patch-notes/3.8.2.md +6 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/rebuild/narration-retry.js +19 -2
- package/dist/slices/studio/plan-model.js +4 -3
- package/dist/slices/studio/plan.js +14 -2
- package/dist/slices/studio/releases.js +15 -4
- package/dist/web/assets/{index-Cc16nRHK.js → index-DzehNHWU.js} +4 -4
- package/dist/web/assets/{pdf-DK_RqCdk.js → pdf-BPNXDDg1.js} +1 -1
- package/dist/web/index.html +1 -1
- 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,7 @@
|
|
|
1
|
+
# Slopify 3.8.1
|
|
2
|
+
|
|
3
|
+
Released 4 October 2026.
|
|
4
|
+
|
|
5
|
+
- **A schedule's release times also take its series' other projects.** A project queued by hand from the schedule's topics (or any project whose title ends with the same series as the schedule's template, the part after "|") takes the schedule's release times too, not only the projects the schedule ran itself.
|
|
6
|
+
- **Deleted on YouTube.** Each upload in the project's On YouTube list has a Deleted on YouTube button: press it after deleting the video in Studio, and Slopify forgets it, so the extension offers the upload again.
|
|
7
|
+
- **No upload deadline to watch.** The release calendar marks each time "goes public", and a video that is ready says "Ready to upload": upload and schedule it whenever you like. Only a video still not uploaded in the last day before it goes public is flagged, "Late · upload now", since YouTube's checks may not finish in time.
|
|
@@ -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.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.8.2",
|
|
4
|
+
"title": "Slopify 3.8.2",
|
|
5
|
+
"version": "3.8.2",
|
|
6
|
+
"date": "2026-10-04"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.8.1",
|
|
10
|
+
"title": "Slopify 3.8.1",
|
|
11
|
+
"version": "3.8.1",
|
|
12
|
+
"date": "2026-10-04"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.8.0",
|
|
4
16
|
"title": "Slopify 3.8.0",
|
|
@@ -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(" ");
|
|
@@ -12,9 +12,10 @@ const rowSchema = z.object({
|
|
|
12
12
|
series: z.string().trim().max(100).default(""),
|
|
13
13
|
long: slotSchema,
|
|
14
14
|
shorts: z.array(slotSchema).max(10),
|
|
15
|
-
// A line from a schedule's release times (never stored here): it takes
|
|
16
|
-
// projects
|
|
17
|
-
// the calendar shows the schedule's name
|
|
15
|
+
// A line from a schedule's release times (never stored here): it takes that schedule's
|
|
16
|
+
// projects and any other of its template's series (`series`), preferring the ones its run day
|
|
17
|
+
// (`runDay`) made, in the schedule's time zone; the calendar shows the schedule's name
|
|
18
|
+
// (`label`).
|
|
18
19
|
schedule: z.string().optional(),
|
|
19
20
|
runDay: z.number().int().min(0).max(6).optional(),
|
|
20
21
|
timeZone: z.string().optional(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { templateById } from "../project-templates/repo.js";
|
|
1
2
|
import { scheduleRows } from "../schedules/repo.js";
|
|
2
3
|
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
3
4
|
// The posting plan: a week of long-video slots, each with its shorts. Each schedule sets its
|
|
@@ -7,7 +8,7 @@ import { readSetting, writeSetting } from "../settings/repo.js";
|
|
|
7
8
|
// after its own long video, so a plan never needs "next week" notes. The extension types the
|
|
8
9
|
// times into Studio's schedule, in the browser's own time zone.
|
|
9
10
|
export { postingPlanSchema, weekdays, } from "./plan-model.js";
|
|
10
|
-
import { postingPlanSchema } from "./plan-model.js";
|
|
11
|
+
import { postingPlanSchema, seriesOf } from "./plan-model.js";
|
|
11
12
|
export const postingPlanKey = "studio.postingPlan";
|
|
12
13
|
const key = postingPlanKey;
|
|
13
14
|
// No plan until one is made: nothing is scheduled, and Prepare upload says so.
|
|
@@ -34,6 +35,17 @@ export function readStoredPlan(db) {
|
|
|
34
35
|
}
|
|
35
36
|
return emptyPlan(localTimeZone());
|
|
36
37
|
}
|
|
38
|
+
// The series of a schedule's template, from its project title ("{{Topic}} | Stories" →
|
|
39
|
+
// "Stories"); "" when the template is gone or its title names none.
|
|
40
|
+
function templateSeries(db, templateId) {
|
|
41
|
+
try {
|
|
42
|
+
const title = templateById(db, templateId)?.document.form.title;
|
|
43
|
+
return title === undefined ? "" : seriesOf({ title });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
// The lines every release is planned from: each schedule's release times (one line for each
|
|
38
50
|
// day it runs), then the stored lines.
|
|
39
51
|
export function readPlan(db) {
|
|
@@ -42,7 +54,7 @@ export function readPlan(db) {
|
|
|
42
54
|
.filter((schedule) => schedule.deletedAt === null && schedule.status !== "canceled")
|
|
43
55
|
.flatMap((schedule) => schedule.releases.map((release) => ({
|
|
44
56
|
name: `s${schedule.id.slice(0, 8)}${String(release.day)}`,
|
|
45
|
-
series:
|
|
57
|
+
series: templateSeries(db, schedule.templateId),
|
|
46
58
|
long: release.long,
|
|
47
59
|
shorts: [...release.shorts],
|
|
48
60
|
schedule: schedule.id,
|
|
@@ -47,9 +47,10 @@ function takenHours(db, exceptProject) {
|
|
|
47
47
|
.filter((release) => release.projectId !== exceptProject)
|
|
48
48
|
.map((release) => Math.floor(Date.parse(release.at) / hourMs)));
|
|
49
49
|
}
|
|
50
|
-
// A schedule's line takes
|
|
50
|
+
// A schedule's line takes that schedule's projects and any other of its template's series (a
|
|
51
|
+
// project queued by hand from its topics); a stored line takes its series, or any.
|
|
51
52
|
const fits = (line, who) => line.schedule !== undefined
|
|
52
|
-
? line.schedule === who.schedule
|
|
53
|
+
? line.schedule === who.schedule || (line.series !== "" && line.series === who.series)
|
|
53
54
|
: line.series === "" || line.series === who.series;
|
|
54
55
|
const zoneOf = (plan, line) => line.timeZone ?? plan.timeZone;
|
|
55
56
|
// The plan's long-video times from `from` to `until`, in order, each with its line.
|
|
@@ -83,6 +84,14 @@ export function freeSlots(db, plan, who, now, count, exceptProject) {
|
|
|
83
84
|
export function planReleases(db, plan, project, now) {
|
|
84
85
|
let releases = releasesOf(db, project.id);
|
|
85
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
|
+
}
|
|
86
95
|
if (long === undefined) {
|
|
87
96
|
// The line of the day its schedule ran comes first, then any line that takes it.
|
|
88
97
|
const slots = freeSlots(db, plan, project, now, 30, project.id);
|
|
@@ -107,10 +116,12 @@ export function planReleases(db, plan, project, now) {
|
|
|
107
116
|
if (release.at !== "")
|
|
108
117
|
taken.add(Math.floor(Date.parse(release.at) / hourMs));
|
|
109
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);
|
|
110
121
|
const times = line.shorts
|
|
111
|
-
.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)))
|
|
112
123
|
.toSorted((left, right) => left.getTime() - right.getTime());
|
|
113
|
-
let after =
|
|
124
|
+
let after = start;
|
|
114
125
|
for (let short = 1; short <= project.shorts; short += 1) {
|
|
115
126
|
const kept = releases.find((release) => release.short === short);
|
|
116
127
|
if (kept !== undefined) {
|