Skill: humboldtjs/pathe-movie (version 1.0.0)

--- name: pathe-movie description: Lookup Pathé Netherlands movies, posters, descriptions, cinemas, and showtimes via the Pathé JSON APIs. Trigger when the user mentions a Pathé movie/show, wants a poster, asks about a description/rating, or requests showtimes for a specific cinema. --- # Pathé Movie Skill ## Summary - Always talk to the `https://www.pathe.nl/api` endpoints with the required browserlike headers (see `scripts/pathe_movie.py`). - Use the config at `config/pathe_movie_config.json` to know which cinemas to assume unless the user explicitly names a different cinema. - Rely on `scripts/pathe_movie.py` for reusable helpers (sanitizing queries, fuzzy matching, best-match selection, and fetching downstream endpoints). - When uncertain, reference `references/api.md` for payload shape, field names, and expected response structures. ## Search flow 1. Clean the user’s movie name by removing filler words (`the`, `a`, `an`, `of`, `in`, `on`, `for`, `and`). 2. Call `/api/search/full?q=...` with the sanitized query. 3. If multiple entries return, run a fuzzy title match (difflib) to pick the closest `title`. Keep the `slug`, `poster` (use `poster.lg`), and `contentRating` fields for later requests. 4. If a poster is required, return the `poster.lg` URL (fall back to `poster.md`/`posterPath` when necessary). ## Movie detail flow - Given a slug, call `/api/show/{slug}?language=nl`. - Pull `contentRating.description` and `synopsis` (some entries have `null`; handle gracefully) plus any extras such as `genres`, `directors`, `actors`, and `trailers` as context. - Poster references now live under `posterPath` before falling back to the search response’s `poster`. ## Cinema flow - Query `/api/show/{slug}/cinemas?language=nl`. Filter the returned cinema keys against `approvedCinemas` in the config unless the user asks for others. - For each cinema we need more detail about, call `/api/cinema/{cinema}?language=nl` to fetch the official `name`, `citySlug`, and `services`/`alerts` metadata. ## Showtimes - Use `/api/show/{slug}/showtimes/{cinema}?language=en` to get schedules. Responses are dictionaries keyed by date (`YYYY-MM-DD`). Each value is an array of showtimes; every entry contains at least a `time` string (plus `screen`, optional `language`, `format`, etc.). - If the array is empty, return a note that there are currently no scheduled showings. ## Testing notes - Ran `/api/search/full?q=matrix` to confirm the payload includes `slug`, `title`, `poster`, `contentRating`, and `genres`. - Called `/api/show/the-matrix-41119` to verify `contentRating.description`, `synopsis`, and `posterPath` fields; the synopsis can be null and the posterPath may be missing, so always null-check. - Queried `/api/cinema/pathe-zaandam` to inspect the returned `name`, `citySlug`, and service metadata (there is no `shows` list, so the cinema object is mostly static info). - Hit [REDACTED_SECRET] to confirm the endpoint returns a list; it was empty for that slug, showing you must handle zero-showtime responses. - Pulled `/api/shows?language=nl` to understand the bulk structure: dozens of entries with `slug`, `posterPath`, `contentRating`, `genres`, and `next24ShowtimesCount`. ## Media delivery notes - Always download poster images (and extra stills) locally before sending them through WhatsApp. Save them under `/tmp` or another temporary location so the gateway can read the file. - When the user explicitly requests a poster via WhatsApp, attach the local path in the `message` tool `media` field (e.g., `/tmp/bluey_poster.jpg`). The WhatsApp docs describe that outbound media accepts local paths, so this ensures the actual image is delivered instead of a URL. - Keep the text part of the `message` tool call descriptive (e.g., "Here’s the Bluey poster you asked for"), and rely on the downloaded file for the visual. Follow these instructions whenever the user asks about search, posters, descriptions, cinema availability, or showtimes so the skill always produces accurate Pathé Netherlands results.

path: scripts/pathe_movie.py
content: """Helper routines for the Pathé Movie skill."""
from pathlib import Path
import difflib
import json
import urllib.parse
import urllib.request

API_BASE = "https://www.pathe.nl/api"
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
                 " AppleWebKit/537.36 (KHTML, like Gecko)"
                 " Chrome/138.0.0.0 Safari/537.36",
    "Accept": "application/json",
}
FILLER_WORDS = {"the", "a", "an", "of", "in", "on", "for", "and"}
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "pathe_movie_config.json"
CONFIG = json.loads(CONFIG_PATH.read_text())
APPROVED_CINEMAS = CONFIG.get("approvedCinemas", [])
PRIMARY_CINEMA = CONFIG.get("primaryCinema") or (APPROVED_CINEMAS[0] if APPROVED_CINEMAS else None)


def _fetch_json(path, query_args=None):
    url = f"{API_BASE}{path}"
    if query_args:
        url += "?" + urllib.parse.urlencode(query_args)
    req = urllib.request.Request(url, headers=HEADERS)
    with urllib.request.urlopen(req) as resp:
        return json.load(resp)


def normalize_query(title):
    parts = [p for p in title.split() if p.lower() not in FILLER_WORDS]
    return " ".join(parts)


def search_movies(name):
    filtered = normalize_query(name)
    return _fetch_json("/search/full", {"q": filtered})


def best_match(name, candidates):
    titles = [movie.get("title", "") for movie in candidates]
    matches = difflib.get_close_matches(name, titles, n=3, cutoff=0.5)
    return [movie for movie in candidates if movie.get("title") in matches]


def get_show(slug):
    return _fetch_json(f"/show/{slug}", {"language": "nl"})


def get_cinemas(slug):
    return _fetch_json(f"/show/{slug}/cinemas", {"language": "nl"})


def get_cinema_details(cinema):
    return _fetch_json(f"/cinema/{cinema}", {"language": "nl"})


def get_showtimes(slug, cinema):
    return _fetch_json(f"/show/{slug}/showtimes/{cinema}", {"language": "en"})


def poster_url(movie_result):
    poster = movie_result.get("poster") or movie_result.get("posterPath")
    if poster:
        return poster.get("lg") or poster.get("md")


def clean_result(movie, query):
    return {
        "slug": movie.get("slug"),
        "title": movie.get("title"),
        "poster": poster_url(movie),
        "rating": movie.get("contentRating"),
    }


def preferred_cinemas(available, include_secondary=False):
    filtered = [cin for cin in available if cin in APPROVED_CINEMAS]
    if not filtered:
        return APPROVED_CINEMAS if include_secondary else ([PRIMARY_CINEMA] if PRIMARY_CINEMA else [])
    if PRIMARY_CINEMA in filtered:
        return [PRIMARY_CINEMA]
    return filtered if include_secondary else filtered[:1]


path: references/api.md
content: # Pathé API Reference

All requests must include a user-agent header that mimics a browser and accept JSON responses:

```
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: application/json
```

## Search (`/api/search/full?q={query}`)
- Returns a JSON array of movie/show objects.
- Each entry has `slug`, `title`, `poster` (and/or `posterPath`), `contentRating`, `synopsis`, `genres`, `duration`, `id`, `rating`, and `warning`.
- Preprocess the query to strip filler words (`the`, `a`, `an`, `of`, `in`, `on`, `for`, `and`) before hitting the API; this helps matching.
- Use difflib-style fuzzy matching on the `title` when multiple results appear.

## Show Details (`/api/show/{slug}?language=nl`)
- Use the slug from search results as the canonical identifier.
- Key fields:
  - `contentRating.description` (age/content rating description)
  - `synopsis` (detailed description; may be null)
  - `posterPath.lg` (big poster)
  - `backgroundPath` / `trailers` for extra context
- The response also contains `next24ShowtimesCount`, `genres`, `directors`, and `actors` for context.

## Cinema List (`/api/show/{slug}/cinemas?language=nl`)
- Returns an object whose keys are cinema slugs (e.g., `pathe-zaandam`).
- The value for each cinema includes metadata about which formats/services are available.
- Filter this list against the approved cinemas in `config/pathe_movie_config.json` unless the user explicitly requests other cinemas.

## Cinema Details (`/api/cinema/{cinema}?language=nl`)
- Provides `name`, `citySlug`, `services`, `alerts`, and parking/access information for each cinema.
- Use `citySlug` when the slug does not include a readable city name (e.g., `pathe-arena`).

## Showtimes (`/api/show/{slug}/showtimes/{cinema}?language=en`)
- Responses are keyed by date (`2026-02-05`) with each value being an array of showtime objects.
- Individual showtimes include `time`, `screen`, and occasionally `format`/`language` fields.
- When the array is empty, there are currently no sessions at that cinema.

## Full Shows List (`/api/shows?language=nl`)
- Returns all current Pathé entries; useful for auditing genre, rating, and posterPath fields.
- We inspected this to confirm that the API returns `slug`, `posterPath`, `contentRating`, `genres`, `next24ShowtimesCount`, and `showRef`.


path: config/pathe_movie_config.json
content: {
  "primaryCinema": "pathe-zaandam",
  "approvedCinemas": [
    "pathe-zaandam",
    "pathe-arena",
    "pathe-amsterdam-noord"
  ]
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)