The film submission system allows producers to submit their films to the festival with comprehensive metadata, media files, and team credits.
Film Data Model
Films are represented by the Movie model with extensive metadata:
// /back/src/models/Movie.js:27-102
const Movie = sequelize . define ( 'Movie' , {
id_movie: {
type: DataTypes . INTEGER ,
primaryKey: true ,
autoIncrement: true
},
title: {
type: DataTypes . STRING ,
allowNull: false
},
description: DataTypes . TEXT ,
duration: DataTypes . INTEGER , // Duration in seconds
main_language: DataTypes . STRING ,
release_year: DataTypes . INTEGER ,
nationality: DataTypes . STRING ,
// Media files
display_picture: DataTypes . STRING ,
picture1: DataTypes . STRING ,
picture2: DataTypes . STRING ,
picture3: DataTypes . STRING ,
trailer: DataTypes . STRING ,
youtube_link: DataTypes . STRING ( 255 ),
subtitle: DataTypes . STRING ( 255 ),
thumbnail: DataTypes . STRING ( 255 ),
// AI-related metadata
production: DataTypes . STRING , // AI classification
workshop: DataTypes . STRING ( 255 ), // AI methodology
ai_tool: DataTypes . STRING ( 255 ), // AI tools used
// Bilingual synopsis
synopsis: DataTypes . TEXT , // French synopsis
synopsis_anglais: DataTypes . TEXT , // English synopsis
translation: DataTypes . STRING ( 255 ), // Translation language
// Review fields
admin_comment: DataTypes . TEXT ,
jury_comment: DataTypes . TEXT ,
// Selection status pipeline
selection_status: {
type: DataTypes . ENUM (
'submitted' , // Initial state after submission
'assigned' , // Assigned to jury for first round
'to_discuss' , // Second round voting open
'candidate' , // Candidate for awards
'awarded' , // Film received an award
'refused' , // Rejected by jury
'selected' , // Legacy: selected status
'finalist' // Legacy: finalist status
),
allowNull: false ,
defaultValue: 'submitted'
},
// Owner
id_user: {
type: DataTypes . INTEGER ,
allowNull: false
}
});
Selection Status Flow
Films progress through a multi-stage selection pipeline:
The status transition rules are enforced in the backend to maintain workflow integrity. Admins can use force_transition to override these rules if needed.
Film Submission Flow
Step 1: User Registration
Producers can register with or without a film:
Option A: Register then submit film
POST / auth / register
{
"first_name" : "Jane" ,
"last_name" : "Smith" ,
"email" : "jane@example.com" ,
"password" : "secure123" ,
"role" : "PRODUCER"
}
Option B: Register with film submission (atomic transaction)
POST / auth / register - film
Content - Type : multipart / form - data
// User fields
first_name : "Jane"
last_name : "Smith"
email : "jane@example.com"
password : "secure123"
// Film fields
filmTitleOriginal : "AI Dreams"
durationSeconds : 90
filmLanguage : "English"
synopsisOriginal : "A short film about..."
aiClassification : "AI-assisted"
aiStack : "Stable Diffusion, RunwayML"
aiMethodology : "Hybrid workflow"
// File uploads
filmFile : < video file >
thumbnail1: < image file >
thumbnail2: < image file >
subtitlesSrt: <.srt file>
Step 2: Film Data Processing
// /back/src/controllers/AuthController.js:183-204
const newMovie = await Movie . create ({
title: filmTitleOriginal ,
description: synopsisOriginal ,
duration: durationNumber ,
main_language: filmLanguage ,
release_year: releaseYear || null ,
nationality ,
translation ,
youtube_link: youtubeLink ,
synopsis: synopsisOriginal ,
synopsis_anglais: synopsisEnglish ,
ai_tool: aiStack ,
workshop: aiMethodology ,
production: aiClassification ,
trailer: filmFile ,
subtitle: subtitleFile ,
picture1: thumb1 ,
picture2: thumb2 ,
picture3: thumb3 ,
thumbnail: thumb1 ,
id_user: newUser . id_user
}, { transaction });
The maximum film duration is 120 seconds (2 minutes). Submissions exceeding this limit are rejected with a 400 error.
Step 3: Collaborator Association
// /back/src/controllers/AuthController.js:206-240
const collaborators = JSON . parse ( req . body . collaborators );
const collaboratorRecords = await Promise . all (
collaborators
. filter ( collab => collab ?. email )
. map ( async ( collab ) => {
const [ record ] = await Collaborator . findOrCreate ({
where: { email: collab . email },
defaults: {
first_name: collab . first_name || "" ,
last_name: collab . last_name || "" ,
email: collab . email ,
job: collab . job || null
},
transaction
});
return record ;
})
);
await newMovie . setCollaborators ( collaboratorRecords , { transaction });
Step 4: Confirmation Email
Users receive confirmation after successful submission:
// /back/src/controllers/AuthController.js:265-286
await sendTemplateEmail ({
to: newUser . email ,
templateId: process . env . BREVO_TEMPLATE_FILM_CANDIDACY_CONFIRMATION ,
params: {
first_name: newUser . first_name ,
last_name: newUser . last_name ,
movie_title: newMovie . title ,
email: newUser . email
}
});
Supported File Types
The platform accepts multiple media types:
Video Files
Images
Subtitles
Film/Trailer Upload
Field: filmFile or trailer
Stored in: /uploads/ directory
Database column: trailer
const files = req . files || {};
const filmFile = files . filmFile ?.[ 0 ]?. filename || null ;
if ( filmFile ) {
await movie . update ({ trailer: filmFile });
}
Thumbnails & Stills
Fields: thumbnail1, thumbnail2, thumbnail3
Database columns: picture1, picture2, picture3, thumbnail
First thumbnail becomes default thumbnail
const thumb1 = files . thumbnail1 ?.[ 0 ]?. filename || null ;
const thumb2 = files . thumbnail2 ?.[ 0 ]?. filename || null ;
const thumb3 = files . thumbnail3 ?.[ 0 ]?. filename || null ;
await movie . update ({
picture1: thumb1 ,
picture2: thumb2 ,
picture3: thumb3 ,
thumbnail: thumb1 // Default thumbnail
});
Automatic Poster Generation If no thumbnail is provided, the system generates one from the video: // /back/src/controllers/MovieController.js:17-34
function generatePosterFromVideo ( videoFilename ) {
return new Promise (( resolve , reject ) => {
ffmpeg ( path . join ( uploadDir , videoFilename ))
. screenshots ({
count: 1 ,
timemarks: [ "1" ], // 1 second into video
filename: posterName ,
folder: uploadDir ,
size: "1280x720"
})
. on ( "end" , () => resolve ( posterName ))
. on ( "error" , ( err ) => reject ( err ));
});
}
Subtitle Files (.srt)
Field: subtitlesSrt
Database column: subtitle
Format: SRT (SubRip)
const subtitleFile = files . subtitlesSrt ?.[ 0 ]?. filename || null ;
if ( subtitleFile ) {
await movie . update ({ subtitle: subtitleFile });
}
File Upload Configuration
Files are handled with Multer middleware:
import multer from "multer" ;
import path from "path" ;
const storage = multer . diskStorage ({
destination : ( req , file , cb ) => {
cb ( null , "uploads/" );
},
filename : ( req , file , cb ) => {
const uniqueSuffix = Date . now () + "-" + Math . round ( Math . random () * 1e9 );
cb ( null , uniqueSuffix + path . extname ( file . originalname ));
}
});
const upload = multer ({ storage });
Category Management
Category Model
// /back/src/models/Categorie.js:5-15
const Categorie = sequelize . define ( 'Categorie' , {
id_categorie: {
type: DataTypes . INTEGER ,
primaryKey: true ,
autoIncrement: true
},
name: DataTypes . STRING
});
Many-to-Many Relationship
Films can belong to multiple categories:
// /back/src/models/Movie.js:134-138
Movie . belongsToMany ( models . Categorie , {
through: 'movies_categories' ,
foreignKey: 'id_movie' ,
otherKey: 'id_categorie'
});
Assigning Categories
During Submission:
const categories = [ 1 , 3 , 5 ]; // Category IDs
await newMovie . setCategories ( categories );
Admin Updates:
// PUT /movies/:id/categories (ADMIN only)
async function updateMovieCategories ( req , res ) {
const { id } = req . params ;
let { categories } = req . body ;
const movie = await Movie . findByPk ( id );
await movie . setCategories ( categoryIds );
const updatedMovie = await Movie . findByPk ( id , {
include: [{ model: Categorie }]
});
res . json ({ message: "Catégories mises à jour" , movie: updatedMovie });
}
Collaborator Credits
Collaborator Model
Track team members:
const Collaborator = sequelize . define ( 'Collaborator' , {
id_collaborator: {
type: DataTypes . INTEGER ,
primaryKey: true ,
autoIncrement: true
},
first_name: DataTypes . STRING ,
last_name: DataTypes . STRING ,
email: {
type: DataTypes . STRING ,
unique: true ,
allowNull: false
},
job: DataTypes . STRING // Role in production
});
Adding Collaborators
// POST /movies/:id/collaborators
const collaborators = [
{
first_name: "John" ,
last_name: "Doe" ,
email: "john@example.com" ,
job: "DIRECTOR"
},
{
first_name: "Jane" ,
last_name: "Smith" ,
email: "jane@example.com" ,
job: "ACTOR"
}
];
await movie . setCollaborators ( collaboratorRecords );
Collaborators are identified by email. If a collaborator already exists in the database, their information is updated rather than creating a duplicate.
Retrieving Films
Get All Films (Admin/Jury)
// GET /movies
async function getMovies ( req , res ) {
const movies = await Movie . findAll ({
include: [
{
model: Categorie ,
through: { attributes: [] }
},
{
model: Collaborator ,
through: { attributes: [] }
},
{
model: Award ,
required: false
},
{
model: User ,
as: "Producer" ,
attributes: [ "id_user" , "first_name" , "last_name" ]
},
{
model: User ,
as: "Juries" ,
attributes: [ "id_user" , "first_name" , "last_name" , "email" ],
through: { attributes: [] },
required: false
}
]
});
res . json ( movies );
}
Get My Films (Producer)
// GET /movies/my
async function getMyMovies ( req , res ) {
const id_user = req . user . id_user ;
const movies = await Movie . findAll ({
where: { id_user },
include: [
{ model: Categorie },
{ model: Collaborator }
]
});
res . json ( movies );
}
Get Assigned Films (Jury)
// GET /movies/assigned
async function getAssignedMovies ( req , res ) {
const id_user = req . user . id_user ;
const movies = await Movie . findAll ({
where: {
selection_status: {
[Op.in]: [ "assigned" , "to_discuss" , "candidate" ]
}
},
include: [
{
model: User ,
as: "Juries" ,
where: { id_user }, // Only films assigned to this jury
through: { attributes: [] }
}
]
});
res . json ( movies );
}
Updating Films
Producer Updates
Producers can update their own films:
// PUT /movies/:id (PRODUCER or ADMIN)
if ( req . user . role !== "ADMIN" && movie . id_user !== req . user . id_user ) {
return res . status ( 403 ). json ({ error: "Accès refusé" });
}
await movie . update ({
title: req . body . title ,
description: req . body . description ,
// ... other fields
});
Admin Status Updates
// PUT /movies/:id/status (ADMIN only)
async function updateMovieStatus ( req , res ) {
const { id } = req . params ;
const { selection_status , jury_comment , force_transition } = req . body ;
const movie = await Movie . findByPk ( id );
const previousStatus = movie . selection_status ;
// Validate transition
const transitionMap = {
submitted: [ "assigned" , "candidate" , "refused" ],
assigned: [ "to_discuss" , "candidate" , "refused" ],
to_discuss: [ "candidate" , "refused" ],
candidate: [ "awarded" , "refused" ],
awarded: [],
refused: []
};
const allowedTargets = transitionMap [ previousStatus ] || [];
if ( ! force_transition && ! allowedTargets . includes ( selection_status )) {
return res . status ( 400 ). json ({
error: `Transition invalide: ${ previousStatus } -> ${ selection_status } `
});
}
movie . selection_status = selection_status ;
if ( jury_comment ) {
movie . jury_comment = jury_comment . trim ();
}
await movie . save ();
res . json ({ message: "Statut mis à jour" , movie });
}
When a film is moved to “refused” status, an automatic rejection email is sent to the producer.
Validation Rules
title - Film title (required)
description or synopsis - Film description (required)
duration - Duration in seconds (optional but recommended)
id_user - Owner/producer ID (automatically set)
if ( durationSeconds && movieDuration > 120 ) {
return res . status ( 400 ). json ({
error: "La durée maximale est de 120 secondes"
});
}
Maximum duration: 120 seconds (2 minutes)
Video file: Required for complete submission
At least one thumbnail: Required (or auto-generated)
Subtitles: Optional
All files stored in /uploads/ directory
API Endpoints Summary
Method Endpoint Role Description POST /auth/register-filmPublic Register user + submit film (atomic) POST /moviesPRODUCER Submit new film GET /moviesADMIN Get all films GET /movies/myPRODUCER Get my films GET /movies/assignedJURY Get assigned films GET /movies/:idALL Get single film details PUT /movies/:idADMIN Update film metadata PUT /movies/:id/statusADMIN Update selection status PUT /movies/:id/categoriesADMIN Assign categories PUT /movies/:id/juriesADMIN Assign jury members PUT /movies/:id/collaboratorsPRODUCER/ADMIN Update collaborators DELETE /movies/:idADMIN Delete film
Example: Complete Submission
// Frontend form submission
const formData = new FormData ();
// User data
formData . append ( "first_name" , "Jane" );
formData . append ( "last_name" , "Smith" );
formData . append ( "email" , "jane@example.com" );
formData . append ( "password" , "secure123" );
// Film data
formData . append ( "filmTitleOriginal" , "AI Dreams" );
formData . append ( "durationSeconds" , 90 );
formData . append ( "filmLanguage" , "English" );
formData . append ( "synopsisOriginal" , "A mesmerizing exploration..." );
formData . append ( "synopsisEnglish" , "A mesmerizing exploration..." );
formData . append ( "aiClassification" , "AI-assisted" );
formData . append ( "aiStack" , "Stable Diffusion, Midjourney" );
formData . append ( "aiMethodology" , "Hybrid human-AI workflow" );
formData . append ( "nationality" , "USA" );
formData . append ( "releaseYear" , 2024 );
// Files
formData . append ( "filmFile" , videoFile );
formData . append ( "thumbnail1" , thumb1 );
formData . append ( "thumbnail2" , thumb2 );
formData . append ( "subtitlesSrt" , srtFile );
// Collaborators
formData . append ( "collaborators" , JSON . stringify ([
{ first_name: "John" , last_name: "Doe" , email: "john@example.com" , job: "DIRECTOR" },
{ first_name: "Alice" , last_name: "Johnson" , email: "alice@example.com" , job: "ACTOR" }
]));
// Submit
const response = await fetch ( "/auth/register-film" , {
method: "POST" ,
body: formData
});