DayWatch

Documentation

📦 Stack : Node.js · Express · Supabase Postgres · Supabase Auth · Knex · Radarr / Sonarr / TMDB · IPTV
🆕 Migration mai 2026 : MySQL → Supabase. L'auth bcrypt+JWT custom est remplacée par Supabase Auth (JWT). Voir docs/SUPABASE_MIGRATION.md.
🗄️ Modèle de données : MCD complet dans docs/DATABASE_SCHEMA.md (21 tables, vue d'ensemble + 5 sous-domaines).
🔐 Base URL en local : http://localhost:5000. Toutes les routes protégées attendent Authorization: Bearer <supabase_access_token>.

Authentification & Système de Profils

L'API utilise Supabase Auth (JWT). Les sessions sont créées via signInWithPassword et le serveur vérifie le token via supabase.auth.getUser().

🎭 Système de profils & appareils
Chaque utilisateur peut créer plusieurs profils (limite définie par le plan : maxProfiles).
Chaque connexion enregistre l'appareil utilisé. Au-delà de plan.maxDevices appareils connectés simultanément, le login renvoie 403 MAX_DEVICES_REACHED.
POST

/api/users/login

Authentifie l'utilisateur via Supabase Auth. Vérifie la limite d'appareils du plan actif puis retourne accessToken, refreshToken et les détails du plan.

Paramètres

Nom Type Description Requis
email String Email de l'utilisateur Oui
password String Mot de passe Oui
deviceId String UUID unique généré côté client (persistant) Recommandé
deviceName String Nom lisible : "Mon iPhone Pro", "Smart TV Salon" Non
deviceType String Desktop, Mobile, Smart TV, Tablet… Non
operatingSystem String iOS 18, Windows 11, Android 14… Non
appVersion String Version de l'app cliente, ex: 1.0.3 Non

Réponse

{
  "success": true,
  "message": "Connexion réussie",
  "data": {
    "userId": "0fe28a3c-…-uuid",
    "username": "alice",
    "email": "alice@example.com",
    "role": "USER",
    "session": {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
      "refreshToken": "v1.MR…",
      "expiresIn": 3600,
      "expiresAt": 1715450000
    },
    "plan": {
      "id": 4,
      "name": "Premium",
      "source": "subscription",
      "maxProfiles": 5,
      "maxDevices": 4,
      "maxConcurrentStreams": 2,
      "features": {
        "hasMovies": true,
        "hasShows": true,
        "hasIPTV": true,
        "allowDownloads": true,
        "hasAds": false
      }
    },
    "profiles": {
      "count": 3,
      "data": [ { "id": 1, "profileName": "Alice", "profileType": "adult", "isDefault": true }, … ]
    }
  }
}
                                

Erreur 403 — Limite d'appareils

{
  "success": false,
  "message": "Limite d'appareils connectés atteinte (4 max pour le plan Premium). Déconnectez un appareil pour continuer.",
  "code": "MAX_DEVICES_REACHED",
  "limit": 4,
  "current": 4
}
                                
POST

/api/users/register

Enregistre un nouvel utilisateur dans le système.

Paramètres

Nom Type Description Requis
username String Nom d'utilisateur Oui
email String Email de l'utilisateur Oui
password String Mot de passe (min. 8 caractères) Oui
phoneNumber String Numéro de téléphone Non
POST

/api/users/verify

Vérifie le compte d'un utilisateur à l'aide d'un token de vérification.

Paramètres

Nom Type Description Requis
token String Token de vérification Oui

Exemple d'utilisation du token Supabase


// JavaScript - Login puis requête authentifiée
const login = async () => {
  const res = await fetch('http://localhost:5000/api/users/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'alice@example.com',
      password: 'Password123!',
      deviceId: crypto.randomUUID(),          // à persister côté client
      deviceName: 'Chrome Desktop',
      deviceType: 'Desktop',
      operatingSystem: navigator.platform,
      appVersion: '1.0.0'
    })
  });
  const { data } = await res.json();
  localStorage.setItem('accessToken', data.session.accessToken);
  localStorage.setItem('refreshToken', data.session.refreshToken);
  return data;
};

const fetchMovies = async () => {
  const token = localStorage.getItem('accessToken');
  const res = await fetch('http://localhost:5000/api/movies', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  return res.json();
};

// Refresh quand le token expire
const refresh = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  const res = await fetch('http://localhost:5000/api/users/refresh-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken })
  });
  const { data } = await res.json();
  localStorage.setItem('accessToken', data.accessToken);
  localStorage.setItem('refreshToken', data.refreshToken);
};
                        

Schéma de la base de données — MCD

Modèle Conceptuel de Données de Supabase Postgres. Bascule entre les vues pour explorer les 21 tables et leurs relations.

erDiagram
    Users ||--o{ Profiles : "1:N"
    Users ||--o{ UserDevices : "1:N"
    Users ||--o{ OTP : "1:N"
    Users ||--o| Admin : "optionnel"
    Users ||--o{ Subscriptions : "1:N"
    Users ||--o{ Payments : "1:N"
    Plans ||--o{ Subscriptions : "1:N"
    Payments ||--o| Subscriptions : "optionnel"
    Users ||--o{ MovieWatchHistory : "1:N"
    Users ||--o{ EpisodeWatchHistory : "1:N"
    Profiles ||--o{ MovieWatchHistory : "1:N"
    Profiles ||--o{ EpisodeWatchHistory : "1:N"
    Users ||--o{ FavoritesMovies : "1:N"
    Users ||--o{ FavoritesShows : "1:N"
    Users ||--o{ FavoritesChannels : "1:N"
    Users ||--o{ MovieComments : "1:N"
    Users ||--o{ ShowComments : "1:N"
    TVShows ||--o{ Seasons : "1:N"
    Seasons ||--o{ Episodes : "1:N"
    Movies ||--o{ MovieWatchHistory : "1:N"
    Movies ||--o{ FavoritesMovies : "1:N"
    Movies ||--o{ MovieComments : "1:N"
    TVShows ||--o{ FavoritesShows : "1:N"
    TVShows ||--o{ ShowComments : "1:N"
    Episodes ||--o{ EpisodeWatchHistory : "1:N"
    IPTVChannels ||--o{ FavoritesChannels : "1:N"

    Users {
        uuid id PK
        string username UK
        string email UK
        string phoneNumber UK
        text profilePictureUrl
        timestamptz trialStartDate
        timestamptz trialEndDate
        timestamptz lastLoginDate
        string lastLoginIp
        string status
        string role
    }
    Profiles {
        bigint id PK
        uuid userID FK
        string profileName
        text profileAvatarUrl
        string profileType
        jsonb preferences
        bool isDefault
        bool isActive
    }
    UserDevices {
        bigint id PK
        uuid userId FK
        string deviceId
        string deviceName
        string deviceType
        string operatingSystem
        string appVersion
        string ipAddress
        bool isConnected
        timestamptz lastActive
    }
    OTP {
        bigint id PK
        uuid userId FK
        string code
        string purpose
        timestamptz expiresAt
        bool isUsed
    }
    Admin {
        bigint id PK
        uuid userId FK,UK
        string role
    }
    Plans {
        bigint id PK
        string name UK
        text description
        numeric price
        int duration
        int maxProfiles
        int maxDevices
        int maxConcurrentStreams
        bool hasMovies
        bool hasShows
        bool hasIPTV
        bool allowDownloads
        bool hasAds
        bool isActive
    }
    Subscriptions {
        bigint id PK
        uuid userId FK
        bigint planId FK
        bigint paymentId FK
        bool autoRenew
        string status
        timestamptz startDate
        timestamptz endDate
    }
    Payments {
        bigint id PK
        uuid userId FK
        numeric amount
        string status
        string paymentMethod
        text transactionId
        timestamptz transactionDate
    }
    Movies {
        bigint id PK
        string tmdbId UK
        string title
        text overview
        text posterPath
        int year
        numeric rating
        string quality
        text videoPath
        bool isAvailable
    }
    TVShows {
        bigint id PK
        string tmdbId UK
        string title
        text overview
        text posterPath
        int year
        numeric rating
        bool isAvailable
    }
    Seasons {
        bigint id PK
        bigint showId FK
        int seasonNumber
        string title
        text overview
    }
    Episodes {
        bigint id PK
        bigint seasonId FK
        int episodeNumber
        string title
        text videoPath
        bool isAvailable
    }
    MovieWatchHistory {
        bigint id PK
        uuid userId FK
        bigint profileId FK
        bigint movieId FK
        int lastWatchedPosition
        bool isCompleted
        timestamptz lastWatchedAt
    }
    EpisodeWatchHistory {
        bigint id PK
        uuid userId FK
        bigint profileId FK
        bigint episodeId FK
        int lastWatchedPosition
        bool isCompleted
        timestamptz lastWatchedAt
    }
    FavoritesMovies {
        bigint id PK
        uuid userId FK
        bigint movieId FK
    }
    FavoritesShows {
        bigint id PK
        uuid userId FK
        bigint showId FK
    }
    FavoritesChannels {
        bigint id PK
        uuid userId FK
        bigint channelId FK
    }
    MovieComments {
        bigint id PK
        uuid userId FK
        bigint movieId FK
        text comment
        smallint rating
    }
    ShowComments {
        bigint id PK
        uuid userId FK
        bigint showId FK
        text comment
        smallint rating
    }
    IPTVChannels {
        bigint id PK
        string channelName
        text streamUrl
        text logoUrl
        text epgUrl
        bool isActive
    }
    LiveEvents {
        bigint id PK
        string title
        string category
        timestamptz eventTime
        jsonb streamLinks
        bool isLive
        string quality
    }
                        
erDiagram
    auth_users ||--|| Users : "trigger SQL"
    Users ||--o{ Profiles : "possède (1:N)"
    Users ||--o{ UserDevices : "utilise (1:N)"
    Users ||--o{ OTP : "reçoit (1:N)"
    Users ||--o| Admin : "joue rôle (optionnel)"

    auth_users {
        uuid id PK
        string email
        jsonb raw_user_meta_data
    }
    Users {
        uuid id PK,FK
        string username UK
        string email UK
        string phoneNumber UK
        timestamptz trialStartDate
        timestamptz trialEndDate
        timestamptz lastLoginDate
        string lastLoginIp
        string status
        string role
    }
    Profiles {
        bigint id PK
        uuid userID FK
        string profileName
        string profileType
        jsonb preferences
        bool isDefault
        bool isActive
    }
    UserDevices {
        bigint id PK
        uuid userId FK
        string deviceId
        string deviceName
        string deviceType
        string ipAddress
        bool isConnected
        timestamptz lastActive
    }
    OTP {
        bigint id PK
        uuid userId FK
        string code
        string purpose
        timestamptz expiresAt
        bool isUsed
    }
    Admin {
        bigint id PK
        uuid userId FK,UK
        string role
    }
                        
erDiagram
    Users ||--o{ Subscriptions : "souscrit (1:N)"
    Users ||--o{ Payments : "paie (1:N)"
    Plans ||--o{ Subscriptions : "définit (1:N)"
    Payments ||--o| Subscriptions : "finance (optionnel)"

    Users {
        uuid id PK
        string username
        string email
    }
    Plans {
        bigint id PK
        string name UK
        numeric price
        int duration
        int maxProfiles
        int maxDevices
        int maxConcurrentStreams
        bool hasMovies
        bool hasIPTV
        bool allowDownloads
        bool hasAds
    }
    Subscriptions {
        bigint id PK
        uuid userId FK
        bigint planId FK
        bigint paymentId FK
        bool autoRenew
        string status
        timestamptz startDate
        timestamptz endDate
    }
    Payments {
        bigint id PK
        uuid userId FK
        numeric amount
        string status
        string paymentMethod
        text transactionId
        timestamptz transactionDate
    }
                        
erDiagram
    TVShows ||--o{ Seasons : "contient (1:N)"
    Seasons ||--o{ Episodes : "compose (1:N)"

    Movies {
        bigint id PK
        string tmdbId UK
        string title
        text overview
        text posterPath
        text backdropPath
        int year
        numeric rating
        string quality
        text videoPath
        bool isAvailable
    }
    TVShows {
        bigint id PK
        string tmdbId UK
        string title
        text overview
        text posterPath
        text backdropPath
        int year
        numeric rating
        bool isAvailable
    }
    Seasons {
        bigint id PK
        bigint showId FK
        int seasonNumber
        string title
        text overview
        text posterPath
    }
    Episodes {
        bigint id PK
        bigint seasonId FK
        int episodeNumber
        string title
        text overview
        text stillPath
        text videoPath
        bool isAvailable
    }
                        
erDiagram
    Users ||--o{ MovieWatchHistory : "regarde N:N"
    Users ||--o{ EpisodeWatchHistory : "regarde N:N"
    Profiles ||--o{ MovieWatchHistory : "par profil"
    Profiles ||--o{ EpisodeWatchHistory : "par profil"
    Movies ||--o{ MovieWatchHistory : ""
    Episodes ||--o{ EpisodeWatchHistory : ""
    Users ||--o{ FavoritesMovies : "favoris N:N"
    Movies ||--o{ FavoritesMovies : ""
    Users ||--o{ FavoritesShows : "favoris N:N"
    TVShows ||--o{ FavoritesShows : ""
    Users ||--o{ FavoritesChannels : "favoris N:N"
    IPTVChannels ||--o{ FavoritesChannels : ""
    Users ||--o{ MovieComments : "commente N:N"
    Movies ||--o{ MovieComments : ""
    Users ||--o{ ShowComments : "commente N:N"
    TVShows ||--o{ ShowComments : ""

    MovieWatchHistory {
        bigint id PK
        uuid userId FK
        bigint profileId FK
        bigint movieId FK
        int lastWatchedPosition
        bool isCompleted
        timestamptz lastWatchedAt
    }
    EpisodeWatchHistory {
        bigint id PK
        uuid userId FK
        bigint profileId FK
        bigint episodeId FK
        int lastWatchedPosition
        bool isCompleted
        timestamptz lastWatchedAt
    }
    FavoritesMovies {
        bigint id PK
        uuid userId FK
        bigint movieId FK
    }
    FavoritesShows {
        bigint id PK
        uuid userId FK
        bigint showId FK
    }
    FavoritesChannels {
        bigint id PK
        uuid userId FK
        bigint channelId FK
    }
    MovieComments {
        bigint id PK
        uuid userId FK
        bigint movieId FK
        text comment
        smallint rating
    }
    ShowComments {
        bigint id PK
        uuid userId FK
        bigint showId FK
        text comment
        smallint rating
    }
                        
flowchart LR
    U[Users]
    PR[Profiles]
    DEV[UserDevices]
    PL[Plans]
    SUB[Subscriptions]
    PAY[Payments]
    MO[Movies]
    TV[TVShows]
    SE[Seasons]
    EP[Episodes]
    IPTV[IPTVChannels]

    A1{possède}
    A2{utilise}
    A3{souscrit}
    A4{définit}
    A5{paie}
    A6{contient}
    A7{compose}
    A8{regarde film}
    A9{regarde épisode}
    A10{favori film}
    A11{favori série}
    A12{favori chaîne}

    U ---|"(1,1)"| A1
    A1 ---|"(0,N)"| PR
    U ---|"(1,1)"| A2
    A2 ---|"(0,N)"| DEV
    U ---|"(1,1)"| A3
    A3 ---|"(0,N)"| SUB
    PL ---|"(1,1)"| A4
    A4 ---|"(0,N)"| SUB
    U ---|"(1,1)"| A5
    A5 ---|"(0,N)"| PAY
    TV ---|"(1,1)"| A6
    A6 ---|"(0,N)"| SE
    SE ---|"(1,1)"| A7
    A7 ---|"(0,N)"| EP
    U ---|"(0,N)"| A8
    A8 ---|"(0,N)"| MO
    U ---|"(0,N)"| A9
    A9 ---|"(0,N)"| EP
    U ---|"(0,N)"| A10
    A10 ---|"(0,N)"| MO
    U ---|"(0,N)"| A11
    A11 ---|"(0,N)"| TV
    U ---|"(0,N)"| A12
    A12 ---|"(0,N)"| IPTV

    classDef entity fill:#1f1f1f,stroke:#e50914,color:#fff,stroke-width:2px
    classDef assoc fill:#c0504d,stroke:#8c3a37,color:#fff
    class U,PR,DEV,PL,SUB,PAY,MO,TV,SE,EP,IPTV entity
    class A1,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11,A12 assoc
                        
Auth/User — Users, Profiles, OTP, Admin, Devices
Billing — Plans, Subscriptions, Payments
Catalogue — Movies, TVShows, Seasons, Episodes, IPTV
Engagement — History, Favorites, Comments
1:N — un parent, plusieurs enfants
N:N — via table de jonction (FavoritesMovies, WatchHistory…)
📖 Doc détaillée : Le fichier docs/DATABASE_SCHEMA.md contient la version complète du MCD Merise (entités · associations · cardinalités min,max) ainsi que le tableau récap des contraintes UNIQUE et ON DELETE.

Système de Profils

Plusieurs profils par compte, chacun avec ses préférences et son historique. Le nombre maximum dépend du plan d'abonnement (maxProfiles : 1 pour Basic, 2 pour Trial, 3 pour Standard, 5 pour Premium/Family).

Fonctionnalités

INFO

Création Automatique

Un profil par défaut est créé automatiquement à l'inscription (trigger SQL on_auth_user_created côté Supabase + creation du Profile par défaut côté API).

INFO

Types de Profils

Deux types disponibles : adult et child. Le type child peut activer un contrôle parental via la clé preferences.matureContent: false.

INFO

Préférences Personnalisées

Chaque profil peut avoir ses propres préférences : langue, qualité vidéo, sous-titres, etc.

INFO

Profil par Défaut

Un seul profil peut être marqué comme défaut par utilisateur. Le profil par défaut est utilisé pour les nouvelles sessions.

Exemple d'utilisation du système de profils

Connexion et récupération des profils


// 1. Connexion utilisateur
const loginResponse = await fetch('/api/users/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'password123'
  })
});

const loginData = await loginResponse.json();

// 2. Récupération des profils dans la réponse
const { token, profiles } = loginData.data;

console.log('Token JWT:', token);
console.log('Nombre de profils:', profiles.count);
console.log('Profils disponibles:', profiles.data);

// 3. Affichage des profils pour sélection
profiles.data.forEach(profile => {
  console.log(`- ${profile.profileName} (${profile.profileType})`);
  if (profile.isDefault) console.log('  → Profil par défaut');
});

// 4. Stockage du profil sélectionné
localStorage.setItem('selectedProfileId', profiles.data[0].id);
localStorage.setItem('jwt_token', token);
                                

Gestion des profils (CRUD)


// Configuration de base
const API_BASE = 'http://localhost:5000/api';
const token = localStorage.getItem('jwt_token');

// Récupérer tous les profils
const getProfiles = async () => {
  const response = await fetch(`${API_BASE}/profiles`, {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  return response.json();
};

// Créer un nouveau profil
const createProfile = async (profileData) => {
  const response = await fetch(`${API_BASE}/profiles`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      profileName: 'Kids Profile',
      profileType: 'child',
      preferences: {
        contentRating: 'G',
        language: 'fr',
        autoplay: false
      }
    })
  });
  return response.json();
};

// Mettre à jour un profil
const updateProfile = async (profileId, updates) => {
  const response = await fetch(`${API_BASE}/profiles/${profileId}`, {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updates)
  });
  return response.json();
};

// Définir le profil par défaut
const setDefaultProfile = async (profileId) => {
  const response = await fetch(`${API_BASE}/profiles/${profileId}/default`, {
    method: 'PATCH',
    headers: { 'Authorization': `Bearer ${token}` }
  });
  return response.json();
};

// Supprimer un profil
const deleteProfile = async (profileId) => {
  const response = await fetch(`${API_BASE}/profiles/${profileId}`, {
    method: 'DELETE',
    headers: { 'Authorization': `Bearer ${token}` }
  });
  return response.json();
};

// Exemple d'utilisation
async function manageProfiles() {
  try {
    // Récupérer les profils existants
    const profiles = await getProfiles();
    console.log('Profils existants:', profiles);

    // Créer un nouveau profil enfant
    const newProfile = await createProfile({
      profileName: 'Kids',
      profileType: 'child'
    });
    console.log('Nouveau profil créé:', newProfile);

    // Mettre à jour les préférences
    await updateProfile(newProfile.id, {
      preferences: {
        contentRating: 'G',
        language: 'fr',
        autoplay: false,
        subtitles: true
      }
    });

  } catch (error) {
    console.error('Erreur:', error);
  }
}
                                

Endpoints API

L'API DayWatch expose 198 endpoints répartis en 29 groupes. Toutes les réponses sont en JSON. Authentification via header Authorization: Bearer <jwt>.

Films (9)

GET

/api/movies

Liste paginée de tous les films disponibles.

GET

/api/movies/:id

Détails d'un film par son ID (Radarr ou tmdb_XXXXX).

GET

/api/movies/coming-soon

Films bientôt disponibles.

GET

/api/movies/popular

Films populaires (basé sur TMDB).

GET

/api/movies/recent

Films récemment ajoutés à la médiathèque.

GET

/api/movies/recent-additions

Films récemment importés (version étendue).

GET

/api/movies/recent-additions/essentials

Recent additions — version allégée (champs essentiels).

GET

/api/movies/top-recommendations

Top recommandations personnalisées.

GET

/api/movies/top-recommendations/essentials

Top recommandations — version allégée.

Radarr (films détaillés, acteurs, plateformes) (26)

GET

/api/radarr/actors/:id

Détail d'un acteur.

GET

/api/radarr/actors/:id/credits

Filmographie d'un acteur.

GET

/api/radarr/actors/popular

Acteurs populaires.

GET

/api/radarr/actors/popular/essentials

Acteurs populaires — champs essentiels.

GET

/api/radarr/movies

Tous les films via Radarr.

GET

/api/radarr/movies/:id

Détail d'un film Radarr.

GET

/api/radarr/movies/:id/credits

Casting & équipe.

GET

/api/radarr/movies/:id/file

Métadonnées du fichier vidéo (codec, taille).

GET

/api/radarr/movies/:id/images

Affiches, fonds, logos.

GET

/api/radarr/movies/:id/stream

Stream HLS du film.

GET

/api/radarr/movies/:id/subtitle/:lang

Sous-titres par langue (.srt/.vtt).

GET

/api/radarr/movies/:id/subtitle/embedded/:subIndex

Sous-titres embedded (index dans le MKV).

GET

/api/radarr/movies/:id/subtitles

Sous-titres disponibles (FS local + Bazarr).

GET

/api/radarr/movies/:id/tracks

Pistes audio disponibles.

GET

/api/radarr/movies/:id/videos

Bandes-annonces & extraits.

GET

/api/radarr/movies/boxoffice

Films actuellement au box-office.

GET

/api/radarr/movies/boxoffice/essentials

Box-office — champs essentiels.

GET

/api/radarr/movies/essentials

Liste films — champs essentiels (perf).

GET

/api/radarr/movies/popular

Films populaires (Radarr).

GET

/api/radarr/movies/popular/essentials

Films populaires — champs essentiels.

GET

/api/radarr/movies/recent

Films récents (Radarr).

GET

/api/radarr/movies/recent/essentials

Films récents — champs essentiels.

GET

/api/radarr/movies/stats

Statistiques globales films.

GET

/api/radarr/movies/upcoming

Films à venir.

GET

/api/radarr/platforms

Plateformes streaming disponibles.

GET

/api/radarr/platforms/:platform/movies

Films par plateforme.

Séries (18)

GET

/api/series

Liste paginée de toutes les séries.

GET

/api/series/:id

Détail d'une série.

GET

/api/series/:id/credits

Casting de la série.

GET

/api/series/:id/episodes

Tous les épisodes.

GET

/api/series/:id/episodes-with-files

Épisodes avec leurs fichiers vidéo.

GET

/api/series/:id/images

Affiches et images.

GET

/api/series/:id/seasons

Liste des saisons.

GET

/api/series/:id/seasons/:seasonNumber

Détail d'une saison.

GET

/api/series/:id/videos

Trailers de la série.

GET

/api/series/anime

Animés japonais.

GET

/api/series/anime/upcoming

Animés à venir.

GET

/api/series/k-drama

Séries coréennes (K-Drama).

GET

/api/series/platforms/:platform

Séries par plateforme (Netflix, Disney+, etc.).

GET

/api/series/popular

Séries populaires.

GET

/api/series/recent

Séries récemment ajoutées.

GET

/api/series/recommended

Séries recommandées.

GET

/api/series/stats

Statistiques séries.

GET

/api/series/upcoming

Séries à venir.

Sonarr (séries détaillées, épisodes) (18)

GET

/api/sonarr/series

Liste séries (Sonarr).

GET

/api/sonarr/series/:id

Détail série Sonarr.

GET

/api/sonarr/series/:id/episodes

Épisodes d'une série.

GET

/api/sonarr/series/:id/episodes-with-files

Épisodes + fichiers.

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/stream

Stream HLS de l'épisode.

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/subtitle/:lang

Sous-titres par langue.

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/subtitle/embedded/:subIndex

Sous-titres embedded.

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/subtitles

Sous-titres d'un épisode.

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/timestamps

Timestamps intro/credits (Jellyfin).

GET

/api/sonarr/series/:id/seasons/:seasonNumber/episodes/:episodeNumber/tracks

Pistes audio d'un épisode.

GET

/api/sonarr/series/essentials

Liste séries — champs essentiels.

GET

/api/sonarr/series/popular

Séries populaires (Sonarr).

GET

/api/sonarr/series/popular/essentials

Séries populaires — champs essentiels.

GET

/api/sonarr/series/recent

Séries récentes.

GET

/api/sonarr/series/recent/essentials

Séries récentes — champs essentiels.

GET

/api/sonarr/series/recommended/essentials

Recommandations — champs essentiels.

GET

/api/sonarr/series/stats

Stats Sonarr.

GET

/api/sonarr/series/upcoming

Séries à venir.

Bandes-annonces (2)

GET

/api/trailers/recent

Bandes-annonces récentes.

GET

/api/trailers/upcoming

Bandes-annonces des films à venir.

Acteurs (2)

GET

/api/actors

Liste générique des acteurs.

GET

/api/actors/:actorId

Détail acteur.

Plateformes de streaming (2)

GET

/api/platforms

Plateformes streaming.

GET

/api/platforms/:id

Détail plateforme.

Recherche universelle (1)

GET

/api/search

Recherche universelle (films, séries, acteurs).

Sports — Big games (3)

GET

/api/sports/big-games/search

Recherche big game.

GET

/api/sports/big-games/today

Big games du jour.

GET

/api/sports/big-games/upcoming/:sports

Big games à venir par sport.

Sports — Live & diffuseurs (6)

GET

/api/sports-live/admin/unavailable-channels

Chaînes non-dispos (admin).

PUT

/api/sports-live/admin/unavailable-channels/:id/resolve

Marquer chaîne comme résolue.

GET

/api/sports-live/events

Événements live.

GET

/api/sports-live/events/:id

Détail événement.

GET

/api/sports-live/events/:id/watch

Stream du match.

POST

/api/sports-live/sync

Sync sports depuis APIs externes.

IPTV — Chaînes locales (7)

GET

/api/iptv/channels

Liste des chaînes IPTV.

POST

/api/iptv/channels

Ajouter chaîne.

DELETE

/api/iptv/channels/:id

Supprimer chaîne.

PUT

/api/iptv/channels/:id

Modifier chaîne.

GET

/api/iptv/channelsFilters

Filtres dispo (pays, genres).

GET

/api/iptv/channelsPages/:page/:limit

Chaînes paginées.

POST

/api/iptv/sync

Sync depuis source M3U.

IPTV-Org — Catalogue mondial (11)

GET

/api/iptv-org/channels/all

Toutes les chaînes mondiales.

GET

/api/iptv-org/channels/all/with-streams

Toutes les chaînes + streams.

GET

/api/iptv-org/channels/french

Chaînes françaises.

GET

/api/iptv-org/channels/french/:channelId

Détail chaîne FR.

GET

/api/iptv-org/channels/french/categories

Catégories chaînes FR.

GET

/api/iptv-org/channels/french/categories/:category

FR — chaînes par catégorie.

GET

/api/iptv-org/channels/french/only-with-streams

FR — uniquement avec streams.

GET

/api/iptv-org/channels/french/search

Recherche dans FR.

GET

/api/iptv-org/channels/french/stats

Stats chaînes FR.

GET

/api/iptv-org/channels/french/streams

Chaînes FR + streams.

GET

/api/iptv-org/test

Test connexion source.

CanalSport — Événements & streams (11)

GET

/api/canalsport/categories

Liste catégories.

GET

/api/canalsport/database/events

Événements en DB.

GET

/api/canalsport/database/stats

Stats DB.

GET

/api/canalsport/events

Événements scrapés.

GET

/api/canalsport/events/category/:category

Par catégorie.

GET

/api/canalsport/events/live

Événements live.

POST

/api/canalsport/scrape-and-save

Scrape + persistance.

GET

/api/canalsport/search

Recherche événement.

GET

/api/canalsport/stats

Stats scraping.

POST

/api/canalsport/stream

Obtenir le stream.

GET

/api/canalsport/test

Test scraping.

Utilisateurs (16)

DELETE

/api/users/account

Suppression compte.

POST

/api/users/forgot-password

Demande de reset password.

POST

/api/users/login

Connexion (JWT).

POST

/api/users/logout

Déconnexion.

PUT

/api/users/password

Changer le mot de passe.

PUT

/api/users/phone

Changer le téléphone.

GET

/api/users/profile

Profil de l'utilisateur courant.

PUT

/api/users/profile

Mise à jour du profil.

POST

/api/users/refresh-token

Refresh du token.

POST

/api/users/register

Inscription utilisateur.

POST

/api/users/resend-otp

Renvoyer le code OTP.

POST

/api/users/resend-verification

Renvoyer le mail de vérification.

POST

/api/users/reset-password

Reset effectif avec token.

GET

/api/users/subscription-status

Statut abonnement de l'user.

GET

/api/users/users

Liste tous les users (admin).

POST

/api/users/verify-email

Vérification email.

Authentification — OTP & vérification (6)

POST

/api/auth/reset-password

Reset password via OTP.

POST

/api/auth/send-email-otp

Envoyer OTP par email.

POST

/api/auth/send-phone-otp

Envoyer OTP par SMS.

GET

/api/auth/status

Statut vérification user.

POST

/api/auth/verify-email-otp

Vérifier OTP email.

POST

/api/auth/verify-phone-otp

Vérifier OTP SMS.

Profils (11)

POST

/api/profiles

Créer un profil.

DELETE

/api/profiles/:profileId

Supprimer profil.

GET

/api/profiles/:profileId

Détail profil.

PUT

/api/profiles/:profileId

Mettre à jour profil.

PATCH

/api/profiles/:profileId/default

Définir profil par défaut.

DELETE

/api/profiles/:profileId/pin

Supprimer le PIN.

PUT

/api/profiles/:profileId/pin

Activer/changer le PIN.

POST

/api/profiles/:profileId/verify-pin

Vérifier le PIN.

GET

/api/profiles/me

Profil actif courant.

GET

/api/profiles/user

Tous les profils du user courant.

GET

/api/profiles/user/:userId

Profils d'un user spécifique.

Devices (sessions multi-écran) (4)

GET

/api/devices

Mes devices.

DELETE

/api/devices/:deviceId

POST

/api/devices/disconnect-others

POST

/api/devices/heartbeat

Plans (abonnements) (5)

GET

/api/plans

Liste plans dispo.

POST

/api/plans

Créer plan (admin).

DELETE

/api/plans/:id

Supprimer plan.

GET

/api/plans/:id

Détail plan.

PUT

/api/plans/:id

Modifier plan.

Abonnements utilisateurs (5)

GET

/api/subscriptions

Toutes les subs (admin).

POST

/api/subscriptions

Créer sub.

DELETE

/api/subscriptions/:id

Supprimer sub.

GET

/api/subscriptions/:id

Détail sub.

PUT

/api/subscriptions/:id

Modifier sub.

Paiements — E-Billing Mobile Money (5)

GET

/api/payments/:id/status

Statut d'un paiement.

POST

/api/payments/:id/verify

Vérifier paiement manuellement.

POST

/api/payments/init

Initier un paiement.

GET

/api/payments/me

Mes paiements.

POST

/api/payments/webhook/ebilling

Webhook E-Billing (Futursowax).

Favoris (films & séries) (6)

DELETE

/api/favorites/movies

Retirer film des favoris.

POST

/api/favorites/movies

Ajouter film en favori.

GET

/api/favorites/movies/:userID

Films favoris d'un user.

DELETE

/api/favorites/shows

Retirer série des favoris.

POST

/api/favorites/shows

Ajouter série en favori.

GET

/api/favorites/shows/:userID

Séries favorites d'un user.

Favoris (chaînes IPTV) (3)

DELETE

/api/favorite-channels/favorites/channels

Retirer chaîne des favoris.

POST

/api/favorite-channels/favorites/channels

Ajouter chaîne en favori.

GET

/api/favorite-channels/favorites/channels/:userID

Chaînes favorites.

Historique de visionnage (4)

POST

/api/history/episodes

Marquer épisode vu.

GET

/api/history/episodes/:userID

Historique épisodes.

POST

/api/history/movies

Marquer film vu.

GET

/api/history/movies/:userID

Historique films.

Commentaires (9)

GET

/api/comments

Liste tous les commentaires.

POST

/api/comments

Ajouter commentaire.

DELETE

/api/comments/:id

Supprimer commentaire.

DELETE

/api/comments/movies

Supprimer comment film.

POST

/api/comments/movies

Comment sur film.

GET

/api/comments/movies/:movieID

Comments d'un film.

DELETE

/api/comments/shows

Supprimer comment série.

POST

/api/comments/shows

Comment sur série.

GET

/api/comments/shows/:showID

Comments d'une série.

Demandes de contenu (1)

POST

/api/content/request

Demander un contenu manquant.

Notifications (1)

GET

/api/notifications

Mes notifications.

Stockage (1)

POST

/api/storage/init

Administration (2)

POST

/api/admin/create

Créer compte admin.

POST

/api/admin/login

Login admin.

Admin — Timestamps intro/credits (3)

GET

/api/admin/timestamps/:tvdbId

GET

/api/admin/timestamps/:tvdbId/:season/:episode

PUT

/api/admin/timestamps/:tvdbId/:season/:episode

Paiements — E-Billing (Mobile Money)

Intégration Futursowax E-Billing pour encaisser les abonnements via Airtel Money, Moov Money et carte bancaire (XAF / FCFA, Gabon). Architecture hybride : le backend orchestre, le frontend redirige vers le portail E-Billing.

🔄 Flow en 6 étapes :
  1. Front → POST /api/payments/init avec planId + phoneNumber
  2. Backend crée Payment PENDING en BD + appelle E-Billing
  3. Front redirige window.location = portalUrl → portail E-Billing
  4. User paie → E-Billing déclenche POST /api/payments/webhook/ebilling
  5. Webhook : update status SUCCESS + crée Subscription ACTIVE
  6. Front poll GET /:id/status toutes les 3s → affiche succès
POST

/api/payments/init

Crée un paiement PENDING et retourne l'URL du portail E-Billing vers laquelle rediriger l'utilisateur. Auth Bearer requise.

Body

NomTypeDescriptionRequis
planIdintID du plan souscrit (non-Trial)Oui
phoneNumberString9 chiffres format Gabon (ex: 074000000)Recommandé
paymentMethodStringMOBILE_MONEY (défaut), CARD, PAYPAL...Non

Réponse 201

{
  "success": true,
  "data": {
    "paymentId": 42,
    "reference": "DW_1746999999999_A1B2C3D4E5F6",
    "portalUrl": "https://futursowax.com/paiement/portal.php?bill=BILL_42&...",
    "billId": "BILL_42",
    "amount": 9990,
    "plan": { "id": 4, "name": "Premium" }
  }
}
                                
POST

/api/payments/webhook/ebilling?secret=...

Public (sans Bearer). Reçoit les notifications server-to-server d'E-Billing. Vérifie le secret partagé, double-check via check_status.php, puis active la Subscription si SUCCESS. Idempotent : retraite-le sans risque.

Body (JSON ou form-urlencoded)

NomTypeDescription
refStringRéférence DayWatch (envoyée à E-Billing au init)
orderStringbillId E-Billing
amountIntMontant payé (FCFA)
statusStringcompleted ou failed
GET

/api/payments/:id/status

Renvoie le statut courant du paiement (depuis la BD locale). Utilisé par le front en polling toutes les 3s après le retour du portail.

Réponse

{
  "success": true,
  "data": {
    "id": 42,
    "status": "SUCCESS",
    "reference": "DW_...",
    "amount": "9990.00",
    "gateway": "EBILLING",
    "planId": 4,
    "transactionDate": "2026-05-12T14:32:11Z"
  }
}
                                
POST

/api/payments/:id/verify

Force une re-vérification auprès d'E-Billing si le webhook a été raté (fallback manuel via bouton "Vérifier mon paiement"). Si confirmé SUCCESS, crée la Subscription.

GET

/api/payments/me

Historique des paiements de l'utilisateur connecté.

Exemple complet (front)


// 1. Initier le paiement
const initPayment = async (planId, phoneNumber) => {
  const res = await fetch('/api/payments/init', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ planId, phoneNumber })
  });
  const { data } = await res.json();
  sessionStorage.setItem('currentPaymentId', data.paymentId);
  window.location.href = data.portalUrl;   // → E-Billing
};

// 2. Sur la page "payment/result" : polling
const pollPaymentStatus = async () => {
  const paymentId = sessionStorage.getItem('currentPaymentId');
  for (let i = 0; i < 100; i++) {                  // max 5min
    const res = await fetch(`/api/payments/${paymentId}/status`, {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    const { data } = await res.json();

    if (data.status === 'SUCCESS') {
      showSuccess('Paiement confirmé ! Abonnement actif.');
      return;
    }
    if (data.status === 'FAILED' || data.status === 'EXPIRED') {
      showError('Paiement échoué : ' + (data.failureReason || ''));
      return;
    }
    await new Promise(r => setTimeout(r, 3000));   // poll 3s
  }
};
                        
📖 Doc complète : docs/PAYMENTS.md contient les détails (sécurité webhook, edge cases, multi-PSP futur, rollback).

Sports Live — Matchs & Diffuseurs

Agrégation des événements sportifs (foot, basket, MMA, boxe) depuis TheSportsDB enrichis via API-Football (free tier 100 req/jour), avec matching automatique broadcaster → chaîne IPTV et signalement si chaîne manquante.

🎯 Le cœur : GET /api/sports-live/events/:id/watch → fuzzy-match les broadcasters (ex: "beIN Sports 1") avec les chaînes IPTV en BD, retourne le streamUrl si trouvé, sinon enregistre un signalement et notifie l'admin par email après 5 reports.
GET

/api/sports-live/events

Liste les événements sportifs à venir. Public.

Query params

NomTypeDescription
sportStringSoccer, Basketball, Fighting...
regionStringFR, INT (défaut : tous)
limitIntMax 50 par défaut

Réponse

{
  "success": true,
  "count": 3,
  "data": [
    {
      "id": 12,
      "title": "PSG vs OM",
      "sport": "Soccer",
      "competition": "Ligue 1",
      "homeTeam": "PSG",
      "awayTeam": "OM",
      "eventTime": "2026-05-15T19:00:00Z",
      "broadcasters": ["Canal+ Sport", "Prime Video"],
      "region": "FR"
    }
  ]
}
                                
GET

/api/sports-live/events/:id/watch

⭐ Cœur de la feature. Trouve le streamUrl en faisant le fuzzy-match broadcaster → chaîne IPTV. Si aucune chaîne dispo, enregistre un signalement et notifie l'admin. Auth Bearer requise.

Réponse 200 — Stream trouvé

{
  "success": true,
  "data": {
    "eventId": 12,
    "eventTitle": "PSG vs OM",
    "streamUrl": "https://iptv.example.com/canalplus_sport.m3u8",
    "channel": {
      "id": 156,
      "name": "Canal+ Sport HD",
      "logoUrl": "https://...",
      "matchedFrom": "Canal+ Sport",
      "matchType": "fuzzy"
    },
    "alternatives": [
      { "channelId": 234, "name": "Prime Video France", "streamUrl": "..." }
    ]
  }
}
                                

Réponse 404 — Chaîne indisponible (signalement créé)

{
  "success": false,
  "code": "CHANNEL_UNAVAILABLE",
  "message": "Aucune chaîne disponible ne diffuse cet event",
  "eventId": 12,
  "eventTitle": "PSG vs OM",
  "missingChannels": ["Canal+ Sport", "Prime Video"],
  "adminNotified": ["Canal+ Sport"]   // si seuil de 5 atteint
}
                                
POST

/api/sports-live/sync

Déclenche le sync depuis TheSportsDB + enrichissement API-Football. Aussi disponible en CLI : npm run sports:sync. À mettre en cron toutes les 4h.

Body (optionnel)

NomTypeDéfaut
sportsString[]['football','basketball','fighting']
daysInt1 (J + J+1)
GET

/api/sports-live/admin/unavailable-channels

Admin. Liste des chaînes IPTV demandées mais absentes du catalogue, triées par reportCount décroissant. Permet d'identifier les chaînes prioritaires à acquérir.

PUT

/api/sports-live/admin/unavailable-channels/:id/resolve

Admin. Marque un signalement comme résolu (ex: après ajout de la chaîne au catalogue IPTV).

📡 Sources de données :
TheSportsDB (gratuit, multi-sport) — key publique 3 ou Patreon ~3€/mois
API-Football (free 100 req/jour) — enrichit les broadcasters foot quand TheSportsDB n'en a pas
Configurer THESPORTSDB_API_KEY et API_FOOTBALL_KEY dans .env.
🔍 Fuzzy match : Le service iptvMatcherService.js normalise les noms (lowercase, sans HD/SD/4K, sans ponctuation) puis utilise la distance de Levenshtein ≤ 2. Exemples reconnus comme une même chaîne :
"beIN Sports 1 HD" · "BEIN SPORT 1" · "beIN Sport 1"

Modèles de données

Structures de données utilisées par l'API avec exemples de réponses JSON.

Utilisateur (User)

Champ Type Description
id INT Identifiant unique de l'utilisateur
username VARCHAR(255) Nom d'utilisateur
email VARCHAR(255) Adresse email (unique)
phoneNumber VARCHAR(20) Numéro de téléphone
status ENUM Statut du compte (PENDING, ACTIVE, SUSPENDED)
role ENUM Rôle de l'utilisateur (USER, ADMIN)
trialStartDate DATETIME Date de début de la période d'essai
trialEndDate DATETIME Date de fin de la période d'essai
createdAt TIMESTAMP Date de création du compte
updatedAt TIMESTAMP Date de dernière mise à jour
{
  "id": 42,
  "username": "johndoe",
  "email": "john.doe@example.com",
  "phoneNumber": "+33612345678",
  "status": "ACTIVE",
  "role": "USER",
  "trialStartDate": "2023-04-15T14:30:45Z",
  "trialEndDate": "2023-05-15T14:30:45Z",
  "createdAt": "2023-04-15T14:30:45Z",
  "updatedAt": "2023-09-22T09:15:33Z",
  "profiles": {
    "count": 2,
    "data": [
      {
        "id": 1,
        "profileName": "johndoe",
        "profileType": "adult",
        "isDefault": true
      },
      {
        "id": 2,
        "profileName": "Kids",
        "profileType": "child",
        "isDefault": false
      }
    ]
  }
}
                            

Profil (Profile)

Champ Type Description
id INT Identifiant unique du profil
userID INT Référence à l'utilisateur propriétaire
profileName VARCHAR(255) Nom du profil
profileAvatarUrl VARCHAR(500) URL de l'avatar du profil
profileType ENUM Type de profil (adult, child, teen)
preferences JSON Préférences personnalisées du profil
isDefault BOOLEAN Indique si c'est le profil par défaut
isActive BOOLEAN Indique si le profil est actif
createdAt TIMESTAMP Date de création du profil
updatedAt TIMESTAMP Date de dernière mise à jour
{
  "id": 1,
  "userID": 42,
  "profileName": "johndoe",
  "profileAvatarUrl": "/avatars/default.png",
  "profileType": "adult",
  "preferences": {
    "language": "fr",
    "contentRating": "R",
    "autoplay": true,
    "subtitles": true,
    "audioQuality": "5.1",
    "videoQuality": "4K"
  },
  "isDefault": true,
  "isActive": true,
  "createdAt": "2023-04-15T14:30:45Z",
  "updatedAt": "2023-09-22T09:15:33Z",
  "watchHistory": {
    "count": 45,
    "lastWatched": "2023-09-22T08:30:00Z"
  },
  "favorites": {
    "count": 12,
    "movies": 8,
    "series": 4
  }
}
                            

Film (Movie)

Champ Type Description
id INT Identifiant unique du film
tmdbId VARCHAR(255) Identifiant TMDB (unique)
title VARCHAR(255) Titre du film
overview TEXT Synopsis du film
posterPath VARCHAR(255) Chemin vers l'affiche
backdropPath VARCHAR(255) Chemin vers l'image de fond
year INT Année de sortie
rating DECIMAL(3,1) Note moyenne (sur 10)
videoUrl VARCHAR(255) URL du fichier vidéo
isPopular BOOLEAN Indique si le film est populaire
{
  "id": 1287,
  "tmdbId": "tt1234567",
  "title": "Exemple de Film",
  "overview": "Un film passionnant qui raconte l'histoire d'un développeur qui crée une API de streaming vidéo.",
  "posterPath": "/images/posters/example-movie.jpg",
  "backdropPath": "/images/backdrops/example-movie.jpg",
  "year": 2023,
  "rating": 8.4,
  "videoUrl": "/videos/example-movie.mp4",
  "isPopular": true,
  "genres": ["Action", "Drame", "Science-Fiction"],
  "duration": 126,
  "director": "Jane Smith",
  "cast": [
    {"name": "Actor One", "character": "Developer"},
    {"name": "Actor Two", "character": "Designer"},
    {"name": "Actor Three", "character": "Project Manager"}
  ],
  "relatedMovies": [
    {"id": 1288, "title": "Exemple de Film 2"},
    {"id": 1289, "title": "Exemple de Film 3"}
  ]
}
                            

Abonnement (Subscription)

Champ Type Description
id INT Identifiant unique de l'abonnement
userId INT Référence à l'utilisateur
planId INT Référence au plan d'abonnement
status ENUM Statut de l'abonnement (PENDING, ACTIVE, EXPIRED)
startDate DATETIME Date de début de l'abonnement
endDate DATETIME Date de fin de l'abonnement
{
  "id": 789,
  "userId": 42,
  "planId": 2,
  "plan": {
    "name": "Premium",
    "description": "Accès à tout le contenu en 4K",
    "price": 14.99,
    "intervalType": "MONTH"
  },
  "status": "ACTIVE",
  "autoRenew": true,
  "startDate": "2023-04-15T00:00:00Z",
  "endDate": "2024-04-15T23:59:59Z",
  "payment": {
    "method": "CARD",
    "lastFour": "1234",
    "nextBillingDate": "2024-04-15T00:00:00Z"
  }
}
                            

Exemples de Code

Exemples pratiques pour interagir avec l'API DayWatch depuis différents environnements de développement.

Classe utilitaire pour l'API


// apiService.js - Classe utilitaire pour interagir avec l'API
class DayWatchAPI {
  constructor(baseUrl = 'http://api.daywatch.local') {
    this.baseUrl = baseUrl;
    this.token = localStorage.getItem('daywatch_token');
  }

  setToken(token) {
    this.token = token;
    localStorage.setItem('daywatch_token', token);
  }

  clearToken() {
    this.token = null;
    localStorage.removeItem('daywatch_token');
  }

  async request(endpoint, method = 'GET', data = null) {
    const url = `${this.baseUrl}${endpoint}`;
    const headers = {
      'Content-Type': 'application/json'
    };

    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }

    const options = {
      method,
      headers
    };

    if (data && (method === 'POST' || method === 'PUT')) {
      options.body = JSON.stringify(data);
    }

    const response = await fetch(url, options);
    
    // Si la réponse est 401, le token est probablement expiré
    if (response.status === 401) {
      this.clearToken();
      throw new Error('Session expirée. Veuillez vous reconnecter.');
    }
    
    const result = await response.json();
    
    if (!response.ok) {
      throw new Error(result.error || 'Une erreur est survenue');
    }
    
    return result;
  }

  // Authentification
  async login(email, password) {
    const data = await this.request('/api/users/login', 'POST', { email, password });
    this.setToken(data.token);
    return data.user;
  }

  // Films
  getMovies(page = 1, limit = 20) {
    return this.request(`/api/movies?page=${page}&limit=${limit}`);
  }

  getPopularMovies() {
    return this.request('/api/movies/popular');
  }

  getMovieDetails(id) {
    return this.request(`/api/movies/${id}`);
  }

  // Utilisateurs
  getUserProfile() {
    return this.request('/api/users/profile');
  }

  updateUserProfile(userData) {
    return this.request('/api/users/profile', 'PUT', userData);
  }

  // Favoris
  getFavorites() {
    return this.request('/api/favorites');
  }

  addToFavorites(mediaId, mediaType = 'movie') {
    return this.request('/api/favorites', 'POST', { mediaId, mediaType });
  }

  removeFromFavorites(id) {
    return this.request(`/api/favorites/${id}`, 'DELETE');
  }
}

// Exemple d'utilisation
const api = new DayWatchAPI();

// Authentification
async function authenticateUser() {
  try {
    const user = await api.login('dev@daywatch.local', 'password123');
    console.log('Connecté:', user);
    return user;
  } catch (error) {
    console.error('Erreur d\'authentification:', error);
  }
}

// Récupérer et afficher les films populaires
async function displayPopularMovies() {
  try {
    const movies = await api.getPopularMovies();
    console.log('Films populaires:', movies);
    
    // Exemple d'affichage dans le DOM
    const container = document.getElementById('movies-container');
    movies.forEach(movie => {
      const movieEl = document.createElement('div');
      movieEl.innerHTML = `
        

${movie.title}

${movie.title}

${movie.overview.substring(0, 100)}...

`; container.appendChild(movieEl); }); } catch (error) { console.error('Erreur:', error); } }

Classe PHP pour l'API


baseUrl = $baseUrl;
        $this->token = isset($_SESSION['daywatch_token']) ? $_SESSION['daywatch_token'] : null;
    }

    public function setToken($token) {
        $this->token = $token;
        $_SESSION['daywatch_token'] = $token;
    }

    public function clearToken() {
        $this->token = null;
        unset($_SESSION['daywatch_token']);
    }

    public function request($endpoint, $method = 'GET', $data = null) {
        $url = $this->baseUrl . $endpoint;
        
        $ch = curl_init($url);
        
        $headers = [
            'Content-Type: application/json',
            'Accept: application/json'
        ];
        
        if ($this->token) {
            $headers[] = 'Authorization: Bearer ' . $this->token;
        }
        
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        
        if ($method === 'POST' || $method === 'PUT') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            if ($data) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            }
        } else if ($method === 'DELETE') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        }
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        
        if (curl_errno($ch)) {
            throw new Exception(curl_error($ch));
        }
        
        curl_close($ch);
        
        $result = json_decode($response, true);
        
        // Si la réponse est 401, le token est probablement expiré
        if ($httpCode === 401) {
            $this->clearToken();
            throw new Exception('Session expirée. Veuillez vous reconnecter.');
        }
        
        if ($httpCode >= 400) {
            throw new Exception($result['error'] ?? 'Une erreur est survenue (' . $httpCode . ')');
        }
        
        return $result;
    }

    // Authentification
    public function login($email, $password) {
        $data = $this->request('/api/users/login', 'POST', [
            'email' => $email,
            'password' => $password
        ]);
        
        $this->setToken($data['token']);
        return $data['user'];
    }

    // Films
    public function getMovies($page = 1, $limit = 20) {
        return $this->request("/api/movies?page=$page&limit=$limit");
    }
    
    public function getPopularMovies() {
        return $this->request('/api/movies/popular');
    }
    
    public function getMovieDetails($id) {
        return $this->request("/api/movies/$id");
    }

    // Utilisateurs
    public function getUserProfile() {
        return $this->request('/api/users/profile');
    }
}

// Exemple d'utilisation:
session_start();
$api = new DayWatchAPI();

try {
    // Connexion
    $user = $api->login('dev@daywatch.local', 'password123');
    echo "Connecté en tant que: " . $user['username'] . "\n";
    
    // Récupérer les films populaires
    $movies = $api->getPopularMovies();
    echo "Films populaires:\n";
    foreach ($movies as $movie) {
        echo " - " . $movie['title'] . " (" . $movie['year'] . ")\n";
    }
    
} catch (Exception $e) {
    echo "Erreur: " . $e->getMessage() . "\n";
}
                            

Classe Python pour l'API


# daywatch_api.py

import requests
import json
from typing import Dict, List, Optional, Any, Union


class DayWatchAPI:
    def __init__(self, base_url: str = "http://api.daywatch.local"):
        self.base_url = base_url
        self.token = None
        self.session = requests.Session()
    
    def set_token(self, token: str) -> None:
        """Définit le token JWT pour l'authentification"""
        self.token = token
        self.session.headers.update({"Authorization": f"Bearer {token}"})
    
    def request(self, endpoint: str, method: str = "GET", 
               data: Optional[Dict] = None) -> Union[Dict, List]:
        """
        Effectue une requête à l'API DayWatch
        
        Args:
            endpoint: Point de terminaison de l'API (débute par /)
            method: Méthode HTTP (GET, POST, PUT, DELETE)
            data: Données à envoyer (pour POST et PUT)
            
        Returns:
            Données JSON désérialisées de la réponse
        """
        url = f"{self.base_url}{endpoint}"
        headers = {"Content-Type": "application/json"}
        
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"
        
        if method == "GET":
            response = self.session.get(url, headers=headers)
        elif method == "POST":
            response = self.session.post(url, headers=headers, 
                                        data=json.dumps(data) if data else None)
        elif method == "PUT":
            response = self.session.put(url, headers=headers, 
                                       data=json.dumps(data) if data else None)
        elif method == "DELETE":
            response = self.session.delete(url, headers=headers)
        else:
            raise ValueError(f"Méthode HTTP non supportée: {method}")
        
        # Vérifier les erreurs
        if response.status_code == 401:
            self.token = None
            raise Exception("Session expirée. Veuillez vous reconnecter.")
        
        try:
            result = response.json()
        except json.JSONDecodeError:
            raise Exception(f"Réponse non-JSON: {response.text}")
        
        if not response.ok:
            raise Exception(result.get("error", f"Erreur API: {response.status_code}"))
        
        return result
    
    # Méthodes d'authentification
    def login(self, email: str, password: str) -> Dict:
        """Authentifie un utilisateur et récupère un token JWT"""
        data = self.request("/api/users/login", "POST", 
                          {"email": email, "password": password})
        self.set_token(data["token"])
        return data["user"]
    
    # Méthodes pour les films
    def get_movies(self, page: int = 1, limit: int = 20) -> Dict:
        """Récupère une liste de films paginée"""
        return self.request(f"/api/movies?page={page}&limit={limit}")
    
    def get_popular_movies(self) -> List[Dict]:
        """Récupère la liste des films populaires"""
        return self.request("/api/movies/popular")
    
    def get_movie_details(self, movie_id: int) -> Dict:
        """Récupère les détails d'un film spécifique"""
        return self.request(f"/api/movies/{movie_id}")
    
    # Méthodes pour les utilisateurs
    def get_user_profile(self) -> Dict:
        """Récupère le profil de l'utilisateur connecté"""
        return self.request("/api/users/profile")


# Exemple d'utilisation
def main():
    api = DayWatchAPI()
    
    try:
        # Connexion
        user = api.login("dev@daywatch.local", "password123")
        print(f"Connecté en tant que: {user['username']}")
        
        # Récupérer les films populaires
        movies = api.get_popular_movies()
        print("\nFilms populaires:")
        for movie in movies:
            print(f" - {movie['title']} ({movie['year']}) - Note: {movie['rating']}/10")
        
        # Récupérer les détails d'un film
        if movies:
            movie_id = movies[0]["id"]
            details = api.get_movie_details(movie_id)
            print(f"\nDétails du film '{details['title']}': {details['overview'][:100]}...")
    
    except Exception as e:
        print(f"Erreur: {e}")


if __name__ == "__main__":
    main()