Feat: Add createPlaylistFolders feature to organize downloads into dedicated folders with toggle settings

This commit is contained in:
MasterAcnolo
2026-04-12 18:13:38 +02:00
parent 8fe4e7c38a
commit f3c37c2498
6 changed files with 111 additions and 2 deletions

View File

@@ -17,7 +17,8 @@ const FEATURE_WHITELIST = new Set([
"verboseLogs", "verboseLogs",
"autoDownloadPlaylist", "autoDownloadPlaylist",
"customCodec", "customCodec",
"theme" "theme",
"createPlaylistFolders"
]); ]);
const configFolderPath = featuresPath; const configFolderPath = featuresPath;

View File

@@ -7,6 +7,7 @@
"addMetadata": true, "addMetadata": true,
"verboseLogs": false, "verboseLogs": false,
"autoDownloadPlaylist": true, "autoDownloadPlaylist": true,
"createPlaylistFolders": true,
"customCodec": "h264", "customCodec": "h264",
"logSystem": true, "logSystem": true,
"outputTitleCheck": true, "outputTitleCheck": true,

View File

@@ -88,6 +88,14 @@
</div> </div>
</div> </div>
<div class="setting-item">
<input type="checkbox" data-key="createPlaylistFolders" id="createPlaylistFolders">
<div class="setting-info">
<span class="setting-title">Create Playlist Folders</span>
<small>Create a dedicated folder for each playlist with its name.</small>
</div>
</div>
<!-- <div class="setting-item"> <!-- <div class="setting-item">
<input type="checkbox" data-key="notifySystem" id="notifySystem"> <input type="checkbox" data-key="notifySystem" id="notifySystem">
<div class="setting-info"> <div class="setting-info">

View File

@@ -33,6 +33,17 @@ async function init() {
const urlInput = document.getElementById("UrlInput"); const urlInput = document.getElementById("UrlInput");
const infoDiv = document.getElementById("videoInfo"); const infoDiv = document.getElementById("videoInfo");
const loaderBox = document.getElementById("loaderBox"); const loaderBox = document.getElementById("loaderBox");
const form = document.getElementById("downloadForm");
// Input caché pour stocker le titre de la playlist
let playlistTitleInput = document.getElementById("playlistTitle");
if (!playlistTitleInput) {
playlistTitleInput = document.createElement("input");
playlistTitleInput.type = "hidden";
playlistTitleInput.name = "playlistTitle";
playlistTitleInput.id = "playlistTitle";
form.appendChild(playlistTitleInput);
}
let lastFetchedUrl = ""; let lastFetchedUrl = "";
@@ -47,6 +58,7 @@ async function init() {
if (!url || url.length < 2) { if (!url || url.length < 2) {
infoDiv.innerHTML = ""; infoDiv.innerHTML = "";
infoDiv.classList.remove("visible", "playlist-mode"); infoDiv.classList.remove("visible", "playlist-mode");
playlistTitleInput.value = "";
lastFetchedUrl = ""; lastFetchedUrl = "";
return; return;
} }
@@ -70,12 +82,14 @@ async function init() {
`; `;
infoDiv.classList.add("visible"); infoDiv.classList.add("visible");
infoDiv.classList.remove("playlist-mode"); infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
loaderBox.style.display = "none"; loaderBox.style.display = "none";
return; return;
} }
// ---------- PLAYLIST ---------- // ---------- PLAYLIST ----------
if (data.type === "playlist") { if (data.type === "playlist") {
playlistTitleInput.value = data.title;
infoDiv.classList.add("playlist-mode"); infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = ` infoDiv.innerHTML = `
<div class="playlist-header"> <div class="playlist-header">
@@ -194,6 +208,7 @@ async function init() {
} }
infoDiv.classList.remove("playlist-mode"); infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
// ---------- VIDEO NORMALE ---------- // ---------- VIDEO NORMALE ----------
const durationStr = data.duration const durationStr = data.duration

View File

@@ -14,6 +14,7 @@ async function downloadController(req, res) {
audioOnly: req.body.audioOnly === "1", audioOnly: req.body.audioOnly === "1",
quality: req.body.quality || "best", quality: req.body.quality || "best",
outputFolder: req.body.savePath || undefined, outputFolder: req.body.savePath || undefined,
playlistTitle: req.body.playlistTitle || undefined,
}; };
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !"); if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");

View File

@@ -6,13 +6,80 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
const notify = require("../helpers/notify.helpers"); const notify = require("../helpers/notify.helpers");
const path = require("path"); const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers"); const { isSafePath } = require("../helpers/validation.helpers");
const { configFeatures } = require("../../config.js");
let currentDownloadProcess = null; let currentDownloadProcess = null;
// Détecte si une URL est une playlist
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 200);
const playlistPath = path.join(basePath, sanitizedTitle);
try {
if (!fs.existsSync(playlistPath)) {
fs.mkdirSync(playlistPath, { recursive: true });
logger.info(`Playlist folder created: ${playlistPath}`);
return playlistPath;
}
let counter = 1;
let newPath;
while (counter <= 1000) {
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
if (!fs.existsSync(newPath)) {
fs.mkdirSync(newPath, { recursive: true });
logger.info(`Playlist folder created with increment: ${newPath}`);
return newPath;
}
counter++;
}
logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
} catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
// Fallback: create folder "Untitled Playlist X"
let counter = 1;
let fallbackPath;
while (counter <= 1000) {
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
if (!fs.existsSync(fallbackPath)) {
try {
fs.mkdirSync(fallbackPath, { recursive: true });
logger.info(`Fallback playlist folder created: ${fallbackPath}`);
return fallbackPath;
} catch (mkErr) {
counter++;
continue;
}
}
counter++;
}
logger.error(`Could not create playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
}
}
function fetchDownload(options, listeners, speedListeners, stageListeners) { function fetchDownload(options, listeners, speedListeners, stageListeners) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const outputFolder = options.outputFolder || defaultDownloadFolder; logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
let outputFolder = options.outputFolder || defaultDownloadFolder;
// Normalize path and validate it's safe (within Users folder) // Normalize path and validate it's safe (within Users folder)
let safeOutputFolder = path.resolve(outputFolder); let safeOutputFolder = path.resolve(outputFolder);
@@ -31,6 +98,22 @@ function fetchDownload(options, listeners, speedListeners, stageListeners) {
return reject(new Error(`Unable to create download folder: ${err.message}`)); return reject(new Error(`Unable to create download folder: ${err.message}`));
} }
// Détecte si c'est une playlist et crée un dossier approprié
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
if (isPlaylist && configFeatures.createPlaylistFolders) {
try {
const playlistName = options.playlistTitle || "Untitled Playlist";
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
logger.info(`Playlist detected, using folder: ${safeOutputFolder}`);
} catch (err) {
logger.error(`Failed to create playlist folder: ${err.message}`);
return reject(err);
}
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
}
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder }); const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
logger.info(`[yt-dlp args] ${args.join(" ")}`); logger.info(`[yt-dlp args] ${args.join(" ")}`);