Tes films, tes séries, ta musique et tes photos de famille, rangés dans une belle interface façon streaming… mais hébergés chez toi. Dans ce projet, tu construis MediaCloud, un Netflix maison complet avec React, Node.js et MySQL, que tu peux installer sur ton PC ou sur un NAS.
Tu en as marre de chercher tes vidéos dans des dossiers mal rangés, ou de confier tes photos de famille à un service en ligne ? Un Netflix maison règle les deux problèmes : une seule application web, accessible depuis ton ordinateur, ton téléphone ou ta télé, et des fichiers qui ne quittent jamais ton réseau. C’est exactement ce que fait MediaCloud, et tout le code source est disponible gratuitement.

Ce que fait ton Netflix maison MediaCloud
MediaCloud n’est pas une simple démo : c’est une vraie médiathèque personnelle, pensée pour être utilisée tous les jours. Voici ce que tu obtiens à la fin du projet.
S01E05 dans le nom du fichier.




Tu touches à tout ce qu’on demande à un développeur full stack : une base de données relationnelle (clés étrangères, index, relation plusieurs-à-plusieurs), une API REST qui reçoit des fichiers de plusieurs Go et les diffuse en streaming, et une interface React moderne avec états globaux et lecteurs audio/vidéo.
Prérequis
Pas besoin d’un serveur hors de prix : un PC sous Windows suffit pour commencer. Tu pourras ensuite déplacer ton Netflix maison sur un NAS ou un Raspberry Pi.
- Node.js 20 ou plus : il exécute le backend et les outils du frontend (nodejs.org).
- WAMP (ou XAMPP, MAMP, MariaDB…) pour le serveur MySQL.
- Un éditeur de code : VS Code est parfait.
- Un navigateur récent : Chrome, Edge ou Firefox.
Ouvre un terminal et tape node -v : tu dois voir v20.x ou plus. Sinon, installe la version LTS depuis le site officiel.
Architecture du projet
MediaCloud est découpé en deux parties qui communiquent par une API REST. Le frontend (React + Vite + Tailwind CSS) affiche l’interface. Le backend (Express en TypeScript) gère la base MySQL et les fichiers rangés dans un dossier stockage/.
En développement, Vite tourne sur le port 3000 et redirige toutes les adresses /api vers le backend sur le port 4000. En production, un seul port (4000) sert l’application et l’API.
L’arborescence des fichiers
Un monorepo piloté par npm
Le package.json racine utilise les workspaces npm et concurrently : une seule commande lance le backend et le frontend en même temps, avec des couleurs différentes dans le terminal.
{
"name": "media-cloud",
"private": true,
"version": "1.0.0",
"workspaces": [
"frontend",
"backend"
],
"scripts": {
"dev": "concurrently -n backend,frontend -c green,cyan \"npm run dev -w backend\" \"npm run dev -w frontend\"",
"build": "npm run build -w frontend && npm run build -w backend",
"start": "npm run start -w backend",
"typecheck": "npm run typecheck -w frontend && npm run typecheck -w backend",
"db:init": "npm run db:init -w backend"
},
"devDependencies": {
"concurrently": "^8.2.2"
},
"description": "MediaCloud — votre plateforme multimédia personnelle (NAS maison). Développé par LM-Code.",
"author": "LM-Code",
"license": "MIT"
}
package.jsonÉtape 1 : la base de données MySQL
Tout repose sur cinq tables : medias (avec une colonne type en ENUM : film, série, vidéo, musique, photo, document), categories, progressions (où tu t’es arrêté dans chaque vidéo), playlists et playlist_medias pour la relation plusieurs-à-plusieurs.
- Les clés étrangères vers
categoriessont enON DELETE SET NULL: supprimer une catégorie ne supprime pas tes films. - Les autres relations sont en
CASCADE: supprimer un média nettoie sa progression et ses entrées de playlists. - Des index accélèrent les tris et les filtres, et l’interclassement
utf8mb4_unicode_cirend la recherche insensible aux accents.
Au démarrage, le backend crée tout seul la base media_cloud, les tables et les catégories par défaut. Tu lances, et ça marche. Pour réviser les jointures et les clés étrangères, jette un œil à notre tutoriel SQL sur une base de données de bibliothèque.
Étape 2 : le backend Node.js et Express
Le backend utilise Express 5 (qui gère nativement les erreurs des fonctions async), mysql2, multer pour recevoir les fichiers, music-metadata pour lire les tags audio, zod pour valider les données et helmet pour les en-têtes de sécurité.
La configuration avec le fichier .env
Copie backend/.env.example en backend/.env et adapte-le. Avec WAMP, l’utilisateur root n’a pas de mot de passe par défaut ; sur un NAS, crée toujours un utilisateur dédié.
# ─── Serveur ──────────────────────────────────────────────────────────────────
PORT=4000
# Adresse du frontend en développement (pour CORS)
FRONTEND_URL=http://localhost:3000
# ─── Base de données MySQL (WAMP : root sans mot de passe par défaut) ────────
DB_HOTE=127.0.0.1
DB_PORT=3306
DB_UTILISATEUR=root
DB_MOT_DE_PASSE=
DB_NOM=media_cloud
# ─── Stockage des fichiers ────────────────────────────────────────────────────
# Dossier où sont rangés les fichiers envoyés (relatif à la racine du projet,
# ou chemin absolu, ex : D:/MonNAS/medias)
DOSSIER_STOCKAGE=stockage
# Taille maximale d'un fichier envoyé, en Mo (20480 = 20 Go)
TAILLE_MAX_MO=20480
backend/.env.exampleProtéger le dossier de stockage
Chaque fichier est enregistré en base avec un chemin relatif. Avant de toucher au disque, cette fonction vérifie que le chemin reste bien dans le dossier de stockage : impossible de demander ../../windows.
/**
* Transforme un chemin relatif en chemin absolu, en vérifiant qu'il reste
* bien DANS le dossier de stockage (sécurité : empêche "../../windows").
*/
export function cheminAbsolu(relatif: string): string {
const absolu = path.resolve(config.dossierStockage, relatif);
if (!absolu.startsWith(config.dossierStockage + path.sep)) {
throw new Error('Chemin de fichier invalide');
}
return absolu;
}backend/src/stockage.tsLe streaming vidéo avec les requêtes Range
C’est le cœur de ton Netflix maison. res.sendFile gère automatiquement les requêtes Range (réponse 206 Partial Content) : le navigateur peut avancer au milieu d’un film sans télécharger tout le fichier.
router.get('/:id/fichier', async (req, res) => {
const id = lireId(req.params.id);
const media = await selectionnerUn<{ fichier: string; mime: string; nom_original: string }>(
'SELECT fichier, mime, nom_original FROM medias WHERE id = ?',
[id],
);
if (!media) throw new ErreurHttp(404, 'Média introuvable.');
const chemin = cheminAbsolu(media.fichier);
if (req.query.telecharger) {
res.download(chemin, media.nom_original);
return;
}
const entetes: Record<string, string> = { 'Content-Type': media.mime };
// Sécurité : une page HTML ou un SVG envoyé comme document ne doit pas
// pouvoir exécuter de JavaScript dans l'application.
if (/html|svg|xml/.test(media.mime)) entetes['Content-Security-Policy'] = 'sandbox';
res.sendFile(chemin, { headers: entetes });
});backend/src/routes/medias.tsUn fichier HTML ou SVG envoyé comme document pourrait contenir du JavaScript. L’en-tête Content-Security-Policy: sandbox l’empêche de s’exécuter dans l’application.
Étape 3 : le frontend React et TypeScript
L’interface est créée avec npm create vite@latest frontend -- --template react-ts, puis enrichie avec React Router (une vraie adresse par page), Radix UI (fenêtres et menus accessibles au clavier), les icônes Lucide et la police Inter embarquée : l’application fonctionne même sans Internet.
- contexts/ : les états globaux (thème, bibliothèque, lecteur audio/vidéo).
- services/ : tous les appels à l’API, regroupés au même endroit.
- hooks/ : la logique réutilisable, comme
useRequetepour charger des données. - pages/ : une page = un fichier (accueil, films, séries, musique, photos…).
Si tu découvres la mise en page d’une plateforme vidéo, notre interface YouTube en HTML et CSS est un bon échauffement avant React.
Des miniatures générées dans le navigateur
Astuce maligne : les miniatures sont créées avant l’envoi, directement dans le navigateur. Pour une vidéo, on se place à 10 % du film et on capture l’image avec un <canvas>. Résultat : aucun logiciel comme ffmpeg à installer sur le serveur.
/** Analyse une vidéo : durée + image d'aperçu. */
export async function analyserVideo(
fichier: File,
): Promise<{ miniature: Blob | null; duree: number | null }> {
const url = URL.createObjectURL(fichier);
const video = document.createElement('video');
video.preload = 'metadata';
video.muted = true;
video.playsInline = true;
video.src = url;
try {
await attendre(video, 'loadedmetadata');
const duree = Number.isFinite(video.duration) ? video.duration : null;
// On se place à 10 % de la vidéo (max 5 min) pour éviter un écran noir
video.currentTime = duree ? Math.min(duree * 0.1, 300) : 1;
await attendre(video, 'seeked');
const miniature = await versJpeg(video, video.videoWidth, video.videoHeight);
return { miniature, duree: duree ? Math.round(duree) : null };
} catch {
// Format que le navigateur ne sait pas lire (certains .mkv, .avi…)
return { miniature: null, duree: null };
} finally {
video.removeAttribute('src');
URL.revokeObjectURL(url);
}
}frontend/src/lib/miniatures.ts
Les lecteurs vidéo et musique
Le lecteur vidéo enregistre régulièrement ta position dans la table progressions : tu reprends exactement là où tu t’étais arrêté, même sur un autre appareil. À la fin d’un épisode, un compte à rebours lance le suivant. Le lecteur musique reste affiché en bas de l’écran et utilise l’API Media Session pour réagir aux touches multimédia du clavier.
Tu veux comprendre les bases d’un lecteur audio avant d’attaquer celui-ci ? Regarde notre lecteur MP3 en Python.



Étape 4 : lancer MediaCloud
Démarre WAMP (icône verte), installe les dépendances une seule fois, puis lance tout avec une seule commande depuis la racine du projet :
git clone https://github.com/LM-Code-Be/media-cloud.git
cd media-cloud
npm install
npm run devTerminalOuvre http://localhost:3000 : l’écran de bienvenue t’invite à envoyer tes premiers fichiers. Pour y accéder depuis ton téléphone, connecte-le au même Wi-Fi et utilise l’adresse IP locale de ton PC (par exemple http://192.168.1.20:3000). Si rien ne s’affiche, autorise Node.js dans le pare-feu Windows.
Étape 5 : passer en production sur un PC ou un NAS
npm run build # frontend → frontend/dist, backend → backend/dist
npm start # http://localhost:4000TerminalPour stocker tes médias sur un autre disque, change simplement DOSSIER_STOCKAGE dans le .env (par exemple E:/MediaCloud sous Windows ou /volume1/medias sur un NAS).
Sur un vrai NAS (Synology, QNAP, Raspberry Pi…), installe Node.js et MariaDB, crée un utilisateur MariaDB dédié plutôt que root, puis utilise PM2 pour que MediaCloud redémarre tout seul après une coupure de courant :
npm install -g pm2
pm2 start npm --name mediacloud -- start
pm2 save
pm2 startupTerminalTu préfères tout emballer dans des conteneurs ? Notre guide Docker en local te montre les bases de docker compose.
MediaCloud n’a pas de comptes ni de mots de passe : il est conçu pour la maison. N’ouvre pas son port sur Internet (pas de redirection de port sur ta box). Pour y accéder à distance, passe par un VPN comme WireGuard, Tailscale ou le VPN intégré à ton NAS.
Ce qui est déjà en place côté sécurité : validation de toutes les données avec Zod, requêtes SQL paramétrées (et liste blanche pour les tris), impossibilité de sortir du dossier de stockage, documents HTML/SVG isolés en sandbox, en-têtes Helmet, et un .env jamais publié sur GitHub.
Formats vidéo et audio : ce qu’il faut savoir
C’est le navigateur qui lit les fichiers : MediaCloud ne les convertit pas, ce qui demanderait un processeur puissant. Voici ce qui fonctionne.
| Format | Lecture dans le navigateur |
|---|---|
| MP4 (H.264 + AAC) | ✅ Partout, le format idéal |
| WebM (VP9 / AV1) | ✅ Chrome, Edge, Firefox |
| MKV | ⚠️ Souvent dans Chrome/Edge si la vidéo est en H.264 |
| HEVC / H.265 | ⚠️ Selon le matériel et le navigateur |
| AVI, WMV | ❌ Rarement |
| MP3, AAC/M4A, FLAC, OGG, WAV | ✅ |
Elle reste stockée et téléchargeable (pour VLC). Pour la rendre lisible en ligne, convertis-la une fois pour toutes en MP4 H.264 avec HandBrake (gratuit), préréglage « Fast 1080p30 ».
Dépannage
| Problème | Solution |
|---|---|
connect ECONNREFUSED (MySQL) | WAMP n’est pas démarré, ou le port n’est pas le bon (DB_PORT). MySQL et MariaDB peuvent tourner sur 3306 et 3307. |
Access denied for user root | Un mot de passe est défini : renseigne DB_MOT_DE_PASSE. |
EADDRINUSE sur le port 4000 | Change PORT dans le .env et l’adresse du proxy dans vite.config.ts. |
| « Serveur injoignable » | Le backend n’est pas lancé ou a planté : regarde le terminal. |
| « Le fichier est trop volumineux » | Augmente TAILLE_MAX_MO dans le .env. |
| Pas de miniature pour une vidéo | Le navigateur ne sait pas décoder ce format : ajoute une image via « Modifier ». |
| Les accents s’affichent mal | La base doit être en utf8mb4 (c’est le cas si MediaCloud l’a créée). |
| Le téléphone n’accède pas au site | Même Wi-Fi ? Pare-feu Windows ? |
Aller plus loin
Le code est volontairement simple à lire, ce qui en fait une excellente base pour tes propres idées :
- Sous-titres : accepter des fichiers
.vttet les ajouter avec une balise<track>. - Pagination infinie : charger les médias par paquets de 50 avec
LIMIT/OFFSET. - Import d’un dossier existant sans renvoyer chaque fichier un par un.
- Comptes utilisateurs : mots de passe hachés avec bcrypt, jeton JWT et progression par utilisateur.
- Application installable (PWA) avec un manifest et un service worker.
- Conversion automatique des MKV/AVI en MP4 avec ffmpeg.
- Docker : un
docker-compose.ymlavec MediaCloud + MariaDB, installable en une commande.
Envie d’un autre projet full stack avec base de données ? Essaie notre application de gestion de stock en PHP et MySQL.
FAQ
MediaCloud est-il gratuit ?
Oui. Le code source est disponible gratuitement sur GitHub sous licence MIT : tu peux l’utiliser, le modifier et le partager.
Faut-il un NAS pour créer un Netflix maison ?
Non. Un simple PC sous Windows avec Node.js et WAMP suffit. Un NAS ou un Raspberry Pi est pratique si tu veux un serveur allumé en permanence.
Puis-je regarder mes films sur mon téléphone ou ma télé ?
Oui, l’interface est responsive. Il suffit d’ouvrir l’adresse IP locale du serveur dans le navigateur d’un appareil connecté au même réseau.
Est-ce que MediaCloud convertit les vidéos ?
Non, le navigateur lit directement les fichiers. Le MP4 en H.264 fonctionne partout ; pour les autres formats, convertis-les avec HandBrake.
Puis-je accéder à MediaCloud depuis l’extérieur ?
Oui, mais uniquement via un VPN comme WireGuard ou Tailscale. N’ouvre jamais son port sur Internet : l’application n’a pas de comptes utilisateurs.
MediaCloud fonctionne-t-il avec MariaDB ?
Oui. MariaDB est compatible MySQL : c’est d’ailleurs le choix conseillé sur un NAS.
Conclusion
Tu as maintenant tout ce qu’il faut pour monter ton propre Netflix maison : une base MySQL bien pensée, une API Express sécurisée capable de diffuser des vidéos en streaming, et une interface React moderne avec lecteurs complets et miniatures automatiques. Et surtout, tes films, tes photos de famille et tes documents restent chez toi.
Clone le projet, installe-le, puis améliore-le à ta façon. Si tu ajoutes les sous-titres ou les comptes utilisateurs, n’hésite pas à partager ton travail !
