« Ce soir au cinéma » : une newsletter automatique avec n8n, Make ou Zapier

Quatre nœuds n8n envoient chaque jour à 16 h un mail avec les séances du soir d’un cinéma et les liens de réservation, regroupées par film, et rien quand il n’y a rien. JSON importable ; même carte pour Make et Zapier.

6 min de lecture

Sur cette page
  1. Ce qu’il vous faut
  2. 1.La clé en credential
  3. 2.Les quatre nœuds
  4. 3.Ou importez-le
  5. 4.Essayez
  6. Variantes
  7. Le même enchaînement dans Make et Zapier

Chaque jour à 16 h, un mail part : Ce soir : Le Champo, 4 films, avec les horaires et les liens de réservation. Personne ne l’écrit. Ce tutoriel le construit dans n8n avec quatre nœuds, dont un qui contient 25 lignes de JavaScript, et montre le même enchaînement dans Make et Zapier. Le résultat est une newsletter pour un cinéma, un ciné-club ou un journal local ; changez le dernier nœud et c’est un post sur Slack, Discord, Telegram ou Mastodon.

Chaque jour à 16 h ──▶ Séances de ce soir ──▶ Écrire l’e-mail ──▶ Envoyer
   (Schedule)            (HTTP Request)          (Code)          (Send Email)
Objet : Ce soir : Le Champo, 4 films

Le Champo, ce soir vendredi 6 novembre

Affreux, sales et méchants (1 h 55)
  18:10 VOST  https://www.cinema-lechampo.com/film.html#/media/574?showId=34577

Safe (1 h 59)
  19:40 VOST  https://www.cinema-lechampo.com/film.html#/media/1438?showId=34614

Ce qu’il vous faut

  • n8n : n8n Cloud, ou votre propre instance (npx n8n en lance une sur votre ordinateur). Ce tutoriel a tourné sur n8n 2.46.
  • 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. Le forfait gratuit donne 100 requêtes par semaine ; ce workflow en consomme une par jour.
  • Le slug du cinéma. Cherchez-le par son nom :
Terminal
curl "https://api.api-cinema.com/v1/cinemas?q=champo" -H "Authorization: Bearer ac_live_…"

La réponse donne "slug": "le-champo-paris-5e-arrondissement". N’importe quel cinéma de l’API marche : France, États-Unis, Australie.

  • Un compte SMTP pour le mail (celui de votre hébergeur, Brevo, Mailgun, Resend…), ou un compte Gmail avec le nœud Gmail.

La clé en credential

Dans n8n, Credentials › Create › Header Auth. Name : Authorization. Value : Bearer ac_live_… (le mot Bearer, une espace, la clé). Appelez-le Clé API Cinéma. La clé est stockée chiffrée et n’apparaît jamais dans le workflow, qui peut donc être exporté et partagé.

Les quatre nœuds

Chaque jour à 16 h : un Schedule Trigger. Trigger interval Days, tous les 1 jour, à 16:00. Dans les réglages du workflow, mettez le fuseau horaire du cinéma (Europe/Paris pour Le Champo, America/New_York pour un cinéma de Manhattan) : le déclencheur et la date du nœud suivant le suivent.

Séances de ce soir : un HTTP Request. Méthode GET, l’URL en expression (le champ accepte les {{ }} dès qu’il commence par =) :

https://api.api-cinema.com/v1/cinemas/le-champo-paris-5e-arrondissement/showtimes?from={{ $today.toFormat('yyyy-MM-dd') }}T18:00&to={{ $today.toFormat('yyyy-MM-dd') }}&include=movie,cinema&limit=1000

Authentication : Generic Credential Type › Header Auth, puis le credential de l’étape 1. from et to sont lus dans l’heure locale du cinéma : de 18 h à la fin de la journée. include=movie,cinema ajoute le film (titre, durée, affiche) et le cinéma (nom, adresse) à chaque séance, aucune seconde requête n’est nécessaire.

Écrire l’e-mail : un nœud Code. Mode Run Once for All Items, langage JavaScript, avec ce script. Il regroupe les séances par film, construit une version texte et une version HTML, et renvoie un item. Quand il n’y a rien ce soir, il ne renvoie aucun item et n8n s’arrête là : pas de mail vide.

Écrire l’e-mail
// Le programme de ce soir regroupé par film, en un e-mail. Aucun item en sortie quand il n’y a rien : rien n’est envoyé.
const showtimes = $input.first().json.data
if (!showtimes.length)
  return []

const cinema = showtimes[0].cinema
const films = new Map()
for (const s of showtimes) {
  const film = films.get(s.movie.id) ?? { title: s.movie.title, durationMin: s.movie.durationMin, times: [] }
  // startsAt est l’heure locale du cinéma
  film.times.push({ time: s.startsAt.slice(11, 16), label: s.version + (s.format === '2D' ? '' : ` ${s.format}`), url: s.bookingUrl })
  films.set(s.movie.id, film)
}

const date = DateTime.fromISO(showtimes[0].startsAt).setLocale('fr').toFormat('cccc d LLLL')
const runtime = min => `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')}`
const esc = t => String(t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c])

const html = `<h2>${esc(cinema.name)} : ce soir</h2><p>${esc(date)}</p>${[...films.values()].map(f => `
<h3>${esc(f.title)}${f.durationMin ? ` <small>${runtime(f.durationMin)}</small>` : ''}</h3>
<p>${f.times.map(t => `${t.url ? `<a href="${esc(t.url)}">${t.time}</a>` : t.time} ${esc(t.label)}`).join(' · ')}</p>`).join('')}`

const text = `${cinema.name}, ce soir ${date}\n\n${[...films.values()].map(f =>
  `${f.title}${f.durationMin ? ` (${runtime(f.durationMin)})` : ''}\n${f.times.map(t => `  ${t.time} ${t.label}${t.url ? `  ${t.url}` : ''}`).join('\n')}`).join('\n\n')}`

return [{ json: { subject: `Ce soir : ${cinema.name}, ${films.size} film${films.size > 1 ? 's' : ''}`, html, text } }]

DateTime est Luxon, que n8n fournit dans chaque nœud Code ; setLocale('fr') donne vendredi 6 novembre. Titres et liens passent par esc avant d’atterrir dans le HTML, puisqu’ils viennent de l’extérieur.

Envoyer : un nœud Send Email. Votre credential SMTP, les adresses d’expéditeur et de destinataire, puis trois expressions : objet {{ $json.subject }}, format Both, HTML {{ $json.html }}, texte {{ $json.text }}. Dans Options, désactivez Append n8n Attribution. Un nœud Gmail prend les trois mêmes expressions.

Ou importez-le

Copiez ce JSON et collez-le sur un canevas n8n vide (Ctrl+V) : les quatre nœuds apparaissent, reliés. Ouvrez le nœud HTTP et choisissez votre credential Clé API Cinéma, ouvrez le nœud Send Email et choisissez votre SMTP, mettez vos adresses, enregistrez.

workflow.json
{
  "name": "Ce soir au cinéma",
  "settings": {
    "timezone": "Europe/Paris",
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "name": "Chaque jour à 16 h",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 16,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "name": "Séances de ce soir",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.api-cinema.com/v1/cinemas/le-champo-paris-5e-arrondissement/showtimes?from={{ $today.toFormat('yyyy-MM-dd') }}T18:00&to={{ $today.toFormat('yyyy-MM-dd') }}&include=movie,cinema&limit=1000",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "apicinema",
          "name": "Clé API Cinéma"
        }
      }
    },
    {
      "name": "Écrire l’e-mail",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        0
      ],
      "parameters": {
        "jsCode": "// Le programme de ce soir regroupé par film, en un e-mail. Aucun item en sortie quand il n’y a rien : rien n’est envoyé.\nconst showtimes = $input.first().json.data\nif (!showtimes.length)\n  return []\n\nconst cinema = showtimes[0].cinema\nconst films = new Map()\nfor (const s of showtimes) {\n  const film = films.get(s.movie.id) ?? { title: s.movie.title, durationMin: s.movie.durationMin, times: [] }\n  // startsAt est l’heure locale du cinéma\n  film.times.push({ time: s.startsAt.slice(11, 16), label: s.version + (s.format === '2D' ? '' : ` ${s.format}`), url: s.bookingUrl })\n  films.set(s.movie.id, film)\n}\n\nconst date = DateTime.fromISO(showtimes[0].startsAt).setLocale('fr').toFormat('cccc d LLLL')\nconst runtime = min => `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')}`\nconst esc = t => String(t).replace(/[&<>\"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c])\n\nconst html = `<h2>${esc(cinema.name)} : ce soir</h2><p>${esc(date)}</p>${[...films.values()].map(f => `\n<h3>${esc(f.title)}${f.durationMin ? ` <small>${runtime(f.durationMin)}</small>` : ''}</h3>\n<p>${f.times.map(t => `${t.url ? `<a href=\"${esc(t.url)}\">${t.time}</a>` : t.time} ${esc(t.label)}`).join(' · ')}</p>`).join('')}`\n\nconst text = `${cinema.name}, ce soir ${date}\\n\\n${[...films.values()].map(f =>\n  `${f.title}${f.durationMin ? ` (${runtime(f.durationMin)})` : ''}\\n${f.times.map(t => `  ${t.time} ${t.label}${t.url ? `  ${t.url}` : ''}`).join('\\n')}`).join('\\n\\n')}`\n\nreturn [{ json: { subject: `Ce soir : ${cinema.name}, ${films.size} film${films.size > 1 ? 's' : ''}`, html, text } }]"
      }
    },
    {
      "name": "Envoyer",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        720,
        0
      ],
      "parameters": {
        "fromEmail": "cinema@example.org",
        "toEmail": "newsletter@example.org",
        "subject": "={{ $json.subject }}",
        "emailFormat": "both",
        "html": "={{ $json.html }}",
        "text": "={{ $json.text }}",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "smtp": {
          "id": "smtp",
          "name": "SMTP"
        }
      }
    }
  ],
  "connections": {
    "Chaque jour à 16 h": {
      "main": [
        [
          {
            "node": "Séances de ce soir",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Séances de ce soir": {
      "main": [
        [
          {
            "node": "Écrire l’e-mail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Écrire l’e-mail": {
      "main": [
        [
          {
            "node": "Envoyer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Essayez

Execute workflow le lance tout de suite, quelle que soit l’heure : le nœud HTTP montre les séances reçues, le nœud Code le mail écrit, et le mail arrive. Avant 18 h il liste toute la soirée ; tard le soir le nœud Code ne renvoie rien et le nœud Send Email reste gris, ce qui est le résultat attendu. Puis Activate : le workflow tourne chaque jour à 16 h tout seul.

Une requête par jour, sept par semaine : le forfait gratuit les couvre vingt fois, et un cinéma qui fait tourner le même workflow pour chaque salle ou chaque jour de la semaine reste loin de la limite.

Variantes

  • Un post plutôt qu’un mail. Remplacez le dernier nœud par Slack, Discord, Telegram ou Mastodon et donnez-lui {{ $json.text }} : le nœud Code écrit déjà une version texte avec les liens.
  • La semaine, chaque mercredi. Schedule Trigger sur Weeks, mercredi 10:00, et dans l’URL from={{ $today.toFormat('yyyy-MM-dd') }} et to={{ $today.plus({ days: 6 }).toFormat('yyyy-MM-dd') }} (les cinémas français changent de programme le mercredi). Dans le script, regroupez d’abord par s.startsAt.slice(0, 10), puis par film.
  • Tous les cinémas d’une ville. GET /v1/showtimes?city=Lyon&from=…&include=movie,cinema : le même script, avec s.cinema.name à côté de chaque horaire.
  • La VO seulement. Ajoutez &version=VOST (ou VO) à l’URL.

Le même enchaînement dans Make et Zapier

Les deux outils ont les trois mêmes étapes après la planification ; le script ci-dessus demande de petites adaptations à leurs étapes de code, prenez-le comme une carte plutôt qu’un copier-coller.

  • Make. Schedule (chaque jour à 16 h) → HTTP › Make a request (l’URL ci-dessus, construite avec le formatDate(now; "YYYY-MM-DD") de Make, un en-tête Authorization: Bearer ac_live_…, Parse response activé) → Iterator sur data → Text aggregator qui écrit une ligne par séance (startsAt, movie.title, bookingUrl) → Email › Send an email ou Gmail. Make n’a pas d’étape de regroupement : l’agrégateur travaille séance par séance, triées par heure, ce qui se lit bien pour une soirée.
  • Zapier. Schedule by Zapier (chaque jour, 16 h) → Webhooks by Zapier › GET (l’URL, l’en-tête Authorization) → Code by Zapier (JavaScript : inputData contient le corps de la réponse en chaîne, JSON.parse, puis le regroupement du script ci-dessus, et renvoyez { subject, html, text }) → Gmail › Send email ou Email by Zapier. Mettez le fuseau horaire du zap sur celui du cinéma dans ses réglages, pour que 16 h soit le 16 h du cinéma.

Tous les paramètres et champs sont dans la documentation. Pour les mêmes données dans un tableur, le tutoriel Google Sheets en remplit un chaque matin ; pour un bot qui répond aux questions plutôt qu’un mail quotidien, voyez le bot Discord.