@danhachuel/leadmaster 0.1.0 → 0.1.2
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 +52 -0
- package/README.md +15 -24
- package/backend/main.py +26 -62
- package/backend/services/llm.py +5 -125
- package/backend/services/storage.py +48 -68
- package/frontend/dist/assets/index-0xtgAs3t.js +90 -0
- package/frontend/dist/assets/{index-eswbuT3S.css → index-pSbjfRli.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -6
- package/scripts/cli.mjs +8 -17
- package/backend/services/moneyprinter.py +0 -102
- package/backend/services/openmontage.py +0 -62
- package/backend/services/pipeline.py +0 -515
- package/backend/services/short_video_maker.py +0 -72
- package/backend/services/youtube_upload.py +0 -163
- package/frontend/dist/assets/index-CANBU7DH.js +0 -93
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
"""Upload para YouTube com a estratégia do LeadMaster:
|
|
2
|
-
|
|
3
|
-
1. Upload primário via serviço local "YouTube-Video-Upload-Frontend-Api"
|
|
4
|
-
(URL e caminhos configuráveis em Ferramentas → Upload YouTube).
|
|
5
|
-
2. Fallback opcional via CLI do `youtube-uploader-mcp`.
|
|
6
|
-
3. Publicação como **não listado**, aguardando 1h e mudando para **público**
|
|
7
|
-
(agendamento feito pelo motor do pipeline).
|
|
8
|
-
4. Limite diário de 50 uploads (contagem local por data).
|
|
9
|
-
|
|
10
|
-
Erros são sempre acionáveis: como instalar/configurar cada peça.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import subprocess
|
|
14
|
-
from datetime import date
|
|
15
|
-
|
|
16
|
-
import httpx
|
|
17
|
-
|
|
18
|
-
from . import storage
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class UploadNaoConfigurado(Exception):
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class LimiteDiarioExcedido(Exception):
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class UploadErro(Exception):
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _conf() -> dict:
|
|
34
|
-
return storage.get_config()["integracoes"]["youtube_upload"]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _contagem() -> dict:
|
|
38
|
-
return storage.get_config()["uploads"]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def verificar() -> bool:
|
|
42
|
-
conf = _conf()
|
|
43
|
-
if not conf.get("url"):
|
|
44
|
-
return bool(conf.get("uploader_mcp_path"))
|
|
45
|
-
try:
|
|
46
|
-
httpx.get(conf["url"].rstrip("/"), timeout=2)
|
|
47
|
-
return True
|
|
48
|
-
except httpx.HTTPError:
|
|
49
|
-
return False
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def contagem_hoje() -> int:
|
|
53
|
-
c = _contagem()
|
|
54
|
-
hoje = date.today().isoformat()
|
|
55
|
-
return c["contador"] if c.get("data") == hoje else 0
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _incrementar_contagem() -> None:
|
|
59
|
-
config = storage.get_config()
|
|
60
|
-
hoje = date.today().isoformat()
|
|
61
|
-
uploads = config.setdefault("uploads", {"data": hoje, "contador": 0})
|
|
62
|
-
if uploads.get("data") != hoje:
|
|
63
|
-
uploads.update({"data": hoje, "contador": 0})
|
|
64
|
-
uploads["contador"] = int(uploads.get("contador", 0)) + 1
|
|
65
|
-
storage.update_config({"uploads": uploads})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def upload_video(titulo: str, descricao: str, arquivo: str = "", video_url: str = "", tags: str = "") -> dict:
|
|
69
|
-
"""Faz o upload como não listado. Retorna {video_id, url, modo, aviso}."""
|
|
70
|
-
conf = _conf()
|
|
71
|
-
|
|
72
|
-
limite = int(conf.get("limite_diario", 50))
|
|
73
|
-
if contagem_hoje() >= limite:
|
|
74
|
-
raise LimiteDiarioExcedido(
|
|
75
|
-
f"Limite diário de {limite} uploads atingido (hoje: {contagem_hoje()}). "
|
|
76
|
-
"O upload continua amanhã automaticamente — clique Repetir no job."
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
if conf.get("url"):
|
|
80
|
-
resultado = _upload_via_servico(conf, titulo, descricao, arquivo, video_url, tags)
|
|
81
|
-
elif conf.get("uploader_mcp_path"):
|
|
82
|
-
resultado = _upload_via_mcp(conf, titulo, descricao, arquivo, video_url)
|
|
83
|
-
else:
|
|
84
|
-
raise UploadNaoConfigurado(
|
|
85
|
-
"Nenhum uploader configurado. Em Ferramentas → Upload YouTube, informe a URL do "
|
|
86
|
-
"YouTube-Video-Upload-Frontend-Api (primário) ou o caminho do youtube-uploader-mcp (fallback)."
|
|
87
|
-
)
|
|
88
|
-
_incrementar_contagem()
|
|
89
|
-
return resultado
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _upload_via_servico(conf: dict, titulo: str, descricao: str, arquivo: str, video_url: str, tags: str) -> dict:
|
|
93
|
-
base = conf["url"].rstrip("/")
|
|
94
|
-
path = conf.get("upload_path", "/api/upload").lstrip("/")
|
|
95
|
-
payload = {
|
|
96
|
-
"title": titulo,
|
|
97
|
-
"description": descricao,
|
|
98
|
-
"tags": tags,
|
|
99
|
-
"privacy": "unlisted", # não listado → agenda de 1h → público
|
|
100
|
-
"video_url": video_url,
|
|
101
|
-
"video_file": arquivo,
|
|
102
|
-
"api_key": conf.get("api_key", ""),
|
|
103
|
-
}
|
|
104
|
-
try:
|
|
105
|
-
r = httpx.post(f"{base}/{path}", json=payload, timeout=600)
|
|
106
|
-
except httpx.HTTPError as e:
|
|
107
|
-
raise UploadErro(
|
|
108
|
-
f"Uploader local ({base}) não respondeu: {e}. Confirme que o "
|
|
109
|
-
"YouTube-Video-Upload-Frontend-Api está ativo e clique Repetir."
|
|
110
|
-
) from e
|
|
111
|
-
if r.status_code >= 400:
|
|
112
|
-
raise UploadErro(f"Uploader local rejeitou o vídeo (HTTP {r.status_code}): {r.text[:300]}")
|
|
113
|
-
try:
|
|
114
|
-
corpo = r.json()
|
|
115
|
-
except ValueError:
|
|
116
|
-
corpo = {"resposta_bruta": r.text[:300]}
|
|
117
|
-
video_id = corpo.get("video_id") or corpo.get("videoId") or corpo.get("id") or ""
|
|
118
|
-
return {
|
|
119
|
-
"video_id": video_id,
|
|
120
|
-
"url": corpo.get("url") or (f"https://youtu.be/{video_id}" if video_id else ""),
|
|
121
|
-
"modo": "servico_local",
|
|
122
|
-
"resposta": corpo,
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _upload_via_mcp(conf: dict, titulo: str, descricao: str, arquivo: str, video_url: str) -> dict:
|
|
127
|
-
cli = conf.get("uploader_mcp_path", "")
|
|
128
|
-
if not arquivo:
|
|
129
|
-
raise UploadErro("O fallback youtube-uploader-mcp precisa do arquivo de vídeo local (produza o vídeo primeiro).")
|
|
130
|
-
try:
|
|
131
|
-
resultado = subprocess.run(
|
|
132
|
-
[cli, arquivo, "--title", titulo, "--description", descricao, "--privacy", "unlisted"],
|
|
133
|
-
capture_output=True,
|
|
134
|
-
text=True,
|
|
135
|
-
timeout=3600,
|
|
136
|
-
shell=False,
|
|
137
|
-
)
|
|
138
|
-
except FileNotFoundError as e:
|
|
139
|
-
raise UploadNaoConfigurado(f"Caminho do youtube-uploader-mcp inválido: {e}") from e
|
|
140
|
-
if resultado.returncode != 0:
|
|
141
|
-
raise UploadErro(f"youtube-uploader-mcp falhou: {resultado.stderr[-400:]}")
|
|
142
|
-
return {
|
|
143
|
-
"video_id": resultado.stdout.strip().splitlines()[-1] if resultado.stdout.strip() else "",
|
|
144
|
-
"url": resultado.stdout.strip().splitlines()[-1] if resultado.stdout.strip() else "",
|
|
145
|
-
"modo": "uploader_mcp",
|
|
146
|
-
"resposta": {"stdout": resultado.stdout[-500:]},
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def tornar_publico(video_id: str) -> dict:
|
|
151
|
-
"""Muda o vídeo de não listado para público (chamado pelo agendador)."""
|
|
152
|
-
conf = _conf()
|
|
153
|
-
if not conf.get("url"):
|
|
154
|
-
raise UploadNaoConfigurado("Uploader local não configurado; publique manualmente o vídeo " f"{video_id}.")
|
|
155
|
-
base = conf["url"].rstrip("/")
|
|
156
|
-
path = conf.get("publicar_path", "/api/videos/{video_id}/publicar").lstrip("/")
|
|
157
|
-
try:
|
|
158
|
-
r = httpx.post(f"{base}/{path.format(video_id=video_id)}", json={"privacy": "public"}, timeout=60)
|
|
159
|
-
except httpx.HTTPError as e:
|
|
160
|
-
raise UploadErro(f"Falha ao publicar {video_id} via uploader local: {e}") from e
|
|
161
|
-
if r.status_code >= 400:
|
|
162
|
-
raise UploadErro(f"Uploader local recusou publicar {video_id} (HTTP {r.status_code}): {r.text[:300]}")
|
|
163
|
-
return {"ok": True, "resposta": r.text[:300]}
|