Gamertag


quarta-feira, 4 de novembro de 2026

A Biblioteca #4 — a parte que cresce

O problema não é a biblioteca crescer. É que ela cresce num lugar diferente de onde é organizada — e a distância entre os dois é onde a bagunça se instala.

O que o post 3 não resolveu

O hash resolveu o passado. Mil e seiscentos e seis jogos cortados em oitenta e um blocos, cada um com o seu post, cada um com a sua amostra aleatória.

Mas ele é uma fotografia. E fotografia envelhece no dia seguinte.

Porque a biblioteca não parou. A Epic entrega de quatro a cinco jogos toda quinta. O Prime entrega vinte a trinta por mês. E a promoção que eu não consigo ignorar entra por cima disso.

Se eu não fizer nada, em um ano são quatrocentos ou quinhentos jogos novos — e a planilha que eu levei três posts para organizar volta a ser um depósito.

A tentação de decidir na entrada

A primeira solução que eu considerei foi criar um portão: o jogo entra, fica trinta dias em observação, e depois vira fila, descarte ou arquivo.

Parece organizado. É errado.

Porque A Biblioteca não é sobre jogar. É sobre catalogar — saber o que eu tenho, onde tenho, de que gênero, se tem conquista, o que já joguei e o que ficou no meio. E decidir o destino de um jogo olhando um pedaço dele é exatamente o oposto de catalogar.

Um portão de decisão na entrada resolve o problema de quem quer jogar. O meu problema é de quem quer saber.

Então o jogo novo entra no acervo. Ponto. Sem estado provisório, sem prazo, sem cobrança.

O que realmente apodrece

O problema não é o volume. É a despadronização.

Se os jogos novos entram sem gênero, sem conquista, com loja errada, em seis meses eu tenho dois acervos: um completo e um pela metade. E o segundo contamina qualquer filtro, porque uma consulta por gênero passa a mentir sem avisar.

A solução não é decidir o destino do jogo. É garantir que a linha dele esteja pronta.

Cinco colunas obrigatórias: título, plataforma, loja, gênero, conquistas. Enquanto faltar uma, a linha fica marcada como incompleta e aparece na lista de pendências. Não é uma decisão sobre o jogo — é uma decisão sobre o dado.

O lugar certo já existia

Aqui está a parte que eu demorei para ver.

Eu já escrevo um post semanal de domingo sobre o que estou jogando. Ele já existe, já tem rotina, já tem pauta. Enfiar a catalogação dentro dele não cria compromisso nenhum — só aproveita um que já estava de pé.

Então o semanal ganha uma segunda parte: o que entrou na biblioteca esta semana. Cinco ou seis jogos, na maioria das vezes. Um parágrafo, não uma lista.

E é ali que eles são catalogados. Na semana em que entraram, dentro de um texto que eu ia escrever de qualquer jeito.

Não criei um sistema novo. Descobri que o post de domingo já era o lugar certo — só faltava um script para não deixar passar nada.

Como o script funciona

Vale explicar o mecanismo, porque ele é a única coisa aqui que eu vou querer reler daqui a dois anos.

A ideia central é simples: o script não sabe o que é um jogo novo. Ele sabe o que já viu antes.

Na primeira execução, ele lê a planilha inteira e grava num arquivo de estado a chave de cada jogo. Na segunda, ele lê de novo e compara: o que está na planilha e não está no estado é novo. O que já está no estado é velho, e ele ignora.

É o mesmo princípio de um backup incremental. Não é preciso saber a data de nada — basta lembrar o que já passou.

A chave é o hash, não o título

O script não compara títulos. Compara o MD5 do título normalizado — a mesma função do post 3.

Isso importa por um motivo prático: "Fallout: London" e "Fallout London" são o mesmo jogo, mas como texto são strings diferentes. Se a comparação fosse por título, o script reportaria o mesmo jogo duas vezes em semanas diferentes, e eu iria catalogar de novo o que já estava catalogado.

Com o hash da versão normalizada — sem acento, sem pontuação, sem artigo no começo — as duas formas produzem a mesma chave. O script reconhece que é o mesmo jogo e não repete.

O estado é um arquivo, não uma coluna

Poderia guardar essa informação numa coluna da planilha. Optei por um arquivo separado, o estado_semana.json.

A razão é que a planilha é minha e o estado é do script. Se eu mexer na planilha, apagar linha, reordenar, corrigir um título — o estado continua intacto, porque ele não depende da estrutura da planilha. Ele só guarda uma lista de hashes.

E se eu quiser recomeçar do zero, apago o arquivo. A próxima execução vai considerar tudo novo, o que é o comportamento certo para uma reinstalação.

O que ele devolve

Três arquivos, cada um com uma função.

O novos_da_semana.csv é a matéria-prima do post de domingo: título, plataforma, loja, gênero, conquista, e a marcação de catalogado ou pendente.

O catalogacao_pendente.csv é a lista de trabalho. Só os jogos que entraram e ainda têm coluna obrigatória vazia, com a indicação exata de qual coluna falta.

E o terminal imprime um resumo: quantos entraram, quantos estão completos, a distribuição por loja e os gêneros que apareceram. É esse resumo que eu leio antes de escrever o domingo, porque ele já me diz sobre o que vale falar.

Por que ele roda no sábado

Não é preferência. É ordem de operação.

Se rodasse no domingo, eu estaria catalogando e escrevendo ao mesmo tempo, e a catalogação viraria a parte chata do post. Rodando no sábado, eu tenho a lista pronta, sei o que falta preencher, e chego no domingo só com a escrita.

É a mesma lógica de separar a coleta da redação em qualquer processo de escrita. Quem junta as duas coisas faz as duas mal.

O script

import csv, re, sys, json, unicodedata, hashlib
from pathlib import Path
from datetime import date, datetime

ESTADO   = "estado_semana.json"
SAIDA    = "novos_da_semana.csv"
PENDENTE = "catalogacao_pendente.csv"

ARTIGOS = {"the","a","an","o","os","as","um","uma"}
MAPA_PLAT = {"steam":"Steam","gog":"GOG","epic":"Epic","amazon":"Amazon",
             "ea":"EA","uplay":"Uplay","windows":"Windows"}
OBRIGATORIAS = ["Titulo","Plataforma","Loja","Genero","TemConquistas"]

def normalizar(t):
    t = unicodedata.normalize("NFKD", (t or "").lower())
    t = "".join(c for c in t if not unicodedata.combining(c))
    t = re.sub(r"\s*[\(\[]\s*[^\)\]]*\s*[\)\]]", " ", t)
    t = re.sub(r"[^a-z0-9 ]", " ", t)
    p = [x for x in t.split() if x]
    while p and p[0] in ARTIGOS:
        p.pop(0)
    return " ".join(p)

def chave(titulo):
    return hashlib.md5(normalizar(titulo).encode("utf-8")).hexdigest()

def corrigir_loja(l):
    loja = (l.get("Loja") or "").strip()
    plat = (l.get("Plataforma") or "").strip()
    if loja.lower() in ("", "local", "windows") and plat:
        return MAPA_PLAT.get(plat.lower(), loja or plat)
    return loja or plat

def faltando(l):
    return ";".join(c for c in OBRIGATORIAS if not (l.get(c) or "").strip())

def main():
    if len(sys.argv) < 2:
        print("Uso: python novos_da_semana.py biblioteca.csv [--post N]")
        return

    post = None
    if "--post" in sys.argv:
        i = sys.argv.index("--post")
        if i + 1 < len(sys.argv):
            post = int(sys.argv[i + 1])

    bruto = Path(sys.argv[1]).read_text(encoding="utf-8-sig", errors="ignore")
    primeira = bruto.split("\n", 1)[0]
    delim = ";" if primeira.count(";") >= primeira.count(",") else ","
    linhas = list(csv.DictReader(bruto.splitlines(), delimiter=delim))

    estado = {"vistos": [], "ultimo_post": 0}
    if Path(ESTADO).exists():
        estado = json.loads(Path(ESTADO).read_text(encoding="utf-8"))
    vistos = set(estado["vistos"])

    if post is None:
        post = estado.get("ultimo_post", 0) + 1

    novos = []
    for l in linhas:
        k = chave(l.get("Titulo"))
        if not k or k in vistos:
            continue
        l["Chave"] = k
        l["Loja"] = corrigir_loja(l)
        l["OrigemAcervo"] = "fluxo"
        l["PostEntrada"] = post
        l["DataEntrada"] = date.today().isoformat()
        l["Faltando"] = faltando(l)
        l["Catalogado"] = "nao" if l["Faltando"] else "sim"
        novos.append(l)

    if not novos:
        print("Nenhum jogo novo desde a ultima execucao.")
        return

    campos = ["Titulo","Plataforma","Loja","Genero","TemConquistas",
              "DataLancamento","Desenvolvedor","Chave","PostEntrada",
              "DataEntrada","Catalogado","Faltando"]
    with open(SAIDA, "w", newline="", encoding="utf-8-sig") as fh:
        w = csv.DictWriter(fh, fieldnames=campos, delimiter=";",
                           extrasaction="ignore")
        w.writeheader()
        w.writerows(novos)

    pend = [l for l in novos if l["Catalogado"] == "nao"]
    with open(PENDENTE, "w", newline="", encoding="utf-8-sig") as fh:
        w = csv.writer(fh, delimiter=";")
        w.writerow(["Titulo","Loja","Faltando"])
        for l in pend:
            w.writerow([l["Titulo"], l["Loja"], l["Faltando"]])

    estado["vistos"] = sorted(vistos | {l["Chave"] for l in novos})
    estado["ultimo_post"] = post
    Path(ESTADO).write_text(json.dumps(estado, ensure_ascii=False, indent=2),
                            encoding="utf-8")

    from collections import Counter
    print("Jornada Gamer Semanal #{}".format(post))
    print("Novos .............", len(novos))
    print("Catalogados .......", len(novos) - len(pend))
    print("Pendentes .........", len(pend))
    print()
    for k, v in Counter(l["Loja"] for l in novos).most_common():
        print("  {:<12} {}".format(k, v))
    print()
    for l in pend[:10]:
        print("  {:<34} {}".format(l["Titulo"][:34], l["Faltando"]))

if __name__ == "__main__":
    main()

O que acontece quando ele roda errado

Três falhas possíveis, e vale saber o que cada uma significa antes de acontecer.

A primeira é o estado apagado ou perdido. Se o estado_semana.json sumir, a próxima execução considera os mil e seiscentos jogos como novos. O script não quebra — ele simplesmente reporta tudo. A saída é não publicar nada naquele domingo e reconstruir o estado rodando uma vez com a planilha completa antes de qualquer jogo novo entrar.

A segunda é o título alterado na planilha. Se eu corrigir o nome de um jogo que já foi catalogado, o hash muda e o script passa a vê-lo como novo. Ele vai aparecer de novo no post. Não é grave, mas é o tipo de coisa que eu preciso reconhecer na hora em que acontecer, em vez de achar que o script está errado.

A terceira é a planilha exportada incompleta. Se o LaunchBox exportar antes de terminar de carregar, o CSV vem truncado e o script reporta menos jogos do que entraram. A conferência é simples: o total de linhas do CSV tem que bater com o total de jogos do LaunchBox. Se não bater, o export falhou.

O dado que me incomodou

Tem uma coisa que só apareceu quando eu montei isso.

O acervo de fluxo é mais bem documentado que o da fotografia.

Os jogos novos vêm com data de entrada, loja corrigida, post de origem, gênero vindo da API. Os mil e seiscentos antigos têm gênero em 43% das linhas e data de aquisição em quase nenhuma.

Ou seja: o fluxo não é o problema do acervo. Ele é o modelo do que o acervo deveria ter sido desde o começo.

O que chega agora é catalogado melhor do que o que eu passei quinze anos acumulando. A parte que cresce virou o padrão da parte que já estava aqui.

Isso muda o que a série precisa fazer daqui para frente. Não é só seguir os blocos de hash — é usar o fluxo como régua, e ir corrigindo o acervo antigo na medida em que os blocos o trazem de volta.

A biblioteca não dobra de tamanho. Ela ganha uma segunda camada, e a segunda camada é a que está certa.

Nenhum comentário:

Postar um comentário