Vlastní produkt · 2026 – dosud Projekt 1 z 20
AniMood
Jedno dobré anime na dnešní večer – podle nálady a vkusu.
Ověřený stav webu: Běží, odezva 81 ms,
Ve zkratce
- Role
- Návrh, vývoj a provoz
- Období
- 2026 – dosud
- Stav
- V provozu
- Technologie
- 9 technologií
Většina anime webů nabízí katalog. AniMood nabízí dnešní večer: nastavíte náladu, projdete krátký balíček karet a každé anime odložíte, uložíte na později, nebo si ho vyberete. Z těch rozhodnutí a z propojeného seznamu na AniListu se skládá vkus, podle kterého se řadí další karty, a zápisník si pamatuje, jak výběr dopadl. Aplikaci jsem navrhl, postavil a provozuji na vlastním serveru – od schématu databáze po noční zálohy a zásady ochrany soukromí.
Proč je to technicky zajímavé
- Doporučování, které umí vysvětlit samo sebe. Vlastní algoritmus váží žánry a témata z poznámek i ze seznamu na AniListu a starší poznámky postupně ztrácejí váhu. Balíček hlídá rozmanitost (podobné tituly nejdou za sebou), zhruba každá šestá karta je mimo obvyklý vkus a karty ukazují, proč se v balíčku objevily.
- Nejdřív první řada. Druhou sérii nebo navazující film nabídne až tomu, kdo dokončil předchozí díl, jinak nabídne začátek série. Pořadí bere ze vztahů na AniListu a doplňuje ho z otevřené databáze anime-offline-database (ODbL), kterou jednou měsíčně importuje – a když data neprojdou kontrolou, celý import odmítne.
- Komunitní doporučení jako graf. Tituly, které na AniListu doporučují fanoušci vašich oblíbených anime, se v balíčku posunou o pár míst výš – nikdy na první místo a nikdy dva za sebou. Graf balíček řídí, ale nepřebírá.
- AniList oběma směry. Změny ze zápisníku se řadí do fronty a zapisují na AniList, změny z AniListu (třeba z aplikace Taiga) se vracejí do zápisníku. Při konfliktu vyhrává novější změna, takže starší stav nikdy nepřepíše novější.
- Osm jazyků (angličtina, čeština, slovenština, němčina, polština, španělština, francouzština a japonština) s vlastním formátem zpráv pro množné číslo a rod, který funguje stejně v PHP i v JavaScriptu. I hravé „tituly vkusu“ mají v každém jazyce správnou shodu v rodě.
- Wrapped na konci sezóny: příběh o tom, co člověk za sezónu viděl, se počítá až při otevření ze zápisníku a mezipaměti. Nic se kvůli němu neukládá a nic se neposílá ven.
Překážky a jak jsem je vyřešil
- SQLite pod souběžným zápisem. Transakce, která už četla, v režimu WAL nezíská zámek pro zápis, pokud mezitím zapsal někdo jiný – SQLite rovnou vrátí „database is locked“ a čekání (busy_timeout) nepomůže. Při zátěžovém testu s dvanácti souběžnými uživateli tak selhal zhruba každý desátý swipe. Zápisy, které nejdřív čtou, teď začínají transakcí BEGIN IMMEDIATE a test to hlídá souběžnými zápisy z více procesů. Druhá past: nedočtený kurzor drží starý snímek databáze, takže zápis po síťovém volání na AniList selhal. Kurzory se proto zavírají před každou pomalou operací.
- Limity AniList API. Všichni uživatelé i všechny funkce čerpají z jednoho společného rozpočtu požadavků a odpověď 429 pozastaví volání pro celý server na tak dlouho, jak AniList určí. Co jde, bere aplikace z mezipaměti a z otevřené databáze, ne z API.
- Malý lokální model neumí čísla. Model qwen2.5:7b nedokázal spolehlivě nastavit posuvníky se zápornými a kladnými hodnotami. Teď vybírá jen z pevných kategorií (třeba nízká, střední nebo vysoká energie), čísla dopočítá server a každou změnu musí model doložit citací z textu uživatele – bez citace se nic nezmění. Potvrzení, které člověk uvidí, skládá server, ne model.
- Content Security Policy bez výjimek. Přísná CSP nepovoluje inline styly, takže atribut style v HTML vloženém skriptem prohlížeč zahodí. Barvy obálek, rozměry i animace se proto nastavují přes CSSOM, které CSP nevadí.
- Dabéři, kteří chyběli. AniList u postav vracel prázdné dabéry, pokud se dotaz neptal i na samotnou postavu, a celý katalog se tak uložil bez nich. Po nalezení příčiny se dotazy opravily a chybějící data doplnila noční úloha.
- Překlady, které nezněly přirozeně. První verze překladů působily jako doslovné převody z angličtiny. Všech sedm jazyků proto prošlo revizí s glosářem pro každý jazyk a s pravidly pro žánry uprostřed věty.
Co není vidět
- Soukromí od návrhu. Účet nepotřebuje e-mail: přístup se obnovuje jednorázovými kódy, ze kterých se ukládá jen otisk, stejně jako z hesel (Argon2id). Jazykový model ve výchozím nastavení běží na stejném serveru, takže popis nálady nikam neodchází, a do promptu se nedostane jméno, identifikátor ani citlivá témata.
- GDPR v kódu, ne jen v zásadách. Export a smazání účtu hlídá test, který selže, když nová tabulka chybí v přehledu osobních dat. Data mají stanovené doby uchování a po jejich uplynutí je smaže denní údržba.
- Ochrana proti zneužití. Přihlášení počítá jen neúspěšné pokusy a cizí člověk jimi nezablokuje majitele účtu. Limity požadavků pracují s IP adresami zahashovanými tajným klíčem, takže v databázi nejsou čitelné adresy. Jména, která napodobují existující účty nebo provozovatele, aplikace odmítne. Text od ostatních se při uložení čistí od neviditelných znaků, které mění směr písma – jediný neplatný bajt uložený ve zprávě předtím dokázal příjemci shodit celou aplikaci.
- Taste Match je volitelný a jen pro dospělé. Fotky se překódují (zmizí metadata EXIF včetně polohy) a procházejí moderací, každý vidí ostatní pod vlastními šifrovanými aliasy a zásah moderátora vždy uvádí, které pravidlo bylo porušeno.
- Provoz. Každou noc záloha databáze s kontrolou integrity a zkušební obnovou z hotového archivu, 30 dní zpětně. Údržba běží až po odeslání odpovědi, takže na ni nikdo nečeká. Komprese a dlouhá mezipaměť verzovaných souborů zmenšily první načtení z 2,4 MB na 0,68 MB. Šestnáct testovacích sad hlídá doporučování, pořadí sérií, lokalizaci, výkon dotazů, zneužití i GDPR.
Technologie
- PHP 8.2 bez frameworku a bez balíčků z Composeru – žádné cizí závislosti, které by bylo potřeba hlídat a aktualizovat.
- SQLite v režimu WAL – jedna databáze v souboru, online zálohy bez odstávky a schéma s verzí (dnes 15, 38 tabulek), které se při startu samo dorovná.
- JavaScript bez frameworku a GSAP – animace přes tenkou vrstvu, která respektuje omezený pohyb; aplikace jde nainstalovat jako PWA.
- Ollama s modelem qwen2.5:7b na grafické kartě serveru, volitelně Claude API s měsíčním stropem útraty. Pomocníci s volným textem jsou zatím jen pro administrátory.
- AniList GraphQL API a OAuth 2.0 – propojení účtu, čtení seznamu a volitelný zápis zpět.
- anime-offline-database (ODbL) – pořadí sérií bez dalších dotazů na API.
- NixOS a časovače systemd – noční zálohy, měsíční import otevřené databáze a doplňování dabérů.
Ukázky
5 snímků
Postaveno na
- PHP 8.2
- SQLite
- JavaScript
- Ollama
- GSAP
- AniList GraphQL API
- OAuth 2.0
- PWA
- NixOS