# API do painel do Hash Player

A API faz o que o **Editor** faz no painel (https://painel-player.hashtagtreinamentos.com):
listar pastas e vídeos, criar pastas, subir vídeos, baixar o original do que subiu pelo painel,
revisar e publicar legendas e mexer na configuração do player de um vídeo. Liberar ou tirar dos
sites, lixeira, versões, renomear pasta, o padrão da conta e os acessos ficam só no painel.

## Acesso

- Base: `https://api-painel-player.hashtagtreinamentos.com`
- Chave: uma por pessoa, gerada pelo ADM do painel (Configurações → Acessos → Gerar chave de API).
  Ela aparece uma vez só; guarde no HashPassword e, na máquina, numa variável de ambiente
  (sugestão: `HASHPLAYER_PAINEL_CHAVE`). Nunca no código, num commit ou num chat.
- Todo pedido leva `Authorization: Bearer <chave>` **e um `User-Agent` seu** (por exemplo
  `hecio-aulas/1.0`): a borda da Cloudflare recusa o `User-Agent` padrão do `urllib` do Python com
  `403 error code: 1010`, antes de o pedido chegar à API.
- Tudo o que a chave faz entra no Registro do painel com o seu e-mail. Chave rolada ou revogada
  deixa de valer em até 2 minutos.
- Respostas em JSON; erro é `{"erro": "..."}` com o status HTTP (400 pedido errado, 401 chave
  inválida, 403 o Editor não pode, 404 não existe, 409 conflito: leia e tente de novo, 502 o player
  não abriu o vídeo, com o motivo no `erro`, 503 tente de novo em instantes).

```bash
curl -s -H "Authorization: Bearer $HASHPLAYER_PAINEL_CHAVE" -H "User-Agent: meu-script/1.0" \
  https://api-painel-player.hashtagtreinamentos.com/api/eu
# {"email":"voce@hashtagtreinamentos.com","papel":"editor","via":"chave"}
```

## Pastas

- `GET /api/pastas` → `{"pastas": [{"id", "nome", "pai_id", "caminho", "dono"}]}`. O `caminho` é o
  da pasta desde a raiz, com `/` entre os nomes (`Cursos/Excel/Módulo 1`), nas que vieram do Panda
  e nas criadas no painel (o painel recusa nome com `/`, e a do nome que veio do Panda virou `-`). O
  `dono` diz de onde a pasta veio, `panda` ou `painel`; a árvore é a do `pai_id`.
- `POST /api/pastas` com `{"nome": "Módulo 1", "pai_id": "<id da pasta mãe ou null>"}` →
  `201 {"pasta": {...}}`. Nome repetido na mesma pasta mãe: 409.

## Vídeos

- `GET /api/videos?pasta=<id>` → `{"videos": [...], "pagina": 0, "mais": false}`. Dentro de uma
  pasta vem a lista inteira. `pasta=sem` (sem pasta) e `pasta=todos` (o padrão) vêm em páginas de
  100: `&pagina=1`, `&pagina=2`... enquanto `mais` for `true`. `&busca=<texto ou id>` procura no
  título (sem acento nem caixa) e pelo id; `&ordem=titulo|recentes|duracao`.
- Cada vídeo: `id` (**o que o portal usa**, o `videoId` da aula), `external_id` (o `?v=` dos sites),
  `titulo`, `pasta_id`, `status` (`CONVERTED` pronto, `CONVERTING` convertendo), `tocavel`,
  `duracao_s`, `largura`, `altura`, `criado_em`, `dono` (`painel` ou `panda`), `liberado` (nos
  sites). Num grupo de versões, a duração e o estado do arquivo que toca vêm em `midia_duracao_s` e
  `midia_status`: use esses quando existirem.
- `GET /api/videos/<id>` → o detalhe de um vídeo.
- `GET /api/videos/<id>/original` → o arquivo original, para o vídeo que subiu pelo painel (aceita
  `Range`). O dos vídeos que vieram do Panda não está aqui.
- `GET /api/videos/<id>/baixar?qualidade=menor|maior` → o vídeo inteiro numa qualidade, em MPEG-TS
  (`.ts`: o ffmpeg, o Whisper e o VLC leem direto), para todo vídeo que toca, inclusive os que
  vieram do Panda. `menor` é a de menos linhas (a que basta para transcrever); `maior`, a de mais e
  o padrão. O nome vem no `Content-Disposition` (`<título> - 360p.ts`). Cada download entra no
  Registro, e o limite é de 300 por pessoa em 24 h: acima dele, `429` (o mais antigo sai da conta 24
  h depois de feito). Vídeo que não existe ou está na lixeira: `404`; que ainda não toca (convertendo, ou copiando para o acervo): `409`; longo demais para um download só (mais de 9.900 trechos de 4 s, umas 11 h; o maior do acervo tem 7,6 h): `413`. Se a
  conexão cair no meio, o download falha (nunca um arquivo curto com cara de inteiro): baixe de novo.

## Subir vídeos

Em partes, como o painel: o arquivo vai em pedaços de `tamanho_parte` bytes (50 MiB; a última
parte é o resto).

1. `POST /api/envios` com `{"arquivo": "aula-01.mp4", "tamanho": <bytes>, "titulo": "Aula 01",
   "pasta_id": "<id>"}` → `{"id", "tamanho_parte", "partes", ...}`. Aceita mp4, m4v, mov, mkv,
   webm e avi, até 100 GiB. Sem `titulo`, vale o nome do arquivo sem a extensão.
2. Para cada `n` de 1 a `partes`: `PUT /api/envios/<id>/partes/<n>` com os bytes da parte no corpo
   (o `Content-Length` exato) → `{"n", "etag"}`. Parte que falhou se manda de novo.
3. `POST /api/envios/<id>/concluir` com `{"partes": [{"n": 1, "etag": "..."}, ...]}` (todas, em
   ordem) → `{"estado", "video_id", "external_id"}`. O `video_id` já é o id do vídeo; a conversão
   começa sozinha e leva alguns minutos (o `status` do vídeo vira `CONVERTED`).

`GET /api/envios` lista os envios recentes e o estado de cada um; `DELETE /api/envios/<id>`
cancela um envio que ainda não terminou.

```python
import json, os, urllib.request

BASE = "https://api-painel-player.hashtagtreinamentos.com"
CAB = {"Authorization": f"Bearer {os.environ['HASHPLAYER_PAINEL_CHAVE']}", "User-Agent": "meu-script/1.0"}

def pedir(metodo, caminho, corpo=None, dados=None):
    cab = dict(CAB)
    if corpo is not None:
        dados = json.dumps(corpo).encode()
        cab["Content-Type"] = "application/json"
    req = urllib.request.Request(BASE + caminho, data=dados, method=metodo, headers=cab)
    with urllib.request.urlopen(req, timeout=300) as r:
        return json.load(r)

def subir(caminho_do_arquivo, pasta_id, titulo=None):
    tamanho = os.path.getsize(caminho_do_arquivo)
    nome = os.path.basename(caminho_do_arquivo)
    e = pedir("POST", "/api/envios", {"arquivo": nome, "tamanho": tamanho, "pasta_id": pasta_id, "titulo": titulo})
    partes = []
    with open(caminho_do_arquivo, "rb") as f:
        for n in range(1, e["partes"] + 1):
            r = pedir("PUT", f"/api/envios/{e['id']}/partes/{n}", dados=f.read(e["tamanho_parte"]))
            partes.append({"n": n, "etag": r["etag"]})
    return pedir("POST", f"/api/envios/{e['id']}/concluir", {"partes": partes})  # {"video_id": ...}
```

## Legendas

Todo vídeo que sobe pelo painel (ou por esta API) ganha um **rascunho de legenda automática**, fora
do ar até alguém revisar e publicar; ele fica pronto alguns minutos depois da conversão. A legenda
que já existia no Panda já está no Hash Player: ela vem na sincronização.

- `GET /api/videos/<id>/legendas` → os idiomas do vídeo, com o que existe em cada um (publicada,
  revisão salva, rascunho).
- `GET /api/videos/<id>/legendas/<idioma>` → o documento para revisar: a revisão salva, o
  rascunho automático ou a publicada, nessa ordem, com `fonte` e o `etag` (`null` enquanto não há
  revisão salva). Idioma sem nada: 404, ou um documento vazio com `?novo=1`. O idioma é o código
  (`pt-BR`, `es`, `en`).
- `POST /api/legendas/ler` com `{"texto": "<conteúdo de um .vtt ou .srt>"}` → `{"falas": [...]}`,
  cada fala `{"ini": <s>, "fim": <s>, "texto": "..."}`: é como se troca o texto por um arquivo seu.
- `PUT /api/videos/<id>/legendas/<idioma>` com `{"falas": [...], "etag": <o etag lido, ou null
  quando ainda não havia revisão salva>, "rotulo": "Español"}` → `{"etag": ...}` (salva a revisão;
  as falas em ordem, sem uma por cima da outra). 409: alguém salvou no meio; leia de novo.
- `POST /api/videos/<id>/legendas/<idioma>/publicar` com `{"etag": "<o do salvar>"}` → a legenda
  vai ao ar no player. `.../despublicar` tira do ar.

## Configuração do player de um vídeo

- `GET /api/videos/<id>/player` → a camada do vídeo e o que vale nele.
- `PUT /api/videos/<id>/player` com a camada → grava (os campos são os da tela do vídeo no painel).

## Registro

- `GET /api/registro` → as últimas 100 mudanças (quem, o quê, quando); `?antes=<id>` pagina.
