@danhachuel/leadmaster 0.1.0 → 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.
@@ -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]}