Un bot Discord pour les séances de cinéma du jour

Construire une commande /showtimes en Node.js avec discord.js : les films du jour dans une ville française ou américaine, avec cinémas et horaires, en trois petits fichiers.

4 min de lecture

Sur cette page
  1. Ce qu’il vous faut
  2. 1.Préparer le projet
  3. 2.Lire les séances
  4. 3.Déclarer la commande
  5. 4.Le bot
  6. Pour aller plus loin

Un serveur de ciné-club, une bande d’amis, une communauté locale : une commande /showtimes qui répond avec ce qui passe aujourd’hui évite bien des allers-retours entre onglets. Ce tutoriel construit ce bot Discord en Node.js avec discord.js, en trois petits fichiers.

/showtimes city:Lyon version:VOST

Le bot répond par une carte listant les films à l’affiche aujourd’hui, les plus programmés d’abord, chacun avec ses cinémas et ses horaires.

Ce qu’il vous faut

  • Node.js 20 ou plus récent.
  • Une clé API Cinéma : créez un compte gratuit, confirmez votre adresse e-mail, puis créez une clé depuis le tableau de bord. L’offre gratuite donne 150 requêtes par semaine pour les tests et les projets non commerciaux, une par commande.
  • Une application Discord : dans le portail développeur de Discord, créez une application, puis dans Bot copiez son jeton. Dans OAuth2 › URL Generator, cochez bot et applications.commands, puis ouvrez l’URL générée pour ajouter le bot à votre serveur.

Préparer le projet

Terminal
mkdir showtimes-bot && cd showtimes-bot
npm init -y
npm pkg set type=module
npm install discord.js

Les trois secrets vont dans des variables d’environnement, jamais dans le code :

Terminal
export DISCORD_TOKEN="…"        # Bot › Token
export DISCORD_APP_ID="…"       # General Information › Application ID
export API_CINEMA_KEY="ac_live_…"

Lire les séances : showtimes.js

Deux fonctions : l’une appelle l’API, l’autre transforme les séances en champs de carte (embed), un par film.

showtimes.js
const API = 'https://api.api-cinema.com/v1'

/** Les prochaines séances du jour dans une ville (à partir de maintenant, à l’heure locale de chaque cinéma). */
export async function fetchShowtimes(city, version) {
  const params = new URLSearchParams({ city, limit: '500' })
  if (version)
    params.set('version', version)
  const res = await fetch(`${API}/showtimes?${params}`, {
    headers: { Authorization: `Bearer ${process.env.API_CINEMA_KEY}` },
  })
  if (!res.ok)
    throw new Error(`API Cinéma a répondu ${res.status}`)
  const { data } = await res.json()
  // ne garder que le premier jour : l’API donne les séances à venir, celles du jour d’abord
  const day = data[0]?.startsAt.slice(0, 10)
  return data.filter(s => s.startsAt.startsWith(day))
}

/** Un champ par film (le plus programmé d’abord) : chaque cinéma sur une ligne avec ses horaires. */
export function toFields(showtimes, maxFilms = 10) {
  const films = new Map()
  for (const s of showtimes) {
    const film = films.get(s.movie.id) ?? { title: s.movie.title, cinemas: new Map(), count: 0 }
    const times = film.cinemas.get(s.cinema.name) ?? []
    times.push(s.startsAt.slice(11, 16) + (s.version === 'VF' || s.version === 'VO' ? '' : ` ${s.version}`))
    film.cinemas.set(s.cinema.name, times)
    film.count++
    films.set(s.movie.id, film)
  }
  return [...films.values()]
    .sort((a, b) => b.count - a.count)
    .slice(0, maxFilms)
    .map((film) => {
      const lines = [...film.cinemas].map(([cinema, times]) => `**${cinema}** · ${times.join(', ')}`)
      let value = lines.join('\n')
      // un champ contient 1 024 caractères au plus
      if (value.length > 1024)
        value = `${value.slice(0, 1020).replace(/\n[^\n]*$/, '')}\n…`
      return { name: film.title.slice(0, 256), value }
    })
}

Quelques détails :

  • Sans date ni from, l’API renvoie les séances à partir de maintenant, à l’heure locale de chaque cinéma, triées par heure. Garder les lignes de la première date donne « le reste de la journée ».
  • startsAt est l’heure du cinéma (2026-10-24T20:30:00) : slice(11, 16) donne directement 20:30, quel que soit le fuseau du serveur du bot.
  • Les cartes Discord acceptent 25 champs et 1 024 caractères par champ : dix films et une coupe à la dernière ligne complète restent dans les clous.

Déclarer la commande : commands.js

commands.js
import { SlashCommandBuilder } from 'discord.js'

export const showtimesCommand = new SlashCommandBuilder()
  .setName('showtimes')
  .setDescription('Les séances du jour dans une ville')
  .addStringOption(option => option
    .setName('city')
    .setDescription('Ville, par exemple Lyon ou Chicago')
    .setRequired(true))
  .addStringOption(option => option
    .setName('version')
    .setDescription('Une seule version')
    .addChoices(
      { name: 'VOST', value: 'VOST' },
      { name: 'VO', value: 'VO' },
      { name: 'VF', value: 'VF' },
    ))

Enregistrez-la une fois auprès de Discord, dans register.js :

JavaScript
import { REST, Routes } from 'discord.js'
import { showtimesCommand } from './commands.js'

const rest = new REST().setToken(process.env.DISCORD_TOKEN)
await rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID), { body: [showtimesCommand.toJSON()] })
console.log('/showtimes enregistrée')
Terminal
node register.js

Le bot : bot.js

bot.js
import { Client, EmbedBuilder, Events, GatewayIntentBits } from 'discord.js'
import { fetchShowtimes, toFields } from './showtimes.js'

const client = new Client({ intents: [GatewayIntentBits.Guilds] })

client.once(Events.ClientReady, c => console.log(`Connecté en tant que ${c.user.tag}`))

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'showtimes')
    return
  const city = interaction.options.getString('city', true)
  const version = interaction.options.getString('version')
  // l’API répond en bien moins d’une seconde, mais Discord n’attend que 3 s une première réponse
  await interaction.deferReply()
  try {
    const showtimes = await fetchShowtimes(city, version)
    if (showtimes.length === 0) {
      await interaction.editReply(`Aucune séance à venir trouvée à ${city}.`)
      return
    }
    const embed = new EmbedBuilder()
      .setTitle(`Séances à ${city}, ${showtimes[0].startsAt.slice(0, 10)}`)
      .setColor(0xB8302F)
      .addFields(toFields(showtimes))
      .setFooter({ text: 'Données : API Cinéma' })
    await interaction.editReply({ embeds: [embed] })
  }
  catch (error) {
    console.error(error)
    await interaction.editReply('Séances indisponibles pour le moment, réessayez plus tard.')
  }
})

client.login(process.env.DISCORD_TOKEN)
Terminal
node bot.js

Tapez /showtimes city:Lyon dans votre serveur. À Lyon, un après-midi de semaine, la première carte ressemble à ceci :

Les Misérables
Ugc Cine Cite Confluence · 13:30, 15:30, 17:00, 19:00, 20:30
Ugc Cine Cite Part-Dieu · 13:30, 15:30, 17:00, 19:00, 20:30
Pathe Bellecour · 14:05, 17:40, 20:00

Ça marche de la même façon avec une ville américaine : /showtimes city:Chicago.

Pour aller plus loin

  • Noms de villes. city compare exactement le nom de la commune : Paris exclut sa banlieue, New York exclut Brooklyn. Pour toute une agglomération, ajoutez des options lat et lng et appelez l’API avec near=lat,lng&radius_km=5 à la place.
  • Un message quotidien. Plutôt qu’une commande, publiez le programme du lendemain dans un salon chaque soir : appelez fetchShowtimes depuis un setInterval ou une tâche cron, puis channel.send({ embeds: [embed] }).
  • Un cache. Chaque commande coûte une requête. Sur un serveur actif, gardez la réponse de chaque ville dans une Map pendant 15 minutes.
  • Affiches et liens. s.movie.posterUrl donne l’affiche (embed.setThumbnail), s.bookingUrl la page de réservation de chaque séance.

Chaque paramètre et chaque champ sont dans la documentation ; le guide pour la France et le guide pour les États-Unis montrent les autres recherches.