Phase 1c — Trakt Import #4

Closed
opened 2026-10-08 07:30:17 +00:00 by agent-pi · 2 comments
Collaborator

Users bring watch history from Trakt that predates — or was removed from — their Jellyfin library.

This issue adds a one-time import of a Trakt data export zip into the ledger, so old favourites still seed recommendations and media since deleted from Jellyfin still suppresses itself from suggestions. It covers the export parser, a per-user upload, a CLI path, and a worker import job.

Users bring watch history from Trakt that predates — or was removed from — their Jellyfin library. This issue adds a one-time import of a Trakt data export zip into the ledger, so old favourites still seed recommendations and media since deleted from Jellyfin still suppresses itself from suggestions. It covers the export parser, a per-user upload, a CLI path, and a worker import job.
Author
Collaborator

Plan of attack for 1c, from spec §8.2 and inspection of a real export (27,213 history entries: 791 movie watches, 26,422 episode watches; 430 movie / 321 show aggregates; 429 movie / 316 show ratings; 47 hidden-recommendations; every movie/show entry carries tmdb+imdb ids; history ids unique).

Findings

  • watch_events dedupe is UNIQUE (user_id, media_id, watched_at, source); the trakt history id rides source_ref as provenance, so zip re-imports are no-ops with no schema change.
  • upsert_media currently overwrites genres/overview unconditionally — a Trakt import (which carries no enrichment) would blank Jellyfin metadata. Needs preserve-if-present semantics, plus trakt_id/tvdb_id support.
  • upsert_rating/add_dismissal take no timestamps; trakt has the true rated_at/hidden_at.

Approach

  • Parser (services/trakt_import.py): pure-local zip validation (zipfile + ≥1 watched-history-*.json member) and per-member streaming parse into typed records; structural damage fails, individual unkeyable entries are skipped and counted.
  • Episode history collapses to show-level watch events (media_type="show"); season/episode granularity stays phase 3. watched-movies/shows aggregates backfill only items with zero history events.
  • Upload: POST /settings/trakt multipart (CSRF, 128 MiB cap), streams to a new XIQUE_IMPORT_DIR (default <tempdir>/xique-imports), enqueues trakt_import {path, cleanup: true}; worker parses, deletes the uploaded copy after a successful parse. Job status shown in settings.
  • CLI: xique import-trakt ZIP [--user NAME] (new xique console script) — validates + enqueues only; the worker is the single parsing path. Without --user, exactly one user must exist. The operator's own file is never deleted (cleanup: false).

Branch: feat/issue-4-phase-1c-trakt-import

Commits: .gitignore trakt-export fold → media/ledger write APIs → parser → import service → worker dispatch → settings upload/status → CLI; then review fix wave.

No new dependencies (zipfile/argparse are stdlib); pylock.toml untouched.

Plan of attack for 1c, from spec §8.2 and inspection of a real export (27,213 history entries: 791 movie watches, 26,422 episode watches; 430 movie / 321 show aggregates; 429 movie / 316 show ratings; 47 hidden-recommendations; every movie/show entry carries tmdb+imdb ids; history ids unique). **Findings** - `watch_events` dedupe is `UNIQUE (user_id, media_id, watched_at, source)`; the trakt history id rides `source_ref` as provenance, so zip re-imports are no-ops with no schema change. - `upsert_media` currently overwrites `genres`/`overview` unconditionally — a Trakt import (which carries no enrichment) would blank Jellyfin metadata. Needs preserve-if-present semantics, plus `trakt_id`/`tvdb_id` support. - `upsert_rating`/`add_dismissal` take no timestamps; trakt has the true `rated_at`/`hidden_at`. **Approach** - Parser (`services/trakt_import.py`): pure-local zip validation (`zipfile` + ≥1 `watched-history-*.json` member) and per-member streaming parse into typed records; structural damage fails, individual unkeyable entries are skipped and counted. - Episode history collapses to show-level watch events (`media_type="show"`); season/episode granularity stays phase 3. `watched-movies/shows` aggregates backfill only items with zero history events. - Upload: `POST /settings/trakt` multipart (CSRF, 128 MiB cap), streams to a new `XIQUE_IMPORT_DIR` (default `<tempdir>/xique-imports`), enqueues `trakt_import` `{path, cleanup: true}`; worker parses, deletes the uploaded copy after a successful parse. Job status shown in settings. - CLI: `xique import-trakt ZIP [--user NAME]` (new `xique` console script) — validates + enqueues only; the worker is the single parsing path. Without `--user`, exactly one user must exist. The operator's own file is never deleted (`cleanup: false`). **Branch:** `feat/issue-4-phase-1c-trakt-import` **Commits:** `.gitignore` trakt-export fold → media/ledger write APIs → parser → import service → worker dispatch → settings upload/status → CLI; then review fix wave. No new dependencies (zipfile/argparse are stdlib); `pylock.toml` untouched.
Author
Collaborator

Merged via PR #9 (c7ac5a8). Parser, worker trakt_import job, settings upload with status, and the xique import-trakt CLI are on main; verified end-to-end against a real export with an idempotent replay.

Merged via PR #9 (c7ac5a8). Parser, worker `trakt_import` job, settings upload with status, and the `xique import-trakt` CLI are on `main`; verified end-to-end against a real export with an idempotent replay.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/xique#4
No description provided.