@gentbajko/slopify 3.8.3 → 3.8.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.
@@ -14,7 +14,7 @@ import { readSetting, writeSetting } from "../../slices/settings/repo.js";
14
14
  import { findDownload } from "../../slices/storage/downloads.js";
15
15
  import { backfillVideos } from "../../slices/studio/backfill.js";
16
16
  import { releaseCalendar } from "../../slices/studio/calendar.js";
17
- import { studioPlaylistMax, studioUploadUrl, } from "../../slices/studio/model.js";
17
+ import { playlistUses, studioPlaylistMax, studioUploadUrl, } from "../../slices/studio/model.js";
18
18
  import { packItem, uploadPack } from "../../slices/studio/pack.js";
19
19
  import { channelPerformance } from "../../slices/studio/performance.js";
20
20
  import { writeUploadPick } from "../../slices/studio/pick.js";
@@ -51,6 +51,8 @@ const playlistsBody = z.object({
51
51
  .array(z.object({
52
52
  name: z.string().max(studioPlaylistMax * 2),
53
53
  byDefault: z.boolean(),
54
+ // Long videos, shorts or both (absent: both).
55
+ for: z.enum(playlistUses).optional(),
54
56
  }))
55
57
  .max(studioPlaylistsMax),
56
58
  // A channel's own list; absent is the default every other channel uses.
@@ -0,0 +1,7 @@
1
+ # Slopify 3.8.4
2
+
3
+ Released 4 October 2026.
4
+
5
+ ## YouTube Studio extension (1.1.4)
6
+
7
+ - **One video that won't open no longer stops the daily analytics read.** The extension reads each video's Analytics one tab after another, opening the next when the last reports back. A video Studio couldn't open (one deleted in Studio, say) never reported, so every video after it went unread that day. Now a tab that hasn't reported within four minutes is closed and the next video opens. Download the extension again from Settings → YouTube Studio and reload it.
@@ -0,0 +1,9 @@
1
+ # Slopify 3.8.5
2
+
3
+ Released 4 October 2026.
4
+
5
+ - **Playlists for long videos, shorts or both.** Each playlist in Settings → YouTube Studio → Playlists says what it takes: Videos and shorts, Long videos only, or Shorts only. A channel with a playlist of its own for shorts no longer gets its shorts added to the long videos' playlist.
6
+
7
+ ## YouTube Studio extension (1.1.5)
8
+
9
+ - **Ad suitability is answered for you.** When an upload reaches Studio's Ad suitability questions, the extension ticks None of the above and presses Submit rating, then says so. If Studio's page doesn't show them as expected, it leaves them to you and says what to do. Download the extension again from Settings → YouTube Studio and reload it.
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "id": "3.8.5",
4
+ "title": "Slopify 3.8.5",
5
+ "version": "3.8.5",
6
+ "date": "2026-10-04"
7
+ },
8
+ {
9
+ "id": "3.8.4",
10
+ "title": "Slopify 3.8.4",
11
+ "version": "3.8.4",
12
+ "date": "2026-10-04"
13
+ },
2
14
  {
3
15
  "id": "3.8.3",
4
16
  "title": "Slopify 3.8.3",
@@ -11,6 +11,8 @@ export const studioPlaylistMax = 150;
11
11
  export const studioAudience = "not_made_for_kids";
12
12
  // Studio's upload page. It opens the upload dialog on the signed-in channel.
13
13
  export const studioUploadUrl = "https://www.youtube.com/upload";
14
+ // What a playlist takes: every upload, only long videos, or only shorts.
15
+ export const playlistUses = ["all", "long", "shorts"];
14
16
  // The steps in Studio's order, as the Prepare upload view lists them.
15
17
  export const studioSteps = [
16
18
  "video",
@@ -57,8 +57,14 @@ export function uploadPack(deps, projectId) {
57
57
  const channelId = projectChannelId(deps.db, projectId);
58
58
  // The channel's playlists (Settings → YouTube Studio), ticked as this project chose.
59
59
  const playlistChoices = projectPlaylists(deps.db, projectId, channelId);
60
- const playlists = playlistChoices.filter((one) => one.chosen).map((one) => one.name);
60
+ // The long video goes into the chosen playlists that take long videos, each short into those
61
+ // that take shorts.
62
+ const into = (kind) => playlistChoices
63
+ .filter((one) => one.chosen && (one.for === "all" || one.for === kind))
64
+ .map((one) => one.name);
65
+ const playlists = into("long");
61
66
  const playlist = playlists[0] ?? null;
67
+ const shortPlaylists = into("shorts");
62
68
  const missing = [];
63
69
  // The channel's setting, then what the project narrates and shows (`disclosure.ts`).
64
70
  const setting = channelById(deps.db, channelId)?.aiDisclosure ?? "auto";
@@ -202,8 +208,8 @@ export function uploadPack(deps, projectId) {
202
208
  thumbnails: [],
203
209
  audience: studioAudience,
204
210
  alteredContent: disclosure("short"),
205
- playlists,
206
- playlist,
211
+ playlists: shortPlaylists,
212
+ playlist: shortPlaylists[0] ?? null,
207
213
  ...((at) => (at == null ? {} : { scheduleAt: at }))(schedule?.shortsAt[clip.number - 1]),
208
214
  ...(longVideo?.uploadState === "done" ? { relatedVideoId: longVideo.videoId } : {}),
209
215
  });
@@ -217,7 +223,7 @@ export function uploadPack(deps, projectId) {
217
223
  items,
218
224
  ...(schedule === undefined ? {} : { schedule }),
219
225
  missing,
220
- playlistChoices,
226
+ playlistChoices: playlistChoices.map(({ name, chosen }) => ({ name, chosen })),
221
227
  ...(uploadedClips > 0 ? { footage: { clips: uploadedClips, real: realFootage } } : {}),
222
228
  },
223
229
  };
@@ -1,7 +1,7 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import { readSetting, writeSetting } from "../settings/repo.js";
4
- import { studioPlaylistMax } from "./model.js";
4
+ import { playlistUses, studioPlaylistMax, } from "./model.js";
5
5
  // Rows of the key/value `settings` table. The playlists travel with a backup; the
6
6
  // pairing stays on this machine (`slices/storage/portable.ts`): its token is what lets the
7
7
  // browser extension read a project's upload pack.
@@ -26,6 +26,7 @@ export const studioPlaylistsMax = 20;
26
26
  export const studioPlaylistSchema = z.object({
27
27
  name: z.string().trim().min(1).max(studioPlaylistMax),
28
28
  byDefault: z.boolean(),
29
+ for: z.enum(playlistUses).optional(),
29
30
  });
30
31
  // What a row holds: the list, or the one name a row held before lists.
31
32
  export const storedPlaylistsSchema = z.union([
@@ -90,7 +91,12 @@ export function studioPlaylistsProblem(list) {
90
91
  // the default again). Blank names are dropped.
91
92
  export function saveStudioPlaylists(db, list, channelId) {
92
93
  const value = list
93
- .map((one) => ({ name: one.name.trim(), byDefault: one.byDefault }))
94
+ .map((one) => ({
95
+ name: one.name.trim(),
96
+ byDefault: one.byDefault,
97
+ // "all" is the default, so a list that never narrows stays as it was saved.
98
+ ...(one.for === undefined || one.for === "all" ? {} : { for: one.for }),
99
+ }))
94
100
  .filter((one) => one.name !== "");
95
101
  const key = playlistKey(channelId);
96
102
  if (value.length === 0) {
@@ -123,6 +129,7 @@ export function projectPlaylists(db, projectId, channelId) {
123
129
  const own = projectChoices(db)[projectId];
124
130
  return list.map((one) => ({
125
131
  name: one.name,
132
+ for: one.for ?? "all",
126
133
  chosen: own === undefined
127
134
  ? one.byDefault
128
135
  : own.some((name) => name.toLowerCase() === one.name.toLowerCase()),