ProgressBar + Playlist

This commit is contained in:
MasterAcnolo
2025-11-14 18:42:15 +01:00
parent 96c1c6717d
commit c2e4e12512
12 changed files with 188 additions and 127 deletions

View File

@@ -4,6 +4,6 @@ module.exports = {
version: packageJson.version,
applicationPort: "8787",
debugMode: true,
localMode: true,
localMode: false,
DiscordRPCID: "1410934537051181146",
}

View File

@@ -46,9 +46,11 @@
<option value="480">480p</option>
</select>
<button type="submit">Télécharger</button>
<button type="submit" onclick="startProgress()">Télécharger</button>
</form>
<!--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
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
@@ -62,6 +64,13 @@
<button id="changePath">Modifier</button>
</div>
<div id="downloadProgressWrapper">
<div id="downloadProgress"></div>
<div id="downloadProgressText">0%</div>
</div>
<div id="downloadStatus"></div>
<div class="infos-container">
@@ -81,7 +90,7 @@
<script src="script/custompath.js"></script>
<script src="script/downloadstatus.js"></script>
<script src="script/fetchinfo.js"></script>
<script src="script/chirac.js"></script>
<script src="script/progressBar.js"></script>
<script src="script/customthemes.js"></script>
</body>

View File

@@ -1,45 +1,15 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Ce script attend que le DOM soit complètement chargé pour initialiser
l'affichage et la gestion du chemin de sauvegarde personnalisé.
1. Récupère le chemin de téléchargement par défaut depuis le main process via l'API exposée.
2. Met à jour le texte affiché dans l'élément avec l'id "savePath".
3. Crée un input caché nommé "savePath" dans le formulaire pour envoyer ce chemin lors du submit.
4. Ajoute un écouteur sur le bouton "changePath" pour permettre à l'utilisateur
de choisir un dossier via une boîte de dialogue native.
5. Met à jour l'affichage et la valeur cachée du chemin sélectionné.
*/
window.addEventListener("DOMContentLoaded", async () => {
const savePathElem = document.getElementById("savePath");
// 1Essayer de charger depuis le localStorage
// Essayer de charger depuis le localStorage
let savedPath = localStorage.getItem("customDownloadPath");
// 2Sinon demander le chemin par défaut à l'API Electron
// Sinon demander le chemin par défaut à l'API Electron
if (!savedPath) {
savedPath = await window.electronAPI.getDefaultDownloadPath();
}
// 3 Afficher le chemin
// Afficher le chemin
if (savePathElem) {
savePathElem.textContent = savedPath;
}

View File

@@ -1,20 +1,3 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Définition des thèmes disponibles
// Chaque thème a un label (affiché dans le select) et un subtitle (texte sous le titre)

View File

@@ -1,21 +1,3 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const form = document.getElementById("downloadForm");
const statusDiv = document.getElementById("downloadStatus");
const button = form.querySelector("button");

View File

@@ -1,21 +1,3 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function formatDate(dateStr) {
if (!dateStr || dateStr.length !== 8) return "Inconnue";
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
@@ -33,7 +15,7 @@ async function fetchVideoInfo(url) {
body: new URLSearchParams({ url }),
});
if (!res.ok) return { error: `Erreur serveur: ${res.status}` };
if (!res.ok) return { error: `Erreur Lors de la récupération des informations` };
const data = await res.json();
if (!data) return { error: "Données manquantes" };
@@ -52,9 +34,10 @@ document.addEventListener("DOMContentLoaded", () => {
urlInput.addEventListener("input", async () => {
const url = urlInput.value.trim();
if (!url || url.length < 5) {
// Si champ vide -> reset total
if (!url || url.length < 2) {
infoDiv.innerHTML = "";
infoDiv.classList.remove("visible");
infoDiv.classList.remove("visible", "playlist-mode");
lastFetchedUrl = "";
return;
}
@@ -63,25 +46,41 @@ document.addEventListener("DOMContentLoaded", () => {
lastFetchedUrl = url;
const data = await fetchVideoInfo(url);
// Gestion des erreurs
if (data.error) {
infoDiv.innerHTML = `${data.error}`;
infoDiv.classList.remove("visible");
infoDiv.innerHTML = `
<div style="
padding:12px;
background:#380a0a;
border:1px solid #a93333;
border-radius:6px;
margin-top:10px;
">
<strong>Erreur :</strong> ${data.error}
</div>
`;
infoDiv.classList.add("visible");
infoDiv.classList.remove("playlist-mode");
return;
}
// ---------- PLAYLIST ----------
if (data.type === "playlist") {
infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = `
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist détectée: ${data.title}</strong></h3>
<h3 style="color:var(--video-info-heading-color);"><strong>Nombre de vidéos: ${data.count}</strong></h3>
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
<div id="playlistVideos"></div>
`;
const listDiv = document.getElementById("playlistVideos");
data.videos.forEach(v => {
const durationStr = `${Math.floor(v.duration/60)}m ${(v.duration%60).toString().padStart(2,"0")}s`;
const durationStr = v.duration
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
listDiv.innerHTML += `
@@ -90,27 +89,24 @@ document.addEventListener("DOMContentLoaded", () => {
<p><strong>${v.title}</strong></p>
<p>Durée : ${durationStr}</p>
<p><a href="${videoUrl}" target="_blank">URL</a>
<button class="copy-btn" data-url="${videoUrl}">
📋
</button>
<button class="copy-btn" data-url="${videoUrl}">📋</button>
</p>
</div>
`;
});
listDiv.addEventListener("click", (e) => {
if (e.target.classList.contains("copy-btn")) {
const btn = e.target;
// Gestion du bouton copier
listDiv.addEventListener("click", (event) => {
if (event.target.classList.contains("copy-btn")) {
const btn = event.target;
if (btn.disabled) return;
btn.disabled = true;
const url = btn.dataset.url;
navigator.clipboard.writeText(url)
.then(() => {
const original = btn.textContent;
// fade out + shrink
btn.style.opacity = 0;
btn.style.transform = "scale(0.7)";
@@ -127,13 +123,12 @@ document.addEventListener("DOMContentLoaded", () => {
btn.textContent = original;
btn.style.opacity = 1;
btn.style.transform = "scale(1)";
btn.disabled = false; // réactive le bouton
btn.disabled = false;
}, 300);
}, 1000);
}, 300);
})
.catch(() => {
const original = btn.textContent;
@@ -146,19 +141,17 @@ document.addEventListener("DOMContentLoaded", () => {
}
});
infoDiv.classList.add("visible");
return;
} else {
}
infoDiv.classList.remove("playlist-mode");
}
// ---------- VIDEO NORMALE ----------
const durationStr = data.duration
? `${Math.floor(data.duration/60)}m ${(data.duration%60).toString().padStart(2,"0")}s`
: "Inconnue";
// Vidéo normale
const durationStr = `${Math.floor(data.duration/60)}m ${(data.duration%60)
.toString()
.padStart(2,"0")}s`;
const sizeStr = formatSize(data.filesize_approx);
const readableDate = formatDate(data.upload_date);
const categories = data.categories?.join(", ") || "Non spécifiées";
@@ -178,6 +171,8 @@ document.addEventListener("DOMContentLoaded", () => {
<li><strong>Catégories :</strong> ${categories}</li>
</ul>
`;
infoDiv.classList.add("visible");
});
})

View File

@@ -0,0 +1,29 @@
const progressWrapper = document.getElementById("downloadProgressWrapper");
const progressBar = document.getElementById("downloadProgress");
function startProgress() {
progressWrapper.style.display = "block";
progressBar.style.width = "0%";
}
function updateProgress(percent) {
progressBar.style.width = `${percent}%`;
}
// Réinitialise et cache
function resetProgress() {
progressBar.style.width = "0%";
progressWrapper.style.display = "none";
}
// Connexion SSE, c'est Server-sent events est une technologie grâce à laquelle un navigateur reçoit des mises à jour automatiques à partir d'un serveur via une connexion HTTP.
const evtSource = new EventSource("/download/progress");
evtSource.onmessage = e => {
const percent = parseFloat(e.data);
if (!isNaN(percent)) {
updateProgress(percent);
if (percent >= 100) {
setTimeout(resetProgress, 500);
}
}
};

View File

@@ -0,0 +1,42 @@
/* Wrapper de la barre de téléchargement */
#downloadProgressWrapper {
width: 100%;
max-width: 400px;
height: 12px;
background-color: #444;
border-radius: 6px;
overflow: hidden;
margin: 10px auto;
display: none;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
}
/* Barre de progression */
#downloadProgress {
height: 100%;
width: 0%;
background-color: var(--video-info-link-color, #4caf50);
border-radius: 6px 0 0 6px;
transition: width 0.2s ease;
}
#downloadProgressText {
text-align: center;
font-size: 0.8rem;
color: #fff;
position: absolute;
width: 100%;
top: -20px;
}
/* Responsive pour mobile */
@media (max-width: 480px) {
#downloadProgressWrapper {
max-width: 90%;
height: 10px;
}
#downloadProgressText {
font-size: 0.7rem;
}
}

View File

@@ -10,6 +10,7 @@
@import url("theme/themeimport.css");
@import url("components/editpathbutton.css");
@import url("components/progressBar.css");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

0
server/helpers/ytDlp.js Normal file
View File

View File

@@ -12,6 +12,8 @@ const { notifyDownloadFinished } = require("../helpers/notify");
const { userYtDlp } = require("../helpers/path");
const listeners = [];
router.post("/", (req, res) => {
try {
const options = {
@@ -31,6 +33,7 @@ router.post("/", (req, res) => {
logger.info(args)
const child = execFile(userYtDlp, args);
child.on("error", err => {
logger.error(`Erreur yt-dlp : ${err.message}`);
res.status(500).send(`❌ Erreur yt-dlp : ${err.message}`);
@@ -48,10 +51,44 @@ router.post("/", (req, res) => {
child.stderr.on("data", data => data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`)));
}
child.stdout.on("data", data => {
const lines = data.toString().split("\n");
lines.forEach(line => {
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) {
const percent = parseFloat(match[1]);
if (listeners.length > 0) {
listeners.forEach(fn => fn(percent));
}
}
});
});
} catch (err) {
logger.error(`Erreur serveur dans /download : ${err.message}`);
res.status(500).send(`Erreur serveur : ${err.message}`);
}
});
// SSE endpoint
router.get("/progress", (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendProgress = (percent) => {
res.write(`data: ${percent}\n\n`);
};
listeners.push(sendProgress);
req.on('close', () => {
listeners.splice(listeners.indexOf(sendProgress), 1);
res.end();
});
});
module.exports = router;

View File

@@ -3,11 +3,7 @@ const router = express.Router();
const { execFile } = require("child_process");
const { logger } = require("../logger");
const { userYtDlp } = require("../helpers/path");
const getUserBrowser = require("../helpers/getBrowser")
// const ytDlpPath = path.join(__dirname, '../../ressources/yt-dlp.exe');
// const userYtDlp = path.join(process.env.APPDATA || process.env.USERPROFILE, 'FreedomLoader', 'yt-dlp.exe');
const getUserBrowser = require("../helpers/getBrowser");
function isValidUrl(url) {
try {
@@ -24,7 +20,16 @@ router.post("/", (req, res) => {
logger.info(`Requête /info reçue pour ${url}`);
const args = ["--dump-single-json", "--ignore-errors", "--yes-playlist", url, "--cookies-from-browser" , `${getUserBrowser()}`];
const args = [
"--dump-single-json",
"--ignore-errors",
"--yes-playlist",
url,
"--cookies-from-browser",
`${getUserBrowser()}`,
"--extractor-args",
"youtube:player_client=default"
];
execFile(userYtDlp, args, { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
@@ -69,13 +74,21 @@ router.post("/", (req, res) => {
}
// Vidéo unique
if (!data.title) {
return res.status(500).send("❌ Impossible de récupérer les infos pour cette vidéo.");
}
logger.info(`Vidéo unique récupérée : ${data.title}`);
res.json({ type: "video", ...data });
} catch (e) {
// fallback aussi si JSON est illisible
logger.error(`Erreur parsing JSON: ${e.message}`);
return res.status(500).send("❌ JSON illisible.");
return res.status(500).send("❌ Impossible de récupérer les infos (JSON illisible).");
}
});
});