https://github.com/bigspawn/anilist-mal-sync
Program to synchronize your AniList and MyAnimeList accounts.
https://github.com/bigspawn/anilist-mal-sync
anilist cli myanimelist sync
Last synced: about 2 months ago
JSON representation
Program to synchronize your AniList and MyAnimeList accounts.
- Host: GitHub
- URL: https://github.com/bigspawn/anilist-mal-sync
- Owner: bigspawn
- License: mit
- Created: 2024-10-05T17:07:33.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-02-01T22:00:10.000Z (4 months ago)
- Last Synced: 2026-02-02T05:52:31.659Z (4 months ago)
- Topics: anilist, cli, myanimelist, sync
- Language: Go
- Homepage:
- Size: 653 KB
- Stars: 12
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# anilist-mal-sync [](https://github.com/bigspawn/anilist-mal-sync/actions) [](https://codecov.io/gh/bigspawn/anilist-mal-sync)
> **Note:** This project is under development. Feedback, suggestions, and issues are highly appreciated!
Program to synchronize your AniList and MyAnimeList accounts.
## Features
- Bidirectional sync between AniList and MyAnimeList (anime and manga)
- Favorites synchronization (MAL → AniList with add-only, AniList → MAL report-only)
- OAuth2 authentication
- CLI interface
- Manual ID mappings and ignore rules via `mappings.yaml`
- Duplicate target detection with automatic conflict resolution
- Unmapped entries tracking with interactive management (`unmapped` command)
- Offline ID mapping using anime-offline-database (prevents incorrect season matches)
- Optional ARM API integration for online ID lookups
## What gets synced
For each entry in your list the following fields are synchronized from source to target:
| Field | Synced |
|-------|--------|
| Status (watching / completed / on-hold / dropped / plan to watch) | ✅ |
| Score (automatically normalized between AniList and MAL score formats) | ✅ |
| Progress (episodes watched / chapters + volumes read) | ✅ |
| Start date | ✅ (nil source date never overwrites a set target date) |
| Finish date | ✅ (only when status is Completed) |
| Favorites | ✅ optional, via `--favorites` flag |
**Conflict rule:** the source service always wins. In the default direction (AniList → MAL) your AniList data overwrites MAL. Use `--reverse-direction` to flip.
See [docs/date-sync.md](docs/date-sync.md) for detailed date synchronization rules.
## Prerequisites
| Deployment | Requirements |
|---|---|
| **Docker** | [Docker](https://docs.docker.com/get-docker/) + [docker compose](https://docs.docker.com/compose/install/) (v2, built-in) or legacy `docker-compose` (v1) |
| **Binary (go install)** | [Go 1.25+](https://go.dev/dl/) |
| **Local build** | [Go 1.25+](https://go.dev/dl/), git |
## Quick Start (Docker)
### Step 1: Create OAuth applications
**AniList:**
1. Go to [AniList Developer Settings](https://anilist.co/settings/developer)
2. Create New Client with redirect URL: `http://localhost:18080/callback`
3. Save Client ID and Client Secret
**MyAnimeList:**
1. Go to [MAL API Settings](https://myanimelist.net/apiconfig)
2. Create Application with redirect URL: `http://localhost:18080/callback`
3. Save Client ID and Client Secret
### Step 2: Configure
Download and rename the example compose file, then fill in your credentials:
```bash
cp docker-compose.example.yaml docker-compose.yaml
```
Edit `docker-compose.yaml` with your credentials:
```yaml
services:
sync:
image: ghcr.io/bigspawn/anilist-mal-sync:latest
command: ["watch", "--once"]
ports:
- "18080:18080"
environment:
# User/Group ID for volume permissions (run: id -u / id -g)
- PUID=1000
- PGID=1000
# Required: API credentials
- ANILIST_CLIENT_ID=your_anilist_client_id
- ANILIST_CLIENT_SECRET=your_anilist_secret
- ANILIST_USERNAME=your_anilist_username
- MAL_CLIENT_ID=your_mal_client_id
- MAL_CLIENT_SECRET=your_mal_secret
- MAL_USERNAME=your_mal_username
# Watch mode interval (min: 1h, max: 168h / 7 days)
- WATCH_INTERVAL=12h
# Optional: Manual mappings file path
# - MAPPINGS_FILE_PATH=/home/appuser/.config/anilist-mal-sync/mappings.yaml
# Optional: ID Mapping settings
# - OFFLINE_DATABASE_ENABLED=true # Enable offline DB (default: true)
# - HATO_API_ENABLED=true # Enable Hato API (default: true)
# - HATO_API_URL=https://hato.malupdaterosx.moe # Hato API base URL
# - HATO_API_CACHE_DIR=/home/appuser/.config/anilist-mal-sync/hato-cache # Cache directory
# - HATO_API_CACHE_MAX_AGE=720h # Cache max age (default: 720h / 30 days)
# - ARM_API_ENABLED=false # Enable ARM API (default: false)
# - ARM_API_URL=https://arm.haglund.dev # ARM API base URL
# - JIKAN_API_ENABLED=false # Enable Jikan API for manga ID mapping
# - JIKAN_API_CACHE_DIR=/home/appuser/.config/anilist-mal-sync/jikan-cache
# - JIKAN_API_CACHE_MAX_AGE=168h
# - FAVORITES_SYNC_ENABLED=false # Enable favorites sync (requires Jikan API)
volumes:
- tokens:/home/appuser/.config/anilist-mal-sync
restart: unless-stopped
volumes:
tokens:
```
### Step 3: Authenticate
```bash
docker-compose run --rm --service-ports sync login
```
The tool will print two URLs — one for MyAnimeList and one for AniList. For each:
1. Copy the URL and open it in your browser
2. Authorize the application on the website
3. Your browser will redirect to `http://localhost:18080/callback` — the tool captures this automatically
4. Repeat for both services (MAL first, then AniList)
> **Note:** The `--service-ports` flag is required here so that the OAuth redirect to port 18080 reaches the container. Make sure port 18080 is free on your host.
Tokens are saved into the `tokens` Docker volume and persist across restarts.
### Step 4: Run
#### Step 4.1: Preview changes (dry run)
**Recommended before the first real sync.** On a large list the tool may update hundreds of
entries at once — dry run lets you see exactly what will change without touching anything.
```bash
docker-compose run --rm sync sync --dry-run --all
```
#### Step 4.2: Start the sync daemon
`docker-compose up -d` starts the container in **watch mode** (`--once` flag causes an
immediate first sync, then it repeats every `WATCH_INTERVAL` hours in the background).
```bash
docker-compose up -d
```
Done! The service will sync your lists every 12 hours (or whatever you set in `WATCH_INTERVAL`).
To check that the service started correctly and view sync output:
```bash
docker-compose logs -f sync
```
> **Note:** `WATCH_INTERVAL` accepts values between `1h` and `168h` (7 days).
> To run a one-off sync instead of the daemon use:
> ```bash
> docker-compose run --rm sync sync
> ```
## Commands
| Command | Description |
|---------|-------------|
| `login` | Authenticate with services |
| `logout` | Remove authentication tokens |
| `status` | Check authentication status |
| `sync` | Synchronize anime/manga lists |
| `watch` | Run sync on interval |
| `unmapped` | Show and manage unmapped entries from last sync |
**Global options** (available for all commands):
| Short | Long | Description |
|-------|------|-------------|
| `-c` | `--config` | Path to config file (optional, uses env vars if not specified) |
**Login/Logout options:**
| Short | Long | Description |
|-------|------|-------------|
| `-s` | `--service` | Service: `anilist`, `myanimelist`, `all` (default: `all`) |
**Sync options:**
| Short | Long | Description |
|-------|------|-------------|
| `-f` | `--force` | Force sync all entries |
| `-d` | `--dry-run` | Dry run without making changes |
| | `--manga` | Sync manga instead of anime |
| | `--all` | Sync both anime and manga |
| | `--verbose` | Enable verbose logging |
| | `--reverse-direction` | Sync from MyAnimeList to AniList |
| | `--offline-db` | Enable offline database for anime ID mapping (default: `true`, ignored for `--manga`) |
| | `--offline-db-force-refresh` | Force re-download offline database |
| | `--arm-api` | Enable ARM API for anime ID mapping (default: `false`, ignored for `--manga`) |
| | `--arm-api-url` | ARM API base URL |
| | `--jikan-api` | Enable Jikan API for manga ID mapping (default: `false`, ignored for anime) |
| | `--favorites` | Sync favorites between services (requires Jikan API for MAL favorites) |
**Watch options:**
| Short | Long | Description |
|-------|------|-------------|
| `-i` | `--interval` | Sync interval: 1h-168h (overrides config) |
| | `--once` | Run one sync immediately, then start the interval loop. **Without this flag the first sync is delayed by the full interval.** |
Interval can be set via `--interval` flag or in `config.yaml` under `watch.interval`.
**Unmapped options:**
| Short | Long | Description |
|-------|------|-------------|
| | `--fix` | Interactively fix unmapped entries (ignore or map to MAL ID) |
| | `--ignore-all` | Add all unmapped entries to ignore list |
For backward compatibility, running `anilist-mal-sync [options]` without a command will execute sync.
## Configuration
### Config file
Full `config.yaml` example:
```yaml
oauth:
port: "18080"
redirect_uri: "http://localhost:18080/callback"
anilist:
client_id: "your_client_id"
client_secret: "your_secret"
auth_url: "https://anilist.co/api/v2/oauth/authorize"
token_url: "https://anilist.co/api/v2/oauth/token"
username: "your_username"
myanimelist:
client_id: "your_client_id"
client_secret: "your_secret"
auth_url: "https://myanimelist.net/v1/oauth2/authorize"
token_url: "https://myanimelist.net/v1/oauth2/token"
username: "your_username"
token_file_path: "" # Leave empty for default: ~/.config/anilist-mal-sync/token.json
mappings_file_path: "" # Leave empty for default: ~/.config/anilist-mal-sync/mappings.yaml
watch:
interval: "24h" # Sync interval for watch mode (1h-168h), can be overridden with --interval flag
http_timeout: "30s" # HTTP client timeout for API requests (default: 30s)
offline_database:
enabled: true
cache_dir: "" # Default: ~/.config/anilist-mal-sync/aod-cache
auto_update: true
arm_api:
enabled: false
base_url: "https://arm.haglund.dev" # Default: https://arm.haglund.dev
hato_api:
enabled: true # Enable Hato API for ID mapping (default: true)
base_url: "https://hato.malupdaterosx.moe" # Hato API base URL
cache_dir: "" # Leave empty for default: ~/.config/anilist-mal-sync/hato-cache
cache_max_age: "720h" # Cache max age (default: 720h / 30 days)
jikan_api:
enabled: false # Enable Jikan API for manga ID mapping (default: false)
cache_dir: "" # Default: ~/.config/anilist-mal-sync/jikan-cache
cache_max_age: "168h" # Cache max age (default: 168h / 7 days)
favorites:
enabled: false # Enable favorites synchronization (default: false)
```
## ID Mapping Strategies
The tool uses different ID mapping strategies for anime and manga, and the chain differs by direction.
### Forward direction (AniList → MAL, default)
**Anime** (`sync` or `sync --all`):
1. **Manual Mapping** - User-defined AniList↔MAL mappings from `mappings.yaml`
2. **Direct ID lookup** - If the entry already exists in your target list
3. **Offline Database** (optional, enabled by default) - Local database from [anime-offline-database](https://github.com/manami-project/anime-offline-database)
4. **Hato API** (optional, enabled by default) - Online API for anime/manga ID mapping
5. **ARM API** (optional, disabled by default) - Online fallback to [arm-server](https://arm.haglund.dev)
6. **Title matching** - Match by title similarity
7. **API search** - Search the MAL API
**Manga** (`sync --manga` or `sync --all`):
1. **Manual Mapping** - User-defined AniList↔MAL mappings from `mappings.yaml`
2. **Direct ID lookup** - If the entry already exists in your target list
3. **Hato API** (optional, enabled by default) - Online API for manga ID mapping
4. **Title matching** - Match by title similarity
5. **Jikan API** (optional, disabled by default) - Online API for manga ID mapping via [Jikan](https://jikan.moe/) (unofficial MAL API)
6. **API search** - Search the MAL API
### Reverse direction (MAL → AniList, `--reverse-direction`)
**Anime** (`sync --reverse-direction`):
1. **Manual Mapping** - User-defined AniList↔MAL mappings from `mappings.yaml`
2. **Direct ID lookup** - If the entry already exists in your target list
3. **Offline Database** (optional, enabled by default)
4. **Hato API** (optional, enabled by default)
5. **ARM API** (optional, disabled by default)
6. **Title matching**
7. **MAL ID lookup** - Find AniList entry by MAL ID directly
8. **API search** - Search the AniList API
**Manga** (`sync --manga --reverse-direction`):
1. **Manual Mapping**
2. **Direct ID lookup**
3. **Hato API** (optional, enabled by default)
4. **Title matching**
5. **Jikan API** (optional, disabled by default)
6. **MAL ID lookup** - Find AniList entry by MAL ID directly
7. **API search** - Search the AniList API
**Notes:**
- The offline database and ARM API are anime-only and automatically disabled when using `--manga` flag (without `--all`) to improve startup performance.
- Hato API supports both anime and manga and is enabled by default.
### Manual Mappings & Ignore Rules
You can define manual ID mappings and ignore rules in a YAML file (`mappings.yaml`):
```yaml
manual_mappings:
- anilist_id: 12345
mal_id: 67890
comment: "Season 2 mapped manually"
ignore:
anilist_ids:
- 99999 # Title Name : reason for ignoring
titles:
- "Some Title to Ignore"
```
Default location: `~/.config/anilist-mal-sync/mappings.yaml`
You can also manage ignore rules interactively:
```bash
# Show unmapped entries from last sync
anilist-mal-sync unmapped
# Interactively fix unmapped entries (ignore or map to MAL ID)
anilist-mal-sync unmapped --fix
# Add all unmapped entries to ignore list
anilist-mal-sync unmapped --ignore-all
```
### Environment variables
Configuration can be provided entirely via environment variables (recommended for Docker):
**Required:**
- `ANILIST_CLIENT_ID` - AniList Client ID
- `ANILIST_CLIENT_SECRET` - AniList Client Secret (also accepts `CLIENT_SECRET_ANILIST`)
- `ANILIST_USERNAME` - AniList username
- `MAL_CLIENT_ID` - MyAnimeList Client ID
- `MAL_CLIENT_SECRET` - MyAnimeList Client Secret (also accepts `CLIENT_SECRET_MYANIMELIST`)
- `MAL_USERNAME` - MyAnimeList username
**Required for `watch` mode:**
- `WATCH_INTERVAL` - Sync interval (e.g., `12h`, `24h`); range `1h`–`168h`. Without this (or `--interval` flag) the watch command fails.
**Optional:**
- `HTTP_TIMEOUT` - HTTP client timeout for API requests (default: `30s`, e.g., `10s`, `1m`)
- `OAUTH_PORT` - OAuth server port (default: `18080`)
- `OAUTH_REDIRECT_URI` - OAuth redirect URI (default: `http://localhost:18080/callback`)
- `TOKEN_FILE_PATH` - Token file path (default: `~/.config/anilist-mal-sync/token.json`)
- `MAPPINGS_FILE_PATH` - Path to manual mappings YAML file (default: `~/.config/anilist-mal-sync/mappings.yaml`)
- `PUID` / `PGID` - User/Group ID for Docker volume permissions
- `OFFLINE_DATABASE_ENABLED` - Enable offline database for anime ID mapping (default: `true`, not used for manga-only sync)
- `OFFLINE_DATABASE_CACHE_DIR` - Cache directory (default: `~/.config/anilist-mal-sync/aod-cache`)
- `OFFLINE_DATABASE_AUTO_UPDATE` - Auto-update database (default: `true`)
- `HATO_API_ENABLED` - Enable Hato API for ID mapping (default: `true`, supports both anime and manga)
- `HATO_API_URL` - Hato API base URL (default: `https://hato.malupdaterosx.moe`)
- `HATO_API_CACHE_DIR` - Hato API cache directory (default: `~/.config/anilist-mal-sync/hato-cache`)
- `HATO_API_CACHE_MAX_AGE` - Hato API cache max age (default: `720h` / 30 days)
- `ARM_API_ENABLED` - Enable ARM API for anime ID mapping (default: `false`, not used for manga-only sync)
- `ARM_API_URL` - ARM API base URL (default: `https://arm.haglund.dev`)
- `JIKAN_API_ENABLED` - Enable Jikan API for manga ID mapping (default: `false`, not used for anime sync)
- `JIKAN_API_CACHE_DIR` - Jikan API cache directory (default: `~/.config/anilist-mal-sync/jikan-cache`)
- `JIKAN_API_CACHE_MAX_AGE` - Jikan API cache max age (default: `168h` / 7 days)
- `FAVORITES_SYNC_ENABLED` - Enable favorites synchronization (default: `false`)
## Favorites Synchronization
Favorites sync is an optional feature that synchronizes your favorited anime and manga between AniList and MyAnimeList. It runs as a separate phase after the main status/progress synchronization.
### API Limitations
| Direction | Read | Write | Behavior |
|-----------|------|-------|----------|
| MAL → AniList | ✅ via Jikan API | ✅ via ToggleFavourite mutation | Full sync (add missing favorites) |
| AniList → MAL | ✅ via isFavourite field | ❌ MAL API v2 has no favorites endpoint | Report only |
### Enabling Favorites Sync
**Via CLI flag:**
```bash
# Sync with favorites enabled
anilist-mal-sync sync --favorites
# Reverse sync (MAL → AniList) with favorites
anilist-mal-sync sync --favorites --reverse-direction
```
**Via environment variable:**
```bash
export FAVORITES_SYNC_ENABLED=true
anilist-mal-sync sync
```
**Via config file:**
```yaml
favorites:
enabled: true
```
**Via Docker:**
```yaml
environment:
- FAVORITES_SYNC_ENABLED=true
```
Note: The `--favorites` flag automatically enables Jikan API (required for reading MAL favorites).
### Behavior
#### MAL → AniList (with `--reverse-direction`)
- Reads your MAL favorites via Jikan API (public user profile)
- Compares with your AniList list entries
- **Adds** missing favorites on AniList
- **Does not remove** favorites that exist only on AniList (you may have intentionally favorited different items)
Example output:
```
★ [Favorites] Added "Cowboy Bebop" to AniList favorites
★ [Favorites] Added "Monster" to AniList favorites
★ Favorites sync complete: +2 added on AniList (15 skipped)
```
#### AniList → MAL (default direction)
- Reads your AniList favorites from list entries (via `isFavourite` field)
- Reads your MAL favorites via Jikan API
- Reports differences (cannot write to MAL)
Example output:
```
★ [Favorites] anime "Cowboy Bebop" is only on AniList
★ [Favorites] manga "Berserk" is only on MAL
★ Favorites: 2 mismatches (AniList→MAL, report only)
```
For detailed documentation, see [docs/favorites-sync.md](docs/favorites-sync.md).
## Advanced
### Install as binary (without Docker)
Requires **Go 1.25+** ([download](https://go.dev/dl/)).
**Option A — install from registry:**
```bash
go install github.com/bigspawn/anilist-mal-sync@latest
```
**Option B — clone and build locally:**
```bash
git clone https://github.com/bigspawn/anilist-mal-sync.git
cd anilist-mal-sync
go build -o anilist-mal-sync .
```
**First run:**
```bash
# 1. Create config file
cp config.example.yaml config.yaml
# Edit config.yaml with your AniList and MAL credentials
# 2. Authenticate (opens OAuth flow on port 18080)
anilist-mal-sync -c config.yaml login
# 3. Preview changes before syncing
anilist-mal-sync -c config.yaml sync --dry-run --all
# 4. Run sync
anilist-mal-sync -c config.yaml sync
```
Tokens are saved to `~/.config/anilist-mal-sync/token.json` by default.
### Docker
> **docker compose vs docker-compose:** Examples use `docker-compose` (CLI v1). If your system has Docker Compose v2 (bundled with modern Docker Desktop / Engine), replace `docker-compose` with `docker compose` (no hyphen).
See [Quick Start](#quick-start-docker) for the recommended setup.
**Using config file instead of environment variables:**
```bash
docker run --rm -p 18080:18080 \
-e PUID=$(id -u) -e PGID=$(id -g) \
-v $(pwd)/config.yaml:/etc/anilist-mal-sync/config.yaml:ro \
-v $(pwd)/tokens:/home/appuser/.config/anilist-mal-sync \
ghcr.io/bigspawn/anilist-mal-sync:latest -c /etc/anilist-mal-sync/config.yaml sync
```
### Watch mode
Enable continuous sync by setting `WATCH_INTERVAL` environment variable:
```yaml
environment:
- WATCH_INTERVAL=12h # Sync every 12 hours
```
Or run watch command manually:
```bash
docker-compose run --rm sync watch --interval=12h
```
**Interval limits:** 1h - 168h (7 days). `WATCH_INTERVAL` (or `--interval`) is **required** for watch mode — without it the command exits with an error.
### Scheduling (non-Docker)
Use your system's scheduler for periodic sync:
```bash
# Linux/macOS cron (daily at 2 AM)
0 2 * * * /usr/local/bin/anilist-mal-sync sync
```
## Troubleshooting
**"Required environment variables not set"**
- Set required env vars: `ANILIST_CLIENT_ID`, `ANILIST_CLIENT_SECRET`, `ANILIST_USERNAME`, `MAL_CLIENT_ID`, `MAL_CLIENT_SECRET`, `MAL_USERNAME`
- Or use config file with `-c /path/to/config.yaml`
**Authentication fails**
- Check redirect URL matches exactly: `http://localhost:18080/callback`
- Verify client ID and secret are correct
- Ensure port 18080 is not already in use
**Sync appears frozen**
- Both services have rate limits. Wait a few minutes and try again
- Use `--verbose` to see progress
**Token expired**
- Run `anilist-mal-sync status` to check
- Run `anilist-mal-sync login` to reauthenticate (re-authenticates both services)
## Disclaimer
This project is not affiliated with AniList or MyAnimeList. Use at your own risk.
## Roadmap
- [ ] Sync rewatching and rereading counts
## Credits
- [anime-offline-database](https://github.com/manami-project/anime-offline-database) for JSON based anime dataset
- [arm-server](https://github.com/BeeeQueue/arm-server) for API anime dataset
- [Hato](https://github.com/Atelier-Shiori/Hato) for JSON API anime and manga
- [Jikan](https://jikan.moe/) for unofficial MyAnimeList API