Všechny projekty

Vlastní produkt · 2026 – dosud Projekt 1 z 20

AniMood

Jedno dobré anime na dnešní večer – podle nálady a vkusu.

Otevřít web

Ověřený stav webu: Běží, odezva 81 ms,

Úvodní stránka AniMood s nadpisem Tonight, one good anime a formulářem pro založení účtu

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ů.

Postaveno na

  • PHP 8.2
  • SQLite
  • JavaScript
  • Ollama
  • GSAP
  • AniList GraphQL API
  • OAuth 2.0
  • PWA
  • NixOS

Klávesové zkratky.

Klávesové zkratky webu. Při psaní do formuláře nereagují.

Web

?
Tahle nápověda
g
Mřížka 12 sloupců, komponenty a měření stránky
d
Vzhled: podle systému, světlý, tmavý, Linkin Park
Ctrl P
Vytisknout CV místo webu
Esc
Zavřít dialog nebo mřížku

Modrou tečku za jménem v úvodu jde chytit a hodit.

Linkin Park

l setlist lp
Setlist, skladby po albech
papercut
Přes stránku se roztrhne papír
numb
Web na pár vteřin otupí
faint
Stránka omdlí a zase se probere
meteora
Meteorický roj a přímý zásah do tečky za jménem
fromzero emily
Web zhasne do nuly a znovu se zrodí, s díky pro Emily Armstrong
concert koncert
Koncert: web projede barvy všech alb
castleofglass
Stránka zesklovatí, praskne a sklo se sesype
iridescent
Nadpisy a tečky se na chvíli přelijí duhou
breakingthehabit
Co je vidět, se rozsype a zase složí
intheend
Závěrečné titulky jako v kině
victim morethevictim
Web se rozpadne v glitchi a zůstane rozladěný (do obnovení, dalšího slova nebo Esc)
finalmasquerade
Škraboška přes celý web, kamera proletí okem
lost
Odchod na stránku 404

Slova napište kdekoli mimo formulář, velikost písmen ani mezery nevadí. S omezeným pohybem přijde jen krátké oznámení.

Na telefonu otevře setlist pět ťuknutí na modrou tečku za jménem.