@praeko/cli 0.0.0-stage → 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/arauto.mjs +11 -0
- package/dist/chunks/adaptadores-X3NUBMLT.mjs +18 -0
- package/dist/chunks/buffer-5TUDOBVN.mjs +14 -0
- package/dist/chunks/chunk-6YARHJZ2.mjs +56 -0
- package/dist/chunks/chunk-IZEX746E.mjs +238 -0
- package/dist/chunks/chunk-LCFK6X55.mjs +16 -0
- package/dist/chunks/chunk-LLXYUCTT.mjs +184 -0
- package/dist/chunks/chunk-QDZLRBTP.mjs +1125 -0
- package/dist/chunks/chunk-SAHLBARV.mjs +415 -0
- package/dist/chunks/chunk-UK3UKRPR.mjs +363 -0
- package/dist/chunks/chunk-XQO6MI7C.mjs +851 -0
- package/dist/chunks/chunk-Z325UM27.mjs +34 -0
- package/dist/chunks/instagram-GNY73ESY.mjs +293 -0
- package/dist/chunks/instagram-meta-COY6BTNX.mjs +412 -0
- package/dist/chunks/local-EUJHQ76T.mjs +14 -0
- package/dist/chunks/registro-FL2CVMYM.mjs +12 -0
- package/dist/chunks/remoto-OAB5Y2MS.mjs +13 -0
- package/dist/chunks/tiktok-QOUHOWDU.mjs +311 -0
- package/dist/chunks/tiktok-api-36DV2H43.mjs +309 -0
- package/dist/chunks/youtube-RZ4XTUZW.mjs +199 -0
- package/dist/chunks/youtube-analytics-3KIW2ZIW.mjs +109 -0
- package/dist/chunks/youtube-api-Z2RP5D2V.mjs +94 -0
- package/dist/chunks/youtube-comunidade-OAK5GGK4.mjs +25 -0
- package/dist/praeko.mjs +11 -0
- package/package.json +48 -4
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../core/src/provedores/youtube-api.mjs
|
|
4
|
+
var nome = "youtube";
|
|
5
|
+
var rede = "youtube";
|
|
6
|
+
var imediato = true;
|
|
7
|
+
var REF_CLIENTE = "youtube/cliente";
|
|
8
|
+
var REF_TOKEN = "youtube/token";
|
|
9
|
+
var ESCOPOS_BASE = ["https://www.googleapis.com/auth/youtube.upload", "https://www.googleapis.com/auth/youtube.force-ssl"];
|
|
10
|
+
var ESCOPO_ANALYTICS = "https://www.googleapis.com/auth/yt-analytics.readonly";
|
|
11
|
+
var ESCOPOS = [...ESCOPOS_BASE, ESCOPO_ANALYTICS];
|
|
12
|
+
var AUTH = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
13
|
+
var TOKEN = "https://oauth2.googleapis.com/token";
|
|
14
|
+
var REVOGAR = "https://oauth2.googleapis.com/revoke";
|
|
15
|
+
var API = "https://www.googleapis.com/youtube/v3";
|
|
16
|
+
var UP = "https://www.googleapis.com/upload/youtube/v3";
|
|
17
|
+
var BLOCO = 8 * 1024 * 1024;
|
|
18
|
+
var LIMITE_MINIATURA = 2 * 1024 * 1024;
|
|
19
|
+
var TENTATIVAS = 5;
|
|
20
|
+
var b64url = (bytes) => {
|
|
21
|
+
const u = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
22
|
+
let s = "";
|
|
23
|
+
for (const b of u) s += String.fromCharCode(b);
|
|
24
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
+
};
|
|
26
|
+
var aleatorioPadrao = (n) => crypto.getRandomValues(new Uint8Array(n));
|
|
27
|
+
async function desafioPkceAsync(verificador) {
|
|
28
|
+
return b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verificador)));
|
|
29
|
+
}
|
|
30
|
+
var dormirPadrao = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
var bytesDe = (x) => x instanceof Uint8Array ? x : new Uint8Array(x);
|
|
32
|
+
function deps(ctx) {
|
|
33
|
+
const d = ctx.deps || {};
|
|
34
|
+
return {
|
|
35
|
+
...d,
|
|
36
|
+
dormir: d.dormir || dormirPadrao,
|
|
37
|
+
agora: d.agora || (() => Date.now()),
|
|
38
|
+
aleatorio: d.aleatorio || aleatorioPadrao,
|
|
39
|
+
abrirArquivo: d.abrirArquivo || (ctx.midia ? (ref) => ctx.midia.abrir(ref) : () => {
|
|
40
|
+
throw new Error("sem acesso a arquivo: o contexto precisa de ctx.midia.abrir (ou ctx.deps.abrirArquivo)");
|
|
41
|
+
}),
|
|
42
|
+
converterImagem: d.converterImagem || (() => {
|
|
43
|
+
throw new Error("a capa passa de 2 MB e n\xE3o h\xE1 convers\xE3o neste ambiente; envie a capa j\xE1 em JPEG/PNG menor");
|
|
44
|
+
})
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function mensagemErro(status2, corpo) {
|
|
48
|
+
let j = null;
|
|
49
|
+
try {
|
|
50
|
+
j = JSON.parse(corpo);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
const e = j?.error, razao = e?.errors?.[0]?.reason || e?.status || "", msg = e?.message || String(corpo || "").slice(0, 200);
|
|
54
|
+
if (/quotaExceeded|rateLimitExceeded|dailyLimitExceeded/i.test(razao) || /quota/i.test(msg)) return `cota da API do YouTube esgotada (${razao || status2}): uploads custam 1 unidade e h\xE1 um teto di\xE1rio; tente depois da meia-noite do Pac\xEDfico`;
|
|
55
|
+
if (/uploadLimitExceeded/i.test(razao)) return "o canal atingiu o limite de uploads do dia";
|
|
56
|
+
if (/forbidden|insufficientPermissions/i.test(razao) || status2 === 403) return `YouTube negou (403 ${razao}): ${msg}. Confira os escopos (praeko conta youtube conectar) e se o canal est\xE1 verificado para miniaturas`;
|
|
57
|
+
if (status2 === 401) return "token recusado (401): rode `praeko conta youtube conectar` de novo";
|
|
58
|
+
return `YouTube ${status2} ${razao}: ${msg}`;
|
|
59
|
+
}
|
|
60
|
+
var falha = (status2, corpo) => Object.assign(new Error(mensagemErro(status2, corpo)), { status: status2 });
|
|
61
|
+
async function cliente(ctx, args = []) {
|
|
62
|
+
const f = (k) => {
|
|
63
|
+
const i = args.indexOf("--" + k);
|
|
64
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
65
|
+
};
|
|
66
|
+
const id = f("cliente-id"), segredo = f("cliente-segredo");
|
|
67
|
+
if (id && segredo) return { client_id: id, client_secret: segredo };
|
|
68
|
+
const salvo = await ctx.segredos.obter(REF_CLIENTE);
|
|
69
|
+
if (salvo) {
|
|
70
|
+
const j = JSON.parse(salvo);
|
|
71
|
+
if (j.client_id && j.client_secret) return j;
|
|
72
|
+
}
|
|
73
|
+
throw new Error("sem credencial OAuth do Google: praeko conta youtube conectar --cliente-id <id> --cliente-segredo <segredo> (veja docs/youtube.md)");
|
|
74
|
+
}
|
|
75
|
+
function urlAutorizacao({ clientId, redirect, desafio, estado }) {
|
|
76
|
+
const q = new URLSearchParams({ client_id: clientId, redirect_uri: redirect, response_type: "code", scope: ESCOPOS.join(" "), code_challenge: desafio, code_challenge_method: "S256", state: estado, access_type: "offline", prompt: "consent" });
|
|
77
|
+
return `${AUTH}?${q}`;
|
|
78
|
+
}
|
|
79
|
+
async function trocarCodigo(ctx, cl, { codigo, verificador, redirect }) {
|
|
80
|
+
const r = await ctx.fetchFn(TOKEN, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ code: codigo, client_id: cl.client_id, client_secret: cl.client_secret, code_verifier: verificador, redirect_uri: redirect, grant_type: "authorization_code" }).toString() });
|
|
81
|
+
const j = await r.json();
|
|
82
|
+
if (!r.ok) throw new Error(`Google recusou o c\xF3digo: ${j.error_description || j.error}`);
|
|
83
|
+
if (!j.refresh_token) throw new Error("o Google n\xE3o devolveu refresh_token (revogue o acesso em myaccount.google.com/permissions e conecte de novo)");
|
|
84
|
+
return j;
|
|
85
|
+
}
|
|
86
|
+
function registroToken(j, agora) {
|
|
87
|
+
return JSON.stringify({ access_token: j.access_token, refresh_token: j.refresh_token, expiry: agora + (j.expires_in || 3600) * 1e3, escopos: (j.scope || ESCOPOS_BASE.join(" ")).split(" ") });
|
|
88
|
+
}
|
|
89
|
+
async function tokenValido(ctx) {
|
|
90
|
+
const d = deps(ctx), bruto = await ctx.segredos.obter(REF_TOKEN);
|
|
91
|
+
if (!bruto) throw new Error("YouTube n\xE3o conectado: praeko conta youtube conectar");
|
|
92
|
+
const t = JSON.parse(bruto);
|
|
93
|
+
if (t.access_token && t.expiry - 6e4 > d.agora()) return t.access_token;
|
|
94
|
+
const cl = await cliente(ctx);
|
|
95
|
+
const r = await ctx.fetchFn(TOKEN, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: cl.client_id, client_secret: cl.client_secret, refresh_token: t.refresh_token, grant_type: "refresh_token" }).toString() });
|
|
96
|
+
const j = await r.json();
|
|
97
|
+
if (!r.ok) throw new Error(j.error === "invalid_grant" ? "o Google revogou o refresh token (app em modo Testing expira em 7 dias): rode `praeko conta youtube conectar` e publique o app como Em produ\xE7\xE3o (docs/youtube.md)" : `n\xE3o consegui renovar o token: ${j.error_description || j.error}`);
|
|
98
|
+
const novo = { ...t, access_token: j.access_token, expiry: d.agora() + (j.expires_in || 3600) * 1e3, ...j.refresh_token ? { refresh_token: j.refresh_token } : {} };
|
|
99
|
+
await ctx.segredos.guardar(REF_TOKEN, JSON.stringify(novo));
|
|
100
|
+
return novo.access_token;
|
|
101
|
+
}
|
|
102
|
+
async function revogar(ctx) {
|
|
103
|
+
try {
|
|
104
|
+
const bruto = await ctx.segredos.obter(REF_TOKEN);
|
|
105
|
+
if (!bruto) return false;
|
|
106
|
+
const t = JSON.parse(bruto), token = t.refresh_token || t.access_token;
|
|
107
|
+
if (!token) return false;
|
|
108
|
+
const r = await ctx.fetchFn(REVOGAR, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token }).toString() });
|
|
109
|
+
return r.ok;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function api(ctx, url, opcoes = {}) {
|
|
115
|
+
const token = await tokenValido(ctx);
|
|
116
|
+
return ctx.fetchFn(url, { ...opcoes, headers: { authorization: `Bearer ${token}`, ...opcoes.headers || {} } });
|
|
117
|
+
}
|
|
118
|
+
async function status({ ctx }) {
|
|
119
|
+
if (!await ctx.segredos.obter(REF_TOKEN)) return { conectado: false, detalhe: "rode: praeko conta youtube conectar --cliente-id \u2026 --cliente-segredo \u2026" };
|
|
120
|
+
try {
|
|
121
|
+
const r = await api(ctx, `${API}/channels?part=snippet&mine=true`);
|
|
122
|
+
const txt = await r.text();
|
|
123
|
+
if (!r.ok) return { conectado: false, detalhe: mensagemErro(r.status, txt) };
|
|
124
|
+
const c = JSON.parse(txt).items?.[0];
|
|
125
|
+
return c ? { conectado: true, detalhe: c.snippet.title, ...c.id ? { canalId: c.id } : {} } : { conectado: false, detalhe: "a conta autorizada n\xE3o tem canal do YouTube" };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { conectado: false, detalhe: e.message };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function modoDe(item) {
|
|
131
|
+
const m = item.opcoes?.modo || "privado";
|
|
132
|
+
if (!["privado", "agendado", "publico"].includes(m)) throw new Error(`modo do YouTube inv\xE1lido: ${m} (use privado, agendado ou publico)`);
|
|
133
|
+
return m;
|
|
134
|
+
}
|
|
135
|
+
function montarMetadados(item, agora = Date.now()) {
|
|
136
|
+
const o = item.opcoes || {}, modo = modoDe(item);
|
|
137
|
+
let titulo = item.textos?.titulo || item.titulo || item.slug;
|
|
138
|
+
if (titulo.length > 100) titulo = titulo.slice(0, 99).trimEnd() + "\u2026";
|
|
139
|
+
const status2 = { privacyStatus: modo === "publico" ? "public" : "private", selfDeclaredMadeForKids: false, embeddable: true, publicStatsViewable: true };
|
|
140
|
+
if (modo === "agendado") {
|
|
141
|
+
const t = Date.parse(item.quando);
|
|
142
|
+
if (!(t > agora + 6e4)) throw new Error(`modo agendado: ${item.quando} j\xE1 passou ou \xE9 daqui a menos de 1 minuto`);
|
|
143
|
+
status2.publishAt = new Date(t).toISOString();
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
snippet: { title: titulo, description: item.textos?.texto || "", categoryId: String(o.categoriaId || "28"), defaultLanguage: "pt", defaultAudioLanguage: "pt", ...o.tags ? { tags: o.tags } : {} },
|
|
147
|
+
status: status2
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
var dormir = (ms, d) => d.dormir(ms);
|
|
151
|
+
var lerRange = (rg) => rg ? Number(rg.split("-")[1]) + 1 : 0;
|
|
152
|
+
async function iniciarUpload({ ctx, total, metadados, avisarInscritos = false }) {
|
|
153
|
+
const q = new URLSearchParams({ uploadType: "resumable", part: "snippet,status", notifySubscribers: String(!!avisarInscritos) });
|
|
154
|
+
const ini = await api(ctx, `${UP}/videos?${q}`, { method: "POST", headers: { "content-type": "application/json; charset=UTF-8", "x-upload-content-length": String(total), "x-upload-content-type": "video/mp4" }, body: JSON.stringify(metadados) });
|
|
155
|
+
if (!ini.ok) throw falha(ini.status, await ini.text());
|
|
156
|
+
const sessao = ini.headers.get("location");
|
|
157
|
+
if (!sessao) throw new Error("o YouTube n\xE3o devolveu o endere\xE7o da sess\xE3o de upload");
|
|
158
|
+
return { sessao, total };
|
|
159
|
+
}
|
|
160
|
+
async function sincronizarUpload({ ctx, sessao, total }) {
|
|
161
|
+
const r = await ctx.fetchFn(sessao, { method: "PUT", headers: { "content-length": "0", "content-range": `bytes */${total}` } });
|
|
162
|
+
if (r.status === 200 || r.status === 201) return { fim: true, video: await r.json() };
|
|
163
|
+
if (r.status === 308) return { fim: false, pos: lerRange(r.headers.get("range")) };
|
|
164
|
+
if (r.status === 404) throw Object.assign(new Error("a sess\xE3o de upload expirou (404): refa\xE7a o upload"), { sessaoExpirada: true });
|
|
165
|
+
throw Object.assign(falha(r.status, await r.text()), { transitorio: r.status >= 500 });
|
|
166
|
+
}
|
|
167
|
+
async function enviarBloco({ ctx, f, sessao, total, pos, sincronizar = true }) {
|
|
168
|
+
if (sincronizar) {
|
|
169
|
+
const s = await sincronizarUpload({ ctx, sessao, total });
|
|
170
|
+
if (s.fim) return s;
|
|
171
|
+
pos = s.pos;
|
|
172
|
+
}
|
|
173
|
+
if (pos >= total) return { fim: false, pos };
|
|
174
|
+
const bloco = bytesDe(await f.ler(pos, BLOCO)), fim = pos + bloco.length - 1;
|
|
175
|
+
let r;
|
|
176
|
+
try {
|
|
177
|
+
r = await ctx.fetchFn(sessao, { method: "PUT", headers: { "content-length": String(bloco.length), "content-range": `bytes ${pos}-${fim}/${total}`, "content-type": "video/mp4" }, body: bloco });
|
|
178
|
+
} catch (e) {
|
|
179
|
+
throw Object.assign(new Error(`falha de rede no upload: ${e.message}`), { transitorio: true });
|
|
180
|
+
}
|
|
181
|
+
if (r.status === 200 || r.status === 201) return { fim: true, video: await r.json() };
|
|
182
|
+
if (r.status === 308) return { fim: false, pos: lerRange(r.headers.get("range")) };
|
|
183
|
+
if ([500, 502, 503, 504].includes(r.status)) throw Object.assign(new Error(`YouTube ${r.status} no upload`), { transitorio: true });
|
|
184
|
+
throw falha(r.status, await r.text());
|
|
185
|
+
}
|
|
186
|
+
async function enviarVideo({ ctx, arquivo, metadados, avisarInscritos = false }) {
|
|
187
|
+
const d = deps(ctx), f = await d.abrirArquivo(arquivo), total = f.tamanho;
|
|
188
|
+
try {
|
|
189
|
+
const { sessao } = await iniciarUpload({ ctx, total, metadados, avisarInscritos });
|
|
190
|
+
let pos = 0, falhas = 0;
|
|
191
|
+
while (pos < total) {
|
|
192
|
+
try {
|
|
193
|
+
const r = await enviarBloco({ ctx, f, sessao, total, pos, sincronizar: false });
|
|
194
|
+
if (r.fim) return r.video;
|
|
195
|
+
pos = r.pos;
|
|
196
|
+
falhas = 0;
|
|
197
|
+
} catch (e) {
|
|
198
|
+
if (!e.transitorio) throw e;
|
|
199
|
+
if (++falhas > TENTATIVAS) throw new Error(`upload interrompido depois de ${TENTATIVAS} tentativas (${e.message})`);
|
|
200
|
+
await dormir(Math.min(3e4, 1e3 * 2 ** falhas), d);
|
|
201
|
+
const s = await sincronizarUpload({ ctx, sessao, total }).catch(() => null);
|
|
202
|
+
if (s?.fim) return s.video;
|
|
203
|
+
if (s) pos = s.pos;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
throw new Error("o upload terminou sem resposta do YouTube");
|
|
207
|
+
} finally {
|
|
208
|
+
f.fechar?.();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function esperarProcessamento(ctx, id, d = deps(ctx), tentativas = 12) {
|
|
212
|
+
for (let i = 0; i < tentativas; i++) {
|
|
213
|
+
const r = await api(ctx, `${API}/videos?part=processingDetails&id=${id}`);
|
|
214
|
+
if (r.ok) {
|
|
215
|
+
const s = (await r.json()).items?.[0]?.processingDetails?.processingStatus;
|
|
216
|
+
if (s === "succeeded") return true;
|
|
217
|
+
if (s === "failed" || s === "terminated") return false;
|
|
218
|
+
}
|
|
219
|
+
await d.dormir(1e4);
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
var sniffMime = (b) => b[0] === 137 && b[1] === 80 ? "image/png" : b[0] === 255 && b[1] === 216 ? "image/jpeg" : null;
|
|
224
|
+
var MIME_EXT = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg" };
|
|
225
|
+
async function definirMiniatura(ctx, id, capa, d = deps(ctx)) {
|
|
226
|
+
let arq = capa, f = await d.abrirArquivo(arq);
|
|
227
|
+
if (f.tamanho > LIMITE_MINIATURA) {
|
|
228
|
+
f.fechar?.();
|
|
229
|
+
arq = await d.converterImagem(arq);
|
|
230
|
+
f = await d.abrirArquivo(arq);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const corpo = bytesDe(await f.ler(0, f.tamanho));
|
|
234
|
+
const ext = /\.([a-z0-9]+)$/i.exec(String(arq))?.[1]?.toLowerCase();
|
|
235
|
+
const mime = f.mime || MIME_EXT[ext] || sniffMime(corpo) || "image/jpeg";
|
|
236
|
+
const r = await api(ctx, `${UP}/thumbnails/set?videoId=${id}`, { method: "POST", headers: { "content-type": mime, "content-length": String(corpo.length) }, body: corpo });
|
|
237
|
+
if (!r.ok) throw falha(r.status, await r.text());
|
|
238
|
+
return true;
|
|
239
|
+
} finally {
|
|
240
|
+
f.fechar?.();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function acoesSeguintes(item, id, metadados, d) {
|
|
244
|
+
const texto = item.textos?.comentarioFixo;
|
|
245
|
+
if (!texto || item.opcoes?.comentar === false) return [];
|
|
246
|
+
const base = { slug: item.slug, rede: "youtube", provedor: "youtube", acao: "comentar", videoId: id, textos: { comentarioFixo: texto }, opcoes: {}, video: item.video, pedido: item.pedido };
|
|
247
|
+
const modo = modoDe(item);
|
|
248
|
+
if (modo === "agendado") return [{ ...base, quando: new Date(Date.parse(metadados.status.publishAt) + 2 * 6e4).toISOString() }];
|
|
249
|
+
if (modo === "publico") return [{ ...base, quando: new Date(d.agora() + 6e4).toISOString() }];
|
|
250
|
+
return [{ ...base, quando: null, manual: true }];
|
|
251
|
+
}
|
|
252
|
+
async function finalizarPublicacao({ item, ctx, video, metadados }) {
|
|
253
|
+
const d = deps(ctx), avisos = [], id = video.id;
|
|
254
|
+
let miniatura = false;
|
|
255
|
+
if (item.capa) {
|
|
256
|
+
try {
|
|
257
|
+
const pronto = await esperarProcessamento(ctx, id, d);
|
|
258
|
+
if (!pronto) avisos.push("o v\xEDdeo ainda estava processando; a miniatura pode n\xE3o ter pegado");
|
|
259
|
+
miniatura = await definirMiniatura(ctx, id, item.capa, d);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
avisos.push(`miniatura n\xE3o definida (${e.message}); troque no Studio`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const a of avisos) ctx.log?.(` \u26A0 ${a}`);
|
|
265
|
+
const seguinte = acoesSeguintes(item, id, metadados, d);
|
|
266
|
+
if (seguinte.length && seguinte[0].manual) ctx.log?.(" \u2139 coment\xE1rio fixo guardado: depois de publicar no Studio, rode `praeko fila comentar <id>` (a API n\xE3o comenta em v\xEDdeo privado de forma garantida e n\xE3o fixa coment\xE1rio)");
|
|
267
|
+
return { id, url: `https://youtu.be/${id}`, studio: `https://studio.youtube.com/video/${id}/edit`, privacidade: metadados.status.privacyStatus, publishAt: metadados.status.publishAt, miniatura, avisos, seguinte };
|
|
268
|
+
}
|
|
269
|
+
async function publicar({ item, ctx }) {
|
|
270
|
+
if (item.acao === "comentar") return comentar({ item, ctx });
|
|
271
|
+
if (item.acao === "agendar") return agendarPublicacao({ item, ctx });
|
|
272
|
+
const d = deps(ctx);
|
|
273
|
+
const metadados = montarMetadados(item, d.agora());
|
|
274
|
+
ctx.log?.(` YouTube: enviando ${String(item.video).split("/").pop()} (${metadados.status.privacyStatus})\u2026`);
|
|
275
|
+
const video = await enviarVideo({ ctx, arquivo: item.video, metadados, avisarInscritos: item.opcoes?.avisarInscritos });
|
|
276
|
+
return finalizarPublicacao({ item, ctx, video, metadados });
|
|
277
|
+
}
|
|
278
|
+
async function comentar({ item, ctx }) {
|
|
279
|
+
const canal = await ctx.segredos.obter(REF_TOKEN) && await canalId(ctx);
|
|
280
|
+
const corpo = { snippet: { channelId: canal, videoId: item.videoId, topLevelComment: { snippet: { textOriginal: item.textos.comentarioFixo } } } };
|
|
281
|
+
const r = await api(ctx, `${API}/commentThreads?part=snippet`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(corpo) });
|
|
282
|
+
const txt = await r.text();
|
|
283
|
+
if (!r.ok) throw new Error(mensagemErro(r.status, txt) + " (coment\xE1rio: o v\xEDdeo precisa estar publicado e o token com o escopo youtube.force-ssl; reconecte com `praeko conta youtube conectar`)");
|
|
284
|
+
const id = JSON.parse(txt).id;
|
|
285
|
+
return { id, comentario: true, url: `https://youtu.be/${item.videoId}?lc=${id}`, lembrete: "a API n\xE3o fixa coment\xE1rio: fixe pelo app" };
|
|
286
|
+
}
|
|
287
|
+
async function canalId(ctx) {
|
|
288
|
+
const r = await api(ctx, `${API}/channels?part=id&mine=true`);
|
|
289
|
+
if (!r.ok) throw falha(r.status, await r.text());
|
|
290
|
+
const c = (await r.json()).items?.[0];
|
|
291
|
+
if (!c) throw new Error("a conta autorizada n\xE3o tem canal do YouTube");
|
|
292
|
+
return c.id;
|
|
293
|
+
}
|
|
294
|
+
async function planejar({ item }) {
|
|
295
|
+
if (item.acao === "comentar") return [`coment\xE1rio no v\xEDdeo ${item.videoId}`];
|
|
296
|
+
const modo = modoDe(item), m = montarMetadados(item, Date.now());
|
|
297
|
+
return [
|
|
298
|
+
`t\xEDtulo: ${m.snippet.title}`,
|
|
299
|
+
`modo: ${modo} \u2192 privacidade ${m.status.privacyStatus}${m.status.publishAt ? ` (publica sozinho em ${m.status.publishAt})` : modo === "privado" ? " (voc\xEA troca a capa e publica no Studio)" : ""}`,
|
|
300
|
+
`arquivo: ${item.tamanhoTexto || "?"}; capa: ${item.capa ? String(item.capa).split("/").pop() + " (thumbnails.set)" : "nenhuma"}`
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
async function agendarPublicacao({ item, ctx, agora = Date.now() }) {
|
|
304
|
+
const quando = new Date(item.quando);
|
|
305
|
+
if (Number.isNaN(quando.getTime())) throw new Error(`hor\xE1rio inv\xE1lido: ${item.quando}`);
|
|
306
|
+
if (quando.getTime() < agora + 5 * 6e4) throw new Error("o hor\xE1rio precisa ser daqui a pelo menos 5 minutos");
|
|
307
|
+
const r0 = await api(ctx, `${API}/videos?part=status&id=${encodeURIComponent(item.videoId)}`);
|
|
308
|
+
if (!r0.ok) throw falha(r0.status, await r0.text());
|
|
309
|
+
const v = (await r0.json()).items?.[0];
|
|
310
|
+
if (!v) throw new Error(`o v\xEDdeo ${item.videoId} n\xE3o existe neste canal`);
|
|
311
|
+
const st = v.status || {};
|
|
312
|
+
const status2 = { privacyStatus: "private", publishAt: quando.toISOString(), embeddable: st.embeddable ?? true, license: st.license || "youtube", publicStatsViewable: st.publicStatsViewable ?? true, selfDeclaredMadeForKids: st.selfDeclaredMadeForKids ?? false };
|
|
313
|
+
const r = await api(ctx, `${API}/videos?part=status`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: item.videoId, status: status2 }) });
|
|
314
|
+
if (!r.ok) throw falha(r.status, await r.text());
|
|
315
|
+
return { id: item.videoId, publishAt: status2.publishAt, privacidade: "private", url: `https://youtu.be/${item.videoId}` };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export {
|
|
319
|
+
nome,
|
|
320
|
+
rede,
|
|
321
|
+
imediato,
|
|
322
|
+
REF_CLIENTE,
|
|
323
|
+
REF_TOKEN,
|
|
324
|
+
ESCOPOS_BASE,
|
|
325
|
+
ESCOPO_ANALYTICS,
|
|
326
|
+
ESCOPOS,
|
|
327
|
+
AUTH,
|
|
328
|
+
TOKEN,
|
|
329
|
+
REVOGAR,
|
|
330
|
+
API,
|
|
331
|
+
UP,
|
|
332
|
+
BLOCO,
|
|
333
|
+
LIMITE_MINIATURA,
|
|
334
|
+
b64url,
|
|
335
|
+
aleatorioPadrao,
|
|
336
|
+
desafioPkceAsync,
|
|
337
|
+
deps,
|
|
338
|
+
mensagemErro,
|
|
339
|
+
falha,
|
|
340
|
+
cliente,
|
|
341
|
+
urlAutorizacao,
|
|
342
|
+
trocarCodigo,
|
|
343
|
+
registroToken,
|
|
344
|
+
tokenValido,
|
|
345
|
+
revogar,
|
|
346
|
+
api,
|
|
347
|
+
status,
|
|
348
|
+
modoDe,
|
|
349
|
+
montarMetadados,
|
|
350
|
+
iniciarUpload,
|
|
351
|
+
sincronizarUpload,
|
|
352
|
+
enviarBloco,
|
|
353
|
+
enviarVideo,
|
|
354
|
+
esperarProcessamento,
|
|
355
|
+
definirMiniatura,
|
|
356
|
+
acoesSeguintes,
|
|
357
|
+
finalizarPublicacao,
|
|
358
|
+
publicar,
|
|
359
|
+
comentar,
|
|
360
|
+
canalId,
|
|
361
|
+
planejar,
|
|
362
|
+
agendarPublicacao
|
|
363
|
+
};
|