A self-hosted Niconico VOCALOID video search engine with a modern Spotify-like interface.
Before exposing this service outside a local or otherwise trusted environment, set a unique JWT secret and restrict allowed hosts/origins to the exact values you need. Do not deploy the example defaults as-is on an internet-facing system. If you plan to self-host this for other users, review authentication, reverse-proxy, and frontend/backend exposure settings first.
Inspired by ニコニコ超検索 – a popular Niconico video search service.
- Modern Interface: Spotify-inspired UI with playlist support and continuous playback via embedded player
- Infinite Scroll: Modern infinite scrolling design instead of fixed pagination
- Dark/Light Mode: Toggle between dark and light themes
- Multi-language Support: Switch between interface languages
- Compact Mode: Resize window to a compact PIP-like mode for multitasking
- Self-Hosted: Full control over your data and deployment
- Custom Formula Sorting & Filtering: Weight videos by view count, mylist count, comment count, and like count using your own formula
- Watch History: Track and filter videos you've already watched
- Customizable Scraper: Define your own query conditions to curate the video database
- Android Support (Prototype): PWA/TWA packaging for mobile use
This project is a result of vibe coding – building something useful while exploring modern web technologies and the NixOS ecosystem. It's functional but has rough edges.
| Layer | Technology |
|---|---|
| Backend | Python 3.11+, FastAPI, Uvicorn |
| Frontend | TypeScript, React, Vite, Tailwind CSS |
| Database | SQLite with FTS5 full-text search |
| Data Source | Niconico Snapshot API v2 + GetThumbInfo API (user info) |
| Package Management | uv (Python), npm (Node.js) |
| Deployment | NixOS Flakes, systemd services |
- Full-text Search: SQLite FTS5 enables fast keyword and tag searches with AND/OR/NOT operators
- JWT Authentication: Stateless user authentication for multi-user support
- Embedded Player: Uses official Niconico embed player for seamless playback
- Infinite Scroll: Dynamic loading for both search results and watch history
- Formula-based Scoring: Custom weighted scoring system for flexible sorting and filtering
Note: This project has only been tested on NixOS with Flakes. Generic Linux builds are provided but untested.
# flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
vocaloid-search.url = "github:anton1615/VOCALOID-Search";
};
outputs = { self, nixpkgs, vocaloid-search, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
modules = [
vocaloid-search.nixosModules.default
{
services.vocaloid-search = {
enable = true;
port = 8765;
jwtSecret = "your-secret-key";
allowedHosts = [ "example.com" ];
};
}
];
};
};
}| Option | Type | Default | Description |
|---|---|---|---|
enable |
bool | false |
Enable the service |
port |
port | 8765 |
Backend API port |
frontendPort |
port | 5173 |
Frontend dev server port |
jwtSecret |
string | change-me-in-production |
JWT secret key |
dataDir |
path | /var/lib/vocaloid-search |
Data directory |
allowedHosts |
list of string | [] |
Vite allowed hosts |
| Service | Type | Description |
|---|---|---|
vocaloid-search.service |
Simple | FastAPI backend |
vocaloid-frontend.service |
Simple | Vite dev server |
vocaloid-scraper.service |
Oneshot | Sync videos from Niconico API |
vocaloid-scraper.timer |
Timer | Daily sync at 06:00 |
- Python 3.11+
- Node.js 18+
- uv Python package manager
git clone https://github.com/anton1615/VOCALOID-Search.git
cd VOCALOID-Search
# Development mode (backend + frontend)
./scripts/start.sh dev
# Backend only
./scripts/start.sh backend
# Frontend only
./scripts/start.sh frontend
# Run scraper
./scripts/start.sh scraperexport VOCALOID_DATA_DIR=/var/lib/vocaloid-search
export VOCALOID_JWT_SECRET=your-secret-key
./scripts/start.sh install-systemd
systemctl --user enable --now vocaloid-search.service
systemctl --user enable --now vocaloid-frontend.serviceThe scraper reads configuration from $VOCALOID_DATA_DIR/scraper-config.json. If the file doesn't exist, default values are used.
{
"query": "VOCALOID",
"max_age_days": 365,
"targets": "tags",
"category_filter": "MUSIC"
}| Option | Type | Default | Description |
|---|---|---|---|
query |
string | VOCALOID |
Search query for Niconico Snapshot API |
max_age_days |
int or null | 365 |
Only fetch videos newer than this many days. null = unlimited |
targets |
string | tags |
Search targets (see below) |
category_filter |
string or null | MUSIC |
Niconico category filter |
| Value | Description |
|---|---|
tags |
Keyword search, matches tags containing the query |
tagsExact |
Exact match, tag must equal the query exactly |
title |
Search in video titles |
description |
Search in video descriptions |
| Combined | tags,title or tags,title,description |
| Value | Niconico Genre |
|---|---|
MUSIC |
音楽・サウンド (Music & Sound) |
GAME |
ゲーム (Game) |
ANIME |
アニメ (Anime) |
ENTERTAINMENT |
エンターテイメント (Entertainment) |
DANCE |
ダンス (Dance) |
OTHER |
その他 (Other) |
UTAU songs only:
{
"query": "UTAU",
"targets": "tags",
"category_filter": "MUSIC",
"max_age_days": null
}CeVIO and Synthesizer V:
{
"query": "CeVIO OR Synthesizer_V OR SynthV",
"targets": "tags",
"category_filter": "MUSIC"
}All entertainment (not just music):
{
"query": "VOCALOID",
"targets": "tags",
"category_filter": "ENTERTAINMENT"
}| Variable | Default | Description |
|---|---|---|
VOCALOID_DATA_DIR |
/var/lib/vocaloid-search (NixOS) / ./data (other) |
Data directory (database + config) |
VOCALOID_JWT_SECRET |
change-me-in-production |
JWT secret (must set in production) |
VOCALOID_PORT |
8765 |
Backend API port |
VITE_ALLOWED_HOSTS |
(empty) | Vite allowed hosts, comma-separated |
uv sync
uv run uvicorn vocaloid_search.main:app --reload
uv run python -m vocaloid_search.scraper --once --init-dbcd frontend
npm install
npm run dev
npm run build- Android interface not polished: The PWA/TWA support is a prototype and hasn't been thoroughly tested
- Background playback limitation: Continuous playback doesn't work when the browser tab is in the background due to browser resource management
- Player reset on mode switch: Switching between maximized view and narrow window (PIP mode) resets the video player
- Region-locked videos: Videos with regional restrictions interrupt continuous playback and cannot be marked as watched
- Rudimentary login management: User authentication is minimal
- SQLite limitations: Not suitable for high-traffic deployments
- Ghost history entries: Videos in watch history that no longer exist in the video database become invisible in the history page
- Performance on low-resource hardware: On resource-constrained systems, count queries can take too long and cause the frontend to hang on "searching". Workarounds: (1) use
vmtouchto lock the SQLite database in RAM, (2) limit the number of videos inscraper-config.jsonby narrowing the query or settingmax_age_days
- Built-in playlist system: Implement "watch later" and custom playlists similar to Niconico's あとで見る feature, but without Niconico's playlist limits
- Multiple daily scraper updates: Workaround for Niconico Snapshot API limitations to enable more frequent database updates

