@praeko/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,238 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ carregarTodos
4
+ } from "./chunk-VJRMAQCR.mjs";
5
+ import {
6
+ PROVEDORES,
7
+ validarPedido
8
+ } from "./chunk-Z325UM27.mjs";
9
+ import {
10
+ __export,
11
+ __glob
12
+ } from "./chunk-LCFK6X55.mjs";
13
+
14
+ // ../core/src/adaptadores/local.mjs
15
+ var local_exports = {};
16
+ __export(local_exports, {
17
+ MODO_PADRAO: () => MODO_PADRAO,
18
+ criar: () => criar,
19
+ estadoDe: () => estadoDe
20
+ });
21
+
22
+ // ../core/src/fila.mjs
23
+ import { existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
24
+ import { dirname } from "node:path";
25
+ import { randomBytes } from "node:crypto";
26
+
27
+ // ../core/src/estado.mjs
28
+ import { existsSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ var PASTA_ESTADO = ".praeko";
31
+ var PASTA_ESTADO_ANTIGA = ".arauto";
32
+ var pastaEstado = (raiz) => join(raiz, PASTA_ESTADO);
33
+ var caminhoEstado = (raiz, ...nome) => join(raiz, PASTA_ESTADO, ...nome);
34
+ function caminhoLeitura(raiz, ...nome) {
35
+ const novo = caminhoEstado(raiz, ...nome);
36
+ if (existsSync(novo)) return novo;
37
+ const velho = join(raiz, PASTA_ESTADO_ANTIGA, ...nome);
38
+ return existsSync(velho) ? velho : novo;
39
+ }
40
+
41
+ // ../core/src/fila.mjs
42
+ var MAX_TENTATIVAS = 3;
43
+ var arquivoFila = (raiz) => caminhoEstado(raiz, "fila.json");
44
+ function lerFila(raiz) {
45
+ const f = caminhoLeitura(raiz, "fila.json");
46
+ return existsSync2(f) ? JSON.parse(readFileSync(f, "utf8")) : { itens: [] };
47
+ }
48
+ function salvarFila(raiz, fila) {
49
+ const f = arquivoFila(raiz);
50
+ mkdirSync(dirname(f), { recursive: true });
51
+ writeFileSync(f + ".tmp", JSON.stringify(fila, null, 2) + "\n");
52
+ renameSync(f + ".tmp", f);
53
+ }
54
+ function enfileirar(raiz, item) {
55
+ const fila = lerFila(raiz);
56
+ const it = { id: randomBytes(4).toString("hex"), estado: "pendente", tentativas: 0, criadoEm: (/* @__PURE__ */ new Date()).toISOString(), ...item };
57
+ fila.itens.push(it);
58
+ salvarFila(raiz, fila);
59
+ return it;
60
+ }
61
+ function liberarItem(raiz, id) {
62
+ const fila = lerFila(raiz), it = fila.itens.find((x) => x.id === id || x.id.startsWith(id));
63
+ if (!it) throw new Error(`item ${id} n\xE3o existe na fila`);
64
+ if (!it.manual) throw new Error(`item ${it.id} n\xE3o \xE9 manual`);
65
+ it.manual = false;
66
+ it.quando = (/* @__PURE__ */ new Date()).toISOString();
67
+ it.tentativas = 0;
68
+ it.estado = "pendente";
69
+ salvarFila(raiz, fila);
70
+ return it;
71
+ }
72
+ function cancelarItem(raiz, id) {
73
+ const fila = lerFila(raiz), it = fila.itens.find((x) => x.id === id || x.id.startsWith(id));
74
+ if (!it) throw new Error(`item ${id} n\xE3o existe na fila`);
75
+ if (it.estado === "enviado") throw new Error(`item ${it.id} j\xE1 foi enviado; desfa\xE7a pela pr\xF3pria rede`);
76
+ it.estado = "cancelado";
77
+ it.canceladoEm = (/* @__PURE__ */ new Date()).toISOString();
78
+ salvarFila(raiz, fila);
79
+ return it;
80
+ }
81
+ function proximaAcao(it, prov, agora) {
82
+ if (it.manual) return null;
83
+ if (!["pendente", "preparado"].includes(it.estado) || it.tentativas >= MAX_TENTATIVAS) return null;
84
+ const t = Date.parse(it.quando), ant = it.acao ? 0 : prov.antecedenciaMs ?? 0;
85
+ if (prov.imediato && !it.acao) return it.estado === "pendente" ? "publicar" : null;
86
+ if (it.estado === "pendente" && prov.preparar && +agora >= t - ant && +agora < t) return "preparar";
87
+ if (+agora >= t) return "publicar";
88
+ return null;
89
+ }
90
+ async function executar({ raiz, provedores, ctx, agora = /* @__PURE__ */ new Date(), simular = false, soIds = null, log = console.log }) {
91
+ const fila = lerFila(raiz), feitos = [];
92
+ for (const it of fila.itens) {
93
+ if (soIds && !soIds.includes(it.id)) continue;
94
+ const prov = provedores[it.provedor];
95
+ if (!prov) {
96
+ if (["pendente", "preparado"].includes(it.estado)) log(`\u26A0 ${it.id}: provedor "${it.provedor}" n\xE3o instalado`);
97
+ continue;
98
+ }
99
+ let acao = proximaAcao(it, prov, agora);
100
+ while (acao) {
101
+ if (simular) {
102
+ log(`\u2022 ${it.id} ${it.rede} (${it.slug}): ${acao}`);
103
+ break;
104
+ }
105
+ try {
106
+ if (acao === "preparar") {
107
+ it.prep = await prov.preparar({ item: it, ctx });
108
+ it.estado = "preparado";
109
+ log(`\u2713 ${it.id} ${it.rede}: preparado`);
110
+ } else {
111
+ it.resultado = await prov.publicar({ item: it, ctx });
112
+ it.estado = "enviado";
113
+ it.enviadoEm = (/* @__PURE__ */ new Date()).toISOString();
114
+ log(`\u2713 ${it.id} ${it.rede}${it.acao ? " (" + it.acao + ")" : ""}: enviado${it.resultado?.url ? " " + it.resultado.url : ""}`);
115
+ for (const s of it.resultado?.seguinte || []) fila.itens.push({ id: randomBytes(4).toString("hex"), estado: "pendente", tentativas: 0, criadoEm: (/* @__PURE__ */ new Date()).toISOString(), origem: it.id, ...s });
116
+ delete it.resultado.seguinte;
117
+ }
118
+ feitos.push(it.id);
119
+ } catch (e) {
120
+ it.tentativas++;
121
+ it.erro = String(e.message || e).slice(0, 500);
122
+ if (it.tentativas >= MAX_TENTATIVAS) it.estado = "erro";
123
+ log(`\u2717 ${it.id} ${it.rede}: ${it.erro}${it.estado === "erro" ? " (desisti)" : ` (tentativa ${it.tentativas}/${MAX_TENTATIVAS})`}`);
124
+ }
125
+ salvarFila(raiz, fila);
126
+ acao = it.estado === "enviado" ? null : acao === "preparar" && it.estado === "preparado" ? proximaAcao(it, prov, agora) : null;
127
+ }
128
+ }
129
+ return { feitos, fila };
130
+ }
131
+ function plistLaunchd({ rotulo = "br.praeko.fila", node, praeko, arauto, cwd, minutos = 5, log }) {
132
+ return `<?xml version="1.0" encoding="UTF-8"?>
133
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
134
+ <plist version="1.0"><dict>
135
+ <key>Label</key><string>${rotulo}</string>
136
+ <key>ProgramArguments</key><array><string>${node}</string><string>${praeko ?? arauto}</string><string>fila</string><string>executar</string></array>
137
+ <key>WorkingDirectory</key><string>${cwd}</string>
138
+ <key>StartInterval</key><integer>${minutos * 60}</integer>
139
+ <key>RunAtLoad</key><true/>
140
+ <key>StandardOutPath</key><string>${log}</string><key>StandardErrorPath</key><string>${log}</string>
141
+ </dict></plist>
142
+ `;
143
+ }
144
+
145
+ // import("./adaptadores/**/*.mjs") in ../core/src/publicador.mjs
146
+ var globImport_adaptadores_mjs = __glob({
147
+ "./adaptadores/buffer.mjs": () => import("./buffer-SP52FWTI.mjs"),
148
+ "./adaptadores/index.mjs": () => import("./adaptadores-ESUZ36N2.mjs"),
149
+ "./adaptadores/local.mjs": () => import("./local-FMMUWECN.mjs"),
150
+ "./adaptadores/remoto.mjs": () => import("./remoto-OAB5Y2MS.mjs")
151
+ });
152
+
153
+ // ../core/src/publicador.mjs
154
+ async function criarPublicador({ config, provedor, ctx } = {}) {
155
+ const nome = provedor || config?.provedor || "buffer";
156
+ if (!PROVEDORES.includes(nome)) throw new Error(`provedor "${nome}" desconhecido (use ${PROVEDORES.join(", ")})`);
157
+ const mod = await globImport_adaptadores_mjs(`./adaptadores/${nome}.mjs`);
158
+ return mod.criar({ config, ctx });
159
+ }
160
+
161
+ // ../core/src/adaptadores/local.mjs
162
+ import { randomBytes as randomBytes2 } from "node:crypto";
163
+ var MODO_PADRAO = { youtube: "privado", tiktok: "rascunho", instagram: "agora" };
164
+ function estadoDe(it) {
165
+ if (it.estado === "cancelado") return "cancelado";
166
+ if (it.estado === "erro") return "erro";
167
+ if (it.estado === "enviado") {
168
+ if (it.acao) return "enviado";
169
+ if (it.rede === "youtube" && it.resultado?.privacidade === "private" && !it.resultado?.publishAt) return "privado";
170
+ if (it.rede === "tiktok" && it.resultado?.modo === "rascunho") return "rascunho";
171
+ return it.rede === "youtube" && it.resultado?.publishAt ? "agendado" : "enviado";
172
+ }
173
+ return "agendado";
174
+ }
175
+ var itemDoContrato = (it) => ({ rede: it.rede, id: it.id, estado: estadoDe(it), quando: it.quando, ...it.acao ? { acao: it.acao } : {}, ...it.manual ? { manual: true } : {}, ...it.resultado?.url ? { url: it.resultado.url } : {}, ...it.resultado?.studio ? { studio: it.resultado.studio } : {}, ...it.erro ? { aviso: it.erro } : {} });
176
+ function criar({ config, ctx, provedores }) {
177
+ const todos = async () => provedores || carregarTodos();
178
+ const agora = () => ctx.agora ? ctx.agora() : /* @__PURE__ */ new Date();
179
+ const doPedido = (id) => lerFila(ctx.raiz).itens.filter((x) => x.pedido === id);
180
+ return {
181
+ nome: "local",
182
+ async publicar(pedido) {
183
+ const erros = validarPedido(pedido);
184
+ if (erros.length) throw new Error(`pedido inv\xE1lido: ${erros.join("; ")}`);
185
+ const id = pedido.id || `p${randomBytes2(3).toString("hex")}`, provs = await todos();
186
+ const faltam = pedido.destinos.filter((d) => !provs[d.rede]).map((d) => d.rede);
187
+ if (faltam.length) throw new Error(`provedor n\xE3o instalado: ${faltam.join(", ")}`);
188
+ const novos = pedido.destinos.map((d) => {
189
+ const op = { ...config[d.rede] || {}, ...pedido.opcoes?.[d.rede] || {} };
190
+ op.modo = d.modo || op.modo || MODO_PADRAO[d.rede];
191
+ return enfileirar(ctx.raiz, { pedido: id, slug: id, rede: d.rede, provedor: d.rede, quando: d.quando || agora().toISOString(), video: pedido.midia.video, capa: pedido.midia.capa || null, textos: pedido.textos[d.rede], opcoes: op, formato: pedido.opcoes?.formato || "short", titulo: pedido.titulo });
192
+ });
193
+ await executar({ raiz: ctx.raiz, provedores: provs, ctx, agora: agora(), soIds: novos.map((n) => n.id), log: ctx.log });
194
+ return { id, itens: doPedido(id).map(itemDoContrato) };
195
+ },
196
+ async status(id) {
197
+ const it = doPedido(id);
198
+ if (!it.length) throw new Error(`pedido ${id} n\xE3o existe na fila`);
199
+ return { id, itens: it.map(itemDoContrato) };
200
+ },
201
+ async cancelar(id) {
202
+ const cancelados = [], mantidos = [];
203
+ for (const it of doPedido(id)) {
204
+ if (["enviado", "cancelado"].includes(it.estado)) {
205
+ if (it.estado === "enviado") mantidos.push(itemDoContrato(it));
206
+ } else {
207
+ cancelarItem(ctx.raiz, it.id);
208
+ cancelados.push(it.id);
209
+ }
210
+ }
211
+ return { cancelados, mantidos };
212
+ },
213
+ async canais() {
214
+ const provs = await todos(), out = [];
215
+ for (const [rede, p] of Object.entries(provs)) {
216
+ const s = await p.status({ ctx });
217
+ out.push({ rede, conectado: s.conectado, detalhe: s.detalhe });
218
+ }
219
+ return out;
220
+ }
221
+ };
222
+ }
223
+
224
+ export {
225
+ pastaEstado,
226
+ caminhoEstado,
227
+ caminhoLeitura,
228
+ lerFila,
229
+ liberarItem,
230
+ cancelarItem,
231
+ executar,
232
+ plistLaunchd,
233
+ MODO_PADRAO,
234
+ estadoDe,
235
+ criar,
236
+ local_exports,
237
+ criarPublicador
238
+ };
@@ -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
+ };
@@ -0,0 +1,56 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ __export,
4
+ __glob
5
+ } from "./chunk-LCFK6X55.mjs";
6
+
7
+ // ../core/src/provedores/registro.mjs
8
+ var registro_exports = {};
9
+ __export(registro_exports, {
10
+ NOMES: () => NOMES,
11
+ carregar: () => carregar,
12
+ carregarTodos: () => carregarTodos
13
+ });
14
+
15
+ // import("./**/*.mjs") in ../core/src/provedores/registro.mjs
16
+ var globImport_mjs = __glob({
17
+ "./instagram-meta.mjs": () => import("./instagram-meta-COY6BTNX.mjs"),
18
+ "./instagram.mjs": () => import("./instagram-GNY73ESY.mjs"),
19
+ "./registro.mjs": () => import("./registro-5PYVQ5LJ.mjs"),
20
+ "./tiktok-api.mjs": () => import("./tiktok-api-36DV2H43.mjs"),
21
+ "./tiktok.mjs": () => import("./tiktok-QOUHOWDU.mjs"),
22
+ "./youtube-analytics.mjs": () => import("./youtube-analytics-3KIW2ZIW.mjs"),
23
+ "./youtube-api.mjs": () => import("./youtube-api-Z2RP5D2V.mjs"),
24
+ "./youtube-comunidade.mjs": () => import("./youtube-comunidade-OAK5GGK4.mjs"),
25
+ "./youtube.mjs": () => import("./youtube-RZ4XTUZW.mjs")
26
+ });
27
+
28
+ // ../core/src/provedores/registro.mjs
29
+ var NOMES = ["youtube", "instagram", "tiktok"];
30
+ async function carregar(nome) {
31
+ if (!NOMES.includes(nome)) throw new Error(`provedor desconhecido: ${nome}`);
32
+ try {
33
+ return await globImport_mjs(`./${nome}.mjs`);
34
+ } catch (e) {
35
+ if (e.code === "ERR_MODULE_NOT_FOUND" && String(e.message).includes(`${nome}.mjs`)) throw new Error(`provedor "${nome}" ainda n\xE3o foi escrito`);
36
+ throw e;
37
+ }
38
+ }
39
+ async function carregarTodos() {
40
+ const o = {};
41
+ for (const n of NOMES) {
42
+ try {
43
+ o[n] = await carregar(n);
44
+ } catch (e) {
45
+ if (!/ainda não foi escrito/.test(e.message)) throw e;
46
+ }
47
+ }
48
+ return o;
49
+ }
50
+
51
+ export {
52
+ NOMES,
53
+ carregar,
54
+ carregarTodos,
55
+ registro_exports
56
+ };