diff --git a/.env.example b/.env.example index c44b3e5..a5bd985 100644 --- a/.env.example +++ b/.env.example @@ -18,11 +18,13 @@ NTFY_URL=https://ntfy.sh # NTFY_TOKEN=tk_... # only if your server (or topic) requires auth # ── District (district.in) ──────────────────────────────────────────────────── -# Leave DISTRICT_MOVIE_CODE unset to skip District entirely. +# Leave DISTRICT_CONTENT_ID unset to skip District entirely. # Read these off the movie's page URL, e.g. # district.in/movies/the-odyssey-movie-tickets-in-ahmedabad-MV187151?frmtid=y7EZNLXN5Y -DISTRICT_MOVIE_CODE=y7EZNLXN5Y # ?frmtid= — identifies movie + language -DISTRICT_CONTENT_ID=187151 # digits after MV +DISTRICT_CONTENT_ID=187151 # digits after MV; this is what enables District +DISTRICT_MOVIE_CODE=y7EZNLXN5Y # optional ?frmtid=, pins one language/format variant. + # Leave empty for a film with no showtimes yet, since + # District only publishes the code once shows exist. DISTRICT_CITY_KEY=ahmedabad # city slug in the path DISTRICT_LAT=23.033938956712916 # any coordinates in that city; the API needs them DISTRICT_LNG=72.57696301365426 diff --git a/README.md b/README.md index 9089cac..961a50b 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ support. ## Finding your IDs Configure either site, or both. Skip a site by leaving its main ID unset: no -`DISTRICT_MOVIE_CODE` means District isn't watched, no `BMS_EVENT_CODE` means BookMyShow isn't. +`DISTRICT_CONTENT_ID` means District isn't watched, no `BMS_EVENT_CODE` means BookMyShow isn't. ### District @@ -118,7 +118,9 @@ https://www.district.in/movies/the-odyssey-movie-tickets-in-ahmedabad-MV187151?f ``` - `DISTRICT_CONTENT_ID` is the number after `MV` -- `DISTRICT_MOVIE_CODE` is the `frmtid` parameter, which pins down the movie *and* the language +- `DISTRICT_MOVIE_CODE` is the `frmtid` parameter, which pins down the movie *and* the language. + It's optional, and a film that hasn't been given showtimes yet doesn't have one, so leave it + empty when you're waiting on an unreleased title. - `DISTRICT_CITY_KEY` is the city slug in the path - `DISTRICT_LAT` and `DISTRICT_LNG` can be any coordinates inside that city. The API refuses to answer without them, though results are city-wide either way. @@ -159,7 +161,8 @@ with comments in [`.env.example`](.env.example). |---|---|---| | `TARGET_DATE` | *required* | The date you're waiting for, `YYYY-MM-DD` | | `NTFY_TOPIC` | *required* | The topic you subscribe to. Treat it like a password. | -| `DISTRICT_MOVIE_CODE` | | Set to watch District. Plus `DISTRICT_CONTENT_ID`, `DISTRICT_CITY_KEY`, `DISTRICT_LAT`, `DISTRICT_LNG` | +| `DISTRICT_CONTENT_ID` | | Set to watch District. Plus `DISTRICT_CITY_KEY`, `DISTRICT_LAT`, `DISTRICT_LNG` | +| `DISTRICT_MOVIE_CODE` | *(any variant)* | Optional `frmtid` to pin one language/format | | `DISTRICT_CINEMA_ID` | *(any)* | Restrict District to one cinema | | `DISTRICT_FORMAT_TAG` | `imax_2d` | Session tag that counts as a match | | `DISTRICT_FORMAT_MATCH` | `IMAX` | Screen-format substring that counts as a match | @@ -184,7 +187,8 @@ with comments in [`.env.example`](.env.example). | `PROXY_URL` | | SOCKS5 proxy for both sites' checks, see below | v1 used flat names (`MOVIE_CODE`, `CINEMA_ID` and friends) for District. Those still work, so an -existing `.env` keeps running unchanged. +existing `.env` keeps running unchanged. They're read per field, so a `DISTRICT_*` value you set +deliberately always wins over a leftover flat one, and the log warns when any flat name is in use. ## Status endpoint diff --git a/src/config.js b/src/config.js index 0730579..042b6b5 100644 --- a/src/config.js +++ b/src/config.js @@ -22,36 +22,50 @@ const num = (name, fallback) => { return n; }; -/** Old single-provider variable names, still honoured so v1 .env files keep working. */ -function legacyDistrict() { - if (!process.env.MOVIE_CODE || process.env.DISTRICT_MOVIE_CODE) return null; - return { - movieCode: env("MOVIE_CODE"), - contentId: env("CONTENT_ID"), - cityKey: env("CITY_KEY"), - lat: env("LAT"), - lng: env("LNG"), - venueId: env("CINEMA_ID"), - formatTag: process.env.FORMAT_TAG ?? "imax_2d", - formatMatch: process.env.FORMAT_MATCH ?? "IMAX", - bookingUrl: env("BOOKING_URL"), - }; +/** + * District settings. v1 used flat names (MOVIE_CODE, CINEMA_ID and so on) and those are + * still honoured, but per field, so a stale leftover cannot override a DISTRICT_* value + * that was set deliberately. Enabled by the content id, since the movie code is optional. + */ +const LEGACY = { + DISTRICT_MOVIE_CODE: "MOVIE_CODE", + DISTRICT_CONTENT_ID: "CONTENT_ID", + DISTRICT_CITY_KEY: "CITY_KEY", + DISTRICT_LAT: "LAT", + DISTRICT_LNG: "LNG", + DISTRICT_CINEMA_ID: "CINEMA_ID", + DISTRICT_FORMAT_TAG: "FORMAT_TAG", + DISTRICT_FORMAT_MATCH: "FORMAT_MATCH", + DISTRICT_BOOKING_URL: "BOOKING_URL", +}; + +const pick = (name) => process.env[name] ?? process.env[LEGACY[name]]; + +export function legacyNamesInUse() { + return Object.entries(LEGACY) + .filter(([nu, old]) => process.env[nu] === undefined && process.env[old] !== undefined) + .map(([, old]) => old); } function districtConfig() { - const legacy = legacyDistrict(); - if (legacy) return legacy; - if (!process.env.DISTRICT_MOVIE_CODE) return null; + const contentId = pick("DISTRICT_CONTENT_ID"); + if (!contentId) return null; // District not configured + const need = (name) => { + const v = pick(name); + if (!v) throw new Error(`${name} is required when District is configured (see .env.example)`); + return v; + }; return { - movieCode: required("DISTRICT_MOVIE_CODE"), - contentId: required("DISTRICT_CONTENT_ID"), - cityKey: required("DISTRICT_CITY_KEY"), - lat: required("DISTRICT_LAT"), - lng: required("DISTRICT_LNG"), - venueId: env("DISTRICT_CINEMA_ID"), - formatTag: process.env.DISTRICT_FORMAT_TAG ?? "imax_2d", - formatMatch: process.env.DISTRICT_FORMAT_MATCH ?? "IMAX", - bookingUrl: env("DISTRICT_BOOKING_URL"), + // Optional: District answers without it, and an unreleased film has no format code yet. + movieCode: pick("DISTRICT_MOVIE_CODE") ?? "", + contentId, + cityKey: need("DISTRICT_CITY_KEY"), + lat: need("DISTRICT_LAT"), + lng: need("DISTRICT_LNG"), + venueId: pick("DISTRICT_CINEMA_ID") ?? "", + formatTag: pick("DISTRICT_FORMAT_TAG") ?? "imax_2d", + formatMatch: pick("DISTRICT_FORMAT_MATCH") ?? "IMAX", + bookingUrl: pick("DISTRICT_BOOKING_URL") ?? "", }; } @@ -83,6 +97,10 @@ export function loadConfig() { new Date(Date.parse(`${targetDate}T00:00:00Z`) + 86400000 - tzOffsetMinutes * 60000).toISOString(); const built = { district: districtConfig(), bookmyshow: bookmyshowConfig() }; + const stale = legacyNamesInUse(); + if (stale.length) { + console.warn(`warning: using v1 variable names ${stale.join(", ")}. Rename them to DISTRICT_* to avoid confusion.`); + } // PROVIDERS is an explicit allow-list; without it, every configured provider runs. const wanted = env("PROVIDERS").split(",").map((s) => s.trim()).filter(Boolean); for (const w of wanted) { @@ -90,7 +108,7 @@ export function loadConfig() { } const keys = (wanted.length ? wanted : Object.keys(built)).filter((k) => built[k]); if (!keys.length) { - throw new Error("No provider configured. Set DISTRICT_MOVIE_CODE and/or BMS_EVENT_CODE (see .env.example)"); + throw new Error("No provider configured. Set DISTRICT_CONTENT_ID and/or BMS_EVENT_CODE (see .env.example)"); } for (const w of wanted) { if (!built[w]) throw new Error(`PROVIDERS lists "${w}" but its settings are missing (see .env.example)`); diff --git a/test/providers.test.js b/test/providers.test.js index b83d438..f03ca35 100644 --- a/test/providers.test.js +++ b/test/providers.test.js @@ -132,3 +132,34 @@ test("district: 400 code 32 is a waiting state, other 400s are failures", async globalThis.fetch = realFetch; } }); + +// A leftover v1 name silently overriding a DISTRICT_* value that was set on purpose is +// the worst kind of config bug: the monitor watches the wrong film and still says "closed". +test("config: DISTRICT_* wins per field over leftover v1 names", async () => { + const { loadConfig, legacyNamesInUse } = await import("../src/config.js"); + const saved = { ...process.env }; + try { + Object.assign(process.env, { + TARGET_DATE: "2026-09-25", NTFY_TOPIC: "t", + MOVIE_CODE: "OLDCODE", CONTENT_ID: "187151", CITY_KEY: "ahmedabad", + LAT: "23.03", LNG: "72.57", CINEMA_ID: "1032362", + DISTRICT_CONTENT_ID: "227928", DISTRICT_CINEMA_ID: "", + BMS_EVENT_CODE: "ET1", BMS_REGION_CODE: "AHD", + }); + const d = loadConfig().providers.find((p) => p.key === "district"); + assert.equal(d.contentId, "227928", "the explicitly set new name must win"); + assert.equal(d.venueId, "", "an explicitly emptied new name must not fall back to the old one"); + assert.equal(d.movieCode, "OLDCODE", "fields with no new value still inherit"); + assert.ok(legacyNamesInUse().includes("MOVIE_CODE"), "and the leftovers are reported"); + + delete process.env.DISTRICT_CONTENT_ID; + delete process.env.CONTENT_ID; + const only = loadConfig().providers; + assert.equal(only.find((p) => p.key === "district"), undefined, + "no content id means District is simply not watched"); + assert.equal(only.length, 1, "and the other site carries on alone"); + } finally { + for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k]; + Object.assign(process.env, saved); + } +});