A slideout-based media browser for Anime, Manga, and Novels built as a
daemon plugin for DankMaterialShell (DMS).
It ships three self-contained Python backend servers (one per media type) that the
QML frontend talks to over 127.0.0.1, plus a full reader/player experience:
stream anime in MPV, read manga chapters as scrollable images, read novel chapters
as paginated text — with libraries, offline downloads, and per-module settings.
- Search across anipy-cli's provider stack (
anidbappprimary,animehubfallback). - Browse — Popular = current-year catalog; Latest = current-season catalog (country filter accepted).
- Cards enriched with posters, score, media type, and episode counts (scraped from anidb.app).
- Episode list with sub/dub support; direct HLS stream links (no referrer needed).
- Plays fullscreen via MPV (launched with
yt-dlphook only as a resolution fallback).
- Browse popular and latest manga from MangaDex (via the WeebCentral API mirror).
- Search, filter by type, sort by update recency.
- Scrollable image reader with zoom (Ctrl+Wheel / +/- buttons, 0.3×–1.0×).
- Add to library, track progress, download chapters for offline reading.
- Browse popular (ranking) and latest (recent updates) with infinite scroll.
- Search via LightNovelCrawler across curated sources (novelfire.net, novelbuddy.com) plus a raw FreeWebNovel fallback provider.
- Provider-agnostic backend — every ID is prefixed with the provider name, so favorites, downloads, and chapter URLs never collide across sources.
- Text reader with 3 themes (dark / sepia / light), chapter prev/next navigation, library tracking, and offline chapter downloads.
- Right-side slideout panel (docked right, expandable to full width).
- Persisted library per module (favorites JSON + progress tracking).
- Persisted settings (
~/.local/share/quickshell/mediahub_settings.json). - Infinite scroll with position preservation across tabs.
plugin.json # Plugin manifest (daemon, slideout)
MediaHub.qml # Plugin root (daemon component)
├── Content.qml # Tab container (Manga / Novel / Anime / Settings)
├── MediaHubSettings.qml # Settings entry point (plugin manifest)
│
├── anime/ # Anime module
│ ├── AnimePanel.qml
│ └── components/
│ ├── BrowseView.qml
│ ├── DetailView.qml # Episode list, sub/dub, MPV launch
│ └── LibraryView.qml
│
├── manga/ # Manga module
│ ├── MangaReader.qml
│ └── components/
│ ├── BrowseView.qml
│ ├── DetailView.qml
│ ├── LibraryView.qml
│ └── ReaderView.qml # Flickable + Column image reader (zoomable)
│
├── novel/ # Novel module
│ ├── NovelReader.qml
│ └── components/
│ ├── BrowseView.qml
│ ├── DetailView.qml
│ ├── LibraryView.qml
│ └── ReaderView.qml # Text reader (theming, prev/next)
│
├── settings/ # Settings tab
│ └── SettingsPanel.qml
│
├── services/ # QML service singletons (HTTP clients)
│ ├── Anime.qml # ↔ anime_server.py :5050
│ ├── Manga.qml # ↔ manga_server.py :5150
│ ├── Novel.qml # ↔ novel_server/ :5151
│ ├── Settings.qml # persisted settings singleton
│ ├── HubTheme.qml # hub/reader theme definitions
│ └── qmldir
│
└── scripts/ # Python backend servers
├── anime_server.py # Flask, anipy-api wrapper :5050
├── manga_server.py # http.server, MangaDex wrapper :5150
└── novel_server/
├── main.py # entry point (argparse)
├── server.py # http.server routing
├── storage.py # favorites + offline downloads
└── providers/
├── base.py # NovelProvider interface + helpers
├── freewebnovel.py # FreeWebNovel scraper
├── lncrawler.py # LightNovelCrawler wrapper + novelbuddy browse
├── utils.py # fetch (curl_cffi), caching, text cleaning
└── __init__.py # provider registry + ID prefixing
The QML Process components launch each backend lazily (on first use) from the
plugin's own venv — paths are always plugin-relative (_scriptDir), never
hardcoded to a user home, which keeps the plugin portable.
All three servers listen on loopback only and auto-start on demand.
| Port | Module | Server | Entry point |
|---|---|---|---|
| 5050 | Anime | Flask (+ anipy-api) | scripts/anime_server.py |
| 5150 | Manga | http.server |
scripts/manga_server.py |
| 5151 | Novel | http.server |
scripts/novel_server/main.py (env NOVEL_PORT overrides) |
| Endpoint | Description |
|---|---|
GET / |
Endpoint documentation + provider list |
GET /health |
{"status": "ok"} |
GET /search?q=<query>&mode=sub|dub |
Multi-provider search |
GET /popular?size=20&page=1 |
Current-year catalog (paged) |
GET /latest?limit=26&page=1&mode=sub|dub&country=ALL |
Current-season catalog (paged) |
GET /episodes?id=<provider:identifier>&mode=sub|dub&check=1 |
Episode list + details |
GET /links?id=<provider:identifier>&ep=<n>&mode=sub|dub&quality=best |
Stream links |
- Show IDs cross the API as
"anidbapp:<urlencoded-identifier>"(e.g.anidbapp:3880). modeselects subtitle language:sub/dub(anidbapp serves HLS with both when available).- All responses TTL-cached (per-key single-flight); every upstream call runs under a timeout — the underlying anipy-api library has none.
- Enrichment (posters/score/type/episode counts) requires the anidb.app browser UI and sends a desktop Chrome user agent.
| Endpoint | Description |
|---|---|
GET /health |
{"ok": true} |
GET /hot |
Popular/trending list |
GET /latest?page=1 |
Recently updated (paged) |
GET /search?q=<query>&type=<manga|manhwa|...>&offset=0&sort=Latest+Updates |
Search |
GET /info?id=<manga-id> |
Series details + chapter list |
GET /pages?chapterId=<chapter-id> |
Image page URLs |
GET /image?url=<encoded-url> |
Cover/image proxy (avoid hotlink bans) |
GET /favorites /favorites/check |
Library list / batch status |
POST /favorites/add /favorites/remove /favorites/mark-seen |
Library management |
GET /dl/list /dl/progress /dl/pages |
Offline download queries |
POST /dl/start /dl/delete |
Offline download control |
| Endpoint | Description |
|---|---|
GET /health |
{"ok": true, "provider": "<active>"} |
GET /provider/list |
Registered providers |
GET /provider/active |
Current provider name/label |
POST /provider/switch |
{"provider": "lncrawl"} |
GET /hot |
Popular list (current provider) |
GET /latest?page=1 |
Recent updates, {results, hasMore, nextPage} |
GET /search?q=<query>&genre=&status=All&page=1 |
Search, {results, hasMore, nextPage} |
GET /info?id=<prefixed-novel-id> |
Novel details + full chapter list (offline-first metadata) |
GET /chapter?id=<prefixed-chapter-id> |
Chapter body {paragraphs, wordCount, prevId, nextId} — serves offline copy first |
GET /image?url=<encoded-url> |
Cover proxy |
GET /favorites /favorites/check |
Library / progress lookup |
POST /favorites/add /favorites/remove /favorites/mark-seen |
Library management |
GET /dl/list /dl/progress /dl/chapter |
Offline download queries |
POST /dl/start /dl/delete |
Offline download control |
Internal (provider sees): "novel/some-slug"
External (client sees): "freewebnovel:novel/some-slug" or "lncrawl:https://..."
Every ID crossing the HTTP boundary carries its provider prefix, so favorites and downloads never collide. Unknown prefixes are tolerated (kept as-is) so old records still load.
| Provider | Source | Used for |
|---|---|---|
lncrawl (default) |
LightNovelCrawler package + novelbuddy.me browse | search (novelfire/novelbuddy), popular (/ranking), latest (/latest?page=N), info + chapter crawling |
freewebnovel |
freewebnovel.com scrapers | browse/hot/latest + chapters (currently in maintenance → falls back automatically) |
Browsing is resilient: if the active provider fails, the other provider's list is
served transparently (require_results=True). lncrawl novel/chapter URLs work
directly — huge catalogs (3,000+ chapters) load fast via per-novel TTL caching.
| Package | Purpose |
|---|---|
| Python ≥ 3.10 | Backend servers |
| Flask | Anime HTTP API routing |
| requests | HTTP client |
| curl_cffi | Browser TLS fingerprinting (Cloudflare bypass) |
| anipy-api | Anime providers/streams (anipy-cli's API package) |
| lightnovel-crawler | Novel search + crawling engine |
| Dependency | Purpose |
|---|---|
mpv (system package) |
Anime video playback |
yt-dlp |
MPV resolution fallback for embed-based sources |
# Arch Linux
sudo pacman -S mpv # yt-dlp is venv-installed by install.sh
# Ubuntu / Debian
sudo apt install mpv# Clone or copy the plugin directory
git clone <this-repo> ~/Projects/plugin_media
# Run the installer
cd ~/Projects/plugin_media
chmod +x install.sh
./install.shThe installer:
- Cleans and copies all plugin files to
~/.config/DankMaterialShell/plugins/mediaHub/. - Creates a fresh Python virtual environment with Flask, requests, curl_cffi, anipy-api, lightnovel-crawler and yt-dlp.
- Strips
__pycache__from the install target.
Restart DMS to load the plugin:
systemctl --user restart dms.serviceNote:
quickshell restartis not a valid command — DMS runs as the systemd user unitdms.service. The three backend servers then auto-start on first use.
Verify:
curl http://127.0.0.1:5050/health # anime
curl http://127.0.0.1:5150/health # manga
curl http://127.0.0.1:5151/health # novelThe plugin opens as a right-side slideout. Toggle it at any time with the DMS IPC call:
dms ipc call plugins toggle mediaHubOther useful plugin IPC commands:
dms ipc call plugins status mediaHub # "loaded" when active
dms ipc call plugins enable mediaHub # force-enable
dms ipc call plugins disable mediaHub # force-disable| What | Where |
|---|---|
| Plugin files | ~/.config/DankMaterialShell/plugins/mediaHub/ |
| Plugin venv | ~/.config/DankMaterialShell/plugins/mediaHub/.venv/ |
| Hub/reader settings | ~/.local/share/quickshell/mediahub_settings.json |
| Manga library + downloads | ~/.local/share/quickshell-manga/ |
| Novel library + downloads | ~/.local/share/quickshell-novel/ |
- MPV playback: DetailView launches
mpv --fs --force-window=yes --title=<name>with the plugin venv'syt-dlpwired as the resolve hook and a--referreronly when the source requires one. Direct HLS from hls.anidb.app needs no referrer. - Chapter downloads render the text to JSON on the server and are served
offline-first by
/chapter— download a chapter once, and it reads instantly even if the source is down. - Reader themes: dark / sepia / light for novels, plus hub-wide auto/dark/light in the Settings tab.
- Novel browsing fallback chain: default provider is
lncrawl(novelbuddy.me); if it fails, the server automatically triesfreewebnovel— and vice versa.
| Symptom | Fix |
|---|---|
| Nothing loads after updating QML | systemctl --user restart dms.service (QML Process commands are fixed at shell load) |
Port already in use (Address already in use) |
A stale server from an old install holds the port — pgrep -af "novel_server|anime_server|manga_server" and kill it; the QML watcher respawns its own |
| Novel browse empty while freewebnovel is maintenanced | Switch provider via POST /provider/switch {"provider": "lncrawl"} or rely on the automatic fallback |
| anidb.app enrichment fails | The API expects a desktop UA (Mozilla/5.0 (Windows NT 10.0; WOW64) … Chrome/86) — the server sends it automatically |
mpv won't start |
Install mpv system-wide (sudo pacman -S mpv / sudo apt install mpv) |
- DankMaterialShell — the shell this plugin extends
- anipy-cli / anipy-api — anime providers & stream resolution
- LightNovelCrawler — novel search + crawling engine
- MangaDex / WeebCentral — manga catalog + chapter images
- NovelBuddy (novelbuddy.me) — novel browse (popular/latest) fallback