@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.
@@ -0,0 +1,199 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ API,
4
+ AUTH,
5
+ BLOCO,
6
+ ESCOPOS,
7
+ ESCOPOS_BASE,
8
+ ESCOPO_ANALYTICS,
9
+ LIMITE_MINIATURA,
10
+ REF_CLIENTE,
11
+ REF_TOKEN,
12
+ REVOGAR,
13
+ TOKEN,
14
+ UP,
15
+ acoesSeguintes,
16
+ agendarPublicacao,
17
+ aleatorioPadrao,
18
+ api,
19
+ b64url,
20
+ canalId,
21
+ cliente,
22
+ comentar,
23
+ definirMiniatura,
24
+ deps,
25
+ desafioPkceAsync,
26
+ enviarBloco,
27
+ enviarVideo,
28
+ esperarProcessamento,
29
+ falha,
30
+ finalizarPublicacao,
31
+ imediato,
32
+ iniciarUpload,
33
+ mensagemErro,
34
+ modoDe,
35
+ montarMetadados,
36
+ nome,
37
+ planejar,
38
+ publicar,
39
+ rede,
40
+ registroToken,
41
+ revogar,
42
+ sincronizarUpload,
43
+ status,
44
+ tokenValido,
45
+ trocarCodigo,
46
+ urlAutorizacao
47
+ } from "./chunk-UK3UKRPR.mjs";
48
+ import "./chunk-LCFK6X55.mjs";
49
+
50
+ // ../core/src/provedores/youtube.mjs
51
+ import { createServer } from "node:http";
52
+ import { createHash, randomBytes } from "node:crypto";
53
+ import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
54
+ import { spawn, spawnSync } from "node:child_process";
55
+ import { join } from "node:path";
56
+ import { tmpdir } from "node:os";
57
+ var b64url2 = (b) => Buffer.from(b).toString("base64url");
58
+ var desafioPkce = (verificador) => createHash("sha256").update(verificador).digest("base64url");
59
+ function abrirArquivoReal(caminho) {
60
+ const fd = openSync(caminho, "r"), tamanho = fstatSync(fd).size;
61
+ return {
62
+ tamanho,
63
+ ler(inicio, n) {
64
+ const buf = Buffer.alloc(Math.min(n, tamanho - inicio));
65
+ readSync(fd, buf, 0, buf.length, inicio);
66
+ return buf;
67
+ },
68
+ fechar() {
69
+ closeSync(fd);
70
+ }
71
+ };
72
+ }
73
+ function converterParaJpeg(entrada) {
74
+ const saida = join(tmpdir(), `praeko-miniatura-${Date.now()}.jpg`);
75
+ const r = spawnSync("ffmpeg", ["-v", "error", "-y", "-i", entrada, "-q:v", "3", saida], { stdio: "inherit" });
76
+ if (r.status !== 0) throw new Error("ffmpeg n\xE3o conseguiu reduzir a miniatura");
77
+ return saida;
78
+ }
79
+ function comNode(ctx) {
80
+ return {
81
+ ...ctx,
82
+ deps: {
83
+ abrirArquivo: ctx.midia ? void 0 : abrirArquivoReal,
84
+ converterImagem: converterParaJpeg,
85
+ aleatorio: (n) => randomBytes(n),
86
+ abrirNavegador: (url) => {
87
+ try {
88
+ spawn("open", [url], { stdio: "ignore", detached: true }).unref();
89
+ } catch {
90
+ }
91
+ },
92
+ criarServidor: createServer,
93
+ ...ctx.deps
94
+ }
95
+ };
96
+ }
97
+ var tokenValido2 = (ctx) => tokenValido(comNode(ctx));
98
+ var status2 = ({ ctx }) => status({ ctx: comNode(ctx) });
99
+ var enviarVideo2 = (o) => enviarVideo({ ...o, ctx: comNode(o.ctx) });
100
+ var publicar2 = ({ item, ctx }) => publicar({ item, ctx: comNode(ctx) });
101
+ var comentar2 = ({ item, ctx }) => comentar({ item, ctx: comNode(ctx) });
102
+ async function planejar2({ item, ctx }) {
103
+ let tam = "?";
104
+ try {
105
+ tam = (statSync(item.video).size / 1048576).toFixed(1) + " MB";
106
+ } catch {
107
+ }
108
+ return planejar({ item: { ...item, tamanhoTexto: tam }, ctx });
109
+ }
110
+ async function conectar({ ctx: ctx0, args = [] }) {
111
+ const ctx = comNode(ctx0), d = deps(ctx), cl = await cliente(ctx, args);
112
+ const verificador = b64url2(d.aleatorio(48)), estado = b64url2(d.aleatorio(16));
113
+ let aguardo;
114
+ const codigoP = new Promise((ok, no) => {
115
+ aguardo = { ok, no };
116
+ });
117
+ const srv = d.criarServidor((req, res) => {
118
+ const u = new URL(req.url, "http://127.0.0.1");
119
+ if (u.pathname !== "/") {
120
+ res.writeHead(404).end();
121
+ return;
122
+ }
123
+ const erro = u.searchParams.get("error"), codigo = u.searchParams.get("code");
124
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
125
+ res.end(`<meta charset="utf-8"><body style="font-family:sans-serif"><h2>${erro || !codigo ? "N\xE3o deu certo" : "Pronto"}</h2><p>${erro || !codigo ? "Volte ao terminal." : "Pode fechar esta aba e voltar ao terminal."}</p>`);
126
+ if (erro) aguardo.no(new Error(`autoriza\xE7\xE3o negada: ${erro}`));
127
+ else if (u.searchParams.get("state") !== estado) aguardo.no(new Error("state n\xE3o confere (tente de novo)"));
128
+ else if (codigo) aguardo.ok(codigo);
129
+ });
130
+ await new Promise((ok) => srv.listen(0, "127.0.0.1", ok));
131
+ const redirect = `http://127.0.0.1:${srv.address().port}`;
132
+ const url = urlAutorizacao({ clientId: cl.client_id, redirect, desafio: desafioPkce(verificador), estado });
133
+ ctx.log(`Abrindo o navegador para autorizar o Praeko no YouTube.
134
+ Se n\xE3o abrir, copie este endere\xE7o:
135
+ ${url}`);
136
+ d.abrirNavegador(url);
137
+ const limite = setTimeout(() => aguardo.no(new Error("tempo esgotado (5 min) esperando a autoriza\xE7\xE3o")), d.limiteMs ?? 5 * 6e4);
138
+ try {
139
+ const codigo = await codigoP;
140
+ const j = await trocarCodigo(ctx, cl, { codigo, verificador, redirect });
141
+ await ctx.segredos.guardar(REF_CLIENTE, JSON.stringify(cl));
142
+ await ctx.segredos.guardar(REF_TOKEN, registroToken(j, d.agora()));
143
+ } finally {
144
+ clearTimeout(limite);
145
+ srv.close();
146
+ }
147
+ const s = await status2({ ctx });
148
+ ctx.log(s.conectado ? `\u2713 YouTube conectado: ${s.detalhe}` : "\u26A0 autorizado, mas n\xE3o consegui ler o canal");
149
+ return s.conectado ? 0 : 1;
150
+ }
151
+ export {
152
+ API,
153
+ AUTH,
154
+ BLOCO,
155
+ ESCOPOS,
156
+ ESCOPOS_BASE,
157
+ ESCOPO_ANALYTICS,
158
+ LIMITE_MINIATURA,
159
+ REF_CLIENTE,
160
+ REF_TOKEN,
161
+ REVOGAR,
162
+ TOKEN,
163
+ UP,
164
+ acoesSeguintes,
165
+ agendarPublicacao,
166
+ aleatorioPadrao,
167
+ api,
168
+ b64url,
169
+ canalId,
170
+ cliente,
171
+ comNode,
172
+ comentar2 as comentar,
173
+ conectar,
174
+ definirMiniatura,
175
+ deps,
176
+ desafioPkce,
177
+ desafioPkceAsync,
178
+ enviarBloco,
179
+ enviarVideo2 as enviarVideo,
180
+ esperarProcessamento,
181
+ falha,
182
+ finalizarPublicacao,
183
+ imediato,
184
+ iniciarUpload,
185
+ mensagemErro,
186
+ modoDe,
187
+ montarMetadados,
188
+ nome,
189
+ planejar2 as planejar,
190
+ publicar2 as publicar,
191
+ rede,
192
+ registroToken,
193
+ revogar,
194
+ sincronizarUpload,
195
+ status2 as status,
196
+ tokenValido2 as tokenValido,
197
+ trocarCodigo,
198
+ urlAutorizacao
199
+ };
@@ -0,0 +1,109 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ ESCOPOS_BASE,
4
+ ESCOPO_ANALYTICS,
5
+ REF_TOKEN,
6
+ api,
7
+ falha
8
+ } from "./chunk-UK3UKRPR.mjs";
9
+ import "./chunk-LCFK6X55.mjs";
10
+
11
+ // ../core/src/provedores/youtube-analytics.mjs
12
+ var ANALYTICS = "https://youtubeanalytics.googleapis.com/v2/reports";
13
+ var REPORTING = "https://youtubereporting.googleapis.com/v1";
14
+ var TIPO_RELATORIO_ALCANCE = "channel_reach_basic_a1";
15
+ var NOME_JOB_ALCANCE = "praeko-alcance";
16
+ var erroReautorizar = (detalhe) => Object.assign(new Error(`o token do YouTube n\xE3o tem o escopo yt-analytics.readonly: reconecte o canal para liberar reten\xE7\xE3o, impress\xF5es e CTR${detalhe ? ` (${detalhe})` : ""}`), { status: 403, reautorizar: true });
17
+ async function escoposConcedidos(ctx) {
18
+ const bruto = await ctx.segredos.obter(REF_TOKEN);
19
+ if (!bruto) return null;
20
+ let t;
21
+ try {
22
+ t = JSON.parse(bruto);
23
+ } catch {
24
+ return null;
25
+ }
26
+ return Array.isArray(t.escopos) ? t.escopos : ESCOPOS_BASE;
27
+ }
28
+ var temEscopoAnalytics = async (ctx) => (await escoposConcedidos(ctx))?.includes(ESCOPO_ANALYTICS) ?? false;
29
+ async function lerRespostaGoogle(r) {
30
+ const txt = await r.text();
31
+ if (r.ok) return txt ? JSON.parse(txt) : {};
32
+ if (r.status === 403 && /insufficient|scope|permission|access.?not.?configured|accessNotConfigured/i.test(txt)) {
33
+ if (/accessNotConfigured|has not been used|is disabled/i.test(txt)) throw Object.assign(falha(r.status, txt), { apiDesligada: true });
34
+ throw erroReautorizar();
35
+ }
36
+ if (r.status === 401) throw erroReautorizar("token recusado");
37
+ throw falha(r.status, txt);
38
+ }
39
+ var dataISO = (ms) => new Date(ms).toISOString().slice(0, 10);
40
+ var arredonda = (x, n = 4) => Number.isFinite(+x) ? Math.round(+x * 10 ** n) / 10 ** n : null;
41
+ var linhasDe = (j) => {
42
+ const cols = (j.columnHeaders || []).map((c) => c.name);
43
+ return (j.rows || []).map((l) => Object.fromEntries(cols.map((c, i) => [c, l[i]])));
44
+ };
45
+ async function retencaoDoVideo({ ctx, videoId, publicadoEm, agora = Date.now() }) {
46
+ const inicio = dataISO(Math.min(Date.parse(publicadoEm) || agora, agora) - 864e5), fim = dataISO(agora);
47
+ const consulta = (extra) => `${ANALYTICS}?${new URLSearchParams({ ids: "channel==MINE", startDate: inicio, endDate: fim, ...extra })}`;
48
+ const curvaJ = await lerRespostaGoogle(await api(ctx, consulta({ metrics: "audienceWatchRatio,relativeRetentionPerformance", dimensions: "elapsedVideoTimeRatio", filters: `video==${videoId};audienceType==ORGANIC` })));
49
+ const curva = linhasDe(curvaJ).map((l) => [arredonda(l.elapsedVideoTimeRatio), arredonda(l.audienceWatchRatio), arredonda(l.relativeRetentionPerformance)]).filter((p) => p[0] != null && p[1] != null).sort((a, b) => a[0] - b[0]);
50
+ const totJ = await lerRespostaGoogle(await api(ctx, consulta({ metrics: "views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage", filters: `video==${videoId}` })));
51
+ const t = linhasDe(totJ)[0] || {};
52
+ return { curva, views: t.views ?? null, minutos: t.estimatedMinutesWatched ?? null, duracaoMediaSeg: t.averageViewDuration ?? null, percentualMedio: t.averageViewPercentage ?? null };
53
+ }
54
+ var jobs = async (ctx) => (await lerRespostaGoogle(await api(ctx, `${REPORTING}/jobs`))).jobs || [];
55
+ async function garantirJobAlcance({ ctx }) {
56
+ const existente = (await jobs(ctx)).find((j2) => j2.reportTypeId === TIPO_RELATORIO_ALCANCE);
57
+ if (existente) return { id: existente.id, criado: false };
58
+ const j = await lerRespostaGoogle(await api(ctx, `${REPORTING}/jobs`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reportTypeId: TIPO_RELATORIO_ALCANCE, name: NOME_JOB_ALCANCE }) }));
59
+ return { id: j.id, criado: true };
60
+ }
61
+ async function relatoriosDoJob({ ctx, jobId, criadosApos }) {
62
+ const out = [];
63
+ let pagina;
64
+ for (let i = 0; i < 10; i++) {
65
+ const q = new URLSearchParams({ pageSize: "100", ...criadosApos ? { createdAfter: criadosApos } : {}, ...pagina ? { pageToken: pagina } : {} });
66
+ const j = await lerRespostaGoogle(await api(ctx, `${REPORTING}/jobs/${encodeURIComponent(jobId)}/reports?${q}`));
67
+ for (const r of j.reports || []) out.push({ id: r.id, inicio: r.startTime, fim: r.endTime, criadoEm: r.createTime, url: r.downloadUrl });
68
+ pagina = j.nextPageToken;
69
+ if (!pagina) break;
70
+ }
71
+ return out.sort((a, b) => String(a.criadoEm).localeCompare(String(b.criadoEm)));
72
+ }
73
+ function lerCsvAlcance(csv) {
74
+ const linhas = String(csv || "").split(/\r?\n/).filter((l) => l.trim());
75
+ if (linhas.length < 2) return [];
76
+ const cab = linhas[0].split(",").map((c) => c.trim()), ix = (n) => cab.indexOf(n);
77
+ const iD = ix("date"), iV = ix("video_id"), iI = ix("video_thumbnail_impressions"), iC = ix("video_thumbnail_impressions_ctr");
78
+ if (iD < 0 || iV < 0 || iI < 0) return [];
79
+ const out = [];
80
+ for (const l of linhas.slice(1)) {
81
+ const c = l.split(","), d = String(c[iD] || "").trim();
82
+ if (!/^\d{8}$/.test(d) || !c[iV]?.trim()) continue;
83
+ const imp = Number(c[iI]), ctr = iC >= 0 && c[iC]?.trim() !== "" ? Number(c[iC]) : null;
84
+ if (!Number.isFinite(imp)) continue;
85
+ out.push({ dia: `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`, videoId: c[iV].trim(), impressoes: Math.round(imp), ctr: ctr != null && Number.isFinite(ctr) ? ctr : null });
86
+ }
87
+ return out;
88
+ }
89
+ async function baixarRelatorioAlcance({ ctx, url }) {
90
+ if (!/^https:\/\/youtubereporting\.googleapis\.com\//.test(String(url))) throw new Error("URL de relat\xF3rio inesperada");
91
+ const r = await api(ctx, url);
92
+ if (!r.ok) await lerRespostaGoogle(r);
93
+ return lerCsvAlcance(await r.text());
94
+ }
95
+ export {
96
+ ANALYTICS,
97
+ ESCOPO_ANALYTICS,
98
+ NOME_JOB_ALCANCE,
99
+ REPORTING,
100
+ TIPO_RELATORIO_ALCANCE,
101
+ baixarRelatorioAlcance,
102
+ erroReautorizar,
103
+ escoposConcedidos,
104
+ garantirJobAlcance,
105
+ lerCsvAlcance,
106
+ relatoriosDoJob,
107
+ retencaoDoVideo,
108
+ temEscopoAnalytics
109
+ };
@@ -0,0 +1,94 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ API,
4
+ AUTH,
5
+ BLOCO,
6
+ ESCOPOS,
7
+ ESCOPOS_BASE,
8
+ ESCOPO_ANALYTICS,
9
+ LIMITE_MINIATURA,
10
+ REF_CLIENTE,
11
+ REF_TOKEN,
12
+ REVOGAR,
13
+ TOKEN,
14
+ UP,
15
+ acoesSeguintes,
16
+ agendarPublicacao,
17
+ aleatorioPadrao,
18
+ api,
19
+ b64url,
20
+ canalId,
21
+ cliente,
22
+ comentar,
23
+ definirMiniatura,
24
+ deps,
25
+ desafioPkceAsync,
26
+ enviarBloco,
27
+ enviarVideo,
28
+ esperarProcessamento,
29
+ falha,
30
+ finalizarPublicacao,
31
+ imediato,
32
+ iniciarUpload,
33
+ mensagemErro,
34
+ modoDe,
35
+ montarMetadados,
36
+ nome,
37
+ planejar,
38
+ publicar,
39
+ rede,
40
+ registroToken,
41
+ revogar,
42
+ sincronizarUpload,
43
+ status,
44
+ tokenValido,
45
+ trocarCodigo,
46
+ urlAutorizacao
47
+ } from "./chunk-UK3UKRPR.mjs";
48
+ import "./chunk-LCFK6X55.mjs";
49
+ export {
50
+ API,
51
+ AUTH,
52
+ BLOCO,
53
+ ESCOPOS,
54
+ ESCOPOS_BASE,
55
+ ESCOPO_ANALYTICS,
56
+ LIMITE_MINIATURA,
57
+ REF_CLIENTE,
58
+ REF_TOKEN,
59
+ REVOGAR,
60
+ TOKEN,
61
+ UP,
62
+ acoesSeguintes,
63
+ agendarPublicacao,
64
+ aleatorioPadrao,
65
+ api,
66
+ b64url,
67
+ canalId,
68
+ cliente,
69
+ comentar,
70
+ definirMiniatura,
71
+ deps,
72
+ desafioPkceAsync,
73
+ enviarBloco,
74
+ enviarVideo,
75
+ esperarProcessamento,
76
+ falha,
77
+ finalizarPublicacao,
78
+ imediato,
79
+ iniciarUpload,
80
+ mensagemErro,
81
+ modoDe,
82
+ montarMetadados,
83
+ nome,
84
+ planejar,
85
+ publicar,
86
+ rede,
87
+ registroToken,
88
+ revogar,
89
+ sincronizarUpload,
90
+ status,
91
+ tokenValido,
92
+ trocarCodigo,
93
+ urlAutorizacao
94
+ };
@@ -0,0 +1,25 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ apagarVideo,
4
+ editarVideo,
5
+ estatisticasCanal,
6
+ estatisticasVideos,
7
+ lerVideo,
8
+ listarComentarios,
9
+ listarVideos,
10
+ responderComentario,
11
+ trocarHashtags
12
+ } from "./chunk-LLXYUCTT.mjs";
13
+ import "./chunk-UK3UKRPR.mjs";
14
+ import "./chunk-LCFK6X55.mjs";
15
+ export {
16
+ apagarVideo,
17
+ editarVideo,
18
+ estatisticasCanal,
19
+ estatisticasVideos,
20
+ lerVideo,
21
+ listarComentarios,
22
+ listarVideos,
23
+ responderComentario,
24
+ trocarHashtags
25
+ };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
3
+ import "./chunks/chunk-XQO6MI7C.mjs";
4
+ import "./chunks/chunk-LLXYUCTT.mjs";
5
+ import "./chunks/chunk-UK3UKRPR.mjs";
6
+ import "./chunks/chunk-SAHLBARV.mjs";
7
+ import "./chunks/chunk-IZEX746E.mjs";
8
+ import "./chunks/chunk-6YARHJZ2.mjs";
9
+ import "./chunks/chunk-QDZLRBTP.mjs";
10
+ import "./chunks/chunk-Z325UM27.mjs";
11
+ import "./chunks/chunk-LCFK6X55.mjs";
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@praeko/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CLI do Praeko: publica e agenda vídeos curtos no YouTube, Instagram e TikTok, e fala com a API do serviço (praeko.app).",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "praeko",
9
+ "cli",
10
+ "social-media",
11
+ "youtube",
12
+ "instagram",
13
+ "tiktok",
14
+ "scheduler"
15
+ ],
16
+ "homepage": "https://praeko.app/docs",
17
+ "bugs": {
18
+ "url": "https://github.com/Praeko/praeko-js/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Praeko/praeko-js.git",
23
+ "directory": "packages/cli"
24
+ },
25
+ "bin": {
26
+ "praeko": "dist/praeko.mjs",
27
+ "arauto": "dist/arauto.mjs"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "LICENSE"
34
+ ],
35
+ "sideEffects": false,
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "node ../../scripts/build-cli.mjs",
41
+ "prepack": "npm run build"
42
+ },
43
+ "devDependencies": {
44
+ "@praeko/core": "*",
45
+ "@praeko/sdk": "*"
46
+ },
47
+ "engines": {
48
+ "node": ">=22"
49
+ }
50
+ }