# Книга целиком: пример

Файл книги → цена → озвучка всех глав → M4B. Один скрипт на Python и на JavaScript.

Путь книги через API — тот же, что в кабинете:

1. загрузить файл — программа разделит текст на главы, найдёт героев и подберёт им голоса;
2. узнать цену — уже озвученное бесплатно;
3. поставить главы в очередь и дождаться;
4. собрать M4B (или ZIP с MP3 по главам) и скачать.

Вместо опроса задач раз в 15 секунд можно подключить [уведомления](/developers/webhooks).

```python
import os, time, requests

API = "https://golosar-kniga.tech/api/v1"
H = {"x-api-key": os.environ["GOLOSAR_KEY"]}

def wait(job_id):
    while True:
        job = requests.get(f"{API}/jobs/{job_id}", headers=H).json()["job"]
        if job["status"] not in ("queued", "running"):
            return job
        time.sleep(15)

# 1. книга из файла
r = requests.post(f"{API}/books/upload", headers=H, files={"file": open("book.fb2", "rb")})
r.raise_for_status()
book_id = r.json()["book"]["id"]
book = requests.get(f"{API}/books/{book_id}", headers=H).json()["book"]
ids = [c["id"] for c in book["chapters"] if c["status"] != "ready"]

# 2. цена до озвучки
q = requests.post(f"{API}/books/{book_id}/quote", headers=H, json={"chapter_ids": ids}).json()
print("спишется", q["credits"], "знаков, на балансе", q["left"], "готово примерно через", q["ready_s"], "с")
if q["short"] > 0:
    raise SystemExit("не хватает знаков: " + str(q["short"]))

# 3. озвучить все главы
jobs = []
for cid in ids:
    r = requests.post(f"{API}/chapters/{cid}/render", headers={**H, "Idempotency-Key": f"render-{cid}"})
    r.raise_for_status()
    jobs.append(r.json()["job"]["id"])
for j in jobs:
    done = wait(j)
    assert done["status"] == "done", done["error"]

# 4. собрать M4B и скачать
job = requests.post(f"{API}/books/{book_id}/export", headers=H, json={"format": "m4b"}).json()["job"]
wait(job["id"])
url = requests.get(f"{API}/books/{book_id}/exports", headers=H).json()["files"]["m4b"]["download_url"]
open("book.m4b", "wb").write(requests.get(url).content)
```

```javascript
import { openAsBlob } from "node:fs";
import { writeFile } from "node:fs/promises";

const API = "https://golosar-kniga.tech/api/v1";
const KEY = { "x-api-key": process.env.GOLOSAR_KEY };
const H = { ...KEY, "content-type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const call = async (path, opts = {}) => {
  const r = await fetch(`${API}${path}`, { headers: H, ...opts });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  return r.json();
};
const wait = async (id) => {
  for (;;) {
    const { job } = await call(`/jobs/${id}`);
    if (!["queued", "running"].includes(job.status)) return job;
    await sleep(15000);
  }
};

// 1. книга из файла
const form = new FormData();
form.append("file", await openAsBlob("book.fb2"), "book.fb2");
const up = await call("/books/upload", { method: "POST", headers: KEY, body: form });
const { book } = await call(`/books/${up.book.id}`);
const ids = book.chapters.filter((c) => c.status !== "ready").map((c) => c.id);

// 2. цена до озвучки
const q = await call(`/books/${book.id}/quote`, { method: "POST", body: JSON.stringify({ chapter_ids: ids }) });
console.log("спишется", q.credits, "знаков; на балансе", q.left);
if (q.short > 0) throw new Error(`не хватает ${q.short} знаков`);

// 3. озвучить все главы
for (const id of ids) {
  const { job } = await call(`/chapters/${id}/render`, { method: "POST", headers: { ...H, "Idempotency-Key": `render-${id}` } });
  const done = await wait(job.id);
  if (done.status !== "done") throw new Error(done.error);
}

// 4. собрать M4B и скачать
const ex = await call(`/books/${book.id}/export`, { method: "POST", body: JSON.stringify({ format: "m4b" }) });
await wait(ex.job.id);
const { files } = await call(`/books/${book.id}/exports`);
await writeFile("book.m4b", Buffer.from(await (await fetch(files.m4b.download_url)).arrayBuffer()));
```
