@gentbajko/slopify 3.6.3 → 3.7.1
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/projects.js +25 -1
- package/dist/edge/http/studio.js +48 -1
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.7.0.md +8 -0
- package/dist/patch-notes/3.7.1.md +16 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/studio/releases.js +12 -0
- package/dist/web/assets/{index-CDNk-_g2.css → index-CC9XywV_.css} +1 -1
- package/dist/web/assets/index-glbbg3ZG.js +310 -0
- package/dist/web/assets/{pdf-JGOPFmA1.js → pdf-B6O50xdO.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-J8vymPif.js +0 -310
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
4
5
|
import { derive, progressOf } from "../../kernel/runner/graph.js";
|
|
5
6
|
import { listProjects, projectById, projectTrashed, runDraftSchema, stageStandingsByProject, stagesOf, } from "../../slices/admission/repo.js";
|
|
6
7
|
import { defaultChannelId } from "../../slices/channels/model.js";
|
|
7
|
-
import { projectChannels } from "../../slices/channels/repo.js";
|
|
8
|
+
import { channelById, projectChannels, setProjectChannel } from "../../slices/channels/repo.js";
|
|
8
9
|
import { withProjectControl } from "../../slices/control/lock.js";
|
|
9
10
|
import { stagesWithEta } from "../../slices/eta/view.js";
|
|
10
11
|
import { videoActivity } from "../../slices/rebuild/activity.js";
|
|
@@ -115,6 +116,29 @@ export function projectRoutes(deps) {
|
|
|
115
116
|
})
|
|
116
117
|
// Moves the project to the trash (Settings → Trash) for 30 days; removing it for good is
|
|
117
118
|
// Delete now there, or the daily purge (`slices/trash`).
|
|
119
|
+
// Moves projects to another channel: one from its page, or every one the Projects list
|
|
120
|
+
// shows at once (a search like "ground." and Move).
|
|
121
|
+
.post("/move-channel", zValidator("json", z
|
|
122
|
+
.object({
|
|
123
|
+
projectIds: z.array(z.string().min(1).max(64)).min(1).max(2000),
|
|
124
|
+
channelId: z.string().min(1).max(64),
|
|
125
|
+
})
|
|
126
|
+
.strict(), onInvalid), (c) => {
|
|
127
|
+
const { projectIds, channelId } = c.req.valid("json");
|
|
128
|
+
if (channelById(deps.db, channelId) === undefined)
|
|
129
|
+
return problem(c, {
|
|
130
|
+
status: 404,
|
|
131
|
+
title: titleOf(404),
|
|
132
|
+
detail: "That channel no longer exists. Reload the page and pick one from the list, or make it again in Channels.",
|
|
133
|
+
});
|
|
134
|
+
const live = new Set(listProjects(deps.db).map((project) => project.id));
|
|
135
|
+
const moving = [...new Set(projectIds)].filter((id) => live.has(id));
|
|
136
|
+
transact(deps.db, () => {
|
|
137
|
+
for (const id of moving)
|
|
138
|
+
setProjectChannel(deps.db, id, channelId);
|
|
139
|
+
});
|
|
140
|
+
return c.json({ moved: moving.length, channelId });
|
|
141
|
+
})
|
|
118
142
|
.delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
119
143
|
const id = c.req.valid("param").id;
|
|
120
144
|
return withProjectControl(deps.db, id, () => {
|
package/dist/edge/http/studio.js
CHANGED
|
@@ -5,6 +5,7 @@ import { zValidator } from "@hono/zod-validator";
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { bodyLimit } from "hono/body-limit";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
8
9
|
import { derive } from "../../kernel/runner/graph.js";
|
|
9
10
|
import { listProjects, projectById, stagesOf } from "../../slices/admission/repo.js";
|
|
10
11
|
import { channelById } from "../../slices/channels/repo.js";
|
|
@@ -19,7 +20,7 @@ import { writeUploadPick } from "../../slices/studio/pick.js";
|
|
|
19
20
|
import { postingPlanSchema, readPlan, writePlan } from "../../slices/studio/plan.js";
|
|
20
21
|
import { leadHoursMax, seriesOf } from "../../slices/studio/plan-model.js";
|
|
21
22
|
import { enqueueFill, fillNow, readFillQueue, removeFill, } from "../../slices/studio/queue.js";
|
|
22
|
-
import { allReleases, freeSlots, planReleases, readLeadHours, releasesOf, setRelease, writeLeadHours, } from "../../slices/studio/releases.js";
|
|
23
|
+
import { allReleases, freeSlots, planReleases, readLeadHours, releasesOf, setRelease, swapReleases, writeLeadHours, } from "../../slices/studio/releases.js";
|
|
23
24
|
import { parseStudioExport, readReport, saveReport } from "../../slices/studio/report.js";
|
|
24
25
|
import { autoCommentKey, bearerToken, isExtensionOrigin, pairStudioExtension, readChannelPlaylists, readStudioPlaylists, resetStudioPairing, saveProjectPlaylists, saveRealFootage, saveStudioPlaylists, studioPairing, studioPlaylistsMax, studioPlaylistsProblem, studioRequestAllowed, } from "../../slices/studio/settings.js";
|
|
25
26
|
import { abResults, abVariantSchema, projectStats, saveAbResult, saveStats, } from "../../slices/studio/stats.js";
|
|
@@ -254,6 +255,52 @@ export function studioRoutes(deps) {
|
|
|
254
255
|
weeks: c.req.valid("query").weeks ?? 2,
|
|
255
256
|
candidates: finished.map((project) => project.id),
|
|
256
257
|
}));
|
|
258
|
+
})
|
|
259
|
+
// One project's release times (its long video and each short, with their upload-by), the
|
|
260
|
+
// plan's free times for its series, for the project page's Release block.
|
|
261
|
+
.get("/releases/:projectId", zValidator("param", projectParam, onInvalid), (c) => {
|
|
262
|
+
const { projectId } = c.req.valid("param");
|
|
263
|
+
planned(projectId);
|
|
264
|
+
const pack = uploadPack(deps, projectId);
|
|
265
|
+
if (!pack.ok)
|
|
266
|
+
return unknownProject(c);
|
|
267
|
+
const leadMs = readLeadHours(deps.db) * 3_600_000;
|
|
268
|
+
const releases = releasesOf(deps.db, projectId);
|
|
269
|
+
return c.json({
|
|
270
|
+
leadHours: readLeadHours(deps.db),
|
|
271
|
+
items: pack.pack.items
|
|
272
|
+
.filter((item) => item.video !== null || item.kind === "video")
|
|
273
|
+
.map((item) => {
|
|
274
|
+
const short = item.short ?? 0;
|
|
275
|
+
const at = releases.find((release) => release.short === short)?.at ?? "";
|
|
276
|
+
return {
|
|
277
|
+
short,
|
|
278
|
+
title: item.title,
|
|
279
|
+
at: at === "" ? null : at,
|
|
280
|
+
uploadBy: at === "" ? null : new Date(Date.parse(at) - leadMs).toISOString(),
|
|
281
|
+
onYoutube: videoOf(deps.db, projectId, item.short ?? null)?.uploadState === "done",
|
|
282
|
+
};
|
|
283
|
+
}),
|
|
284
|
+
free: slotChoices(projectId, pack.pack.series),
|
|
285
|
+
});
|
|
286
|
+
})
|
|
287
|
+
// Swap with…: two videos trade their release times; their shorts follow each.
|
|
288
|
+
.post("/releases/:projectId/swap", zValidator("param", projectParam, onInvalid), zValidator("json", z.object({ with: id }).strict(), onInvalid), (c) => {
|
|
289
|
+
const denied = samePage(c);
|
|
290
|
+
if (denied !== undefined)
|
|
291
|
+
return denied;
|
|
292
|
+
const { projectId } = c.req.valid("param");
|
|
293
|
+
const other = c.req.valid("json").with;
|
|
294
|
+
const swapped = transact(deps.db, () => swapReleases(deps.db, projectId, other, deps.clock.now()));
|
|
295
|
+
if (!swapped)
|
|
296
|
+
return problem(c, {
|
|
297
|
+
status: 409,
|
|
298
|
+
title: titleOf(409),
|
|
299
|
+
detail: "Both videos need a release time to swap. Give this one a time first, or reload the page and pick another video.",
|
|
300
|
+
});
|
|
301
|
+
planned(projectId);
|
|
302
|
+
planned(other);
|
|
303
|
+
return c.json({ releases: releasesOf(deps.db, projectId) });
|
|
257
304
|
})
|
|
258
305
|
// Moves one release (a time), sets it to "not scheduled" (null), or puts a project into a
|
|
259
306
|
// free time of the plan (short 0 with its line).
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Slopify 3.7.0
|
|
2
|
+
|
|
3
|
+
Released 4 October 2026.
|
|
4
|
+
|
|
5
|
+
## Projects by channel
|
|
6
|
+
|
|
7
|
+
- **Move projects to another channel.** In Projects, search (for example `ground.`) or pick a filter, then **Move the N shown to** a channel. A project's page has **Move to channel…** in its More menu. Nothing a project made changes; it shows in that channel's list and uses its brand and playlists.
|
|
8
|
+
- **One list per channel.** With All channels picked in the sidebar, the Projects list groups projects under each channel's name. Pick one channel in the sidebar to see only its projects.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Slopify 3.7.1
|
|
2
|
+
|
|
3
|
+
Released 4 October 2026.
|
|
4
|
+
|
|
5
|
+
## Release times on the project
|
|
6
|
+
|
|
7
|
+
- **Set release dates in the project itself.** The Video section has a Release block: the long video's and each short's release time, editable, with the time each must be uploaded and scheduled by. The long video can take one of the posting plan's free times, or be set to Not scheduled. They are the same times Calendar → Releases, Prepare upload and the Studio extension use.
|
|
8
|
+
- **Swap with…** in the Release block: two videos trade their release times, and each one's shorts move with it.
|
|
9
|
+
|
|
10
|
+
## Project page
|
|
11
|
+
|
|
12
|
+
- **A run clock you can see.** While a project runs, its header shows "Working · 1:03:27", counting every second and checked against the server every 20 seconds. A stopped run shows its working time so far.
|
|
13
|
+
|
|
14
|
+
## YouTube Studio extension (1.1.3)
|
|
15
|
+
|
|
16
|
+
- **One click uploads one video.** Studio rebuilds its file picker while an upload starts; the extension took each rebuild for a new upload dialog and added the same short again (and Upload all seemed to start every short at once). Now each dialog takes one video, and the same upload is never added twice within a minute and a half. Download the extension again from Settings → YouTube Studio and reload it.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.7.1",
|
|
4
|
+
"title": "Slopify 3.7.1",
|
|
5
|
+
"version": "3.7.1",
|
|
6
|
+
"date": "2026-10-04"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.7.0",
|
|
10
|
+
"title": "Slopify 3.7.0",
|
|
11
|
+
"version": "3.7.0",
|
|
12
|
+
"date": "2026-10-04"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.6.3",
|
|
4
16
|
"title": "Slopify 3.6.3",
|
|
@@ -123,6 +123,18 @@ export function setRelease(db, projectId, short, at, line, now) {
|
|
|
123
123
|
if (short === 0)
|
|
124
124
|
db.prepare("DELETE FROM releases WHERE project_id=? AND short>0 AND by='plan'").run(projectId);
|
|
125
125
|
}
|
|
126
|
+
// Two projects trade their long videos' release times (and lines); each one's plan-placed
|
|
127
|
+
// shorts are dropped, to be placed again after its new time. False when either has no time.
|
|
128
|
+
export function swapReleases(db, first, second, now) {
|
|
129
|
+
const longOf = (projectId) => releasesOf(db, projectId).find((release) => release.short === 0 && release.at !== "");
|
|
130
|
+
const a = longOf(first);
|
|
131
|
+
const b = longOf(second);
|
|
132
|
+
if (a === undefined || b === undefined || first === second)
|
|
133
|
+
return false;
|
|
134
|
+
setRelease(db, first, 0, b.at, b.line, now);
|
|
135
|
+
setRelease(db, second, 0, a.at, a.line, now);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
126
138
|
export function scheduleOf(db, projectId) {
|
|
127
139
|
const releases = releasesOf(db, projectId);
|
|
128
140
|
const long = releases.find((release) => release.short === 0);
|