Skip to content

feat: Trakt OAuth integration + stable dynamic catalog IDs#133

Open
mistertomlinson wants to merge 3 commits intoTimilsinaBimal:mainfrom
mistertomlinson:feature/trakt-integration
Open

feat: Trakt OAuth integration + stable dynamic catalog IDs#133
mistertomlinson wants to merge 3 commits intoTimilsinaBimal:mainfrom
mistertomlinson:feature/trakt-integration

Conversation

@mistertomlinson
Copy link
Copy Markdown

Closes #132

Summary

Adds Trakt as an alternative login provider on the configure page, so users can track their watch history and get recommendations without needing a Stremio account. Also fixes a long-standing issue where dynamic catalog ordering couldn't be saved in Stremio clients like Nuvio.

Changes

Trakt Integration

  • New tabbed login UI on the configure page (Stremio | Trakt tabs)
  • Trakt OAuth2 popup flow — users never leave the configure page to authorize
  • Trakt watch history fetched and normalised into the same shape as the Stremio library, so the entire recommendation engine works unchanged
  • Background catalog refresh support for Trakt accounts
  • Trakt access token stored encrypted via the existing token_store encryption
  • Username never exposed in the manifest URL (hashed with TOKEN_SALT)
  • Trakt tab is automatically hidden if TRAKT_CLIENT_ID/TRAKT_CLIENT_SECRET are not set on the server

Stable Dynamic Catalog IDs (bonus fix)

  • Dynamic catalog IDs like watchly.watched.tt0209144 and watchly.theme.a:g27... are replaced with stable positional slot IDs (watchly.watched.slot0, watchly.theme.slot0) in the manifest
  • A slot→real ID mapping is stored in Redis so the catalog service resolves them transparently at fetch time
  • This allows users to reorder "Because you watched" and theme catalogs in Nuvio/aiostreams and have that order persist across library refreshes
  • Applies to all users (Stremio and Trakt)

Setup required (for server operators)

  1. Register a Trakt app at https://trakt.tv/oauth/applications/new. Note: Registering a Trakt app is free — no paid account required.
  2. Set the redirect URI to https://your_host/tokens/trakt/callback
  3. Add to your environment:

TRAKT_CLIENT_ID=your_client_id
TRAKT_CLIENT_SECRET=your_client_secret

If these are not set, the Trakt tab is hidden and the addon works exactly as before.

Testing

Tested locally with a real Trakt account. Stremio login flow verified to be unaffected.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

@mistertomlinson is attempting to deploy a commit to the Bimal Timilsina's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Trakt integration, enabling users to authenticate via Trakt and sync their watch history for personalized recommendations. It introduces a new Trakt OAuth flow, library normalization services, and a catalog stabilization mechanism using Redis to maintain consistent IDs across external apps. Review feedback highlights the need to move in-memory OAuth states to Redis for multi-worker compatibility, implement token refresh logic for background updates, and optimize API interactions through concurrency and connection pooling. Cleanup of unused constants and more secure JavaScript serialization were also recommended.

Comment thread app/api/endpoints/trakt.py Outdated
# In-memory store for short-lived OAuth state values.
# This is per-process only; for multi-worker deployments Redis would be better,
# but CSRF protection is still improved versus nothing.
_oauth_states: dict[str, str] = {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _oauth_states dictionary is stored in-memory, which will cause authentication failures in multi-worker or distributed environments (e.g., Gunicorn/Uvicorn with multiple workers). Additionally, states for abandoned login attempts are never cleared, leading to a memory leak over time.

Consider using a shared store like Redis (which is already integrated into the project) with a short TTL (e.g., 5-10 minutes) to manage these state values.

Comment on lines +172 to +174
async def _refresh_trakt_catalogs(
self, token: str, credentials: dict[str, Any], update_timestamp: bool = True
) -> bool:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Trakt catalog refresh logic does not handle token expiration. Trakt access tokens typically expire after 90 days. If the access_token (stored as authKey) expires, background updates will fail.

It is recommended to check the trakt_expires_at timestamp and use the trakt_refresh_token to obtain a new access token via trakt_bundle.auth.refresh_token when the current one is near expiration.

Comment thread app/api/endpoints/trakt.py Outdated
Comment on lines +315 to +318
if value is None:
return "null"
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return f'"{escaped}"'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Manual string escaping for JavaScript injection is fragile and potentially insecure. Using json.dumps is a safer and more standard way to serialize Python values for use in JavaScript templates.

Suggested change
if value is None:
return "null"
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return f'"{escaped}"'
import json
return json.dumps(value)

Comment thread app/services/trakt/library.py Outdated
Comment on lines +36 to +37
movies = await self._get_history("movies")
shows = await self._get_history("shows")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Fetching movie and show history sequentially increases the total response time for the library fetch. These requests can be performed concurrently to improve performance.

Suggested change
movies = await self._get_history("movies")
shows = await self._get_history("shows")
import asyncio
movies, shows = await asyncio.gather(self._get_history("movies"), self._get_history("shows"))

Comment thread app/services/trakt/library.py Outdated
Comment on lines +15 to +16
# How many pages of history to pull (1 000 items / page)
MAX_PAGES = 10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

MAX_PAGES is defined but not used. Additionally, the comment suggests paging is needed, but the /users/me/watched/type endpoint used in _get_history returns the full history in a single response, making paging logic unnecessary for this specific endpoint.

Comment thread app/services/trakt/auth.py Outdated

TOKEN_URL = "https://api.trakt.tv/oauth/token"
AUTHORIZE_URL = "https://trakt.tv/oauth/authorize"
DEVICE_CODE_URL = "https://api.trakt.tv/oauth/device/code"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

DEVICE_CODE_URL is defined but not used in the current implementation.

Comment thread app/services/trakt/auth.py Outdated
"grant_type": "authorization_code",
}
try:
async with httpx.AsyncClient(timeout=15.0) as client:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new httpx.AsyncClient for every request is inefficient as it does not reuse underlying TCP connections. Consider using a persistent client instance or passing the existing TraktClient to the auth service to leverage connection pooling.

@mistertomlinson
Copy link
Copy Markdown
Author

I see there are some issues to address (I've also found another bug that needs fixing). Work in progress.

- Move OAuth state storage from in-memory to Redis (multi-worker safe, auto-expiring)
- Add Trakt token refresh logic in background catalog updater
- Use json.dumps for safe JavaScript serialization in callback HTML
- Fetch movie and show history concurrently with asyncio.gather
- Remove unused DEVICE_CODE_URL constant and MAX_PAGES constant
- Fix catalog slot map Redis key to use watchly: namespace prefix
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
watchly Ready Ready Preview, Comment May 7, 2026 6:39am

Copy link
Copy Markdown
Owner

@TimilsinaBimal TimilsinaBimal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not got time to review the code in details. But one of my concern is,
if someone wants to use stremio but with trakt account, this doesn't allow them to do that. as its either trakt or stremio.
maybe weshould also look into this.
could you check this PR: #126 I have also worked on similar functionalities, with both trakt and Simkl functionality but those always require Stremio login, we can keep stremio as optional on those cases. I cannot merge both of the PRs since they solve the exact same issue. So, I suggest you to look into that PR and see if we can merge these into one.

@mistertomlinson
Copy link
Copy Markdown
Author

I'm still finding issues with my implementation. Specifically, the slots/ID system when using AIOStreams. So, it's a work in progress atm.

Is Simultaneous Stremio and Trakt use important so the web UI can open Stremio to continue installation? Is the concern that this would be lost using Trakt only? Otherwise, I can't figure out why anyone would need to use both.

I have not got time to review the code in details. But one of my concern is,
if someone wants to use stremio but with trakt account, this doesn't allow them to do that. as its either trakt or stremio.
maybe weshould also look into this.
could you check this PR: #126 I have also worked on similar functionalities, with both trakt and Simkl functionality but those always require Stremio login, we can keep stremio as optional on those cases. I cannot merge both of the PRs since they solve the exact same issue. So, I suggest you to look into that PR and see if we can merge these into one.

@TimilsinaBimal
Copy link
Copy Markdown
Owner

Is Simultaneous Stremio and Trakt use important so the web UI can open Stremio to continue installation? Is the concern that this would be lost using Trakt only? Otherwise, I can't figure out why anyone would need to use both.

Well Some people want to use trakt history on their Stremio account. If they do that, then the current flow doesn't support it. You can either use Stremio or Trakt.

Other than that, I don't have much concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Nuvio or Trakt support possible?

2 participants