feat: add user data export and sofa export re-import support

- Add `GET /api/export/user-data` route that streams a JSON attachment of the authenticated user's full library data, named `sofa-export-<name>-<date>.json`
- Add `generateUserExport` in `@sofa/core/export` and a matching `parseSofaExport` parser so exported files can be re-imported via the existing import pipeline
- Register `sofa` as a new `ImportSource` in the API contract/schemas and wire up `parseSofaExport` in the `parseFile` procedure
- Add `EXPORT_FAILED` error code to the API error registry and surface it in web and native error-message maps
- Update the account settings section with export/import UI (download button, import progress)
- Add tests for `generateUserExport`, `parseSofaExport`, and round-trip fidelity
This commit is contained in:
2026-03-21 17:12:15 -04:00
parent 58cf2e689f
commit 61013ee41f
23 changed files with 1214 additions and 39 deletions
@@ -8,12 +8,12 @@ _openapi:
headings: []
contents:
- content: >-
Upload and parse an export file from Trakt, Simkl, or Letterboxd.
Returns a preview of items found without importing anything.
Upload and parse an export file from Trakt, Simkl, Letterboxd, or
Sofa. 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.
Upload and parse an export file from Trakt, Simkl, Letterboxd, or Sofa. Returns a preview of items found without importing anything.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/parse-file","method":"post"}]} />
+87 -21
View File
@@ -6136,7 +6136,7 @@
"post": {
"operationId": "imports.parseFile",
"summary": "Parse import file",
"description": "Upload and parse an export file from Trakt, Simkl, or Letterboxd. Returns a preview of items found without importing anything.",
"description": "Upload and parse an export file from Trakt, Simkl, Letterboxd, or Sofa. Returns a preview of items found without importing anything.",
"tags": [
"Imports"
],
@@ -6151,10 +6151,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"file": {
"type": "string",
@@ -6175,10 +6176,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"file": {
"type": "string",
@@ -6208,10 +6210,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"movies": {
"type": "array",
@@ -6320,6 +6323,20 @@
"tv"
],
"type": "string"
},
"status": {
"enum": [
"watchlist",
"in_progress",
"completed"
],
"type": "string",
"description": "Library status (default: watchlist)"
},
"addedAt": {
"type": "string",
"format": "date-time",
"description": "When the item was added to library"
}
},
"required": [
@@ -6548,10 +6565,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"movies": {
"type": "array",
@@ -6660,6 +6678,20 @@
"tv"
],
"type": "string"
},
"status": {
"enum": [
"watchlist",
"in_progress",
"completed"
],
"type": "string",
"description": "Library status (default: watchlist)"
},
"addedAt": {
"type": "string",
"format": "date-time",
"description": "When the item was added to library"
}
},
"required": [
@@ -6750,10 +6782,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"movies": {
"type": "array",
@@ -6862,6 +6895,20 @@
"tv"
],
"type": "string"
},
"status": {
"enum": [
"watchlist",
"in_progress",
"completed"
],
"type": "string",
"description": "Library status (default: watchlist)"
},
"addedAt": {
"type": "string",
"format": "date-time",
"description": "When the item was added to library"
}
},
"required": [
@@ -7019,10 +7066,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"movies": {
"type": "array",
@@ -7131,6 +7179,20 @@
"tv"
],
"type": "string"
},
"status": {
"enum": [
"watchlist",
"in_progress",
"completed"
],
"type": "string",
"description": "Library status (default: watchlist)"
},
"addedAt": {
"type": "string",
"format": "date-time",
"description": "When the item was added to library"
}
},
"required": [
@@ -7244,10 +7306,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"status": {
"enum": [
@@ -7525,10 +7588,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"status": {
"enum": [
@@ -7674,10 +7738,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"status": {
"enum": [
@@ -7903,10 +7968,11 @@
"enum": [
"trakt",
"simkl",
"letterboxd"
"letterboxd",
"sofa"
],
"type": "string",
"description": "External service to import from"
"description": "Service to import from"
},
"status": {
"enum": [