AniList-MAL-Sync

AniList-MAL-Sync

Docker app from Tareku99's Repository

Overview

Bidirectional sync service between AniList and MyAnimeList with OAuth authentication, conflict resolution, and Docker deployment.

AniList ⇄ MyAnimeList Sync

Docker

A service to synchronize anime lists between AniList and MyAnimeList with support for bidirectional sync.

Credits

Inspired by the original idea from bigspawn/anilist-mal-sync.
This is a clean-room rewrite in Python with a fresh codebase and architecture.

Features

  • Bidirectional sync with conflict resolution (latest update wins)
  • One-way sync modes: AniList → MAL or MAL → AniList
  • OAuth authentication for both services
  • Docker deployment with auto-configuration
  • YAML-based configuration with validation
  • Rate limiting and automatic retry logic
  • Web UI for monitoring and configuration

Planned Features

  • Manga sync support
  • Custom sync schedules per list type
  • Webhooks for sync notifications

Quick Start (Deployment)

1. Get API Credentials

AniList:

  1. Visit https://anilist.co/settings/developer
  2. Click "Create Client"
  3. Set Redirect URL to (comma-separated, no spaces):
    http://localhost:18080/callback,http://YOUR-SERVER-IP:18080/callback
    
  4. Copy the Client ID and Client Secret

MyAnimeList:

  1. Visit https://myanimelist.net/apiconfig
  2. Click "Create ID"
  3. Set App Redirect URL to (one per line):
    http://localhost:18080/callback
    http://YOUR-SERVER-IP:18080/callback
    
  4. Copy the Client ID and Client Secret

2. Deploy

Option A: Docker

docker pull ghcr.io/tareku99/anilist-mal-sync:latest
docker-compose up -d

Option B: Unraid

  • Via Community Applications: Search for "AniList MAL Sync" in the Community Apps store
  • Manual Install: Copy anilist-mal-sync.xml from this repo to /boot/config/plugins/dockerMan/templates-user/ on your Unraid server

3. Configure

The container creates data/config.yaml on first run. You can edit it through the Web UI at http://YOUR-SERVER-IP:23080 (recommended) or directly on the filesystem:

nano data/config.yaml  # Or your preferred editor
oauth:
  port: 18080                                                # REQUIRED
  redirect_uri: "http://YOUR-SERVER-IP:18080/callback"       # REQUIRED - Use your server's IP

anilist:
  client_id: "your_anilist_client_id"                        # REQUIRED
  client_secret: "your_anilist_client_secret"                # REQUIRED
  username: "your_anilist_username"                          # REQUIRED
  # auth_url: "https://anilist.co/api/v2/oauth/authorize"    # OPTIONAL - has defaults
  # token_url: "https://anilist.co/api/v2/oauth/token"       # OPTIONAL - has defaults

myanimelist:
  client_id: "your_mal_client_id"                            # REQUIRED
  client_secret: "your_mal_client_secret"                    # REQUIRED
  username: "your_mal_username"                              # REQUIRED
  # auth_url: "https://myanimelist.net/v1/oauth2/authorize"  # OPTIONAL - has defaults
  # token_url: "https://myanimelist.net/v1/oauth2/token"     # OPTIONAL - has defaults

sync:
  mode: "bidirectional"                                      # OPTIONAL
  interval: 360                                              # OPTIONAL - Minutes between syncs
  log_level: "INFO"                                          # OPTIONAL
  dry_run: false                                             # OPTIONAL

# token_file_path: "/app/data/tokens.json"                   # OPTIONAL - has default

Restart: docker-compose restart (or wait 60s for auto-reload)

4. Authenticate

Check container logs for OAuth URLs, open them in your browser to authorize, and the service will start syncing automatically.

5. Access Web UI

Once running, access the web dashboard at:

http://YOUR-SERVER-IP:23080

The web UI provides:

  • Real-time status: View current sync status, last sync time, next scheduled sync
  • Configuration editor: Edit config.yaml directly from the browser with validation
  • Manual sync trigger: Force an immediate sync without waiting for the schedule
  • Live monitoring: See total sync count and last result

Local Development

Python (Direct)

git clone https://github.com/Tareku99/anilist-mal-sync
cd anilist-mal-sync
py install.py  # Creates venv and installs dependencies

# Activate virtual environment
.venv\Scripts\activate  # Windows
source .venv/bin/activate  # Linux/macOS

# Configure
cp config.example.yaml data/config.yaml
notepad data/config.yaml  # Edit with your credentials

# Authenticate
anilist-mal-sync auth

# Run
anilist-mal-sync run

Note: Use http://localhost:18080/callback for redirect_uri when testing locally.

Docker (Local Build)

# In docker-compose.yml, uncomment: build: .
docker-compose build
docker-compose up

# View logs for OAuth URLs, authorize in browser

Useful for testing Docker builds before deploying.

Available Commands

auth - Interactive authentication setup for AniList and MyAnimeList

Options:

  • --service (default: both) Options: anilist, mal, both - Which service to authenticate with
# Authenticate both services
anilist-mal-sync auth

# Authenticate individual services
anilist-mal-sync auth --service anilist  # AniList only
anilist-mal-sync auth --service mal      # MyAnimeList only

Opens browser for OAuth authorization. Tokens are saved to data/tokens.json.

Token Expiration:

  • MyAnimeList: Tokens expire after 31 days, auto-refresh using refresh token
  • AniList: Tokens expire after 1 year, requires manual re-auth

run - Run sync service (default: continuous sync with web UI)

Default: Runs continuous sync at specified interval with web UI. Use --once for single sync, --no-web-ui for headless mode.

Options:

  • --mode (default: bidirectional) Options: anilist-to-mal, mal-to-anilist, bidirectional - Sync mode: one-way or bidirectional
  • --dry-run - Simulate sync without making changes
  • --interval (default: 360) - Sync interval in minutes (default: 360 = 6 hours)
  • --log-level (default: INFO) Options: DEBUG, INFO, WARNING, ERROR - Logging level
  • --once - Run sync once and exit (no web UI, no continuous loop)
  • --no-web-ui - Disable web UI (continuous sync only)
  • --port (default: 23080) - Web UI port (default: 23080)
  • --host (default: 0.0.0.0) - Web UI host
# Run with default settings (continuous sync with web UI)
anilist-mal-sync run

# Run sync once and exit
anilist-mal-sync run --once

# Run continuous sync without web UI
anilist-mal-sync run --no-web-ui

# Custom interval (in minutes)
anilist-mal-sync run --interval 60    # Every hour
anilist-mal-sync run --interval 1440  # Daily

Press Ctrl+C to stop.

Sync configuration is set in config.yaml:

sync:
  mode: "bidirectional"  # Options: anilist-to-mal, mal-to-anilist, bidirectional
  dry_run: false         # Set to true to preview changes without applying

How It Works

The service follows a unified flow for all commands, with automatic configuration validation, token management, and error handling.

High-Level Execution Flow

flowchart TD
    Start([User Runs Command]) --> Init[1. INITIALIZATION<br/>Setup logging]
    
    Init --> ConfigExists{2. CONFIG FILE EXISTS?}
    
    ConfigExists -->|No| CreateTemplate[Create template from config.example<br/>Show message: Please edit config]
    ConfigExists -->|Yes| LoadConfig[Load config from YAML]
    
    CreateTemplate --> ConfigValidate
    LoadConfig --> ConfigValidate
    
    ConfigValidate{3. CONFIG VALIDATION<br/>Validate credentials}
    
    ConfigValidate -->|Valid| CommandSplit{4. Which command?}
    ConfigValidate -->|Not Valid| WaitLoop[WAIT 60s retry loop<br/>Show error, wait 60s<br/>All commands wait<br/>Ctrl+C to exit]
    
    WaitLoop --> ConfigValidate
    
    CommandSplit -->|AUTH| AuthFlow[OAuth Flow: Auth both services, Save tokens, Exit]
    CommandSplit -->|RUN| RunModeCheck{5. RUN MODE?}
    
    RunModeCheck -->|--once| ExecuteSync[6. EXECUTE SYNC]
    RunModeCheck -->|default| WebUIStart[Start Web UI server]
    RunModeCheck -->|--no-web-ui| MainLoop[7. RUN MAIN LOOP]
    
    WebUIStart --> MainLoop
    
    ExecuteSync --> TokenSyncDetails[6. EXECUTE SYNC<br/>See Token Loading & Sync Execution below]
    MainLoop --> ConfigCheck{Check config valid?}
    
    ConfigCheck -->|Not Valid| Wait10[Wait 10s<br/>gives user time to update/fix config]
    ConfigCheck -->|Valid| ExecuteSyncLoop[6. EXECUTE SYNC<br/>See Token Loading & Sync Execution below]
    
    Wait10 --> ConfigCheck
    
    TokenSyncDetails --> SyncResult{Sync Result}
    ExecuteSyncLoop --> SyncResultLoop{Sync Result}
    
    SyncResult -->|SUCCESS| PrintResults[Print results]
    SyncResult -->|ERROR| ExitOnce[Exit with error]
    
    SyncResultLoop -->|SUCCESS| WaitInterval[Wait for interval]
    SyncResultLoop -->|ERROR| WaitInterval
    
    PrintResults --> ExitOnce[Exit 0 or 1]
    WaitInterval --> ConfigCheck
    
    style ExecuteSync fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style ExecuteSyncLoop fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style TokenSyncDetails fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    
    style Start fill:#1f883d,stroke:#1a7f37,stroke-width:2px,color:#fff
    style Init fill:#9a6700,stroke:#7c4a00,stroke-width:2px,color:#fff
    style ConfigCheck fill:#9a6700,stroke:#7c4a00,stroke-width:2px,color:#fff
    style ConfigValidate fill:#9a6700,stroke:#7c4a00,stroke-width:2px,color:#fff
    style AuthFlow fill:#1f883d,stroke:#1a7f37,stroke-width:2px,color:#fff
    style MainLoop fill:#cf222e,stroke:#a40e26,stroke-width:2px,color:#fff
    style WebUIStart fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style RunModeCheck fill:#9a6700,stroke:#7c4a00,stroke-width:2px,color:#fff
    style WaitLoop fill:#bc4c00,stroke:#953800,stroke-width:2px,color:#fff
    style ExitOnce fill:#82071e,stroke:#660018,stroke-width:2px,color:#fff

Token Loading & Sync Execution

All sync operations follow this internal flow:

flowchart TD
    TokenSyncFlow[START: Token Loading & Sync Execution] --> TokenLoad[5. TOKEN LOADING]
    
    TokenLoad --> TokensFound{Tokens found?}
    
    TokensFound -->|Yes| CheckExpired{Token expired?}
    TokensFound -->|No| AutoAuth[Auto-authenticate authenticate_services]
    
    AutoAuth -->|Success| TokenReady
    AutoAuth -->|Fail| ExitAuth[EXIT or Continue Loop]
    
    CheckExpired -->|Not Expired| TokenReady
    CheckExpired -->|Expired| RefreshToken{Try refresh MAL only}
    
    RefreshToken -->|Success| TokenReady
    RefreshToken -->|Fail| AutoAuth
    
    TokenReady[Tokens Ready] --> ClientCreate[6. CLIENT CREATION<br/>AniListClient, MALClient, SyncEngine]
    
    ClientCreate --> SyncExecute[7. EXECUTE SYNC<br/>engine.sync mode]
    
    SyncExecute --> SyncResult{Sync Result}
    
    SyncResult -->|SUCCESS| ReturnSuccess[Return Success]
    SyncResult -->|HTTP ERROR 401/403| ReAuth[Re-authenticate reauthenticate_services]
    SyncResult -->|OTHER ERROR| ReturnError[Return Error]
    
    ReAuth -->|Success| RetrySync[Retry sync once with new tokens]
    ReAuth -->|Fail| ReturnError
    
    RetrySync -->|Success| ReturnSuccess
    RetrySync -->|Fail| ReturnError
    
    style TokenSyncFlow fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style TokenLoad fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style ClientCreate fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style SyncExecute fill:#0969da,stroke:#0550ae,stroke-width:2px,color:#fff
    style ReturnSuccess fill:#1f883d,stroke:#1a7f37,stroke-width:2px,color:#fff
    style ReturnError fill:#cf222e,stroke:#a40e26,stroke-width:2px,color:#fff
    style ExitAuth fill:#cf222e,stroke:#a40e26,stroke-width:2px,color:#fff

Key Behaviors:

  • Config Validation: All commands wait for valid configuration (60s retry loop)
  • Token Management: Automatic token refresh for MAL, manual re-auth for AniList
  • Error Recovery: Auto re-authentication on 401/403 errors with single retry
  • Continuous Operation: run command continuously validates config and adapts to changes

Error Scenarios

Scenario 1: No Config File

  • Config file doesn't exist → Creates template from config.example.yaml
  • Shows message: "Please edit the config file with your credentials"
  • All commands continue after template creation

Scenario 2: Invalid Config (Initial Validation)

  • Config has placeholder values → Shows detailed error message
  • All commands wait in retry loop (60s intervals) until config is valid
  • User can press Ctrl+C to exit

Scenario 3: Invalid Config (During RUN Loop)

  • Config becomes invalid while run command is running
  • Service pauses sync, waits 10s, then retries validation
  • Service stays running, waiting for config to be fixed

Scenario 4: No Tokens

  • Token file doesn't exist or tokens missing
  • Auto-triggers OAuth authentication flow
  • Continues with sync if auth succeeds

Scenario 5: Expired Tokens

  • MAL: Auto-refreshes using refresh token
  • AniList: Requires manual re-auth (no refresh token available)

Scenario 6: Authentication Error During Sync

  • HTTP 401/403 during sync → Auto re-authenticates
  • Retries sync once with new tokens
  • Exits on failure (--once mode) or continues loop (continuous modes)

Scenario 7: Config Changes During RUN

  • Config file modified while service running
  • Reloads and revalidates config automatically
  • Resumes sync if valid, pauses if invalid

Token Refresh Logic

MAL Token Refresh

flowchart TD
    A[Token expired?] -->|No| B[Use existing token]
    A -->|Yes| C{Has refresh token?}
    C -->|Yes| D[Refresh via API]
    C -->|No| B
    D --> E{Success?}
    E -->|Yes| F[Save new tokens]
    E -->|No| G[Return None, trigger re-auth]

AniList Token Refresh

flowchart TD
    A[Token expired?] --> B[AniList has NO refresh tokens: 1 year expiry, manual re-auth required]
    B --> C[Return existing token: will fail on API call, trigger re-auth]

Project Structure

src/anilist_mal_sync/
├── config.py          # Pydantic-based YAML configuration
├── cli.py             # Click CLI commands
├── anilist_client.py  # AniList API client
├── mal_client.py      # MyAnimeList API client
├── sync_engine.py     # Sync logic and conflict resolution
└── oauth.py           # OAuth flow and token management

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Additional Resources

Support the Project

If you find this project useful, consider supporting its development:

Ko-fi

Buy me a coffee on Ko-fi

Install AniList-MAL-Sync on Unraid in a few clicks.

Find AniList-MAL-Sync in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for AniList-MAL-Sync Review the template variables and paths Click Install

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/tareku99/anilist-mal-sync:latest
Last Updated2026-07-07
First Seen2026-07-01

Runtime arguments

Web UI
http://[IP]:[PORT:23080]
Network
bridge
Shell
sh
Privileged
false
Extra Params
--restart unless-stopped

Template configuration

OAuth PortPorttcp

OAuth callback port for authentication

Target
18080
Default
18080
Value
18080
Web UI PortPorttcp

Web UI dashboard port

Target
23080
Default
23080
Value
23080
Data DirectoryPathrw

Contains config.yaml and tokens.json

Target
/app/data
Default
/mnt/user/appdata/anilist-mal-sync
Value
/mnt/user/appdata/anilist-mal-sync
TimezoneVariable

Container timezone (e.g., America/New_York, Europe/London, Asia/Tokyo)

Target
TZ
Default
America/Boise
Value
America/Boise