LE POULET

This commit is contained in:
MasterAcnolo
2025-06-28 09:49:36 +02:00
parent 83b31cb645
commit f7abca90cb
8 changed files with 234 additions and 83 deletions

View File

@@ -1,5 +1,9 @@
{ {
"dependencies": { "dependencies": {
"express": "^5.1.0" "express": "^5.1.0"
} },
"scripts": {
"start": "node server/server.js",
"dev": "nodemon server/server.js"
}
} }

View File

@@ -9,26 +9,29 @@
</head> </head>
<body> <body>
<div class="container">
<form action="/download" method="POST" id="downloadForm"> <form action="/download" method="POST" id="downloadForm">
<input name="url" placeholder="URL YouTube" id="UrlInput"> <input name="url" placeholder="URL YouTube" id="UrlInput">
<input type="checkbox" name="audioOnly" value="1"> MP3 seulement <input type="checkbox" name="audioOnly" value="1"> MP3 seulement
<select name="quality"> <select name="quality">
<option value="best">Meilleure qualité</option> <option value="best">Meilleure qualité</option>
<option value="worst">Qualité minimale</option> <option value="worst">Qualité minimale</option>
</select> </select>
<button type="submit">Télécharger</button> <button type="submit">Télécharger</button>
</form> </form>
<div id="downloadStatus" style="margin-top: 1em; font-weight:bold;"></div> <div id="downloadStatus" style="margin-top: 1em; font-weight:bold;"></div>
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur <!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
les données à traiter. Pour cela on définit une route vers /download, ca va les données à traiter. Pour cela on définit une route vers /download, ca va
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit
que l'on envoie des données.--> que l'on envoie des données.-->
<div id="videoInfo" style="margin-top: 20px;"></div> <div id="videoInfo" style="margin-top: 20px;"></div>
</div>
<script src="script/downloadstatus.js"></script> <script src="script/downloadstatus.js"></script>
<script src="script/fetchinfo.js"></script> <script src="script/fetchinfo.js"></script>

View File

@@ -1,29 +1,55 @@
// Récupération du formulaire de téléchargement
const form = document.getElementById("downloadForm"); const form = document.getElementById("downloadForm");
// Div où on affiche l'état de progression (en cours, erreur, succès)
const statusDiv = document.getElementById("downloadStatus"); const statusDiv = document.getElementById("downloadStatus");
form.addEventListener("submit", async (e) => { // Récupération du bouton de validation pour pouvoir le désactiver pendant la requête
e.preventDefault(); // empêche la soumission classique (rechargement) const button = form.querySelector("button");
// On intercepte la soumission du formulaire
form.addEventListener("submit", async (e) => {
e.preventDefault(); // empêche le rechargement de la page par le navigateur
// On désactive le bouton pour éviter les clics multiples
button.disabled = true;
// Affiche un message d'attente à l'utilisateur
statusDiv.textContent = "⏳ Téléchargement en cours..."; statusDiv.textContent = "⏳ Téléchargement en cours...";
// Prépare les données du formulaire sous forme URL-encoded
const formData = new FormData(form); const formData = new FormData(form);
const params = new URLSearchParams(formData); const params = new URLSearchParams(formData);
try { try {
// Envoi de la requête POST vers /download avec les données du formulaire
const res = await fetch("/download", { const res = await fetch("/download", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params, body: params,
}); });
// Si la réponse n'est pas un succès (code 4xx ou 5xx)
if (!res.ok) { if (!res.ok) {
statusDiv.textContent = "❌ Erreur pendant le téléchargement."; statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
return; return;
} }
// On récupère le texte de la réponse du serveur
const text = await res.text(); const text = await res.text();
statusDiv.textContent = text; // affiche « Téléchargement terminé » ou autre message serveur
// Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé »)
statusDiv.textContent = text;
} catch { } catch {
// Gestion d'erreur en cas de problème réseau ou autre
statusDiv.textContent = "❌ Une erreur sest produite."; statusDiv.textContent = "❌ Une erreur sest produite.";
} finally {
// Réactive le bouton pour autoriser d'autres soumissions
button.disabled = false;
// Efface le message de status après 5 secondes pour garder l'UI propre
setTimeout(() => {
statusDiv.textContent = "";
}, 5000);
} }
}); });

View File

@@ -1,73 +1,78 @@
const urlInput = document.getElementById("UrlInput"); // Récupère le champ input où l'utilisateur entre l'URL document.addEventListener("DOMContentLoaded", () => {
const infoDiv = document.getElementById("videoInfo"); // Récupère la div où on affichera les infos const urlInput = document.getElementById("UrlInput");
const infoDiv = document.getElementById("videoInfo");
let lastFetchedUrl = ""; // Pour éviter de refaire plusieurs requêtes pour la même URL let lastFetchedUrl = "";
// On écoute chaque changement dans le champ URL urlInput.addEventListener("input", async () => {
urlInput.addEventListener("input", async () => { const url = urlInput.value.trim();
const url = urlInput.value; // On prend la valeur actuelle du champ console.log("Input détecté :", url);
// Si pas d'URL, ou URL trop courte, ou déjà traitée, on sort // Si URL vide ou trop courte : on vide et cache la div
if (!url || url.length < 5 || url === lastFetchedUrl) return; if (!url || url.length < 5) {
infoDiv.innerHTML = "";
lastFetchedUrl = url; // On mémorise l'URL traitée infoDiv.classList.remove("visible");
lastFetchedUrl = "";
try {
// Envoi d'une requête POST vers le backend /info avec l'URL dans le corps
const res = await fetch("/info", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ url }) // Encode l'URL au format x-www-form-urlencoded
});
// Si la réponse n'est pas OK (erreur 4xx ou 5xx)
if (!res.ok) {
infoDiv.innerHTML = "❌ Impossible de récupérer les infos."; // Message d'erreur utilisateur
return; return;
} }
// Si même URL que précédemment, on ne fait rien
if (url === lastFetchedUrl) return;
// On récupère la réponse JSON (les infos de la vidéo) lastFetchedUrl = url;
const data = await res.json();
// Formatage de la durée en minutes et secondes (ex: 4m 35s) try {
const minutes = Math.floor(data.duration / 60); const res = await fetch("/info", {
const seconds = data.duration % 60; method: "POST",
const durationStr = `${minutes}m ${seconds.toString().padStart(2, '0')}s`; headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ url }),
});
// Formatage de la date d'upload depuis YYYYMMDD vers JJ/MM/AAAA if (!res.ok) {
const date = data.upload_date || ""; infoDiv.innerHTML = "❌ Impossible de récupérer les infos.";
const readableDate = date infoDiv.classList.remove("visible");
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}` return;
: "Inconnue"; }
// Taille estimée en Mo, ou "Inconnue" si pas dispo const data = await res.json();
const sizeMB = data.filesize_approx
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
: "Inconnue";
// Catégories en chaîne séparée par des virgules, ou "Non spécifiées" const minutes = Math.floor(data.duration / 60);
const categories = data.categories const seconds = data.duration % 60;
? data.categories.join(", ") const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
: "Non spécifiées";
// Mise à jour du contenu HTML de la div avec toutes les infos formatées const date = data.upload_date || "";
infoDiv.innerHTML = ` const readableDate = date
<h3>${data.title}</h3> ? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
<img src="${data.thumbnail}" width="320"> : "Inconnue";
<ul>
<li><strong>Durée :</strong> ${durationStr}</li> const sizeMB = data.filesize_approx
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li> ? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
<li><strong>Date dupload :</strong> ${readableDate}</li> : "Inconnue";
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li> const categories = data.categories
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li> ? data.categories.join(", ")
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li> : "Non spécifiées";
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
<li><strong>Catégories :</strong> ${categories}</li> infoDiv.innerHTML = `
</ul> <h3>${data.title}</h3>
`; <img src="${data.thumbnail}" width="320" alt="Thumbnail">
} catch (e) { <ul>
// Si une erreur survient lors de la récupération ou du parsing <li><strong>Durée :</strong> ${durationStr}</li>
infoDiv.innerHTML = "❌ Erreur lors de la récupération."; <li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
} <li><strong>Date dupload :</strong> ${readableDate}</li>
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></li>
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li>
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
<li><strong>Catégories :</strong> ${categories}</li>
</ul>
`;
// Affiche la div avec la classe pour transition douce
infoDiv.classList.add("visible");
} catch (e) {
console.error("Erreur lors de la récupération :", e);
infoDiv.innerHTML = "❌ Erreur lors de la récupération.";
infoDiv.classList.remove("visible");
}
});
}); });

View File

@@ -0,0 +1,13 @@
.container {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #121212;
padding: 1em;
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
}

View File

@@ -0,0 +1,12 @@
#downloadStatus {
min-height: 1.5em; /* pour garder la place même vide */
font-weight: bold;
text-align: center;
color: #007bff;
}
label {
display: flex;
align-items: center;
font-size: 0.9em;
}

View File

@@ -0,0 +1,44 @@
form {
background-color: #fff;
padding: 2em;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
width: 100%;
gap: 1em;
}
form input[type="text"],
form input[type="url"],
form select {
padding: 0.75em;
border: 1px solid #ccc;
border-radius: 8px;
}
form input[type="checkbox"] {
margin-right: 0.5em;
}
form button {
background-color: #007bff;
color: #fff;
border: none;
padding: 0.75em;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
form button:hover {
background-color: #0056b3;
}
form#downloadForm {
display: flex;
gap: 0.5em;
max-width: 600px;
width: 100%;
justify-content: center;
}

View File

@@ -1,3 +1,7 @@
@import url("layout/form.css");
@import url("layout/container.css");
@import url("layout/ctr-infos.css");
*{ *{
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -5,6 +9,46 @@
} }
body{ body {
background-color:rgb(83, 83, 83) ; margin: 0;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#downloadStatus {
margin-top: 0.5em;
font-weight: bold;
color: #eee;
min-height: 1.5em;
}
#videoInfo {
margin-top: 100px;
padding: 1em;
max-width: 600px;
margin-left: auto;
margin-right: auto;
border-radius: 8px;
color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
transition: opacity 0.5s ease, max-height 0.5s ease;
opacity: 0;
max-height: 0;
overflow: hidden;
}
#videoInfo.visible {
opacity: 1;
max-height: 1000px;
}
label {
display: flex;
align-items: center;
font-size: 0.9em;
} }