Import your watch history from a SIMKL backup file into Trakt.tv. Handles movies, series, and anime with per-episode watch timestamps.
  • JavaScript 100%
Find a file
2026-04-01 22:35:41 -07:00
src fix(simkl): exclude watchlist-only items from import 2026-04-01 22:35:41 -07:00
test fix(simkl): exclude watchlist-only items from import 2026-04-01 22:35:41 -07:00
testing test: expand offline unit coverage 2026-04-01 21:57:46 -07:00
.env.example feat(sync): import watch history from simkl api 2026-04-01 19:03:49 -07:00
.gitignore feat(importer): resolve episodes canonically and centralize runtime data 2026-04-01 21:29:46 -07:00
AGENTS.md test: expand offline unit coverage 2026-04-01 21:57:46 -07:00
package-lock.json feat: implement SIMKL to Trakt watch history importer 2026-03-18 19:18:50 -07:00
package.json feat(importer): resolve episodes canonically and centralize runtime data 2026-04-01 21:29:46 -07:00
README.md fix(dedupe): normalize watched_at to Trakt minute precision 2026-04-01 22:16:08 -07:00
TESTS.md fix(simkl): exclude watchlist-only items from import 2026-04-01 22:35:41 -07:00

SIMKL to Trakt Watch History Importer

Import watch history from SIMKL into Trakt.tv with a strict append-only model. The importer can read directly from the SIMKL API or from a local SimklBackup.json fallback.

Guarantees

  • Append-only Trakt writes - this tool never deletes or removes anything from Trakt.
  • Event-level dedupe - a watch record is uniquely identified as content + Trakt-normalized watched_at timestamp because Trakt currently round-trips imported history at minute precision.
  • Rewatch-safe - a new timestamp is treated as a new watch; the same timestamp is never intentionally written twice.
  • Crash-safe reruns - before writing anything, the importer seeds existing Trakt history and skips exact matches already present on the account.
  • SIMKL-safe sync - normal SIMKL runs use /sync/activities plus date_from instead of repeated full syncs.
  • Trakt-canonical episode matching - episode imports are resolved against Trakt's own episode catalog before they are written.

Prerequisites

  • Node.js 18+
  • A Trakt.tv API app with:
    • redirect URI set to urn:ietf:wg:oauth:2.0:oob
  • A SIMKL API app for direct API imports with:
    • app created at https://simkl.com/settings/developer/new/
    • redirect URI set to urn:ietf:wg:oauth:2.0:oob

Setup

  1. Create a SIMKL app at https://simkl.com/settings/developer/new/
  2. In the SIMKL app settings, set the redirect URI to urn:ietf:wg:oauth:2.0:oob
  3. Copy .env.example to .env
  4. Fill in:
    • SIMKL_CLIENT_ID from your SIMKL app for direct SIMKL API sync
    • TRAKT_CLIENT_ID
    • TRAKT_CLIENT_SECRET
  5. Install dependencies:
    npm install
    

Usage

Command What it does Notes
npm start Runs the normal import Uses SIMKL API when SIMKL_CLIENT_ID is set, otherwise falls back to SimklBackup.json
npm run preview Runs a dry run Skips Trakt history seeding by default so it stays non-interactive
node src/index.js --dry-run --seed-trakt Runs a duplicate-aware dry run Uses Trakt history seeding for an exact preview against your account
npm run preview:backup Runs a dry run against SimklBackup.json Useful when testing backup-file mode without writing anything
node src/index.js --source backup Forces backup-file mode Reads from local SimklBackup.json
node src/index.js --source simkl Forces SIMKL API mode Bypasses automatic source selection
node src/index.js --source simkl --full-reconcile Runs a full reconcile Repair/bootstrap mode only; still append-only on Trakt

Dry runs still perform episode resolution when TRAKT_CLIENT_ID is configured, so resolver decisions are visible before any write happens. Every run also writes a structured JSON log file under data/logs/.

Episode Matching Policy

Plain-language flow

Step What happens
1 Read the watch event from SIMKL or SimklBackup.json
2 Check whether the same watch event already exists in local state or seeded Trakt history after normalizing watched_at to the precision Trakt stores
3 Ask Trakt what the show's episode catalog looks like
4 Try the source season and episode directly on Trakt
5 If that fails, try a very small set of safe remap rules
6 Import only if one exact Trakt episode is found
7 If the match is unclear, leave the event pending instead of guessing

Why this exists

Some sources split seasons differently from Trakt, especially for anime and alternate ordering schemes.

Example:

  • A source may label an episode as Season 2 Episode 1
  • Trakt may record the same content as Season 1 Episode 36
  • The importer checks Trakt's catalog and remaps only if that match is unique

If the importer cannot prove the match, it does not write the event.

Technical policy

  • Every episode keeps both a source identity and a Trakt-canonical identity
  • Local dedupe and Trakt dedupe use the canonical Trakt episode identity once resolution succeeds
  • Direct matches are preferred over remaps
  • The first remap rule is bounded absolute-number remapping for source data that clearly uses a single continuous episode order
  • Blind count-based season shifting is not allowed
  • Fuzzy matching by title alone is not allowed
  • Ambiguous or unresolved episodes stay pending
  • When unresolved events remain, the SIMKL checkpoint is not advanced
  • Resolved episodes are written to Trakt using canonical episode IDs

Resolver Output

Resolver summary

The importer prints a summary like this during dry runs and live imports:

Episode resolution: direct-mapped=67, direct-source=652, absolute-number-remap=25, unresolved=1
Label Meaning
direct-mapped The source's mapped season/episode already matches a Trakt episode exactly
direct-source The raw source season/episode already matches a Trakt episode exactly
absolute-number-remap The source numbering did not match directly, but the importer safely remapped a continuous episode number to one unique Trakt episode
ambiguous More than one Trakt episode looked plausible, so nothing was written
unresolved No unique Trakt episode could be proven, so the event was left pending

Resolver decisions by show

The importer also prints a per-show summary:

Resolver decisions by show:
  - Naruto: direct-mapped=27, direct-source=18, absolute-number-remap=25, unresolved=1

This tells you how each show's episodes were matched before any write happens.

Resolver decision meanings

Resolver decision What it means Example output
direct-mapped A source-side mapped season/episode matched Trakt exactly direct-mapped | source S01E07
direct-source The original source season/episode matched Trakt exactly direct-source | source S01E36
absolute-number-remap A safe remap converted a continuous source episode number into the Trakt-canonical season/episode absolute-number-remap | S01E53 -> S02E53
ambiguous The importer found multiple plausible Trakt matches and skipped the event ambiguous | source S01E71 | mapped S02E36
unresolved No unique Trakt match could be proven and the event was left pending unresolved | source S01E71 | mapped S02E36 | No unique canonical Trakt episode could be resolved.

What "safe remap" means

A remap is only allowed when all of the following are true:

  • the source numbering does not directly match Trakt
  • the importer can apply an explicit approved rule
  • the rule produces one and only one Trakt episode

If more than one Trakt episode is plausible, the event is skipped and reported.

Run Logs

Every run writes a JSON log file to data/logs/ with a timestamped name such as data/logs/run-2026-04-02T03-12-45-123Z-12345.json.

Section What it contains
meta Run id, timestamps, flags, source mode, and log file path
source Source stats and checkpoint information for the run
resolver Resolver summary stats and per-show decision summaries
traktSeed Whether Trakt history was seeded and how many records were loaded
dedupe Candidate counts, duplicate skips, and resolver-pending counts
syncAttempts Raw Trakt sync attempt results for movie batches, movie retries, and show groups
events Per-event records with resolver data, status history, and final outcome

Per-episode log data

Each episode entry in events includes:

Field group Meaning
keys Source, legacy, current, and canonical event keys
sourceEpisode Source season/episode, raw source numbering, and mapped numbering
canonicalEpisode The final Trakt-canonical season/episode and usable episode IDs when resolution succeeds
resolution Resolver status, method, reason, and any ambiguity metadata
statusHistory Lifecycle transitions such as source-loaded, resolver-resolved, skipped-ledger, imported-trakt-write, or unresolved-trakt

These run logs are ignored by Git and are meant for troubleshooting and auditability.

State Files

The importer stores runtime auth and dedupe state under data/:

  • data/trakt-token.json - Trakt OAuth tokens
  • data/simkl-token.json - SIMKL access token from the PIN/device flow
  • data/import-state.json - versioned importer state, including:
    • committed SIMKL activity checkpoint
    • durable imported-event ledger

Notes

  • SIMKL deletions do not remove anything from Trakt.
  • Missing or ambiguous timestamps are skipped rather than guessed.
  • Episode resolution prefers Trakt-confirmed canonical matches over source-side numbering.