feat: add watch history import from Trakt, Simkl, and Letterboxd (#13)

This commit is contained in:
2026-03-17 11:02:58 -04:00
committed by GitHub
parent 25736f684a
commit 710c43a01f
41 changed files with 10230 additions and 106 deletions
@@ -0,0 +1,19 @@
---
title: Cancel import job
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Cancel a pending or running import job. Already-imported items are not
rolled back.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Cancel a pending or running import job. Already-imported items are not rolled back.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}/cancel","method":"post"}]} />
@@ -0,0 +1,17 @@
---
title: Create import job
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: Create and start a background import job from previously parsed data.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Create and start a background import job from previously parsed data.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs","method":"post"}]} />
@@ -0,0 +1,17 @@
---
title: Get import job status
full: true
_openapi:
method: GET
toc: []
structuredData:
headings: []
contents:
- content: Get the current status and progress of an import job.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Get the current status and progress of an import job.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}","method":"get"}]} />
@@ -0,0 +1,19 @@
---
title: Stream import job events
full: true
_openapi:
method: GET
toc: []
structuredData:
headings: []
contents:
- content: >-
SSE stream of import job progress events. Yields progress updates and
a final complete event.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
SSE stream of import job progress events. Yields progress updates and a final complete event.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}/events","method":"get"}]} />
@@ -0,0 +1,19 @@
---
title: Parse import file
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Upload and parse an export file from Trakt, Simkl, or Letterboxd.
Returns a preview of items found without importing anything.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Upload and parse an export file from Trakt, Simkl, or Letterboxd. Returns a preview of items found without importing anything.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/parse-file","method":"post"}]} />
@@ -0,0 +1,20 @@
---
title: Preview normalized import data
full: true
_openapi:
method: POST
toc: []
structuredData:
headings: []
contents:
- content: >-
Accept pre-normalized import data from the OAuth proxy and return a
preview with item counts. No parsing is needed — data is already in
NormalizedImport format.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Accept pre-normalized import data from the OAuth proxy and return a preview with item counts. No parsing is needed — data is already in NormalizedImport format.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/parse-payload","method":"post"}]} />
+91
View File
@@ -0,0 +1,91 @@
---
title: Importing from Other Apps
description: Transfer your library from Trakt, Simkl, or Letterboxd.
---
Sofa can import your existing watch history, watchlist, and ratings from other tracking services. This lets you switch to Sofa without losing your data.
## Supported Services
| Service | OAuth Connect | File Upload | Data Imported |
|---|---|---|---|
| **Trakt** | Yes | JSON export | Watches, watchlist, ratings |
| **Simkl** | Yes | JSON export | Watches, watchlist, ratings, anime (as TV) |
| **Letterboxd** | No | ZIP export | Watches, watchlist, ratings (movies only) |
## Importing via OAuth (Trakt & Simkl)
The fastest way to import. Sofa uses a device code flow — no passwords are shared.
1. Go to **Settings → Import** and click **Connect** next to Trakt or Simkl
2. A code is displayed — click the link to open the provider's website
3. Enter the code and authorize Sofa
4. Sofa fetches your library and shows a preview
5. Choose which categories to import (watches, watchlist, ratings) and confirm
The authorization token is used only for the one-time data fetch and is not stored.
## Importing via File Upload
For Letterboxd (which has no public API) or if you prefer not to use OAuth:
### Letterboxd
1. Go to [letterboxd.com/settings/data](https://letterboxd.com/settings/data/) and click **Export Your Data**
2. Download the ZIP file
3. In Sofa, go to **Settings → Import** and click **Upload** next to Letterboxd
4. Select the ZIP file — Sofa reads `diary.csv`, `watched.csv`, `watchlist.csv`, and `ratings.csv`
### Trakt & Simkl
You can also upload a JSON export file instead of using OAuth. Export your data from the provider's account settings, then upload the file in Sofa.
## Preview & Options
Before importing, Sofa shows a preview of what was found:
- **Item counts** — movies, episodes, watchlist items, and ratings detected in the export
- **Category toggles** — choose which types of data to import (watches, watchlist, ratings)
- **Warnings** — any issues found during parsing (e.g. missing files in a ZIP, unrecognized ratings)
Items without external IDs (like Letterboxd exports, which only include title and year) are resolved via TMDB search. The preview notes how many items will need title-based matching, which may be less accurate for ambiguous titles.
## How It Works
When you confirm the import, Sofa creates a background job that:
1. Resolves each item to a TMDB title using available IDs (TMDB, IMDB, TVDB) or title search
2. Fetches metadata for any titles not already in your Sofa library
3. Logs watches, sets watchlist status, and stores ratings using the original timestamps from the source
4. Reports progress in real time — you can close the tab and come back; the import continues server-side
### Deduplication
Imports are safe to re-run. Sofa checks for existing data before writing:
- **Watches** — skipped if you already have a watch record for that movie or episode
- **Watchlist** — skipped if a status already exists for that title
- **Ratings** — skipped if you already have a rating for that title
### Rating Conversion
Different services use different rating scales. Sofa converts to its 15 star scale:
| Source | Scale | Conversion |
|---|---|---|
| Trakt | 110 | `round(rating / 2)`, clamped to 15 |
| Simkl | 110 | `round(rating / 2)`, clamped to 15 |
| Letterboxd | 0.55 (half-stars) | `round(rating)`, clamped to 15 |
### Timestamps
Sofa preserves the original watch dates and rating dates from the source export. If the source only provides a date without a time (e.g. Letterboxd diary entries), the date is stored as-is.
## Troubleshooting
Check the import results summary for details on any failures:
- **"Could not resolve movie/show"** — the title couldn't be matched to a TMDB entry. This is more common with Letterboxd imports (title-only matching) or obscure titles.
- **"Season/episode not found"** — the show was found but the specific episode doesn't exist in TMDB's data. This can happen with recently aired episodes or special episodes.
- **Large imports are slow** — imports with thousands of items may take several minutes due to TMDB rate limits. The progress bar shows real-time status.
- **Import job disappeared** — if the server restarts mid-import, the job will show as failed. You can safely re-run the import; existing data won't be duplicated.
+6
View File
@@ -21,6 +21,12 @@ Import list integrations export your Sofa watchlist so Sonarr and Radarr can aut
- [Sonarr](/docs/integrations/sonarr) — exports TV shows from your watchlist
- [Radarr](/docs/integrations/radarr) — exports movies from your watchlist
## Import Watch History
Migrating from another tracking app? Sofa can import your existing watch history, watchlist, and ratings.
- [Import from Trakt, Simkl, or Letterboxd](/docs/integrations/import) — OAuth connect or file upload
## Setup
All integrations are managed in **Settings → Integrations**. The general flow:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"title": "Integrations",
"pages": ["plex", "jellyfin", "emby", "sonarr", "radarr"]
"pages": ["plex", "jellyfin", "emby", "sonarr", "radarr", "import"]
}
+1 -1
View File
@@ -38,7 +38,7 @@ Sofa automatically updates statuses as you log watches so you don't have to mana
- **Unwatch an episode** on a completed show → status drops back to **In Progress**
- **Mark a movie as watched** → status changes to **Completed**
These same transitions apply when watches are logged automatically via [webhook integrations](/docs/integrations) (Plex, Jellyfin, Emby).
These same transitions apply when watches are logged automatically via [webhook integrations](/docs/integrations) (Plex, Jellyfin, Emby) or [imported from another app](/docs/integrations/import) (Trakt, Simkl, Letterboxd).
### Logging Watches
+1589 -1
View File
File diff suppressed because it is too large Load Diff