Aplicação em Python que exporta seus mapas mentais do MindMeister para arquivos Markdown, prontos para carregar em um vault do Obsidian, preservando:
- a estrutura de pastas do MindMeister no disco local;
- as imagens de cada mapa em uma subpasta
attachments/no mesmo nível do.md; - a hierarquia de tópicos do mapa como cabeçalhos (
#,##,###, ...) e itens de lista (- texto); - os links "anexados" a um nó (cards de link com preview) e vídeos anexados, como uma linha aninhada sob o item — em vez de se perderem;
- links que apontam para outro mapa da mesma conta, como wikilink do Obsidian
(
[[Título]]) em vez de link externo.
Toda a exportação usa a API interna da própria aplicação web do MindMeister
(https://www.mindmeister.com/app/api/...), autenticada com o cookie de
sessão do navegador (_mind_session) — é a única credencial necessária (ver
"Configuração" abaixo).
Isso não é uma API pública/documentada, mas foi confirmada testando exaustivamente contra a conta real, como a única fonte que cobre tudo que a exportação precisa:
- Listagem de mapas e pastas — via GraphQL (
POST /app/api/graphql):Cada mapa já vem com seuquery Maps { maps { id title folderId } } query Folders { folders { id name parentId } }
folderId, sem precisar de filtro por pasta. - Conteúdo de um mapa — via
GET /app/api/maps/content.json?idea_id={id}: retorna a árvore completa de "ideas" (id, hierarquia porparentId/rank, título, nota, imagem, vídeo e os links anexados a cada nó). - Download de imagem — a
imageURLde cada nó, autenticada com o mesmo cookie.
Isso foi descoberto e simplificado ao longo do desenvolvimento — registrando aqui porque explica algumas decisões de arquitetura:
- A API v2 (OAuth2/Personal Access Token) não tem endpoint de listagem de
mapas/pastas (testado exaustivamente:
GET /maps,GET /folderse variações retornam 404 com qualquer escopo de token). O endpoint de exportação (GET /maps/{id}.docx+ Pandoc) também perde os links anexados/vídeos de cada nó e pode distorcer a proporção de imagens. - A API v1 (REST clássica, "deprecated") tem os métodos de listagem
(
mm.maps.getList/mm.folders.getList), mas exige um fluxo de autenticação totalmente separado (api_key + shared secret + auth_token via autorização de 3 pernas) — e ainda assim não dá acesso aos links anexados/vídeos. - A API interna da aplicação web (a que este projeto usa) cobre listagem e conteúdo completo com uma única credencial simples de capturar, então as outras duas deixaram de ser necessárias.
render.py::render_map() percorre a árvore de "ideas" do content.json (por
parentId/rank) e gera o Markdown diretamente — sem Pandoc, sem .docx:
- nó com filhos → heading (
#.."######", nível = profundidade na árvore); - nó sem filhos (folha) → item de lista (
- texto); - nó cujo título tem quebra de linha real (não o wrap automático de texto
longo, que vem como
\r) → bloco de código dentro de um item de lista (- ```...```), preservando indentação exata — o usuário digitou ali um trecho de várias linhas (YAML, config, notas formatadas) e colapsar isso destruiria a formatação; - nota, link anexado, vídeo e imagem de um nó (chamados de "extras") aparecem aninhados (indentados um nível) sob o item/bloco de código do próprio nó — nunca soltos no mesmo nível, senão fica ambíguo a quem pertencem. Quando o nó é um heading, os extras ficam soltos (não há bullet do heading para aninhar embaixo);
- link anexado que aponta para outro mapa da mesma conta
(
mindmeister.com/app/map/{id}) é resolvido para um wikilink do Obsidian ([[Título]]) viaresolve_map_title(callback passado arender_map(), que faz um lookup no dicionário de títulos já carregado — sem chamada extra). Sobrevive a mudanças de pasta, já que o Obsidian resolve[[...]]pelo nome do arquivo, não pelo caminho. Se o mapa de destino não puder ser resolvido, o link externo original é mantido; - imagem do nó → baixada (autenticada com o mesmo cookie) e embutida como
![[nome.png]], sem distorção (tamanho original), com nome no padrãoidea_image_{id}_{largura}x{altura}.{ext}; - sem linha em branco entre itens de lista consecutivos da mesma profundidade (lista compacta); uma linha em branco aparece quando a profundidade diminui entre dois itens consecutivos — ou seja, terminamos os filhos de uma seção e voltamos ao nível de um irmão dela. Sem essa regra, um item irmão de um heading (não filho dele) fica visualmente colado aos filhos desse heading, parecendo pertencer a ele.
pip install -r requirements.txtSem dependências externas ao Python — não precisa de Pandoc nem de nenhuma outra ferramenta instalada no sistema.
Copie .env.example para .env e preencha:
MINDMEISTER_SESSION_COOKIE=_mind_session=...
OBSIDIAN_VAULT_DIR=/caminho/para/seu/vault
- Abra https://www.mindmeister.com/app/ logado no navegador.
- Abra o DevTools (F12) → aba Network → filtre por Fetch/XHR → abra qualquer mapa (ou recarregue a página de um mapa já aberto).
- Ache uma requisição para
content.jsonna lista → aba Headers → Request Headers → copie o valor do cookie_mind_session(dentro do headerCookie:, é o trecho_mind_session=...— não precisa dos outros cookies de analytics que aparecem junto). - Cole no
.env.
Atenção: esse cookie funciona como uma senha temporária da sua conta e
expira periodicamente (login de novo no navegador invalida o anterior).
Quando main.py/discover.py começarem a falhar, repita os passos acima
para capturar um cookie novo. É um endpoint interno da aplicação web, não uma
API pública/documentada — pode mudar sem aviso.
# 1. Checar se o cookie está OK e ver a listagem de pastas/mapas
python discover.py
# 2. Exportar
python main.py --map-id 123 # exporta só um mapa específico (para testar)
python main.py # exporta todos os mapas da conta
# Opções úteis
python main.py --dry-run # mostra o que seria criado, sem baixar nada
python main.py --exclude 123,456 # pula esses IDs de mapa (ex: mapas grandes que dão timeout)
python main.py --heading-offset 1 # desloca os níveis de heading (## -> ###)Tanto com --map-id quanto sem, o script sempre lista todos os mapas/pastas
da conta primeiro (via GraphQL) — isso é o que permite colocar cada mapa na
pasta certa do vault e resolver os wikilinks entre mapas, mesmo exportando um
único mapa por vez.
Mapas muito grandes podem dar timeout (erro de rede ou 524 do
Cloudflare) — o servidor do MindMeister demora demais para responder. Não há
como aumentar esse limite do lado do cliente indefinidamente (o 524 é do
próprio servidor). Nesses casos, use --exclude para pular esses mapas na
exportação em lote e trate-os manualmente depois com --map-id.
mm_api_web.py # cliente da API interna da app web (cookie de sessão) — GraphQL + content.json + download de imagem
render.py # gera o Markdown direto do JSON de mm_api_web.py
organizer.py # dataclasses MMFolder/MMMap + caminho local de cada mapa (espelha as pastas)
discover.py # utilitário de diagnóstico (rode antes do main.py)
main.py # orquestração / CLI