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