# Letterboxd Scraper - Films, Ratings & Reviews (`abotapi/letterboxd-film-reviews-scraper`) Actor

Scrape Letterboxd films and reviews by search phrase, popular or genre lists, or film URL. Extract ratings, directors, runtimes, review text and star ratings. Includes incremental monitoring, resume support and MCP connectors.

- **URL**: https://apify.com/abotapi/letterboxd-film-reviews-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 film records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Letterboxd Film & Reviews Scraper

Extract film metadata and reader reviews from Letterboxd into a clean Apify dataset. The actor supports title-phrase search and direct Letterboxd URL/browse-path mode, then returns ratings, directors, runtime, and tagline for every matched film — with reader reviews as an optional per-film enrichment.

### Why This Scraper?

- Two clear modes: search by film title phrase, or paste Letterboxd film/browse URLs.
- Useful film fields in every row: title, release year, directors, genre, average rating, ratings count, runtime, and tagline.
- Optional reader reviews per film (author, star rating, date, text, likes, spoiler flag), billed only when turned on.
- Output limits apply in both modes, so pasted URL runs can still be capped.
- MCP connector export can send a concise film summary into Notion, Linear, Airtable, or Apify connectors while the full record stays in the dataset.
- **Resume & recurring updates** - turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED films on every scheduled run, or use Resume from a previous run to continue one interrupted crawl. See "Incremental & Dedup Mode" below.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `url` | `https://letterboxd.com/film/dune-part-two/` |
| `slug` | `dune-part-two` |
| `title` | `Dune: Part Two` |
| `image` | `https://a.ltrbxd.com/resized/poster.jpg` |
| `description` | `Paul Atreides unites with Chani and the Fremen...` |
| `genre` | `["Science Fiction", "Adventure"]` |
| `directors` | `["Denis Villeneuve"]` |
| `averageRating` | `4.3` |
| `averageRatingPrecise` | `4.31` |
| `ratingsCount` | `900000` |
| `datePublished` | `2024-03-01` |
| `releaseYear` | `2024` |
| `runtimeMinutes` | `166` |
| `tagline` | `Long live the fighters.` |
| `scrapedAt` | `2026-01-01T00:00:00Z` |

### How to Use

Search by title phrase:

```json
{
  "mode": "search",
  "queries": ["dune"],
  "maxItems": 20
}
```

Search multiple phrases with reviews enrichment:

```json
{
  "mode": "search",
  "queries": ["dune", "oppenheimer"],
  "fetchReviews": true,
  "maxReviewsPerFilm": 25,
  "maxItems": 25
}
```

Use direct Letterboxd URLs:

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://letterboxd.com/films/popular/" },
    { "url": "https://letterboxd.com/film/dune-part-two/" }
  ],
  "maxItems": 10
}
```

Send a film digest into an app connector:

```json
{
  "mode": "search",
  "queries": ["dune"],
  "maxItems": 10,
  "mcpConnectors": ["YOUR_CONNECTOR_ID"],
  "notionParentPageUrl": "https://www.notion.so/your-page",
  "maxNotifyListings": 10
}
```

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | Choose `search` for title-phrase search or `url` for pasted Letterboxd links. |
| `queries` | array | `["dune"]` | Film title phrases for search mode. |
| `startUrls` | array | sample URL | Letterboxd film pages, browse lists, or direct film slugs for URL mode. |
| `browsePath` | string | `/films/popular/` | Used only in URL mode when Start URLs is empty; a site path to browse. |
| `maxItems` | integer | `20` | Maximum matching films to save. Runs can return fewer rows when a search/browse source matches fewer films. |
| `maxPages` | integer | empty (unlimited) | Maximum pages to read per search/browse source. Leave empty to walk every result page; `maxItems` is the primary output cap. |
| `fetchReviews` | boolean | `false` | Turn on to also fetch reader reviews for each returned film (charged as a separate per-film enrichment event). |
| `maxReviewsPerFilm` | integer | `25` | Only used when `fetchReviews` is on. Maximum reviews to collect per film. |
| `proxy` | object | Apify proxy | Connection settings. The default is recommended for Letterboxd. |
| `mcpConnectors` | array | empty | Optional connector ids for sending a concise summary into your apps. |
| `notionParentPageUrl` | string | empty | Required only for Notion page export. |
| `maxNotifyListings` | integer | `50` | Maximum rows to send to each selected connector. Does not affect the dataset. |
| `resumeFromRunId` | string | empty | Optional previous run ID (or dataset ID) from this actor. Films already collected there (matched by `slug`) are skipped, so the run returns only new ones. For recurring daily monitoring of the same search, use `incrementalMode` instead. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Incremental & Dedup Mode" below. |
| `stateKey` | string | empty | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your mode/queries/URLs when left empty. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return films unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return films from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. |

### Incremental & Dedup Mode

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` films by default; duplicates and unchanged films are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per mode/queries/startUrls/browsePath setup automatically; set `stateKey` to name or deliberately share a monitoring campaign.

This is a different tool from `resumeFromRunId`, which continues ONE specific interrupted run from a pasted run/dataset ID. Use `resumeFromRunId` after a run was cut off; use `incrementalMode` for a schedule that runs the same search again and again.

Every returned record in incremental mode also carries:

| Field | Meaning |
| --- | --- |
| `changeType` | One of `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, `EXPIRED`. |
| `changedFields` | Names of the fields that changed since the last run (empty for `NEW`/`UNCHANGED`/`REAPPEARED`/`EXPIRED`). |
| `firstSeenAt` | When this film was first seen by this monitoring campaign. |
| `lastSeenAt` | When this film was last seen. |

`EXPIRED` rows are only produced when a run fully scans the tracked search end to end (no `maxItems` cap hit, no `resumeFromRunId`, every source read to its natural end) and `emitExpired` is on. A capped or resumed run leaves the previous state untouched for those films rather than guessing they disappeared.

Example: run the same search every day and receive only changes:

```json
{ "mode": "search", "queries": ["dune"], "incrementalMode": true }
```

### Output Example

> Sample shape: values are illustrative placeholders, not from a live listing.

```json
{
  "url": "https://letterboxd.com/film/dune-part-two/",
  "slug": "dune-part-two",
  "title": "Dune: Part Two",
  "image": "https://a.ltrbxd.com/resized/poster.jpg",
  "description": "Paul Atreides unites with Chani and the Fremen while seeking revenge.",
  "genre": ["Science Fiction", "Adventure"],
  "directors": ["Denis Villeneuve"],
  "averageRating": 4.3,
  "averageRatingPrecise": 4.31,
  "ratingsCount": 900000,
  "datePublished": "2024-03-01",
  "releaseYear": 2024,
  "runtimeMinutes": 166,
  "tagline": "Long live the fighters.",
  "reviews": [
    {
      "filmSlug": "dune-part-two",
      "author": "hooviesmovies",
      "rating": 4.5,
      "reviewDate": "2024-03-05",
      "text": "Great sequel, the worm ride delivers.",
      "likes": 3,
      "containsSpoilers": false
    }
  ],
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

`reviews` is only present when `fetchReviews` is on.

### Send Results Into Your Apps

The optional MCP connector inputs let you send a condensed, human-readable summary of each selected film into your connected apps. Authorize a connector under Apify > Settings > API & Integrations, select it in `mcpConnectors`, and set `notionParentPageUrl` when using Notion.

The connector export is a side output. It does not change the dataset, and connector write errors do not fail the run. The complete record always remains available in the Apify dataset.

### Plan Requirement

For best reliability, keep the default Apify proxy setting. `maxItems` is a maximum cap, not a guaranteed count; a run can save fewer rows when the selected search phrase, URL, or browse path matches fewer films.

# Actor input Schema

## `mode` (type: `string`):

Search mode looks up films matching the phrases in Search phrases. URL mode reads the pasted Letterboxd URLs (or the Browse path fallback) below.

## `queries` (type: `array`):

One or more film title phrases to search on Letterboxd. Matching films are returned with full metadata.

## `startUrls` (type: `array`):

Letterboxd URLs: film pages, browse lists (e.g. https://letterboxd.com/films/popular/), or direct film slugs.

## `browsePath` (type: `string`):

Used only in URL mode when Start URLs is empty. Site path to browse, e.g. /films/popular/ or /films/genre/sci-fi/by/rating/.

## `maxItems` (type: `integer`):

Maximum films to return across the whole run. 0 is not valid; the cap is always finite.

## `maxPages` (type: `integer`):

Maximum Letterboxd browse/search pages to read per source. Leave empty (0) to walk every result page; max items is the primary output cap.

## `fetchReviews` (type: `boolean`):

Turn on to also fetch reader reviews for each returned film (charged as a separate per-film enrichment event). Off returns just film metadata.

## `maxReviewsPerFilm` (type: `integer`):

Only used when Fetch reader reviews is on. Maximum reviews to collect per film.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID (or dataset ID) from this actor to continue a large job pull without returning or charging for films already collected there (matched by slug). Use this after an interrupted run. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching films as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED films. Turn on "Emit unchanged" or "Emit expired" only when you also want those films returned (and billed). State is kept separately for each search/URL and mode setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode/queries/URLs/browse path: different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return films that have not changed since the last run, marked UNCHANGED. This returns (and bills) extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return films that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search, not when Max items capped it or when Resume was used. This returns (and bills) extra synthetic rows, so leave it off unless you need expiry tracking.

## `proxy` (type: `object`):

Runs through the Apify proxy by default — direct cloud egress is refused by the site. No special tier needed.

## `mcpConnectors` (type: `array`):

Optionally send the scraped films into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify > Settings > API & Integrations, then select it here. Notion gets a page-per-film export; other connectors receive a best-effort write or digest. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which film pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on how many films are written to each selected connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "dune"
  ],
  "startUrls": [
    {
      "url": "https://letterboxd.com/films/popular/"
    }
  ],
  "browsePath": "/films/popular/",
  "maxItems": 20,
  "maxPages": 0,
  "fetchReviews": false,
  "maxReviewsPerFilm": 25,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ]
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "queries": [
        "dune"
    ],
    "startUrls": [
        {
            "url": "https://letterboxd.com/films/popular/"
        }
    ],
    "fetchReviews": false,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "BUYPROXIES94952"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/letterboxd-film-reviews-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "queries": ["dune"],
    "startUrls": [{ "url": "https://letterboxd.com/films/popular/" }],
    "fetchReviews": False,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["BUYPROXIES94952"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/letterboxd-film-reviews-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "queries": [
    "dune"
  ],
  "startUrls": [
    {
      "url": "https://letterboxd.com/films/popular/"
    }
  ],
  "fetchReviews": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ]
  }
}' |
apify call abotapi/letterboxd-film-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/letterboxd-film-reviews-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lKyJo6lfuKGFFA4zj/builds/0MUshTcAFhPauhXPo/openapi.json
