Claude Skill

trakt

Discover and compare Trakt.tv trending, popular, and anticipated movies and shows, and manage user-scoped history and watchlists from the terminal. Do not use this skill for general TMDb catalog metadata, credits, images, or provider lookups; use `tmdb` for those tasks.

LLM Mart · 0 points · 5 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download magnus919-agent-skills-trakt-d0edebb.zip · 14 KB
Part of magnus919/agent-skills — 145 skills

Install

skills CLI npx skills add https://github.com/magnus919/agent-skills/tree/main/trakt
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
Git git clone https://github.com/magnus919/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Trakt — Media Discovery Signals in the Terminal

Why Install This Skill

Give your agent a reliable way to answer "what is everyone watching?" without confusing a current Trakt discovery ranking with a metadata catalog. The skill covers movies and shows that are trending, broadly popular, or anticipated, and produces JSON that can feed media automation.

Public discovery reads need an application Client ID, not a user login. OAuth boundaries, required headers, pagination, rate-limit behavior, and the difference between Trakt IDs and TMDb metadata are documented so workflows fail clearly instead of silently mixing services.

What You Get

Path Purpose
SKILL.md Command guide, recipes, gotchas, and routing
scripts/trakt Executable CLI with JSON and dry-run modes
scripts/test_trakt.py Offline pytest and unittest suite, including header injection and pagination
references/auth-and-request-contract.md Required headers, OAuth boundary, and errors
references/discovery-endpoints.md Trending/popular/anticipated semantics and paging
references/recipes-and-operations.md jq pipelines and rate-safe operations
evals/evals.json Six representative usage-quality cases

Quick Start

export TRAKT_CLIENT_ID="YOUR_TRAKT_CLIENT_ID"
trakt movie trending --limit 10
trakt --json tv anticipated --page 2 | jq '.pagination'

Create a free Client ID at trakt.tv/oauth/applications. Preview commands with trakt --dry-run --json movie popular without credentials or network access.

Triggers

Load this skill for Trakt API discovery, trending movies or shows, popular rankings, anticipated releases, watch-signal pipelines, or Trakt pagination and authentication questions. Do not use it for TMDb catalog metadata, credits, images, or provider lookups.

Requirements

  • Python 3.8 or newer
  • requests
  • A Trakt application Client ID in TRAKT_CLIENT_ID for live reads
  • No OAuth login is needed for the public discovery commands

Skill manifest

Trakt media discovery

Use this skill to inspect what is being watched, what is broadly popular, and what is anticipated. It is a read-only discovery surface, not a catalog metadata service.

Setup and authentication

Register an app at Trakt OAuth applications and export its Client ID:

export TRAKT_CLIENT_ID="YOUR_TRAKT_CLIENT_ID"

Every request must send trakt-api-key: <client id> together with the mandatory companion header trakt-api-version: 2, plus JSON content type and a descriptive User-Agent. Public discovery endpoints use the key header, not Authorization: Bearer. OAuth bearer tokens are for endpoints marked OAuth-required or for user-scoped lists, history, collection, watchlist, and mutations; a bearer token does not replace the key/version pair.

Essential commands

All six discovery commands accept --page N alongside --limit N; both default to 1 and 10 respectively and are forwarded to the API's query string.

Trending: watched in the last 24 hours

trakt movie trending --limit 20
trakt tv trending --limit 20 --page 2 --json

Trending responses wrap each media object in movie or show and include a watchers count.

Popular: broad popularity ranking

trakt movie popular --limit 25 --json
trakt tv popular --page 2 --limit 25

Popular is a ranking based on rating percentage and number of ratings, not a personalized recommendation.

Anticipated: upcoming interest

trakt movie anticipated --page 3 --limit 10
trakt tv anticipated --limit 10 --json

Anticipated reflects list appearances and upcoming interest. It is not the same as a release calendar.

Global flags can appear before or after the resource: --json, --dry-run, --quiet, and --verbose.

Pipeline recipes

Trending handoff to another tool

  1. Run trakt --json movie trending --limit 20.
  2. Unwrap .movie, retaining .watchers as the watch signal.
  3. Pass an available .movie.ids.tmdb or .movie.ids.imdb to a downstream tool; do not assume a missing ID can be synthesized.
trakt --json movie trending --limit 20 |
  jq '.movies[] | {title: (.movie.title // .title), year: (.movie.year // null), watchers: (.watchers // null), ids: (.movie.ids // .ids)}'

Compare discovery signals

Fetch matching pages of trending, popular, and anticipated (e.g. --page 1 for each), then label each dataset before combining it. Trending is recent watching, popular is broad ranking, and anticipated is upcoming interest.

Page through anticipated until the feed ends

Loop --page, read pagination.page_count from JSON output to pick the stop page, and break early if a page returns no items:

for p in $(seq 1 "$(trakt --json movie anticipated --page 1 --limit 100 | jq -r '.pagination.page_count')"); do
  trakt --json movie anticipated --page "$p" --limit 100 |
    jq --arg p "$p" '{page: ($p|tonumber), pagination: .pagination,
                      movies: [.movies[] | {title: (.movie.title // .title), year: (.movie.year // null)}]}'
done

Keep per-page output as labeled NDJSON; merge afterwards. On 429, wait out Retry-After before continuing the loop.

JSON and pagination

--json emits an object with a movies or shows array (trending entries retain their wrapper) plus a pagination object whose keys mirror the API's X-Pagination-* headers: page, limit, page_count, item_count. Pagination keys are ints when the headers were present and the object is empty {} when they were absent, so jq like .pagination.page_count // 1 degrades safely. Human output appends a Page N of M line when the headers are present and stays silent otherwise. The API defaults to page 1 with limit 10 for compatibility; set both explicitly for reproducible automation, and stop at page_count rather than assuming a short page is the end.

Known gotchas

  • Header pair is mandatory: sending trakt-api-key without trakt-api-version: 2 (or vice versa) can yield an invalid-request/authentication-style failure. The bundled script injects both on every live request.
  • 401 versus 403: 401 commonly indicates an OAuth requirement or invalid authorization; 403 indicates an invalid or unapproved application key. Do not retry either blindly.
  • Rate limits: on 429, honor Retry-After and inspect X-Ratelimit. Use bounded retries; transient 502/503/504 responses may be retried with backoff.
  • OAuth refresh: access tokens last seven days and refresh tokens are single-use. Replace the stored refresh token after a successful refresh; invalid_grant requires reauthorization.
  • Trakt is not TMDb: Trakt IDs and discovery rankings are not TMDb metadata. Use the tmdb skill for credits, images, provider metadata, and catalog enrichment.
  • Trending shape: read .movie or .show before title/IDs, while preserving watchers.
  • Pagination is per invocation: one CLI call fetches exactly one page (--page); loop invocations reading pagination.page_count rather than expecting the script to follow links itself.

When to use

Use Trakt for current watching signals, broad popularity, anticipated interest, and identifiers that feed a media workflow.

When not to use

Do not use Trakt for TMDb catalog metadata, credits, images, provider availability, or for writing a user's lists without an explicit OAuth-enabled workflow. Use tmdb for metadata and a dedicated authenticated operation for mutations.

Reference files

File Topic
references/auth-and-request-contract.md Required headers, OAuth boundary, errors, and rate limits
references/discovery-endpoints.md Endpoint semantics, filters, response shapes, and pagination
references/recipes-and-operations.md Pipelines, jq normalization, and operational handling

Available script and prerequisites

  • scripts/trakt is an executable Python CLI using only stdlib and requests.
  • --dry-run works without a Client ID and never performs network I/O.
  • Live discovery requires TRAKT_CLIENT_ID; tests are mock-only.
Files (agent-skills)
  • evals
    • evals.json 2.9 KB
      {
        "schema_version": 1,
        "skill_name": "trakt",
        "evals": [
          {
            "id": "trending-movies",
            "prompt": "Show the 20 movies most watched recently using Trakt.",
            "expected_output": "Use trakt movie trending --limit 20 and explain that trending is a recent-watch signal.",
            "assertions": ["selects the movie trending command", "sets an explicit limit", "describes the recent watching window"]
          },
          {
            "id": "popular-shows-json",
            "prompt": "Get popular TV shows as JSON for a jq pipeline.",
            "expected_output": "Run trakt --json tv popular and process the shows object with jq.",
            "assertions": ["uses the tv popular command", "enables JSON output", "mentions jq processing"]
          },
          {
            "id": "anticipated-pagination",
            "prompt": "Explain how to collect anticipated movies across pages using the Trakt CLI without overrunning limits.",
            "expected_output": "Loop trakt movie anticipated with --page, stop at pagination.page_count from --json output (empty pagination degrades to page 1), and honor Retry-After on 429.",
            "assertions": ["uses --page with each CLI invocation", "stops at the normalized pagination.page_count value", "handles Retry-After for rate limiting"]
          },
          {
            "id": "second-page-trending",
            "prompt": "Fetch page 2 of Trakt trending movies as JSON.",
            "expected_output": "Run trakt --json movie trending --page 2; JSON output keeps the movies array beside a pagination object mirroring X-Pagination headers.",
            "assertions": ["passes --page 2 to the trending command", "names the pagination object keys page limit page_count item_count", "keeps the movies array intact in JSON output"]
          },
          {
            "id": "header-pair-gotcha",
            "prompt": "Why does my Trakt request with trakt-api-key still fail?",
            "expected_output": "Send trakt-api-version: 2 together with trakt-api-key, plus JSON content type; the version header is mandatory.",
            "assertions": ["documents the trakt-api-key header", "documents the trakt-api-version 2 companion header", "identifies the missing-header failure cause"]
          },
          {
            "id": "tmdb-metadata-not-trigger",
            "prompt": "Do not route this request to Trakt: enrich a movie with TMDb credits, images, and provider metadata.",
            "expected_output": "Do not use Trakt; route catalog metadata enrichment to the tmdb skill.",
            "assertions": ["must not trigger for TMDb metadata work", "names the tmdb skill as the alternative"]
          },
          {
            "id": "oauth-boundary",
            "prompt": "Do I need OAuth to see public trending and popular feeds, and what changes for my watchlist?",
            "expected_output": "Public discovery reads use the application key and required version header; user-scoped watchlist operations require OAuth Bearer in addition to the app headers.",
            "assertions": ["distinguishes public reads from user-scoped operations", "requires OAuth for watchlist work", "retains the application header pair"]
          }
        ]
      }
      
  • references
    • auth-and-request-contract.md 2 KB
      # Trakt authentication and request contract
      
      ## Public discovery
      
      Trakt v2 identifies an application with its Client ID in the `trakt-api-key` header. Every request must also send the companion header `trakt-api-version: 2`; sending only one of the pair can produce an invalid request or authentication-style failure. Use `Content-Type: application/json` and an identifying `User-Agent` as well.
      
      ```sh
      curl --fail-with-body 'https://api.trakt.tv/movies/trending?page=1&limit=20' \
        -H 'Content-Type: application/json' \
        -H 'User-Agent: MyAppName/1.0.0' \
        -H "trakt-api-key: ${TRAKT_CLIENT_ID}" \
        -H 'trakt-api-version: 2'
      ```
      
      The bundled CLI uses this application-key mode. It does not put the Client ID in `Authorization: Bearer`; that header is reserved for an OAuth access token.
      
      ## OAuth boundary
      
      Public trending, popular, and anticipated reads do not require a user login. OAuth is needed by endpoints marked as required and is appropriate for user-scoped list, history, collection, watchlist, or mutation operations. A bearer token does not replace the application key and version header when calling the API.
      
      Trakt supports authorization-code and device-code flows. Access tokens last seven days. Refresh tokens are single-use: persist the replacement returned by a successful refresh and discard the old token. A 400/401 response containing `invalid_grant` means the session is no longer usable and requires reauthorization. Never log client secrets, access tokens, or refresh tokens.
      
      ## Failure handling
      
      Treat 401 and 403 as credential or app-approval errors, 400/422 as request validation errors, and 429 as rate limiting. On 429, honor `Retry-After` and inspect `X-Ratelimit`; do not retry forever. Transient 502/503/504 responses can be retried with a bounded backoff. The CLI surfaces status and response details without attempting unsafe retries.
      
      ## Sources
      
      - https://docs.trakt.tv/docs/required-headers
      - https://docs.trakt.tv/docs/getting-started
      - https://docs.trakt.tv/docs/authentication-oauth
      - https://trakt.docs.apiary.io/api-description-document
      
    • discovery-endpoints.md 3 KB
      # Trakt discovery endpoints
      
      All endpoints below are GET requests at `https://api.trakt.tv` and use the request contract in `auth-and-request-contract.md`.
      
      | Endpoint | Meaning | Response shape |
      |---|---|---|
      | `/movies/trending` | Most watched movies in the last 24 hours, ordered by watchers | wrapper objects with `watchers` and nested `movie` |
      | `/movies/popular` | Popularity based on rating percentage and number of ratings | movie objects |
      | `/movies/anticipated` | Upcoming interest based on list appearances | movie objects |
      | `/shows/trending` | Most watched shows in the last 24 hours, ordered by watchers | wrapper objects with `watchers` and nested `show` |
      | `/shows/popular` | Popularity based on rating percentage and number of ratings | show objects |
      | `/shows/anticipated` | Upcoming interest based on list appearances | show objects |
      
      Trending is a short, current watch signal. Popular is a broad popularity ranking, while anticipated is an upcoming-interest signal. Do not treat a trending rank as a release calendar or a popularity score as a personalized recommendation.
      
      ## Paging and filters
      
      These feeds accept `page` and `limit`; compatibility defaults are page 1 and limit 10. Set both explicitly for reproducible automation. Responses provide `X-Pagination-Page`, `X-Pagination-Limit`, `X-Pagination-Page-Count`, and `X-Pagination-Item-Count`. Stop at the reported page count instead of assuming a short page means completion.
      
      The bundled CLI forwards `--page` and `--limit` to the query string and normalizes those four headers into a JSON `pagination` object with the keys `page`, `limit`, `page_count`, and `item_count`. Keys are integers when the headers were present; the object is `{}` when the headers are missing, so downstream jq can fall back with `.pagination.page_count // 1`.
      
      Endpoint pages also document filters such as `extended`, `watchnow`, `genres`, `years`, `ratings`, date ranges, countries, and `ignore_watched`, `ignore_collected`, and `ignore_watchlisted` where supported. Encode comma-separated values as query parameters. `watchnow=any` means any service, while `any_all` and the `free_all`/`subscriptions_all` forms have stricter all-country semantics.
      
      ## Result normalization
      
      For trending responses, unwrap `movie` or `show` before reading title, year, and IDs, but preserve `watchers` if ranking matters. Popular and anticipated responses are already direct media objects. Trakt IDs are not TMDb metadata: use the returned `ids` object to hand an identifier to another tool, and use TMDb when the task is catalog metadata, credits, images, or provider details.
      
      ## Sources
      
      - https://docs.trakt.tv/reference/getmoviestrending
      - https://docs.trakt.tv/reference/getmoviespopular
      - https://docs.trakt.tv/reference/getmoviesanticipated
      - https://docs.trakt.tv/reference/getshowstrending
      - https://docs.trakt.tv/reference/getshowspopular
      - https://docs.trakt.tv/reference/getshowsanticipated
      - https://trakt.docs.apiary.io/reference/movies/trending/get-trending-movies
      
    • recipes-and-operations.md 2.3 KB
      # Trakt recipes and operations
      
      ## Trending to a handoff
      
      1. Run `trakt movie trending --limit 20 --json`.
      2. For each object, read `.movie` and retain `.watchers` as the current-watch signal.
      3. Pass `.movie.ids.tmdb` or `.movie.ids.imdb` to the next tool only when present; do not mistake a Trakt response for TMDb metadata.
      
      ```sh
      trakt --json movie trending --limit 20 |
        jq '.movies[] | {title: (.movie.title // .title), year: (.movie.year // null), watchers: (.watchers // null), ids: (.movie.ids // .ids)}'
      ```
      
      ## Compare discovery signals
      
      Fetch the same page of `movie trending`, `movie popular`, and `movie anticipated`. Trending answers "watched recently"; popular answers "high broad popularity"; anticipated answers "appears on many upcoming-interest lists." Keep these datasets labeled when combining them.
      
      ## Paginate anticipated releases
      
      The CLI fetches exactly one page per invocation; loop it. Drive the bound from the normalized pagination metadata: `--json` output carries `.pagination.page_count` (empty `{}` if a response lacked the headers, so fall back with jq's `// 1`).
      
      ```sh
      pages=$(trakt --json movie anticipated --page 1 --limit 100 | jq -r '.pagination.page_count // 1')
      for p in $(seq 1 "$pages"); do
        trakt --json movie anticipated --page "$p" --limit 100 > "anticipated-$p.json"
      done
      ```
      
      If you call the API directly instead of through the script, inspect the raw `X-Pagination-Page-Count` header and stop at that count; do not stop merely because a page returned fewer items than `--limit`. If the response is 429, wait at least the numeric `Retry-After` value and cap retries before continuing the loop.
      
      ## JSON processing
      
      `--json` emits an object with `movies` or `shows` plus a `pagination` object (`page`, `limit`, `page_count`, `item_count`); trending elements retain their wrapper shape, and human output adds a `Page N of M` footer only when the headers were present. Use `jq` for selection and `@csv` only after explicitly handling null IDs. Human output is for inspection, JSON output is for pipelines.
      
      ## Sources
      
      - https://docs.trakt.tv/docs/required-headers
      - https://docs.trakt.tv/reference/getmoviestrending
      - https://docs.trakt.tv/reference/getmoviesanticipated
      - https://trakt.docs.apiary.io/reference/movies/anticipated/get-most-anticipated-movies
      
  • scripts
    • test_trakt.py 10.7 KB
      #!/usr/bin/env python3
      """Offline tests for the Trakt discovery CLI."""
      import importlib.machinery
      import importlib.util
      import json
      import os
      import subprocess
      import sys
      from pathlib import Path
      from unittest import TestCase, mock
      
      SCRIPT = Path(__file__).with_name("trakt")
      loader = importlib.machinery.SourceFileLoader("trakt_cli", str(SCRIPT))
      spec = importlib.util.spec_from_loader(loader.name, loader)
      trakt = importlib.util.module_from_spec(spec)
      sys.modules[spec.name] = trakt
      loader.exec_module(trakt)
      
      
      def _response(items, headers):
          response = mock.Mock(status_code=200)
          response.json.return_value = items
          response.headers = headers
          return response
      
      
      FULL_PAGINATION_HEADERS = {
          "X-Pagination-Page": "2",
          "X-Pagination-Limit": "1",
          "X-Pagination-Page-Count": "3405",
          "X-Pagination-Item-Count": "10",
      }
      
      
      class TraktCliTests(TestCase):
          """Original CLI surface coverage: help, errors, dry-run, header injection."""
      
          def run_cli(self, *args, **kwargs):
              env = os.environ.copy()
              env.pop("TRAKT_CLIENT_ID", None)
              return subprocess.run([sys.executable, str(SCRIPT), *args], capture_output=True, text=True, env=env)
      
          def test_help_lists_discovery_groups(self):
              result = self.run_cli("--help")
              self.assertEqual(result.returncode, 0)
              self.assertIn("movie", result.stdout)
              self.assertIn("tv", result.stdout)
      
          def test_argument_error_is_nonzero(self):
              result = self.run_cli("movie", "unknown")
              self.assertNotEqual(result.returncode, 0)
              self.assertIn("invalid choice", result.stderr)
      
          def test_dry_run_json_is_valid_without_network(self):
              result = self.run_cli("--dry-run", "--json", "movie", "trending")
              self.assertEqual(result.returncode, 0)
              payload = json.loads(result.stdout)
              self.assertEqual(payload, {"dry_run": True})
      
          @mock.patch.object(trakt.requests, "get")
          def test_client_injects_required_header_pair(self, get):
              response = _response([{"movie": {"title": "Example"}}], {})
              get.return_value = response
              client = trakt.TraktClient(client_id="CLIENT_ID")
              client.movie_trending(limit=4)
              headers = get.call_args.kwargs["headers"]
              self.assertEqual(headers["trakt-api-key"], "CLIENT_ID")
              self.assertEqual(headers["trakt-api-version"], "2")
              self.assertEqual(headers["Content-Type"], "application/json")
      
          @mock.patch.object(trakt, "die")
          @mock.patch.object(trakt.requests, "get")
          def test_client_reports_http_error(self, get, die):
              response = mock.Mock(status_code=403)
              response.json.return_value = {"message": "forbidden"}
              response.headers = {}
              get.return_value = response
              trakt.TraktClient(client_id="CLIENT_ID").movie_popular()
              die.assert_called_once()
              self.assertIn("403", die.call_args.args[0])
      
      
      class PaginationRequestTests(TestCase):
          """--page/--limit flow from argv into request query parameters."""
      
          def setUp(self):
              flags = {"json": True, "dry_run": False, "quiet": False, "verbose": False}
              patcher = mock.patch.object(trakt, "GLOBAL_FLAGS", flags)
              patcher.start()
              self.addCleanup(patcher.stop)
      
          def test_page_two_is_sent_as_query_parameter(self):
              client = trakt.TraktClient(client_id="CLIENT_ID")
              client._get = mock.Mock(return_value=([], {"page": 2}))
              with mock.patch("builtins.print"):
                  trakt.cmd_discovery(client, "movie", "trending", ["--page", "2"])
              client._get.assert_called_once_with("/movies/trending", {"page": 2, "limit": 10})
      
          @mock.patch.object(trakt.requests, "get")
          def test_requests_get_receives_page_and_limit_params(self, get):
              get.return_value = _response([], {})
              client = trakt.TraktClient(client_id="CLIENT_ID")
              client.tv_popular(page=3, limit=25)
              self.assertEqual(get.call_args.kwargs["params"], {"page": 3, "limit": 25})
      
          def test_every_discovery_command_accepts_explicit_page_and_limit(self):
              pairs = [("movie", action) for action in ("trending", "popular", "anticipated")]
              pairs += [("tv", action) for action in ("trending", "popular", "anticipated")]
              segments = {"movie": "movies", "tv": "shows"}
              keys = {"movie": "movies", "tv": "shows"}
              for resource, action in pairs:
                  with self.subTest(command=f"{resource} {action}"):
                      client = trakt.TraktClient(client_id="CLIENT_ID")
                      client._get = mock.Mock(return_value=(None, {}))
                      with mock.patch("builtins.print") as printed:
                          trakt.cmd_discovery(client, resource, action, ["--page", "3", "--limit", "7"])
                      expected_path = f"/{segments[resource]}/{action}"
                      client._get.assert_called_once_with(expected_path, {"page": 3, "limit": 7})
                      if trakt.GLOBAL_FLAGS["json"]:
                          self.assertEqual(json.loads(printed.call_args.args[0]), {keys[resource]: [], "pagination": {}})
                      else:
                          self.assertIn("No", printed.call_args.args[0])
      
          def test_dry_run_json_accepts_page_without_network_or_credentials(self):
              env = os.environ.copy()
              env.pop("TRAKT_CLIENT_ID", None)
              result = subprocess.run(
                  [sys.executable, str(SCRIPT), "--dry-run", "--json", "tv", "anticipated", "--page", "2"],
                  capture_output=True, text=True, env=env,
              )
              self.assertEqual(result.returncode, 0)
              self.assertTrue(json.loads(result.stdout)["dry_run"])
      
      
      class PaginationHeaderTests(TestCase):
          """X-Pagination-* normalization and missing-header degradation."""
      
          def test_all_four_headers_map_onto_stable_keys(self):
              pagination = trakt.normalize_pagination(dict(FULL_PAGINATION_HEADERS))
              self.assertEqual(
                  pagination,
                  {"page": 2, "limit": 1, "page_count": 3405, "item_count": 10},
              )
      
          def test_lowercase_header_names_are_normalized(self):
              lower = {key.lower(): value for key, value in FULL_PAGINATION_HEADERS.items()}
              self.assertEqual(trakt.normalize_pagination(lower)["page_count"], 3405)
      
          def test_missing_headers_degrade_to_empty_object(self):
              self.assertEqual(trakt.normalize_pagination({}), {})
      
          def test_unparseable_and_partial_values_are_skipped(self):
              headers = {"X-Pagination-Page": "2", "X-Pagination-Limit": "", "X-Pagination-Item-Count": "not-a-number"}
              pagination = trakt.normalize_pagination(headers)
              self.assertEqual(pagination, {"page": 2})
      
          @mock.patch.object(trakt.requests, "get")
          def test_response_without_pagination_headers_yields_empty_pagination_object(self, get):
              get.return_value = _response([{"movie": {"title": "Example"}}], {"Content-Type": "application/json"})
              _, pagination = trakt.TraktClient(client_id="CLIENT_ID").movie_trending()
              self.assertEqual(pagination, {})
      
      
      class DiscoveryOutputTests(TestCase):
          """Stable JSON shapes beside the new pagination metadata."""
      
          def json_payload_for(self, resource, endpoint, argv, items, headers=None):
              flags = {"json": True, "dry_run": False, "quiet": False, "verbose": False}
              client = trakt.TraktClient(client_id="CLIENT_ID")
              if hasattr(client, f"{resource}_{endpoint}"):
                  setattr(client, f"{resource}_{endpoint}",
                          mock.Mock(return_value=(items, trakt.normalize_pagination(headers or {}))))
              else:
                  client._get = mock.Mock(return_value=(items, trakt.normalize_pagination(headers or {})))
              with mock.patch.object(trakt, "GLOBAL_FLAGS", flags), mock.patch("builtins.print") as printed:
                  trakt.cmd_discovery(client, resource, endpoint, argv)
              return json.loads(printed.call_args.args[0])
      
          def test_movie_trending_json_keeps_movies_key_beside_pagination(self):
              payload = self.json_payload_for(
                  "movie", "trending", ["--page", "2"],
                  [{"movie": {"title": "Heat", "year": 1995, "ids": {"tmdb": 949}}}],
                  FULL_PAGINATION_HEADERS,
              )
              self.assertIn("movies", payload)
              self.assertEqual(payload["pagination"],
                               {"page": 2, "limit": 1, "page_count": 3405, "item_count": 10})
              entry = payload["movies"][0]
              self.assertEqual(entry["movie"]["title"], "Heat")
              self.assertEqual(entry["movie"]["ids"]["tmdb"], 949)
      
          def test_tv_popular_json_keeps_show_objects_directly_nested(self):
              payload = self.json_payload_for(
                  "tv", "popular", ["--limit", "5"],
                  [{"title": "Poirot", "year": 1989, "ids": {"tvdb": 70739}}][:1],
                  FULL_PAGINATION_HEADERS,
              )
              self.assertIn("shows", payload)
              self.assertEqual(payload["shows"][0]["title"], "Poirot")
              self.assertEqual(payload["shows"][0]["ids"]["tvdb"], 70739)
              self.assertEqual(payload["pagination"]["page_count"], 3405)
      
          def test_tv_trending_keeps_wrapper_shape_in_json(self):
              payload = self.json_payload_for(
                  "tv", "trending", [],
                  [{"show": {"title": "Severance", "ids": {"tvdb": 365278}}}],
                  FULL_PAGINATION_HEADERS,
              )
              self.assertIn("show", payload["shows"][0])
              self.assertEqual(payload["pagination"]["item_count"], 10)
      
          def test_human_output_states_current_and_total_pages(self):
              client = trakt.TraktClient(client_id="CLIENT_ID")
              client.movie_trending = mock.Mock(return_value=(
                  [{"movie": {"title": "Heat", "year": 1995, "ids": {"tmdb": 949}}}],
                  {"page": 2, "limit": 1, "page_count": 3405, "item_count": 10},
              ))
              with mock.patch.object(trakt, "GLOBAL_FLAGS",
                                     {"json": False, "dry_run": False, "quiet": False, "verbose": False}), \
                   mock.patch("builtins.print") as printed:
                  trakt.cmd_discovery(client, "movie", "trending", ["--page", "2"])
              output = printed.call_args.args[0]
              self.assertIn("Heat", output)
              self.assertIn("Page 2 of 3405", output)
      
          def test_human_output_without_pagination_headers_prints_no_page_line(self):
              client = trakt.TraktClient(client_id="CLIENT_ID")
              client.tv_popular = mock.Mock(return_value=(
                  [{"show": {"title": "Fargo", "year": 2014, "ids": {"tvdb": 269584}}}], {},
              ))
              with mock.patch.object(trakt, "GLOBAL_FLAGS",
                                     {"json": False, "dry_run": False, "quiet": False, "verbose": False}), \
                   mock.patch("builtins.print") as printed:
                  trakt.cmd_discovery(client, "tv", "popular", [])
              output = printed.call_args.args[0]
              self.assertIn("Fargo", output)
              self.assertNotIn("Page ", output)
      
      
      if __name__ == "__main__":
          import unittest
      
          unittest.main()
      
    • trakt 8.1 KB · in bundle
  • README.md 2 KB
    # Trakt — Media Discovery Signals in the Terminal
    
    ## Why Install This Skill
    
    Give your agent a reliable way to answer "what is everyone watching?" without confusing a current Trakt discovery ranking with a metadata catalog. The skill covers movies and shows that are trending, broadly popular, or anticipated, and produces JSON that can feed media automation.
    
    Public discovery reads need an application Client ID, not a user login. OAuth boundaries, required headers, pagination, rate-limit behavior, and the difference between Trakt IDs and TMDb metadata are documented so workflows fail clearly instead of silently mixing services.
    
    ## What You Get
    
    | Path | Purpose |
    |---|---|
    | `SKILL.md` | Command guide, recipes, gotchas, and routing |
    | `scripts/trakt` | Executable CLI with JSON and dry-run modes |
    | `scripts/test_trakt.py` | Offline pytest and unittest suite, including header injection and pagination |
    | `references/auth-and-request-contract.md` | Required headers, OAuth boundary, and errors |
    | `references/discovery-endpoints.md` | Trending/popular/anticipated semantics and paging |
    | `references/recipes-and-operations.md` | jq pipelines and rate-safe operations |
    | `evals/evals.json` | Six representative usage-quality cases |
    
    ## Quick Start
    
    ```sh
    export TRAKT_CLIENT_ID="YOUR_TRAKT_CLIENT_ID"
    trakt movie trending --limit 10
    trakt --json tv anticipated --page 2 | jq '.pagination'
    ```
    
    Create a free Client ID at [trakt.tv/oauth/applications](https://trakt.tv/oauth/applications). Preview commands with `trakt --dry-run --json movie popular` without credentials or network access.
    
    ## Triggers
    
    Load this skill for Trakt API discovery, trending movies or shows, popular rankings, anticipated releases, watch-signal pipelines, or Trakt pagination and authentication questions. Do not use it for TMDb catalog metadata, credits, images, or provider lookups.
    
    ## Requirements
    
    - Python 3.8 or newer
    - `requests`
    - A Trakt application Client ID in `TRAKT_CLIENT_ID` for live reads
    - No OAuth login is needed for the public discovery commands
    
  • SKILL.md 6.7 KB
    ---
    name: trakt
    description: >-
      Discover and compare Trakt.tv trending, popular, and anticipated movies and shows, and
      manage user-scoped history and watchlists from the terminal. Do not use this skill for
      general TMDb catalog metadata, credits, images, or provider lookups; use `tmdb` for
      those tasks.
    license: MIT
    compatibility: Requires TRAKT_CLIENT_ID, Python 3.8+, and requests. Public discovery
      reads use an application Client ID; OAuth is only needed for user-scoped operations.
    metadata:
      tags: trakt, media-discovery, movies, tv-shows, trending, api-client
      sources: https://docs.trakt.tv/docs/required-headers
    ---
    
    # Trakt media discovery
    
    Use this skill to inspect what is being watched, what is broadly popular, and what is anticipated. It is a read-only discovery surface, not a catalog metadata service.
    
    ## Setup and authentication
    
    Register an app at [Trakt OAuth applications](https://trakt.tv/oauth/applications) and export its Client ID:
    
    ```sh
    export TRAKT_CLIENT_ID="YOUR_TRAKT_CLIENT_ID"
    ```
    
    Every request must send `trakt-api-key: <client id>` together with the mandatory companion header `trakt-api-version: 2`, plus JSON content type and a descriptive User-Agent. Public discovery endpoints use the key header, not `Authorization: Bearer`. OAuth bearer tokens are for endpoints marked OAuth-required or for user-scoped lists, history, collection, watchlist, and mutations; a bearer token does not replace the key/version pair.
    
    ## Essential commands
    
    All six discovery commands accept `--page N` alongside `--limit N`; both default to 1 and 10 respectively and are forwarded to the API's query string.
    
    ### Trending: watched in the last 24 hours
    
    ```sh
    trakt movie trending --limit 20
    trakt tv trending --limit 20 --page 2 --json
    ```
    
    Trending responses wrap each media object in `movie` or `show` and include a `watchers` count.
    
    ### Popular: broad popularity ranking
    
    ```sh
    trakt movie popular --limit 25 --json
    trakt tv popular --page 2 --limit 25
    ```
    
    Popular is a ranking based on rating percentage and number of ratings, not a personalized recommendation.
    
    ### Anticipated: upcoming interest
    
    ```sh
    trakt movie anticipated --page 3 --limit 10
    trakt tv anticipated --limit 10 --json
    ```
    
    Anticipated reflects list appearances and upcoming interest. It is not the same as a release calendar.
    
    Global flags can appear before or after the resource: `--json`, `--dry-run`, `--quiet`, and `--verbose`.
    
    ## Pipeline recipes
    
    ### Trending handoff to another tool
    
    1. Run `trakt --json movie trending --limit 20`.
    2. Unwrap `.movie`, retaining `.watchers` as the watch signal.
    3. Pass an available `.movie.ids.tmdb` or `.movie.ids.imdb` to a downstream tool; do not assume a missing ID can be synthesized.
    
    ```sh
    trakt --json movie trending --limit 20 |
      jq '.movies[] | {title: (.movie.title // .title), year: (.movie.year // null), watchers: (.watchers // null), ids: (.movie.ids // .ids)}'
    ```
    
    ### Compare discovery signals
    
    Fetch matching pages of trending, popular, and anticipated (e.g. `--page 1` for each), then label each dataset before combining it. Trending is recent watching, popular is broad ranking, and anticipated is upcoming interest.
    
    ### Page through anticipated until the feed ends
    
    Loop `--page`, read `pagination.page_count` from JSON output to pick the stop page, and break early if a page returns no items:
    
    ```sh
    for p in $(seq 1 "$(trakt --json movie anticipated --page 1 --limit 100 | jq -r '.pagination.page_count')"); do
      trakt --json movie anticipated --page "$p" --limit 100 |
        jq --arg p "$p" '{page: ($p|tonumber), pagination: .pagination,
                          movies: [.movies[] | {title: (.movie.title // .title), year: (.movie.year // null)}]}'
    done
    ```
    
    Keep per-page output as labeled NDJSON; merge afterwards. On 429, wait out `Retry-After` before continuing the loop.
    
    ## JSON and pagination
    
    `--json` emits an object with a `movies` or `shows` array (trending entries retain their wrapper) plus a `pagination` object whose keys mirror the API's `X-Pagination-*` headers: `page`, `limit`, `page_count`, `item_count`. Pagination keys are ints when the headers were present and the object is empty `{}` when they were absent, so jq like `.pagination.page_count // 1` degrades safely. Human output appends a `Page N of M` line when the headers are present and stays silent otherwise. The API defaults to page 1 with limit 10 for compatibility; set both explicitly for reproducible automation, and stop at `page_count` rather than assuming a short page is the end.
    
    ## Known gotchas
    
    - **Header pair is mandatory:** sending `trakt-api-key` without `trakt-api-version: 2` (or vice versa) can yield an invalid-request/authentication-style failure. The bundled script injects both on every live request.
    - **401 versus 403:** 401 commonly indicates an OAuth requirement or invalid authorization; 403 indicates an invalid or unapproved application key. Do not retry either blindly.
    - **Rate limits:** on 429, honor `Retry-After` and inspect `X-Ratelimit`. Use bounded retries; transient 502/503/504 responses may be retried with backoff.
    - **OAuth refresh:** access tokens last seven days and refresh tokens are single-use. Replace the stored refresh token after a successful refresh; `invalid_grant` requires reauthorization.
    - **Trakt is not TMDb:** Trakt IDs and discovery rankings are not TMDb metadata. Use the `tmdb` skill for credits, images, provider metadata, and catalog enrichment.
    - **Trending shape:** read `.movie` or `.show` before title/IDs, while preserving `watchers`.
    - **Pagination is per invocation:** one CLI call fetches exactly one page (`--page`); loop invocations reading `pagination.page_count` rather than expecting the script to follow links itself.
    
    ## When to use
    
    Use Trakt for current watching signals, broad popularity, anticipated interest, and identifiers that feed a media workflow.
    
    ## When not to use
    
    Do not use Trakt for TMDb catalog metadata, credits, images, provider availability, or for writing a user's lists without an explicit OAuth-enabled workflow. Use `tmdb` for metadata and a dedicated authenticated operation for mutations.
    
    ## Reference files
    
    | File | Topic |
    |---|---|
    | [references/auth-and-request-contract.md](references/auth-and-request-contract.md) | Required headers, OAuth boundary, errors, and rate limits |
    | [references/discovery-endpoints.md](references/discovery-endpoints.md) | Endpoint semantics, filters, response shapes, and pagination |
    | [references/recipes-and-operations.md](references/recipes-and-operations.md) | Pipelines, jq normalization, and operational handling |
    
    ## Available script and prerequisites
    
    - `scripts/trakt` is an executable Python CLI using only stdlib and `requests`.
    - `--dry-run` works without a Client ID and never performs network I/O.
    - Live discovery requires `TRAKT_CLIENT_ID`; tests are mock-only.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related