Skip to content
Todo o trabalho

Projeto independente

Uma biblioteca para todas as fontes de manga, em iOS e Android

Um leitor de manga self-hosted com aplicações nativas para iOS e Android sobre um back-end em Rust, que unifica muitas fontes numa só biblioteca privada que se lembra exatamente de onde ficou.

Notas de campo

Mantive um velho telemóvel Android vivo só para ler manga, porque os leitores de iOS não eram suficientemente bons. É uma razão tola para andar com um segundo telemóvel, por isso construí o leitor que eu queria.

Atlantic Forge

O que estava em jogo

Durante anos mantive um velho telemóvel Android vivo para exatamente uma tarefa: ler manga. Aos leitores de iOS faltam demasiados dos pequenos confortos que contam ao longo de uma série comprida, e por isso o segundo telemóvel ficava. E mesmo no Android, as boas opções não eram boas: personalização que pára uma definição antes daquilo que realmente queremos, e anúncios a interromper a única atividade que devia ser um prazer. Entretanto, o manga em si vive espalhado por dezenas de fontes, cada uma com o seu site atulhado, o seu próprio catálogo, e nenhuma memória de onde ficámos.

A esta altura da página já se nota um padrão: a ferramenta que eu queria não existia, por isso construí-a. Mas “um bom leitor” é uma promessa mais difícil do que parece. Significa um back-end a apresentar um catálogo calmo e estável sobre um mundo confuso e instável de fontes, e dois clientes nativos que têm de concordar com ele e um com o outro, perfeitamente, para sempre. Três bases de código a descrever uma biblioteca são três oportunidades de divergir, e um leitor nota a divergência de imediato: um marcador que existe no telemóvel mas não no tablet, um capítulo marcado como lido que volta por ler.

A decisão

A concordância é imposta, não desejada. Os dois clientes nativos partilham queries GraphQL idênticas byte a byte contra um só schema, pelo que, se o contrato muda, ambas as aplicações o sentem em tempo de compilação, em vez de ser o leitor a senti-lo à noite. E o back-end guarda todos os detalhes feios nos bastidores: agregação, failover, cache de imagens. Uma fonte pode abrandar, mudar ou desaparecer, e nenhuma das aplicações muda uma linha.

As aplicações pedem uma série, nunca uma fonte. A confusão fica nos bastidores, onde pertence.

A outra escolha deliberada foi para quem é. O Paneru é self-hosted, com contas para um pequeno círculo de leitores convidados, não um serviço público. Isso mantém o sistema honesto quanto ao seu propósito: uma biblioteca privada, a correr em hardware que o dono controla, ao serviço das pessoas para quem foi construída.

O que entregámos

Um sistema self-hosted em três partes. Um back-end em Rust (Actix, async-graphql, Postgres) agrega o MangaDex e várias outras fontes num catálogo único atrás de uma só API GraphQL, com proxy de imagens, cache em WebP e contas com JWT. Por cima, dois clientes nativos: iOS em SwiftUI (iOS 17+) e Android em Kotlin com Jetpack Compose. Ambos oferecem uma biblioteca unificada, leitura paginada e em webtoon, downloads offline que se sentem idênticos a ler online, sincronização de progresso entre dispositivos, marcadores, um atalho de continuar a ler e notificações de capítulos novos. O design segue uma única regra: as capas são a interface enquanto se navega, e o leitor em si é silencioso, com um modo preto AMOLED, um ajuste de brilho próprio e fundos quentes para longas sessões noturnas. Por outras palavras, os pequenos confortos pelos quais o segundo telemóvel se mantinha vivo, finalmente num só lugar, sem anúncios em lado nenhum.

A stack, e porquê

Rust, em vez de Node ou Python. Todo o trabalho do back-end é concorrente e sem glamour: fazer scraping de várias fontes ao mesmo tempo, servir e guardar em cache milhares de imagens, e manter-se de pé sem vigilância numa pequena máquina self-hosted. Um serviço em Node ou Python teria sido mais rápido de escrever, e para um primeiro protótipo essa troca é muitas vezes a certa. Aqui o serviço corre sem ninguém a olhar, em hardware modesto, onde as garantias em tempo de compilação do Rust e a sua pegada pequena valem o arranque mais lento: nenhum runtime para afinar, nenhuma memória a crescer para perseguir, e uma classe de bugs de concorrência que não pode ser lançada porque não pode compilar.

SwiftUI e Jetpack Compose, em vez de Flutter ou React Native. Um leitor vive ou morre pela sensação: a física de scroll de uma longa tira webtoon, carregamento de imagens que nunca engasga, e integração profunda com a plataforma para downloads e notificações. As frameworks multiplataforma são honestas quanto à sua troca: uma só base de código, ao custo de viver a um passo da plataforma. Para uma aplicação de conteúdo essa troca pode ser certa; para uma superfície de leitura é o próprio produto a ser comprometido. Duas aplicações nativas custam mais a construir, e o contrato GraphQL partilhado recupera a maior parte desse custo ao manter a lógica de ambas idêntica.

Postgres, em vez de um armazém de documentos. Uma biblioteca de manga parece documentos até a descrevermos: as séries têm capítulos, os capítulos existem em várias fontes, os leitores têm progresso por capítulo e marcadores por série. Isso são relações e joins, e o Postgres responde-lhes diretamente. Um armazém de documentos empurraria esses joins para o código da aplicação, que é exatamente onde um sistema de três bases de código não precisa de mais lógica para manter em concordância.

O que mudou

  • Uma biblioteca privada substituiu um ritual semanal de separadores, pesquisas e lugares perdidos
  • Ler sabe ao mesmo offline e online, e o progresso acompanha o leitor entre dispositivos
  • As fontes podem mudar e falhar atrás da API sem nenhuma das aplicações dar por isso
  • Ambas as plataformas recebem um leitor genuinamente nativo, não um site embrulhado