<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucas Usamentiaga Aznar</title>
    <description>The latest articles on DEV Community by Lucas Usamentiaga Aznar (@lucasusamentiaga).</description>
    <link>https://dev.to/lucasusamentiaga</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3933950%2Fb031df13-4fcc-42dc-8086-a4f829b23f53.jpeg</url>
      <title>DEV Community: Lucas Usamentiaga Aznar</title>
      <link>https://dev.to/lucasusamentiaga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucasusamentiaga"/>
    <language>en</language>
    <item>
      <title>How I built my own MyAnimeList alternative in Python (FastAPI + SQLite)</title>
      <dc:creator>Lucas Usamentiaga Aznar</dc:creator>
      <pubDate>Fri, 15 May 2026 22:11:24 +0000</pubDate>
      <link>https://dev.to/lucasusamentiaga/how-i-built-my-own-myanimelist-alternative-in-python-fastapi-sqlite-3ai5</link>
      <guid>https://dev.to/lucasusamentiaga/how-i-built-my-own-myanimelist-alternative-in-python-fastapi-sqlite-3ai5</guid>
      <description>&lt;p&gt;After months of side-project work, I just released &lt;strong&gt;v1.0.0&lt;/strong&gt; of &lt;a href="https://github.com/lucasusamentiaga/anime-tracker" rel="noopener noreferrer"&gt;Anime Tracker&lt;/a&gt; — a self-hosted desktop app to manage your anime list. Here's the story of how I built it and the technical decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I wasn't happy with existing anime trackers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MyAnimeList&lt;/strong&gt;: ugly UI, cloud-only, full of ads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AniList&lt;/strong&gt;: better UX but still cloud-dependent and limited customization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spreadsheets&lt;/strong&gt;: zero features (no notifications, no recommendations, no search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing self-hosted alternatives&lt;/strong&gt;: either abandoned or too complex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I decided to build my own. Local-first, no ads, clean UI, with features I actually wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why FastAPI
&lt;/h3&gt;

&lt;p&gt;I considered Flask, Django, and FastAPI. Picked &lt;strong&gt;FastAPI&lt;/strong&gt; because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Async support out of the box (critical for calling 8 external APIs concurrently)&lt;/li&gt;
&lt;li&gt;Automatic OpenAPI docs at &lt;code&gt;/docs&lt;/code&gt; — useful for debugging&lt;/li&gt;
&lt;li&gt;Pydantic models for request/response validation&lt;/li&gt;
&lt;li&gt;Performance comparable to Node.js&lt;/li&gt;
&lt;li&gt;Type hints make the code self-documenting&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why SQLite (not PostgreSQL)
&lt;/h3&gt;

&lt;p&gt;For a single-user desktop app, SQLite is perfect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero config&lt;/strong&gt;: no database server to install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One file&lt;/strong&gt;: the user's entire anime list is in &lt;code&gt;anime_tracker.db&lt;/code&gt;. Easy to back up, easy to move between PCs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast enough&lt;/strong&gt;: even with thousands of anime entries, queries are sub-millisecond&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded migrations&lt;/strong&gt;: I wrote a tiny auto-migration system that runs at startup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why vanilla JS (not React)
&lt;/h3&gt;

&lt;p&gt;The frontend is ~3000 lines of HTML/CSS/JS without any framework. Why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No build step&lt;/strong&gt;: easier for contributors, easier to debug&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller&lt;/strong&gt;: total app size is ~500KB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster initial load&lt;/strong&gt;: no React runtime to download&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: managing state by hand is more code, but I kept things simple&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Interesting technical bits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pluggable scrapers (8 sources)
&lt;/h3&gt;

&lt;p&gt;I built a base class that each scraper implements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseScraper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;buscar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AnimeData&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way I can search across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AniList&lt;/strong&gt; (GraphQL API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jikan v4&lt;/strong&gt; (REST API for MyAnimeList)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kitsu&lt;/strong&gt; (JSON:API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crunchyroll&lt;/strong&gt; (v2 API)&lt;/li&gt;
&lt;li&gt;4 HTML scrapers for specific sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one source fails, the next takes over. The user selects priority order.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Image fallback chain
&lt;/h3&gt;

&lt;p&gt;A common problem: some scrapers return entries without cover images. Empty anime cards look terrible.&lt;/p&gt;

&lt;p&gt;So I built a 3-step fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;anime_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imagen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_find_anime_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anime_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;anime_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imagen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;
&lt;span class="c1"&gt;# Otherwise frontend shows a gradient + 🎌 placeholder
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function &lt;code&gt;_find_anime_image&lt;/code&gt; queries AniList GraphQL by name. Works 95% of the time. The remaining 5% gets a clean gradient placeholder.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Async pattern with &lt;code&gt;run_in_executor&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;requests&lt;/code&gt; is synchronous. FastAPI is async. If I called &lt;code&gt;requests.get()&lt;/code&gt; directly in an async route, I'd block the event loop.&lt;/p&gt;

&lt;p&gt;The solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buscar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the blocking call in a thread pool while the event loop stays free. Concurrent requests to 8 scrapers happen truly in parallel.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Mobile access via local WiFi
&lt;/h3&gt;

&lt;p&gt;This was the trickiest feature. When the user enables "Mobile mode" the app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Binds to &lt;code&gt;0.0.0.0:8765&lt;/code&gt; instead of &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Generates a random PIN + secure token&lt;/li&gt;
&lt;li&gt;Renders a QR code with the URL &lt;code&gt;http://&amp;lt;LOCAL_IP&amp;gt;:8765/m?token=...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Phone scans the QR → opens the mobile route → enters PIN → gets cookie session&lt;/li&gt;
&lt;li&gt;Mobile UI is a PWA with service worker for offline read access&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All while keeping the desktop port &lt;code&gt;127.0.0.1&lt;/code&gt; only for the main UI. The user can disable mobile mode and the network port closes immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. PyInstaller distribution
&lt;/h3&gt;

&lt;p&gt;Compiling Python to a &lt;code&gt;.exe&lt;/code&gt; is famously painful. PyInstaller with &lt;code&gt;--onedir&lt;/code&gt; gives you a folder with &lt;code&gt;python.exe&lt;/code&gt; + dependencies + your code. ~12MB total.&lt;/p&gt;

&lt;p&gt;I built a custom installer in Python (with tkinter UI) that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copies the onedir to &lt;code&gt;Program Files&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates desktop and start menu shortcuts (using PowerShell &lt;code&gt;WScript.Shell&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Registers the uninstaller in Windows Registry&lt;/li&gt;
&lt;li&gt;Sets file associations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No NSIS, no Inno Setup. Just Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scope creep is real&lt;/strong&gt;. I started thinking "just a list manager" and ended with 70+ API routes. Every time I thought I was done I'd find one more thing to polish.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentation is harder than code&lt;/strong&gt;. Writing the README took me 3 hours. Explaining features so users actually discover them is its own skill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Notifications are tricky&lt;/strong&gt;. Web Notifications need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User permission (browser API)&lt;/li&gt;
&lt;li&gt;Service worker for background polling&lt;/li&gt;
&lt;li&gt;State sync between server and client&lt;/li&gt;
&lt;li&gt;Anti-spam (don't notify the same episode twice)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-language support shapes architecture&lt;/strong&gt;. I added &lt;code&gt;i18n.py&lt;/code&gt; with a dictionary per language and a &lt;code&gt;T(key)&lt;/code&gt; function. Translating the UI is now a 5-minute task per language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI matters&lt;/strong&gt;. GitHub Actions running &lt;code&gt;python -m py_compile&lt;/code&gt; on every push has caught more bugs than I'd admit. Free safety net.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Web series / movies (the same architecture works for any media type — Reddit user actually suggested this)&lt;/li&gt;
&lt;li&gt;Auto-detection from VLC/MPV like Taiga does&lt;/li&gt;
&lt;li&gt;Discord rich presence&lt;/li&gt;
&lt;li&gt;More languages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;Code, screenshots, installer for Windows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lucasusamentiaga/anime-tracker" rel="noopener noreferrer"&gt;github.com/lucasusamentiaga/anime-tracker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Stars and feedback appreciated.&lt;/p&gt;




&lt;p&gt;If you want to follow more of my projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Twitter: &lt;a href="https://x.com/T0ff3_x" rel="noopener noreferrer"&gt;@T0ff3_x&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Instagram: &lt;a href="https://www.instagram.com/lucasuazz_/" rel="noopener noreferrer"&gt;@lucasuazz_&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer technical questions in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>anime</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
