Skip to main content
Glama
index.js49.6 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const axios_1 = __importDefault(require("axios")); const cheerio = __importStar(require("cheerio")); const zod_1 = require("zod"); const express_1 = __importDefault(require("express")); // --- Configuration & Constants --- const BASE_URL = "https://www.allocine.fr"; const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; const PORT = process.env.PORT || 8000; const DEFAULT_LOCATION = "121600"; // Saint-Denis (93200) const CITY_MAPPING = { "paris": "115755", "lyon": "113315", "marseille": "87914", "toulouse": "96373", "nice": "85327", "nantes": "112461", "strasbourg": "109748", "montpellier": "97612", "bordeaux": "96943", "lille": "106323", "rennes": "112102", "reims": "109315", "le havre": "99341", "saint-etienne": "109923", "toulon": "87627", "grenoble": "112299", "dijon": "91241", "angers": "113847", "nimes": "95640", "villeurbanne": "113404", "75001": "115755", "75000": "115755", "saint-denis": "121600", "93200": "121600" }; const AMENITY_MAPPING = { "3d": "3D", "imax": "IMAX", "dolbycinema": "Dolby Cinema", "dolbyatmos": "Dolby Atmos", "dolbyvision": "Dolby Vision", "4dx": "4DX", "screenx": "ScreenX", "hfr": "HFR", "4k": "4K", "laser": "Projection laser", "digital": "Numérique", "71": "7.1", "dolby71": "Dolby 7.1", "thx": "THX", "plf": "PLF", "ice": "ICE", "luxury": "Luxury", "infinite": "Infinite", "grandlarge": "Grand Large", "4demotion": "4D E-Motion", "airconditioning": "Air conditionné" }; function mapTagsToAmenities(tags) { const amenities = []; if (!tags || !Array.isArray(tags)) return amenities; tags.forEach(tag => { if (typeof tag !== 'string') return; const t = tag.toLowerCase(); // Priority to longer strings to avoid partial matches (e.g. dolby71 vs 71) const sortedKeys = Object.keys(AMENITY_MAPPING).sort((a, b) => b.length - a.length); for (const key of sortedKeys) { if (t.includes(key)) { amenities.push(AMENITY_MAPPING[key]); break; } } }); return Array.from(new Set(amenities)); } // --- Helper Functions --- function parseAjaxMovieShowtimes(data) { const results = []; if (!data.results || !Array.isArray(data.results)) return results; data.results.forEach((res) => { if (!res.theater) return; const cinemaName = res.theater.name; const cinemaAddress = `${res.theater.location?.address || ""} ${res.theater.location?.zip || ""} ${res.theater.location?.city || ""}`.trim(); const cards = { ugcIllimite: res.theater.loyaltyCards?.includes("UGC_ILLIMITE") || false, patheCinepass: res.theater.loyaltyCards?.includes("CINEPASS") || false, chequeCinemaUniversel: res.theater.loyaltyCards?.includes("CCU") || false, cinecheque: res.theater.loyaltyCards?.includes("CINECHEQUE") || false, cineCarteCip: res.theater.loyaltyCards?.includes("CINE_CARTE_CIP") || false }; const formatsMap = new Map(); const processShowtimes = (stList, diffusionVersion) => { if (!stList) return; stList.forEach((st) => { const type = normalizeVersionLabel(diffusionVersion); const startTime = st.startsAt ? st.startsAt.split("T")[1].substring(0, 5) : ""; const date = st.startsAt ? st.startsAt.split("T")[0] : ""; let bookingUrl; if (st.data?.ticketing) { const defaultTicket = st.data.ticketing.find((t) => t.provider === "default") || st.data.ticketing[0]; if (defaultTicket?.urls?.length > 0) { bookingUrl = defaultTicket.urls[0]; } } const amenities = []; if (bookingUrl) amenities.push("Réservation en ligne"); if (st.tags) { mapTagsToAmenities(st.tags).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } if (st.projection) { mapTagsToAmenities(st.projection).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } if (st.experience) { mapTagsToAmenities([st.experience]).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } const showtime = { startTime, date, bookingUrl, amenities: amenities.length > 0 ? amenities : undefined }; if (!formatsMap.has(type)) formatsMap.set(type, []); formatsMap.get(type).push(showtime); }); }; if (res.showtimes) { if (res.showtimes.original) processShowtimes(res.showtimes.original, "VO"); if (res.showtimes.multiple) processShowtimes(res.showtimes.multiple, "VF"); if (res.showtimes.original_st) processShowtimes(res.showtimes.original_st, "VOSTFR"); } if (formatsMap.size > 0) { results.push({ cinemaName, cinemaAddress, cards, formats: Array.from(formatsMap.entries()).map(([type, showtimes]) => ({ type, showtimes })) }); } }); return results; } function parseAjaxCinemaShowtimes(data, cinemaId, requestedDate) { const program = { cinemaName: "", cinemaId, cards: { ugcIllimite: false, patheCinepass: false, chequeCinemaUniversel: false, cinecheque: false, cineCarteCip: false }, movies: [] }; if (!data.results || !Array.isArray(data.results)) return program; data.results.forEach((res) => { if (!res.movie) return; const movieTitle = res.movie.title; const movieId = res.movie.internalId?.toString() || ""; const formatsMap = new Map(); const processShowtimes = (stList, diffusionVersion) => { if (!stList) return; stList.forEach((st) => { const date = st.startsAt ? st.startsAt.split("T")[0] : ""; // STRICT FILTERING: skip if date requested and doesn't match if (requestedDate && date !== requestedDate) return; const type = normalizeVersionLabel(diffusionVersion); const startTime = st.startsAt ? st.startsAt.split("T")[1].substring(0, 5) : ""; let bookingUrl; if (st.data?.ticketing) { const defaultTicket = st.data.ticketing.find((t) => t.provider === "default") || st.data.ticketing[0]; if (defaultTicket?.urls?.length > 0) { bookingUrl = defaultTicket.urls[0]; } } const amenities = []; if (bookingUrl) amenities.push("Réservation en ligne"); if (st.tags) { mapTagsToAmenities(st.tags).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } if (st.projection) { mapTagsToAmenities(st.projection).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } if (st.experience) { mapTagsToAmenities([st.experience]).forEach(a => { if (!amenities.includes(a)) amenities.push(a); }); } const showtime = { startTime, date, bookingUrl, amenities: amenities.length > 0 ? amenities : undefined }; if (!formatsMap.has(type)) formatsMap.set(type, []); formatsMap.get(type).push(showtime); }); }; if (res.showtimes) { if (res.showtimes.original) processShowtimes(res.showtimes.original, "VO"); if (res.showtimes.multiple) processShowtimes(res.showtimes.multiple, "VF"); if (res.showtimes.original_st) processShowtimes(res.showtimes.original_st, "VOSTFR"); } if (formatsMap.size > 0) { program.movies.push({ movieTitle, movieId, formats: Array.from(formatsMap.entries()).map(([type, showtimes]) => ({ type, showtimes })) }); } }); return program; } async function fetchHTML(url) { try { const response = await axios_1.default.get(url, { headers: { "User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", }, }); return response.data; } catch (error) { throw new Error(`Failed to fetch URL ${url}: ${error.response?.status} ${error.response?.statusText || error.message}`); } } function extractIdFromUrl(url, prefix = "cfilm=") { const regex = new RegExp(`${prefix}(\\w+)`); const match = url.match(regex); return match ? match[1] : null; } function decodeAllocineObfuscation(className) { if (className.length < 4) return null; const noise = className.substring(0, 3); const parts = className.split("#"); const base64Part = parts[0].split(noise).join(""); const hashPart = parts.length > 1 ? "#" + parts[1].split(" ")[0] : ""; try { const decoded = Buffer.from(base64Part, 'base64').toString('utf-8'); if (decoded.startsWith("http") || decoded.startsWith("/") || decoded.includes("fichefilm") || decoded.includes("cfilm=") || decoded.includes("csalle=")) { return decoded + hashPart; } } catch { } return null; } function getRelativeUrl(titleNode) { let relativeUrl = titleNode.attr("href") || null; if (!relativeUrl) { const classes = titleNode.attr("class")?.split(/\s+/) || []; for (const cls of classes) { if (cls === "meta-title-link") continue; const decoded = decodeAllocineObfuscation(cls); if (decoded) return decoded; } } return relativeUrl; } function extractAmenities(timeBlock, $) { const amenities = []; const linkNode = timeBlock.find("span[class*='ACr'], a[class*='ACr']").first(); const experiencesAttr = linkNode.attr("data-experiences"); if (experiencesAttr) { try { const experiences = JSON.parse(experiencesAttr); if (Array.isArray(experiences)) { mapTagsToAmenities(experiences).forEach(a => amenities.push(a)); } } catch { } } // Fallback or addition from icons const amenitiesNode = timeBlock.find(".showtimes-hour-block-amenities"); if (amenitiesNode.length) { amenitiesNode.find("span").each((_, icon) => { const cls = ($(icon).attr("class") || "").toLowerCase(); for (const [key, value] of Object.entries(AMENITY_MAPPING)) { if (cls.includes(`icon-${key}`) && !amenities.includes(value)) { amenities.push(value); } } }); } return Array.from(new Set(amenities)); } const CARD_LABEL_SELECTOR = ".card-type, .badge, .label, [itemprop='paymentAccepted']"; function normalizeLabelText(value) { return value .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/\s+/g, " ") .trim(); } function normalizeWhitespace(value) { return value.replace(/\s+/g, " ").trim(); } function normalizeVersionLabel(label) { let clean = label.replace(/^\d{1,2}\s+[a-zéûà]+\s+\d{4}\s*-\s*/i, ""); clean = clean.replace(/^En\s+/i, ""); return normalizeWhitespace(clean); } function detectCardsFromLabels(container, $) { let ugcIllimite = false; let patheCinepass = false; let chequeCinemaUniversel = false; let cinecheque = false; let cineCarteCip = false; container.find(CARD_LABEL_SELECTOR).each((_, labelEl) => { const labelText = normalizeLabelText($(labelEl).text()); if (!labelText) return; if (labelText.includes("ugc") || labelText.includes("illimite")) ugcIllimite = true; if (labelText.includes("cinepass") || labelText.includes("cine-pass") || labelText.includes("pathe")) patheCinepass = true; if (labelText.includes("cheque") && labelText.includes("universel")) chequeCinemaUniversel = true; if (labelText.includes("cinecheque")) cinecheque = true; if (labelText.includes("cip") || labelText.includes("carte cip")) cineCarteCip = true; }); return { ugcIllimite, patheCinepass, chequeCinemaUniversel, cinecheque, cineCarteCip }; } function extractDateFromUrl(url) { const isoMatch = url.match(/(\d{4}-\d{2}-\d{2})/); if (isoMatch) return isoMatch[1]; const compactMatch = url.match(/(\d{8})/); if (compactMatch) { const compact = compactMatch[1]; return `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`; } return undefined; } function normalizeDateInput(value) { const trimmed = value.trim(); if (!trimmed) return {}; if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return { iso: trimmed, compact: trimmed.replace(/-/g, "") }; } if (/^\d{4}\/\d{2}\/\d{2}$/.test(trimmed)) { const iso = trimmed.replace(/\//g, "-"); return { iso, compact: iso.replace(/-/g, "") }; } const dmYMatch = trimmed.match(/^(\d{2})[\/.-](\d{2})[\/.-](\d{4})$/); if (dmYMatch) { const [, day, month, year] = dmYMatch; const iso = `${year}-${month}-${day}`; return { iso, compact: iso.replace(/-/g, "") }; } if (/^\d{8}$/.test(trimmed)) { const iso = `${trimmed.slice(0, 4)}-${trimmed.slice(4, 6)}-${trimmed.slice(6, 8)}`; return { iso, compact: trimmed }; } return {}; } function buildDateUrls(baseUrl, dateInput) { const normalized = normalizeDateInput(dateInput); const values = new Set(); if (normalized.iso) values.add(normalized.iso); if (normalized.compact) values.add(normalized.compact); // Also add DD/MM/YYYY if possible const dmYMatch = dateInput.trim().match(/^(\d{2})[\/.-](\d{2})[\/.-](\d{4})$/); if (dmYMatch) values.add(`${dmYMatch[1]}/${dmYMatch[2]}/${dmYMatch[3]}`); values.add(dateInput.trim()); const urls = []; const seen = new Set(); const params = ["date", "day", "jour", "d"]; for (const value of values) { if (!value) continue; for (const param of params) { try { const candidate = new URL(baseUrl); candidate.searchParams.set(param, value); const url = candidate.toString(); const key = normalizeUrlKey(url); if (seen.has(key)) continue; seen.add(key); urls.push(url); } catch { } } } return urls; } function filterDayLinksByDate(dayLinks, dateInput) { const normalized = normalizeDateInput(dateInput); const normalizedInput = normalizeLabelText(dateInput); return dayLinks.filter((link) => { const urlDate = extractDateFromUrl(link.url); if (normalized.iso && urlDate === normalized.iso) return true; if (normalized.compact && link.url.includes(normalized.compact)) return true; if (link.label) { const label = normalizeLabelText(link.label); if (normalized.iso && label.includes(normalized.iso)) return true; if (normalized.compact && label.includes(normalized.compact)) return true; if (normalizedInput && label.includes(normalizedInput)) return true; } return false; }); } function resolveUrl(href, baseUrl) { if (!href) return null; try { return new URL(href, baseUrl).toString(); } catch { return null; } } function normalizeUrlKey(url) { try { const parsed = new URL(url); const path = parsed.pathname.replace(/\/$/, ""); return `${parsed.origin}${path}${parsed.search}`; } catch { return url; } } function extractDayLabel($el, resolvedUrl) { const dataDate = $el.attr("data-date") || $el.attr("data-day") || $el.attr("data-show-date") || $el.attr("data-iso-date") || $el.parent().attr("data-date") || $el.parent().attr("data-day"); const fromUrl = resolvedUrl ? extractDateFromUrl(resolvedUrl) : undefined; const text = normalizeWhitespace($el.text()); return dataDate?.trim() || fromUrl || text || undefined; } function extractSelectedDayLabel($) { const selectors = [ ".showtimes-date .active", ".showtimes-date-list .active", ".showtimes-date .current", ".showtimes-date .selected", ".showtimes-date .is-active", ".showtimes-date-list .is-active", ".showtimes-date .on", ]; for (const selector of selectors) { const text = normalizeWhitespace($(selector).first().text()); if (text) return text; } return undefined; } function collectShowtimeDayLinks($, currentUrl, urlMatcher) { const selectors = [ ".showtimes-date a", ".showtimes-date-list a", ".showtimes-date-selector a", ".showtimes-date .item a", ".showtimes-date .tab a", ".tabs-date a", ".showtimes-tabs a", ".date-picker a", ".datepicker-date a", ".day-selector a", "a[data-date]", "a[data-day]", "a[data-show-date]", "a[data-iso-date]", "button[data-href]", "button[data-date]", "button[data-day]", "span[class*='ACr']", ]; const links = []; const seen = new Set(); const addLink = (resolvedUrl, label) => { if (!urlMatcher(resolvedUrl)) return; const key = normalizeUrlKey(resolvedUrl); if (seen.has(key)) return; seen.add(key); links.push({ url: resolvedUrl, label }); }; $(selectors.join(",")).each((_, el) => { const $el = $(el); let href = $el.attr("href") || $el.attr("data-href"); if (!href) { const classes = $el.attr("class")?.split(/\s+/) || []; for (const cls of classes) { if (cls.startsWith("ACr")) { href = decodeAllocineObfuscation(cls); if (href) break; } } } const resolved = resolveUrl(href, currentUrl); if (!resolved) return; const label = extractDayLabel($el, resolved); addLink(resolved, label); }); // Extract dates from data attributes as fallback or reinforcement const datesAttr = $("#theaterpage-showtimes-index-ui").attr("data-showtimes-dates"); if (datesAttr) { try { const dates = JSON.parse(datesAttr); if (Array.isArray(dates)) { dates.forEach(d => { const candidateUrl = new URL(currentUrl); candidateUrl.hash = `shwt_date=${d}`; addLink(candidateUrl.toString(), d); }); } } catch { } } if (links.length === 0) { $("a[href*='day='], a[href*='date='], a[href*='jour='], button[data-href*='day='], button[data-href*='date='], button[data-href*='jour=']").each((_, el) => { const $el = $(el); const href = $el.attr("href") || $el.attr("data-href"); const resolved = resolveUrl(href, currentUrl); if (!resolved) return; const label = extractDayLabel($el, resolved); addLink(resolved, label); }); } const currentKey = normalizeUrlKey(currentUrl); if (!seen.has(currentKey)) { const currentLabel = extractSelectedDayLabel($) || extractDateFromUrl(currentUrl) || "Aujourd'hui"; links.unshift({ url: currentUrl, label: currentLabel }); } return links; } function showtimeKey(showtime) { const amenitiesKey = showtime.amenities ? showtime.amenities.slice().sort().join(",") : ""; return `${showtime.date || ""}|${showtime.startTime}|${showtime.bookingUrl || ""}|${amenitiesKey}`; } function mergeShowtimes(target, incoming) { const seen = new Set(target.map(showtimeKey)); for (const showtime of incoming) { const key = showtimeKey(showtime); if (seen.has(key)) continue; seen.add(key); target.push(showtime); } } function mergeFormats(target, incoming) { const targetByType = new Map(); target.forEach((format) => targetByType.set(format.type, format)); for (const format of incoming) { const existing = targetByType.get(format.type); if (existing) { mergeShowtimes(existing.showtimes, format.showtimes); } else { const cloned = { type: format.type, showtimes: [...format.showtimes] }; target.push(cloned); targetByType.set(format.type, cloned); } } } function mergeCards(target, incoming) { target.ugcIllimite = target.ugcIllimite || incoming.ugcIllimite; target.patheCinepass = target.patheCinepass || incoming.patheCinepass; target.chequeCinemaUniversel = target.chequeCinemaUniversel || incoming.chequeCinemaUniversel; target.cinecheque = target.cinecheque || incoming.cinecheque; target.cineCarteCip = target.cineCarteCip || incoming.cineCarteCip; } async function getNowShowing(maxPages = 10) { let allResults = []; for (let page = 1; page <= maxPages; page++) { const url = `${BASE_URL}/film/aucinema/?page=${page}`; const html = await fetchHTML(url); const $ = cheerio.load(html); const pageResults = []; $(".entity-card-list").each((_, element) => { const titleNode = $(element).find(".meta-title-link"); const title = titleNode.text().trim(); const relativeUrl = getRelativeUrl(titleNode); if (!title || !relativeUrl) return; const id = extractIdFromUrl(relativeUrl, "cfilm=") || extractIdFromUrl(relativeUrl, "csalle="); if (!id) return; pageResults.push({ id, title, url: relativeUrl.startsWith("http") ? relativeUrl : `${BASE_URL}${relativeUrl}`, posterUrl: $(element).find("img.thumbnail-img").attr("data-src") || $(element).find("img.thumbnail-img").attr("src"), releaseDate: $(element).find(".meta-body-item.meta-body-info").text().trim().match(/(\d{1,2} [a-zéûà]+ \d{4})/i)?.[1] }); }); if (pageResults.length === 0) break; allResults = allResults.concat(pageResults); if ($(".pagination-item-next.disabled").length > 0) break; } return allResults; } async function searchMovies(query) { const url = `${BASE_URL}/rechercher/?q=${encodeURIComponent(query)}`; const html = await fetchHTML(url); const $ = cheerio.load(html); const results = []; $(".entity-card-list").each((_, element) => { const titleNode = $(element).find(".meta-title-link"); const title = titleNode.text().trim(); const relativeUrl = getRelativeUrl(titleNode); if (!title || !relativeUrl || !relativeUrl.includes("fichefilm")) return; const id = extractIdFromUrl(relativeUrl, "cfilm="); if (!id) return; results.push({ id, title, url: relativeUrl.startsWith("http") ? relativeUrl : `${BASE_URL}${relativeUrl}`, posterUrl: $(element).find("img.thumbnail-img").attr("data-src") || $(element).find("img.thumbnail-img").attr("src"), releaseDate: $(element).find(".meta-body-item.meta-body-info").text().trim().match(/(\d{1,2} [a-zéûà]+ \d{4})/i)?.[1] }); }); return results; } async function searchCinemas(query) { const url = `${BASE_URL}/rechercher/?q=${encodeURIComponent(query)}`; const html = await fetchHTML(url); const $ = cheerio.load(html); const initialResults = []; const foundIds = new Set(); $("[data-theater]").each((_, el) => { try { const data = JSON.parse($(el).attr("data-theater") || "{}"); if (data.id && data.name && !foundIds.has(data.id)) { foundIds.add(data.id); initialResults.push({ id: data.id, name: data.name, address: "" }); } } catch { } }); $(".entity-card-list").each((_, element) => { const titleNode = $(element).find(".meta-title-link, .title span"); const name = titleNode.text().trim(); const relativeUrl = getRelativeUrl(titleNode); if (!name || !relativeUrl || !relativeUrl.includes("csalle=")) return; const id = extractIdFromUrl(relativeUrl, "csalle="); if (!id || foundIds.has(id)) return; foundIds.add(id); const address = $(element).find(".meta-body-item.meta-body-info").text().trim(); initialResults.push({ id, name, address }); }); // Fetch details for each cinema to get cards const results = await Promise.all(initialResults.slice(0, 5).map(async (item) => { try { const theaterUrl = `${BASE_URL}/seance/salle_gen_csalle=${item.id}.html`; const theaterHtml = await fetchHTML(theaterUrl); const t$ = cheerio.load(theaterHtml); const cards = detectCardsFromLabels(t$.root(), t$); return { ...item, cards }; } catch { return { ...item, cards: { ugcIllimite: false, patheCinepass: false, chequeCinemaUniversel: false, cinecheque: false, cineCarteCip: false } }; } })); return results; } async function getCinemaShowtimes(cinemaId, date) { const url = `${BASE_URL}/seance/salle_gen_csalle=${cinemaId}.html`; if (date) { const normalized = normalizeDateInput(date); const isoDate = normalized.iso || date.trim(); try { const ajaxUrl = `${BASE_URL}/_/showtimes/theater-${cinemaId}/d-${isoDate}`; const ajaxResponse = await axios_1.default.get(ajaxUrl, { headers: { "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest", "Referer": url, "Accept": "application/json" } }); if (ajaxResponse.data && ajaxResponse.data.results) { return parseAjaxCinemaShowtimes(ajaxResponse.data, cinemaId, isoDate); } } catch (e) { console.error(`AJAX path-based call failed for ${isoDate}:`, e); } } const html = await fetchHTML(url); const $ = cheerio.load(html); const dayCinemaName = normalizeWhitespace($(".theater-name-title").text()) || normalizeWhitespace($("h1").first().text()); const dayCards = detectCardsFromLabels($.root(), $); const program = { cinemaName: dayCinemaName, cinemaId, cards: dayCards, movies: [] }; const moviesByKey = new Map(); $(".movie-card-theater").each((_, movieNode) => { const titleNode = $(movieNode).find(".meta-title-link"); const movieTitle = titleNode.text().trim(); if (!movieTitle) return; const movieUrl = getRelativeUrl(titleNode); const movieId = movieUrl ? (extractIdFromUrl(movieUrl, "cfilm=") || "") : ""; const formats = []; $(movieNode).find(".showtimes-version").each((_, versionNode) => { const rawType = $(versionNode).find(".text").text().trim(); const type = normalizeVersionLabel(rawType); const showtimes = []; $(versionNode).find(".showtimes-hour-block").each((_, timeBlock) => { const timeVal = $(timeBlock).text().replace("Réserver", "").trim().match(/(\d{2}:\d{2})/)?.[1]; if (!timeVal) return; let bookingUrl; const linkElement = $(timeBlock).find("[class*='ACr']"); if (linkElement.length) { const classes = linkElement.attr("class")?.split(/\s+/) || []; for (const cls of classes) { if (!cls.startsWith("ACr")) continue; const decoded = decodeAllocineObfuscation(cls); if (decoded && (decoded.startsWith("http") || decoded.startsWith("/"))) { bookingUrl = decoded.startsWith("/") ? `${BASE_URL}${decoded}` : decoded; break; } } } const amenities = extractAmenities($(timeBlock), $); if (bookingUrl) amenities.unshift("Réservation en ligne"); const showtime = { startTime: timeVal, bookingUrl, amenities: amenities.length > 0 ? amenities : undefined }; showtime.date = "Aujourd'hui"; showtimes.push(showtime); }); if (showtimes.length > 0) formats.push({ type, showtimes }); }); if (formats.length > 0) { const key = movieId ? `id:${movieId}` : `title:${normalizeWhitespace(movieTitle).toLowerCase()}`; moviesByKey.set(key, { movieTitle, movieId, formats }); } }); program.movies = Array.from(moviesByKey.values()); return program; } async function getShowtimes(movieId, location, date) { const cleanLocation = (location || "").toLowerCase().trim(); const locationId = CITY_MAPPING[cleanLocation] || location || DEFAULT_LOCATION; const url = `${BASE_URL}/seance/film-${movieId}/pres-de-${encodeURIComponent(locationId)}/`; if (date) { const normalized = normalizeDateInput(date); const isoDate = normalized.iso || date.trim(); try { const ajaxUrl = `${BASE_URL}/_/showtimes/movie-${movieId}/near-${locationId}/d-${isoDate}`; const ajaxResponse = await axios_1.default.get(ajaxUrl, { headers: { "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest", "Referer": url, "Accept": "application/json" } }); if (ajaxResponse.data && ajaxResponse.data.results) { return parseAjaxMovieShowtimes(ajaxResponse.data); } } catch (e) { console.error(`AJAX path-based call failed for movie ${movieId}:`, e); } } const html = await fetchHTML(url); const $ = cheerio.load(html); let dayLinks = collectShowtimeDayLinks($, url, (candidate) => candidate.includes(`/seance/film-${movieId}/`)); const baseKey = normalizeUrlKey(url); const resultsByKey = new Map(); const getCinemaKey = (cinemaName, cinemaAddress, cinemaId) => { if (cinemaId) return `id:${cinemaId}`; const normalizedName = normalizeWhitespace(cinemaName).toLowerCase(); const normalizedAddress = normalizeWhitespace(cinemaAddress).toLowerCase(); return normalizedAddress ? `${normalizedName}|${normalizedAddress}` : normalizedName; }; for (const day of dayLinks) { const dayHtml = normalizeUrlKey(day.url) === baseKey ? html : await fetchHTML(day.url); const dayPage = cheerio.load(dayHtml); const selectedLabel = extractSelectedDayLabel(dayPage); const resolvedDayLabel = selectedLabel || day.label || extractDateFromUrl(day.url); dayPage(".theater-card").each((_, theaterNode) => { let cinemaName = dayPage(theaterNode).find(".theater-name h2, .theater-name a").first().text().trim(); let cinemaAddress = dayPage(theaterNode).find(".theater-address").text().trim(); if (!cinemaName) { const lines = dayPage(theaterNode) .find(".theater-infos") .text() .trim() .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); if (lines.length > 0) { cinemaName = lines[0]; if (lines.length > 1) cinemaAddress = lines.slice(1).join(", "); } } cinemaName = normalizeWhitespace(cinemaName); cinemaAddress = normalizeWhitespace(cinemaAddress); if (!cinemaName) return; const formats = []; dayPage(theaterNode).find(".showtimes-version").each((_, versionNode) => { const rawLabel = dayPage(versionNode).find(".text").text().trim(); const versionLabel = normalizeVersionLabel(rawLabel); const showtimes = []; dayPage(versionNode).find(".showtimes-hour-block").each((_, timeBlock) => { const timeVal = dayPage(timeBlock).text().replace("Réserver", "").trim().match(/(\d{2}:\d{2})/)?.[1]; if (!timeVal) return; let bookingUrl; const linkElement = dayPage(timeBlock).find("[class*='ACr']"); if (linkElement.length) { const classes = linkElement.attr("class")?.split(/\s+/) || []; for (const cls of classes) { if (!cls.startsWith("ACr")) continue; const decoded = decodeAllocineObfuscation(cls); if (decoded) { bookingUrl = decoded.startsWith("/") ? `${BASE_URL}${decoded}` : decoded; break; } } } const amenities = extractAmenities(dayPage(timeBlock), dayPage); if (bookingUrl) amenities.unshift("Réservation en ligne"); const showtime = { startTime: timeVal, bookingUrl, amenities: amenities.length > 0 ? amenities : undefined }; if (resolvedDayLabel) showtime.date = resolvedDayLabel; showtimes.push(showtime); }); if (showtimes.length > 0) formats.push({ type: versionLabel, showtimes }); }); if (formats.length > 0) { const theaterLink = dayPage(theaterNode).find(".theater-name a").first(); const theaterUrl = getRelativeUrl(theaterLink); const cinemaId = theaterUrl ? (extractIdFromUrl(theaterUrl, "csalle=") || "") : ""; const key = getCinemaKey(cinemaName, cinemaAddress, cinemaId); const existing = resultsByKey.get(key); const cards = detectCardsFromLabels(dayPage(theaterNode), dayPage); if (!existing) { resultsByKey.set(key, { cinemaName, cinemaAddress, formats, cards }); } else { if (!existing.cinemaAddress && cinemaAddress) { existing.cinemaAddress = cinemaAddress; } mergeFormats(existing.formats, formats); mergeCards(existing.cards, cards); } } }); } return Array.from(resultsByKey.values()); } async function getCinemasByCard(cardType, location) { const cleanLocation = (location || "").toLowerCase().trim(); const locationId = CITY_MAPPING[cleanLocation] || location || DEFAULT_LOCATION; // card=1: UGC Illimité, card=2: CinéPass const cardId = cardType === 'ugc' ? '1' : '2'; // Use the city-specific salle list URL const url = `${BASE_URL}/salle/cinema/ville-${encodeURIComponent(locationId)}/?card=${cardId}`; console.error(`Fetching cinemas filtered by card: ${url}`); const html = await fetchHTML(url); const $ = cheerio.load(html); const results = []; $(".theater-card").each((_, el) => { let name = $(el).find(".theater-name h2, .theater-name a, .theater-infos h2").first().text().trim(); let address = $(el).find(".theater-address").text().trim(); if (!name) { const infosNode = $(el).find(".theater-infos"); const fullText = infosNode.text().trim(); const lines = fullText.split("\n").map(l => l.trim()).filter(l => l.length > 0); if (lines.length > 0) { name = lines[0]; if (lines.length > 1) address = lines.slice(1).join(", "); } } // Search for obfuscated link to get ID let id = ""; $(el).find("span[class*='ACr'], a[class*='ACr'], a[href*='salle_gen_csalle']").each((_, linkEl) => { const relativeUrl = getRelativeUrl($(linkEl)); if (relativeUrl && relativeUrl.includes("csalle=")) { id = extractIdFromUrl(relativeUrl, "csalle=") || ""; return false; // found it } }); if (name && id) { results.push({ id, name, address, cards: { ugcIllimite: cardType === 'ugc', patheCinepass: cardType === 'pathe', chequeCinemaUniversel: false, // Not detected via this specific filter cinecheque: false, cineCarteCip: false } }); } }); return results; } // --- MCP Server Definition --- const server = new index_js_1.Server({ name: "allocine-mcp-server", version: "1.1.0" }, { capabilities: { tools: {} } }); server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { return { tools: [ { name: "search_movies", description: "Search for a movie on AlloCiné to get its ID and basic info.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Movie title" } }, required: ["query"] } }, { name: "get_movies_now_showing", description: "Get all movies currently in theaters (paginated).", inputSchema: { type: "object", properties: { max_pages: { type: "number", description: "Number of pages to fetch (approx 15 movies per page). Default: 10.", default: 10 } }, } }, { name: "search_cinemas", description: "Search for a cinema to get its ID and see if it accepts UGC Illimité or Pathé Cinépass.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Cinema name or city" } }, required: ["query"] } }, { name: "get_showtimes", description: "Get showtimes for a specific movie in a specific location (defaults to Saint-Denis), over several days.", inputSchema: { type: "object", properties: { movie_id: { type: "string" }, location: { type: "string", description: "Zip code or City (Paris, Lyon...). Default: Saint-Denis" }, date: { type: "string", description: "Optional date to target (YYYY-MM-DD or DD/MM/YYYY). If set, only that day is fetched." } }, required: ["movie_id"] } }, { name: "get_cinema_showtimes", description: "Get the full program (all movies and times) for a specific cinema using its ID, over several days.", inputSchema: { type: "object", properties: { cinema_id: { type: "string", description: "The AlloCiné ID of the cinema (e.g. C0013)" }, date: { type: "string", description: "Optional date to target (YYYY-MM-DD or DD/MM/YYYY). If set, only that day is fetched." } }, required: ["cinema_id"] } }, { name: "get_cinemas_by_card", description: "Get a list of cinemas accepting a specific card (UGC Illimité or Pathé Cinépass) in a location (defaults to Saint-Denis).", inputSchema: { type: "object", properties: { location: { type: "string", description: "Zip code or City. Default: Saint-Denis" }, card_type: { type: "string", enum: ["ugc", "pathe"], description: "The type of card: 'ugc' for UGC Illimité, 'pathe' for Pathé Cinépass" } }, required: ["card_type"] } }, { name: "get_movie_booking_links", description: "Get direct booking links for a specific movie in a specific cinema.", inputSchema: { type: "object", properties: { movie_id: { type: "string", description: "The AlloCiné ID of the movie" }, cinema_id: { type: "string", description: "The AlloCiné ID of the cinema" }, date: { type: "string", description: "Optional date (YYYY-MM-DD)" } }, required: ["movie_id", "cinema_id"] } }, ], }; }); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (name === "search_movies") { const { query } = zod_1.z.object({ query: zod_1.z.string() }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await searchMovies(query), null, 2) }] }; } if (name === "get_movies_now_showing") { const { max_pages } = zod_1.z.object({ max_pages: zod_1.z.number().optional() }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await getNowShowing(max_pages || 10), null, 2) }] }; } if (name === "search_cinemas") { const { query } = zod_1.z.object({ query: zod_1.z.string() }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await searchCinemas(query), null, 2) }] }; } if (name === "get_showtimes") { const { movie_id, location, date } = zod_1.z.object({ movie_id: zod_1.z.string(), location: zod_1.z.string().optional(), date: zod_1.z.string().optional() }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await getShowtimes(movie_id, location, date), null, 2) }] }; } if (name === "get_cinema_showtimes") { const { cinema_id, date } = zod_1.z.object({ cinema_id: zod_1.z.string(), date: zod_1.z.string().optional() }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await getCinemaShowtimes(cinema_id, date), null, 2) }] }; } if (name === "get_cinemas_by_card") { const { location, card_type } = zod_1.z.object({ location: zod_1.z.string().optional(), card_type: zod_1.z.enum(["ugc", "pathe"]) }).parse(args); return { content: [{ type: "text", text: JSON.stringify(await getCinemasByCard(card_type, location), null, 2) }] }; } if (name === "get_movie_booking_links") { const { movie_id, cinema_id, date } = zod_1.z.object({ movie_id: zod_1.z.string(), cinema_id: zod_1.z.string(), date: zod_1.z.string().optional() }).parse(args); const program = await getCinemaShowtimes(cinema_id, date); const movie = program.movies.find(m => m.movieId === movie_id); if (!movie) { return { content: [{ type: "text", text: `Le film ${movie_id} n'est pas à l'affiche dans le cinéma ${cinema_id}${date ? ` le ${date}` : ""} sur la période disponible.` }] }; } return { content: [{ type: "text", text: JSON.stringify({ cinemaName: program.cinemaName, movieTitle: movie.movieTitle, formats: movie.formats }, null, 2) }] }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // --- Start Server --- const transportType = process.argv.includes("--sse") ? "sse" : "stdio"; if (transportType === "sse") { const app = (0, express_1.default)(); let transport = null; app.get("/sse", async (req, res) => { console.error("New SSE connection"); transport = new sse_js_1.SSEServerTransport("/messages", res); await server.connect(transport); }); app.post("/messages", async (req, res) => { if (!transport) { res.status(400).send("No active SSE connection"); return; } await transport.handlePostMessage(req, res); }); app.listen(PORT, () => { console.error(`Allociné MCP Server running on HTTP SSE at http://localhost:${PORT}/sse`); }); } else { const transport = new stdio_js_1.StdioServerTransport(); server.connect(transport).then(() => { console.error("Allociné MCP Server running on Stdio"); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Racimy/Allocin-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server