# Receitas

> Fluxos inteiros prontos para copiar: do link ao post publicado, uma fila de vídeos e os melhores momentos de uma live postados sozinhos.

Cada página do guia mostra uma etapa. Aqui estão as etapas juntas, do jeito que rodam em produção. Troque as URLs, o `template_id` e os `connection_id` e rode com `CUTPRO_API_KEY` no ambiente.

## Do link do YouTube ao post publicado

Analisa, clipa com o seu template, escolhe o clipe de maior nota, renderiza e publica no TikTok e no Instagram.

> `POST /clips` cobra créditos e o post usa a cota diária de cada conta. O resto é grátis.

**Node.js**

```js
const API = "https://api.cut.pro/api/v1";
const HEADERS = { "X-Api-Key": process.env.CUTPRO_API_KEY, "Content-Type": "application/json" };
const CLIPPING = ["queued", "downloading", "transcribing", "video_analysis", "analyzing", "finalizing"];

const SOURCE = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
const TEMPLATE_ID = "7398110254410021";

async function call(path, init) {
	const response = await fetch(`${API}${path}`, { ...init, headers: HEADERS });
	const body = await response.json();
	if (!response.ok) throw new Error(`${response.status} ${body.code}`);
	return body;
}

async function until(read, working, everySeconds) {
	let state = await read();
	while (working.includes(state.status)) {
		await new Promise((resolve) => setTimeout(resolve, everySeconds * 1000));
		state = await read();
	}
	return state;
}

const video = await call("/clips/info", { method: "POST", body: JSON.stringify({ url: SOURCE }) });
console.log(`${video.title}: ${video.credits_cost} créditos`);

const submission = await call("/clips", {
	method: "POST",
	body: JSON.stringify({ video_id: video.id, template_id: TEMPLATE_ID }),
});

const base = `/clips/${video.id}/submissions/${submission.id}`;
const done = await until(() => call(base), CLIPPING, 15);
if (done.status === "failed") throw new Error(done.error_code);

const { clips } = await call(`${base}/clips?sort=rating&order=desc&limit=1`);
const best = clips[0];

const render = await call(`${base}/clips/${best.id}/render`, { method: "POST" });
const finished = render.from_cache ? render : await until(() => call(`/renders/${render.id}`), ["waiting_quality", "queued", "active"], 10);
if (finished.status !== "completed") throw new Error(finished.status);

const post = await call("/posts", {
	method: "POST",
	body: JSON.stringify({
		videos: [
			{
				edit_id: render.edit_id,
				targets: [
					{ connection_id: "7390012299881100", metadata: { tiktok: { title: best.title, privacyLevel: "PUBLIC_TO_EVERYONE" } } },
					{ connection_id: "7390012299881300", metadata: { instagram: { caption: best.title } } },
				],
			},
		],
	}),
});

const published = await until(() => call(`/posts/${post.id}`), ["pending", "processing"], 10);
for (const item of published.items) {
	console.log(item.platform, item.status, item.published_url ?? item.error_code);
}
```

**Python**

```python

API = "https://api.cut.pro/api/v1"
HEADERS = {"X-Api-Key": os.environ["CUTPRO_API_KEY"]}
CLIPPING = ["queued", "downloading", "transcribing", "video_analysis", "analyzing", "finalizing"]

SOURCE = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
TEMPLATE_ID = "7398110254410021"

def call(path, method="GET", **kwargs):
    response = requests.request(method, f"{API}{path}", headers=HEADERS, **kwargs)
    body = response.json()
    if not response.ok:
        raise RuntimeError(f"{response.status_code} {body.get('code')}")
    return body

def until(read, working, every_seconds):
    state = read()
    while state["status"] in working:
        time.sleep(every_seconds)
        state = read()
    return state

video = call("/clips/info", "POST", json={"url": SOURCE})
print(f"{video['title']}: {video['credits_cost']} créditos")

submission = call("/clips", "POST", json={"video_id": video["id"], "template_id": TEMPLATE_ID})

base = f"/clips/{video['id']}/submissions/{submission['id']}"
done = until(lambda: call(base), CLIPPING, 15)
if done["status"] == "failed":
    raise RuntimeError(done["error_code"])

best = call(f"{base}/clips?sort=rating&order=desc&limit=1")["clips"][0]

render = call(f"{base}/clips/{best['id']}/render", "POST")
finished = render if render["from_cache"] else until(lambda: call(f"/renders/{render['id']}"), ["waiting_quality", "queued", "active"], 10)
if finished["status"] != "completed":
    raise RuntimeError(finished["status"])

post = call("/posts", "POST", json={
    "videos": [{
        "edit_id": render["edit_id"],
        "targets": [
            {"connection_id": "7390012299881100", "metadata": {"tiktok": {"title": best["title"], "privacyLevel": "PUBLIC_TO_EVERYONE"}}},
            {"connection_id": "7390012299881300", "metadata": {"instagram": {"caption": best["title"]}}},
        ],
    }],
})

published = until(lambda: call(f"/posts/{post['id']}"), ["pending", "processing"], 10)
for item in published["items"]:
    print(item["platform"], item["status"], item.get("published_url") or item.get("error_code"))
```

1. `POST /clips/info` mostra o custo antes de gastar.
2. `POST /clips` com `template_id` já entrega cada clipe editado.
3. O clipe de maior nota sai de `sort=rating&order=desc&limit=1`.
4. `from_cache: true` quer dizer que o MP4 já existia.
5. O mesmo `edit_id` vai para as duas contas, cada uma com a `metadata` da sua rede.

O render explícito é opcional: o post renderiza o que falta. Ele está aqui para você ter o MP4 antes de publicar.

> Um post pode terminar em `partial`: uma conta publicou e a outra falhou. Olhe item por item e use `POST /posts/{id}/items/{itemId}/retry` no que falhou. O que já está no ar não é tocado.

## Uma fila de vários vídeos

Clipar um canal inteiro é o mesmo fluxo repetido. O cuidado é não disparar tudo de uma vez: o polling de dezenas de submissões juntas é o que encosta no [limite de requisições](/rate-limits). Uma esteira com poucos trabalhadores resolve. Usa `call` e `until` da receita anterior.

```js
const SOURCES = ["https://youtu.be/aaa", "https://youtu.be/bbb", "https://youtu.be/ccc"];
const AT_A_TIME = 3;

async function clipOne(url) {
	const video = await call("/clips/info", { method: "POST", body: JSON.stringify({ url }) });
	const submission = await call("/clips", { method: "POST", body: JSON.stringify({ video_id: video.id }) });
	const base = `/clips/${video.id}/submissions/${submission.id}`;
	const done = await until(() => call(base), CLIPPING, 15);
	if (done.status === "failed") return { url, error: done.error_code };
	const { clips } = await call(`${base}/clips`);
	return { url, clips };
}

const queue = [...SOURCES];
const results = await Promise.all(
	Array.from({ length: AT_A_TIME }, async () => {
		const mine = [];
		while (queue.length > 0) mine.push(await clipOne(queue.shift()));
		return mine;
	}),
);

console.log(results.flat());
```

Antes do lote, some os `credits_cost` de cada `POST /clips/info` e compare com `GET /balance`. Uma submissão parada em `downloading` com `waiting_reason` preenchido está esperando a fonte (estreia, live no ar, gravação em processamento) e segue sozinha. Veja [Fonte que ainda não está no ar](/clipping).

## Os melhores momentos de uma live, postados sozinhos

Um monitor com `template_id` entrega cada clipe da live já editado. Este laço consulta a live a cada minuto e publica todo clipe novo com nota 8 ou mais. Usa `call` da primeira receita.

```js
const MONITOR_ID = "7413020011876610";
const TIKTOK = "7390012299881100";
const posted = new Set();

while (true) {
	const { broadcasts } = await call(`/monitors/${MONITOR_ID}/broadcasts?limit=1`);
	const live = broadcasts[0];

	if (live && !live.recording_expired) {
		const { clips } = await call(`/monitors/${MONITOR_ID}/broadcasts/${live.id}/clips?min_rating=8&limit=100`);
		const fresh = clips.filter((clip) => clip.edit_id && !posted.has(clip.id));

		for (const clip of fresh) {
			await call("/posts", {
				method: "POST",
				body: JSON.stringify({
					videos: [{ edit_id: clip.edit_id, targets: [{ connection_id: TIKTOK, metadata: { tiktok: { title: clip.title, privacyLevel: "PUBLIC_TO_EVERYONE" } } }] }],
				}),
			});
			posted.add(clip.id);
		}
	}

	await new Promise((resolve) => setTimeout(resolve, 60_000));
}
```

- A live gera clipes em blocos de 10 minutos enquanto está no ar. Consultar a cada minuto já é mais que suficiente.
- `posted` vive na memória. Em produção, guarde os `clip.id` publicados no seu banco para não repostar depois de reiniciar.
- O filtro pula clipe sem `edit_id`. Para postar esses também, renderize com `POST .../clips/{clipId}/render` e use o `edit_id` da resposta.
- O monitor cobra por bloco de 10 minutos que gerou clipes enquanto está ligado. Veja [Monitore canais e lives](/monitoring).

> Publicar sozinho posta na conta da pessoa sem revisão. Comece com `privacyLevel: "SELF_ONLY"` no TikTok, confira os primeiros resultados e só depois abra para todos.
