https://github.com/joshuahiggins/moldy-pumpkins
Curated movie & TV lists published daily as JSON for Radarr/Sonarr import lists. Serverless TypeScript on AWS Lambda.
https://github.com/joshuahiggins/moldy-pumpkins
aws-lambda esbuild import-list movies plex radarr rotten-tomatoes self-hosted serverless sonarr tv typescript
Last synced: 13 days ago
JSON representation
Curated movie & TV lists published daily as JSON for Radarr/Sonarr import lists. Serverless TypeScript on AWS Lambda.
- Host: GitHub
- URL: https://github.com/joshuahiggins/moldy-pumpkins
- Owner: joshuahiggins
- License: mit
- Created: 2018-01-31T15:58:44.000Z (over 8 years ago)
- Default Branch: main
- Last Pushed: 2026-06-06T22:34:42.000Z (20 days ago)
- Last Synced: 2026-06-07T00:13:09.336Z (19 days ago)
- Topics: aws-lambda, esbuild, import-list, movies, plex, radarr, rotten-tomatoes, self-hosted, serverless, sonarr, tv, typescript
- Language: TypeScript
- Size: 81.1 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# moldy-pumpkins
Curated movie and TV browse lists, published daily as JSON for import into
**Radarr** (movies) and **Sonarr** (TV) as custom lists. A scheduled AWS Lambda
builds each list, resolves every title to an `imdb_id` (movies, via TMDB) or a
`tvdbId` (TV, via TheTVDB), and writes the results to public S3 files you can
point your \*arr instance at directly.
You do **not** need to run anything to use these lists — the URLs below are live.
## Use the lists in Radarr / Sonarr
All lists are served from `https://moldy-pumpkins.s3.us-east-1.amazonaws.com/`
and refreshed once per day.
| List | App | URL |
| ----------------------------------- | ------ | ------------------------------------------------------------------------------------------- |
| [Movies: Certified Fresh in Theaters](https://www.rottentomatoes.com/browse/movies_in_theaters/critics:certified_fresh~sort:popular) | Radarr | |
| [Movies: Certified Fresh at Home](https://www.rottentomatoes.com/browse/movies_at_home/critics:certified_fresh~sort:popular) | Radarr | |
| [Movies: Most Popular Coming Soon](https://www.rottentomatoes.com/browse/movies_coming_soon/sort:popular) | Radarr | |
| [TV: Certified Fresh Newest](https://www.rottentomatoes.com/browse/tv_series_browse/critics:fresh~sort:newest) | Sonarr | |
| [TV: Certified Fresh Most Popular](https://www.rottentomatoes.com/browse/tv_series_browse/critics:fresh~sort:popular) | Sonarr | |
### Add a movie list to Radarr
1. Go to **Settings → Lists** and click **+** to add a list.
2. Under **Advanced**, choose **Custom Lists**.
3. Give it a **Name**, then paste one of the movie URLs above into **List URL**.
4. Set **Monitor**, **Quality Profile**, **Root Folder**, and any tags you want
applied to titles from this list.
5. Click **Test**, then **Save**.
### Add a TV list to Sonarr
1. Go to **Settings → Import Lists** and click **+** to add a list.
2. Under **Advanced List**, choose **Custom List**.
3. Give it a **Name**, then paste one of the TV URLs above into **List URL**.
4. Set **Quality Profile**, **Root Folder**, and **Monitor** options.
5. Click **Test**, then **Save**.
### Notes
- Lists regenerate once per day; there is no benefit to polling more often.
- Titles drop off as they roll off the source browse lists. To keep what you've
already grabbed, enable a **List Exclusions**-friendly monitor setting or take
a library snapshot rather than letting Radarr/Sonarr unmonitor removed titles.
- Movie entries are `{ "title", "imdb_id", "poster_url" }`; TV entries are
`{ "title", "tvdbId", "poster_url" }`.
---
## Run it yourself
Everything below is for self-hosters who want to publish their own copy of the
lists to their own S3 bucket. Most users do not need this.
### How it works
A single Lambda runs on a daily schedule and, for each configured list:
1. Fetches the source browse page and parses out the titles.
2. Resolves each title to an external ID — movies to `imdb_id` via TMDB, TV to
`tvdbId` via TheTVDB — and attaches a poster URL.
3. Uploads the resolved array as JSON to a public S3 object.
Each list is isolated: one list failing (or returning empty) never overwrites a
healthy live list, and a run only fails outright if _every_ list fails.
### Tech stack
- **Node 22** + **TypeScript** (ESM), bundled with **esbuild**
- **AWS Lambda** + **SAM** / CloudFormation (arm64)
- **cheerio** for HTML parsing, global `fetch` with retry/backoff
- **AWS SDK v3** (`@aws-sdk/client-s3`) for uploads
- `node:test` for tests (no network or credentials required)
### Environment variables
Configuration is read lazily from environment variables (see `src/config.ts`).
| Variable | Required | Default | Purpose |
| ----------------- | -------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `TMDB_KEY` | yes | — | TMDB v3 API key, used to resolve movie titles to `imdb_id`. |
| `TVDB_KEY` | yes | — | TheTVDB v4 API key, used to resolve TV titles to `tvdbId`. |
| `TVDB_PIN` | no | _(empty)_ | TheTVDB subscriber PIN. Only required for user-subscription keys; leave unset for project/v4 keys. |
| `TMDB_POSTER_URL` | no | `https://image.tmdb.org/t/p/w500` | Base URL prepended to TMDB poster paths. |
| `S3_BUCKET` | no | `moldy-pumpkins` | Destination bucket for the JSON lists. |
| `DRY_RUN` | no | `false` | When `true` (or `1`), resolve and log the lists instead of uploading. See [Dry run](#dry-run-no-s3-writes). |
> The bucket variable is `S3_BUCKET`, not `AWS_BUCKET`: AWS Lambda reserves all
> `AWS_`-prefixed environment-variable names, so the function would refuse to
> deploy with that name.
If a required variable (`TMDB_KEY` or `TVDB_KEY`) is unset when the handler runs,
it logs a `missing required configuration` error naming the absent variables, so
a misconfigured deployment is obvious in the logs rather than surfacing only as
unresolved titles.
### Develop
Requirements:
- Node 22 (`nvm use`)
- AWS CLI configured with credentials that can `s3:PutObject` to your bucket
(for a real `npm start` only)
Install and run the checks:
```sh
npm install
npm test # runs against injected fakes + frozen fixtures — no creds/network
npm run lint
npm run typecheck
npm run format # or format:check in CI
```
#### Setting variables locally
Local runs load a git-ignored `.env` file via `dotenv` (wired up in
`src/local.ts`, the local entry point — the deployed Lambda never imports it):
```sh
# .env
TMDB_KEY=your_tmdb_v3_key
TVDB_KEY=your_thetvdb_v4_key
# TVDB_PIN=your_pin # only if your TheTVDB key requires one
# TMDB_POSTER_URL=https://image.tmdb.org/t/p/w500
# S3_BUCKET=moldy-pumpkins # override to write to a test bucket
```
Or export them in your shell before running:
```sh
export TMDB_KEY=your_tmdb_v3_key
export TVDB_KEY=your_thetvdb_v4_key
```
#### Running locally
`npm start` invokes the handler once and prints the per-list summary as JSON.
This performs a **real** fetch, real TMDB/TheTVDB calls, and a real `PutObject`,
so you need both the API keys above and AWS credentials in scope. Point
`S3_BUCKET` at a scratch bucket if you do not want to overwrite live lists.
```sh
npm start
```
#### Dry run (no S3 writes)
Set `DRY_RUN=true` to resolve every list and log the results **without**
uploading. This exercises the full fetch-and-resolve pipeline with only the
TMDB/TheTVDB keys — no AWS credentials or bucket access required.
```sh
npm run start:dry # equivalent to: DRY_RUN=true npm start
```
Each list prints a `dry run - skipping upload` log line with the resolved
records, and the summary marks every list `{ "written": false, "dryRun": true }`.
### Deploy
The deployed Lambda does **not** read a `.env` file — its environment variables
are set by CloudFormation from the SAM template parameters in `template.yaml`.
You supply secret values as parameter overrides at deploy time.
`BucketName` is **required** and has no default: S3 bucket names share one global
namespace, so each deployment must supply its own globally-unique name (which is
also the bucket SAM creates).
First-time, interactive deploy (prompts for region, stack name, bucket name, and
the secret parameters, then saves your answers to `samconfig.toml`):
```sh
npm run deploy:guided
```
When prompted, supply your bucket name and credentials:
```
BucketName: your-unique-bucket-name
TmdbKey:
TvdbKey:
TvdbPin: # leave blank if unused
```
Subsequent deploys reuse the saved config:
```sh
npm run deploy
```
Both scripts run `sam build` first, so the deployed artifact always reflects the
current source. To pass parameters non-interactively (e.g. in CI):
```sh
npm run deploy -- --parameter-overrides \
BucketName=your-unique-bucket-name TmdbKey= TvdbKey=
```
| Template parameter | Maps to env var | Notes |
| ------------------ | ----------------- | -------------------------------------------------------------- |
| `BucketName` | `S3_BUCKET` | **Required**, no default. Names the public bucket SAM creates. |
| `TmdbKey` | `TMDB_KEY` | `NoEcho` — not shown in CloudFormation output. |
| `TvdbKey` | `TVDB_KEY` | `NoEcho`. |
| `TvdbPin` | `TVDB_PIN` | `NoEcho`; defaults to empty. Omit the override if unused. |
| `TmdbPosterUrl` | `TMDB_POSTER_URL` | Defaults to the TMDB w500 base URL. |
To rotate a key, redeploy with a new `--parameter-overrides` value — there is no
need to touch `.env`, which is local-only.
## License
[MIT](LICENSE)