@gentbajko/slopify 3.14.2 → 3.16.0
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/storage.js +26 -1
- package/dist/edge/http/studio.js +1 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/main.js +16 -1
- package/dist/patch-notes/3.15.0.md +5 -0
- package/dist/patch-notes/3.16.0.md +10 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/storage/trim.js +116 -3
- package/dist/slices/studio/unpacked.js +45 -0
- package/dist/tutorials/Where-Your-Files-Live.md +10 -0
- package/dist/web/assets/index-Dvz8bsiD.js +325 -0
- package/dist/web/assets/{pdf-Bw4akucO.js → pdf-C6KhxeKQ.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-wNwk0cqO.js +0 -325
|
@@ -6,7 +6,7 @@ import { BackupBusyError, planBackup, streamBackup, } from "../../slices/storage
|
|
|
6
6
|
import { BackupImportRefused, importBackup } from "../../slices/storage/backup-import.js";
|
|
7
7
|
import { importPortable, portableMaxArchiveBytes, storageUsage, } from "../../slices/storage/portable.js";
|
|
8
8
|
import { reconcileStorage } from "../../slices/storage/reconcile.js";
|
|
9
|
-
import { keepOutputsOnly, projectStorage } from "../../slices/storage/trim.js";
|
|
9
|
+
import { cleanUpAll, deleteAllOldVersions, deleteOldVersions, keepOutputsOnly, projectStorage, } from "../../slices/storage/trim.js";
|
|
10
10
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
11
11
|
const idParam = z.object({
|
|
12
12
|
id: z
|
|
@@ -57,6 +57,31 @@ export function storageRoutes(deps) {
|
|
|
57
57
|
detail: "Only a finished project can drop its working files, and this one is running, waiting or has unfinished steps. Let it finish (or cancel it on its project page), then drop the working files again from the project page or Settings → Storage.",
|
|
58
58
|
});
|
|
59
59
|
})
|
|
60
|
+
// Delete old versions: one project's History files (`slices/storage/trim.ts`).
|
|
61
|
+
.post("/projects/:id/old-versions/delete", zValidator("param", idParam, onInvalid), (c) => {
|
|
62
|
+
const result = deleteOldVersions({ db: deps.db, paths: deps.paths, hasInflight: deps.runner.hasInflight }, c.req.valid("param").id);
|
|
63
|
+
if (result.ok)
|
|
64
|
+
return c.json(result);
|
|
65
|
+
return result.reason === "no-project"
|
|
66
|
+
? problem(c, {
|
|
67
|
+
status: 404,
|
|
68
|
+
title: titleOf(404),
|
|
69
|
+
detail: "This project no longer exists. Reload Settings → Backup & storage to see the current list.",
|
|
70
|
+
})
|
|
71
|
+
: problem(c, {
|
|
72
|
+
status: 409,
|
|
73
|
+
title: titleOf(409),
|
|
74
|
+
detail: "This project is running or has steps waiting to run, and its files stay where they are until then. Let it finish, or cancel its run on its project page, then press Delete old versions again.",
|
|
75
|
+
});
|
|
76
|
+
})
|
|
77
|
+
// Every project's old versions; ones that are running are left for later.
|
|
78
|
+
.post("/old-versions/delete", (c) => c.json(deleteAllOldVersions({
|
|
79
|
+
db: deps.db,
|
|
80
|
+
paths: deps.paths,
|
|
81
|
+
hasInflight: deps.runner.hasInflight,
|
|
82
|
+
})))
|
|
83
|
+
// Clean up: old versions everywhere and working files of every finished project.
|
|
84
|
+
.post("/cleanup", (c) => c.json(cleanUpAll({ db: deps.db, paths: deps.paths, hasInflight: deps.runner.hasInflight })))
|
|
60
85
|
// What Export everything would write, asked before the download starts: a download
|
|
61
86
|
// link cannot show a refusal, the browser would save it as the file.
|
|
62
87
|
.get("/export/summary", (c) => {
|
package/dist/edge/http/studio.js
CHANGED
|
@@ -234,6 +234,7 @@ export function studioRoutes(deps) {
|
|
|
234
234
|
channelPlaylists: readChannelPlaylists(deps.db),
|
|
235
235
|
pairing: studioPairing(deps.db),
|
|
236
236
|
autoComment: readSetting(deps.db, autoCommentKey) === "on",
|
|
237
|
+
unpacked: deps.unpackedExtension ?? null,
|
|
237
238
|
});
|
|
238
239
|
})
|
|
239
240
|
// Post and pin each video's comment once it is public (opt-in: it posts in your name).
|
|
Binary file
|
|
Binary file
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { serve } from "@hono/node-server";
|
|
5
6
|
import ffmpegStatic from "ffmpeg-static";
|
|
@@ -78,6 +79,7 @@ import { documentsDir, nodeDocumentsHost } from "./slices/storage/documents.js";
|
|
|
78
79
|
import { createFilesService, ensureFilesFolders, filesLayoutOf, filesMoveKey, settleFilesLocation, } from "./slices/storage/files-location.js";
|
|
79
80
|
import { createPlaces, recoverPlaces } from "./slices/storage/places.js";
|
|
80
81
|
import { reconcileStorage } from "./slices/storage/reconcile.js";
|
|
82
|
+
import { installUnpackedExtension, unpackedExtensionDir, } from "./slices/studio/unpacked.js";
|
|
81
83
|
import { previewPictures } from "./slices/style-preview/images.js";
|
|
82
84
|
import { ffmpegStylePreview } from "./slices/style-preview/render.js";
|
|
83
85
|
import { createStylePreviews, stylePreviewDir } from "./slices/style-preview/service.js";
|
|
@@ -496,6 +498,18 @@ export async function boot(config, options = {}) {
|
|
|
496
498
|
void autostart.refresh().catch((error) => {
|
|
497
499
|
log.write("warn", "autostart.refresh", { detail: causedBy(error) });
|
|
498
500
|
});
|
|
501
|
+
// The Chrome extension, unpacked in a folder this install keeps current; loaded from there
|
|
502
|
+
// it reloads itself after each update (`slices/studio/unpacked.ts`). A container's data
|
|
503
|
+
// folder isn't the host's, so there it stays a download.
|
|
504
|
+
const extensionDist = fileURLToPath(new URL("../dist/extension", import.meta.url));
|
|
505
|
+
let unpackedExtension;
|
|
506
|
+
if (!container)
|
|
507
|
+
try {
|
|
508
|
+
unpackedExtension = installUnpackedExtension(join(extensionDist, "slopify-studio-chrome.zip"), unpackedExtensionDir(paths.dataDir));
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
log.write("warn", "studio.extension", { detail: causedBy(error) });
|
|
512
|
+
}
|
|
499
513
|
const app = createApp({
|
|
500
514
|
autostart,
|
|
501
515
|
rebuild,
|
|
@@ -541,7 +555,8 @@ export async function boot(config, options = {}) {
|
|
|
541
555
|
log,
|
|
542
556
|
version,
|
|
543
557
|
webDist: fileURLToPath(new URL("../dist/web", import.meta.url)),
|
|
544
|
-
extensionDist
|
|
558
|
+
extensionDist,
|
|
559
|
+
unpackedExtension,
|
|
545
560
|
flushSoon: flusher.soon,
|
|
546
561
|
probe: nodeCliProbe,
|
|
547
562
|
hostCliStatus: hostCli?.status,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Slopify 3.15.0
|
|
2
|
+
|
|
3
|
+
Released 9 October 2026.
|
|
4
|
+
|
|
5
|
+
- **The Studio extension updates itself with Slopify (Chrome, Edge, Brave).** Slopify now keeps the extension in a folder of its own and puts each new version there when it updates. Load the extension once from that folder (Settings → YouTube Studio shows it, with Copy), and from then on it switches to the new version by itself, once you haven't used it for 45 minutes, so a fill is never interrupted. No more downloading, unzipping and reloading after every update, and no waiting for the store. The store version keeps updating through the store as before. (Extension 1.3.3.)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Slopify 3.16.0
|
|
2
|
+
|
|
3
|
+
Released 9 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Free space from inside Slopify.** Settings → Backup & storage → Disk space now shows each project's old versions (its History folder) next to its outputs and working files, and offers:
|
|
6
|
+
- **Delete old versions** on each project, and on a finished project's page under Free space: removes earlier renders and narration, and keeps the current version.
|
|
7
|
+
- **Delete all old versions**, for every project at once.
|
|
8
|
+
- **Clean up everything**: old versions for every project, and the working files of every finished one (Keep outputs only). Projects that are running or not finished keep everything.
|
|
9
|
+
|
|
10
|
+
Each asks first and says how much it frees. Your uploads and anything you put in History yourself are never removed. Going back to an old version, or changing a project later, makes those files again.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.16.0",
|
|
4
|
+
"title": "Slopify 3.16.0",
|
|
5
|
+
"version": "3.16.0",
|
|
6
|
+
"date": "2026-10-09"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.15.0",
|
|
10
|
+
"title": "Slopify 3.15.0",
|
|
11
|
+
"version": "3.15.0",
|
|
12
|
+
"date": "2026-10-09"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.14.2",
|
|
4
16
|
"title": "Slopify 3.14.2",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, rmdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { derive } from "../../kernel/runner/graph.js";
|
|
4
|
-
import { projectExists, projectPaused, stagesOf } from "../admission/repo.js";
|
|
4
|
+
import { liveProject, projectExists, projectPaused, stagesOf } from "../admission/repo.js";
|
|
5
|
+
import { projectSettled } from "./arrange.js";
|
|
5
6
|
import { backupsFolderName, outputPath, projectDir } from "./layout.js";
|
|
6
7
|
import { pieceFile } from "./reconcile.js";
|
|
7
8
|
// Keep outputs only: a finished project drops the working files it was made from (images,
|
|
@@ -37,7 +38,14 @@ export function projectStorage(deps, projectId) {
|
|
|
37
38
|
let workingBytes = 0;
|
|
38
39
|
let removableFiles = 0;
|
|
39
40
|
let removableBytes = 0;
|
|
41
|
+
const history = historyPaths(deps, projectId);
|
|
42
|
+
let historyFiles = 0;
|
|
43
|
+
let historyBytes = 0;
|
|
40
44
|
for (const [path, bytes] of files) {
|
|
45
|
+
if (history.has(path)) {
|
|
46
|
+
historyFiles += 1;
|
|
47
|
+
historyBytes += bytes;
|
|
48
|
+
}
|
|
41
49
|
if (outputs.has(path))
|
|
42
50
|
outputsBytes += bytes;
|
|
43
51
|
else
|
|
@@ -52,9 +60,110 @@ export function projectStorage(deps, projectId) {
|
|
|
52
60
|
workingBytes,
|
|
53
61
|
removableFiles,
|
|
54
62
|
removableBytes,
|
|
63
|
+
historyFiles,
|
|
64
|
+
historyBytes,
|
|
55
65
|
finished: finished(deps, projectId),
|
|
56
66
|
};
|
|
57
67
|
}
|
|
68
|
+
// Delete old versions: the files of older versions that arranging moved to History/, by their
|
|
69
|
+
// stored paths. Never a file the user supplied, nor one they put in History themselves. Going
|
|
70
|
+
// back to such a version makes its files again.
|
|
71
|
+
function historyPaths(deps, projectId) {
|
|
72
|
+
const supplied = suppliedPaths(deps.db, projectId);
|
|
73
|
+
const out = new Set();
|
|
74
|
+
for (const [path, place] of placedFiles(deps.paths, projectId))
|
|
75
|
+
if (place.startsWith("History/") && !supplied.has(path))
|
|
76
|
+
out.add(path);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function placedFiles(paths, projectId) {
|
|
80
|
+
const places = paths.places;
|
|
81
|
+
return places !== undefined && "placesOf" in places
|
|
82
|
+
? places.placesOf(projectId)
|
|
83
|
+
: new Map();
|
|
84
|
+
}
|
|
85
|
+
export function deleteOldVersions(deps, projectId) {
|
|
86
|
+
if (!projectExists(deps.db, projectId))
|
|
87
|
+
return { ok: false, reason: "no-project" };
|
|
88
|
+
if (deps.hasInflight?.(projectId) === true || !projectSettled(deps.db, projectId))
|
|
89
|
+
return { ok: false, reason: "busy" };
|
|
90
|
+
const files = filesOf(deps.paths, projectId);
|
|
91
|
+
let count = 0;
|
|
92
|
+
let bytesFreed = 0;
|
|
93
|
+
for (const path of historyPaths(deps, projectId)) {
|
|
94
|
+
const bytes = files.get(path);
|
|
95
|
+
if (bytes === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
unlinkSync(outputPath(deps.paths, projectId, path));
|
|
98
|
+
count += 1;
|
|
99
|
+
bytesFreed += bytes;
|
|
100
|
+
}
|
|
101
|
+
removeEmptyUnder(join(projectDir(deps.paths, projectId), "History"));
|
|
102
|
+
return { ok: true, files: count, bytesFreed };
|
|
103
|
+
}
|
|
104
|
+
// Every project's old versions, skipping any that runs or waits to run.
|
|
105
|
+
export function deleteAllOldVersions(deps) {
|
|
106
|
+
const busyIds = [];
|
|
107
|
+
let projects = 0;
|
|
108
|
+
let files = 0;
|
|
109
|
+
let bytesFreed = 0;
|
|
110
|
+
let busy = 0;
|
|
111
|
+
for (const row of deps.db.prepare(`SELECT id FROM projects WHERE ${liveProject()}`).all()) {
|
|
112
|
+
const id = String(row.id);
|
|
113
|
+
if (placedFiles(deps.paths, id).size === 0)
|
|
114
|
+
continue;
|
|
115
|
+
const result = deleteOldVersions(deps, id);
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
if (result.reason === "busy") {
|
|
118
|
+
busy += 1;
|
|
119
|
+
busyIds.push(id);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (result.files > 0)
|
|
124
|
+
projects += 1;
|
|
125
|
+
files += result.files;
|
|
126
|
+
bytesFreed += result.bytesFreed;
|
|
127
|
+
}
|
|
128
|
+
return { projects, files, bytesFreed, busy, busyIds };
|
|
129
|
+
}
|
|
130
|
+
// Clean up: every project's old versions, and the working files of every finished one (Keep
|
|
131
|
+
// outputs only). What runs or waits to run is left as it is.
|
|
132
|
+
export function cleanUpAll(deps) {
|
|
133
|
+
const old = deleteAllOldVersions(deps);
|
|
134
|
+
let files = old.files;
|
|
135
|
+
let bytesFreed = old.bytesFreed;
|
|
136
|
+
let skipped = old.busy;
|
|
137
|
+
for (const row of deps.db.prepare(`SELECT id FROM projects WHERE ${liveProject()}`).all()) {
|
|
138
|
+
const id = String(row.id);
|
|
139
|
+
const result = keepOutputsOnly(deps, id);
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
files += result.files;
|
|
142
|
+
bytesFreed += result.bytesFreed;
|
|
143
|
+
}
|
|
144
|
+
else if (result.reason === "not-finished" && !old.busyIds.includes(id))
|
|
145
|
+
skipped += 1;
|
|
146
|
+
}
|
|
147
|
+
return { files, bytesFreed, skipped };
|
|
148
|
+
}
|
|
149
|
+
// The folders under `root` left empty, deepest first, and `root` itself if it is empty then.
|
|
150
|
+
function removeEmptyUnder(root) {
|
|
151
|
+
if (!existsSync(root))
|
|
152
|
+
return;
|
|
153
|
+
const walk = (dir) => {
|
|
154
|
+
let empty = true;
|
|
155
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
156
|
+
if (entry.isDirectory() && walk(join(dir, entry.name))) {
|
|
157
|
+
rmdirSync(join(dir, entry.name));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
empty = false;
|
|
161
|
+
}
|
|
162
|
+
return empty;
|
|
163
|
+
};
|
|
164
|
+
if (walk(root))
|
|
165
|
+
rmdirSync(root);
|
|
166
|
+
}
|
|
58
167
|
export function keepOutputsOnly(deps, projectId) {
|
|
59
168
|
if (!projectExists(deps.db, projectId))
|
|
60
169
|
return { ok: false, reason: "no-project" };
|
|
@@ -114,7 +223,11 @@ function outputPaths(db, projectId) {
|
|
|
114
223
|
// The outputs, and every file the user supplied: an asset any saved revision or the project's
|
|
115
224
|
// settings name, or a legacy output row that came from an upload.
|
|
116
225
|
function keptPaths(db, projectId) {
|
|
117
|
-
|
|
226
|
+
return new Set([...outputPaths(db, projectId), ...suppliedPaths(db, projectId)]);
|
|
227
|
+
}
|
|
228
|
+
// The files the user supplied: nothing could make them again, so nothing removes them.
|
|
229
|
+
function suppliedPaths(db, projectId) {
|
|
230
|
+
const kept = new Set();
|
|
118
231
|
for (const path of pathsOf(db
|
|
119
232
|
.prepare(`SELECT a.path FROM project_assets a WHERE a.project_id=? AND (
|
|
120
233
|
EXISTS(SELECT 1 FROM project_revisions r WHERE r.project_id=a.project_id
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, sep } from "node:path";
|
|
3
|
+
import { strFromU8, unzipSync } from "fflate";
|
|
4
|
+
export function unpackedExtensionDir(dataDir) {
|
|
5
|
+
return join(dataDir, "extension", "chrome");
|
|
6
|
+
}
|
|
7
|
+
// Puts the zip's build in `target` when the version there differs. The new build is written
|
|
8
|
+
// beside it and swapped in whole, so Chrome never reads half of one and half of another.
|
|
9
|
+
export function installUnpackedExtension(zipPath, target) {
|
|
10
|
+
const files = unzipSync(readFileSync(zipPath));
|
|
11
|
+
const manifest = files["manifest.json"];
|
|
12
|
+
if (manifest === undefined)
|
|
13
|
+
throw new Error(`The Slopify Studio extension bundled with Slopify has no manifest.json (${zipPath}). Update or reinstall Slopify.`);
|
|
14
|
+
const version = String(JSON.parse(strFromU8(manifest)).version);
|
|
15
|
+
if (installedVersion(target) === version)
|
|
16
|
+
return { path: target, version };
|
|
17
|
+
const next = `${target}.next`;
|
|
18
|
+
const old = `${target}.old`;
|
|
19
|
+
rmSync(next, { recursive: true, force: true });
|
|
20
|
+
rmSync(old, { recursive: true, force: true });
|
|
21
|
+
for (const [name, bytes] of Object.entries(files)) {
|
|
22
|
+
if (name.endsWith("/"))
|
|
23
|
+
continue;
|
|
24
|
+
const path = normalize(name);
|
|
25
|
+
if (isAbsolute(path) || path === ".." || path.startsWith(`..${sep}`))
|
|
26
|
+
throw new Error(`The Slopify Studio extension bundled with Slopify has a file outside its folder (${name}). Update or reinstall Slopify.`);
|
|
27
|
+
mkdirSync(dirname(join(next, path)), { recursive: true });
|
|
28
|
+
writeFileSync(join(next, path), bytes, { mode: 0o644 });
|
|
29
|
+
}
|
|
30
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
31
|
+
if (existsSync(target))
|
|
32
|
+
renameSync(target, old);
|
|
33
|
+
renameSync(next, target);
|
|
34
|
+
rmSync(old, { recursive: true, force: true });
|
|
35
|
+
return { path: target, version };
|
|
36
|
+
}
|
|
37
|
+
export function installedVersion(target) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(join(target, "manifest.json"), "utf8"));
|
|
40
|
+
return typeof parsed.version === "string" ? parsed.version : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -142,6 +142,16 @@ If you change that project later (edit an image, a caption style or the narratio
|
|
|
142
142
|
|
|
143
143
|
A finished project's own page offers the same in its right rail: **Free space** with **Free 1.2 GB: keep the outputs, drop the working files** (see [Project Page](Project-Page#free-space)).
|
|
144
144
|
|
|
145
|
+
### Delete old versions and Clean up everything
|
|
146
|
+
|
|
147
|
+
**Delete old versions** removes a project's `History/` folder: the files of earlier versions, such as a previous render or the narration before a voice change. The current version in `Upload/` and `Working/`, your uploads and anything you put in History yourself stay. Going back to one of those versions makes its files again, which takes time and provider credits.
|
|
148
|
+
|
|
149
|
+
- **One project:** the **Delete old versions** button on its row in **Disk space**, or under **Free space** on its project page. It says how much it frees.
|
|
150
|
+
- **Every project:** **Delete all old versions** above the **Disk space** list.
|
|
151
|
+
- **Clean up everything** does Delete old versions for every project and **Keep outputs only** for every finished one. Projects that are running or not finished keep everything.
|
|
152
|
+
|
|
153
|
+
Each asks first. Deleting the files in your file manager works the same way.
|
|
154
|
+
|
|
145
155
|
### Clear leftover files
|
|
146
156
|
|
|
147
157
|
**Clear leftover files** deletes files in the projects folder that no project records any more (never anything in a project's `Upload/`, `Working/` or `History/`, and never a folder Slopify did not make), and uploaded files that no draft, template or project uses. Your projects, outputs and library are never touched. Slopify also does this each time it starts; use it after a crash or to free space now.
|