Compare commits

..

2 Commits

6 changed files with 66 additions and 103 deletions

View File

@@ -2,6 +2,7 @@ const { fetchInfo } = require("../services/info.services");
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers"); const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
const { logger } = require("../logger"); const { logger } = require("../logger");
const { isValidUrl } = require("../helpers/validation.helpers"); const { isValidUrl } = require("../helpers/validation.helpers");
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
/** /**
* Handles metadata retrieval for a video or playlist. * Handles metadata retrieval for a video or playlist.
@@ -36,7 +37,7 @@ async function infoController(req, res) {
logger.info(`Info request received. ENCODED: ${encodedUrl}`); logger.info(`Info request received. ENCODED: ${encodedUrl}`);
// Lightweight heuristic to detect playlist URLs. It works for Youtube. // Lightweight heuristic to detect playlist URLs. It works for Youtube.
const isPlaylistUrl = url.includes("&list") || url.includes("?list"); const isPlaylistUrl = isUrlPlaylist(url);
logger.info( logger.info(
isPlaylistUrl isPlaylistUrl

View File

@@ -1,6 +1,6 @@
const path = require("path"); const path = require("path");
const getUserBrowser = require("./getBrowser.helpers.js"); const {getUserBrowser} = require("./getBrowser.helpers.js");
const { ffmpegPath, denoPath} = require("./path.helpers.js"); const { ffmpegPath, denoPath} = require("./path.helpers.js");
const { configFeatures } = require("../../config.js"); const { configFeatures } = require("../../config.js");
const { logger } = require("../logger.js"); const { logger } = require("../logger.js");

View File

@@ -1,7 +1,6 @@
const fs = require("fs"); const fs = require("fs");
const os = require("os"); const os = require("os");
const path = require("path"); const notify = require("./notify.helpers");
const notify = require("./notify.helpers")
const { logger } = require("../logger"); const { logger } = require("../logger");
/** /**
@@ -22,41 +21,38 @@ const { logger } = require("../logger");
* @returns {string} The detected browser identifier. * @returns {string} The detected browser identifier.
*/ */
function getUserBrowser() { function getUserBrowser() {
//const userProfile = os.homedir(); const platform = process.platform;
const firefoxPath = getFirefoxProfilePath(); let isFirefoxHere = false;
/** if (platform === "win32") {
* Browsers supported by yt-dlp cookie extraction. const paths = [
* "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
* Only Firefox is currently enabled and tested. "C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
* Additional browsers can be enabled in the future `${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
* once compatibility has been verified. ];
*/
const browsers = [
{ name: "firefox", path: firefoxPath },
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
];
// Search for the first available browser profile. isFirefoxHere = paths.some(p => fs.existsSync(p));
for (const browser of browsers) {
if (browser.path && fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`);
return browser.name;
}
} }
// No supported browser found => Notify User if (platform === "linux") {
logger.warn("No supported browser found on the system"); const paths = [
"/usr/bin/firefox",
"/usr/bin/firefox-esr",
"/usr/local/bin/firefox",
"/snap/bin/firefox",
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
];
isFirefoxHere = paths.some(p => fs.existsSync(p));
}
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad if (!isFirefoxHere) {
if (process.platform === "win32") { // If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
// No supported browser found => Notify User
logger.warn("No supported browser found on the system");
notify.notifyFirefoxBrowserMissing(); notify.notifyFirefoxBrowserMissing();
} else {
logger.info("Browser found: firefox");
} }
// Fallback to Firefox and let yt-dlp handle the error gracefully. // Fallback to Firefox and let yt-dlp handle the error gracefully.
@@ -64,39 +60,4 @@ function getUserBrowser() {
return "firefox"; return "firefox";
} }
function getFirefoxProfilePath() { module.exports = {getUserBrowser};
const home = os.homedir();
if (process.platform === "win32") {
return path.join(
home,
"AppData",
"Roaming",
"Mozilla",
"Firefox",
"Profiles"
);
}
if (process.platform === "linux") {
return path.join(
"usr",
"bin",
"firefox"
);
}
if (process.platform === "darwin") {
return path.join(
home,
"Library",
"Application Support",
"Firefox",
"Profiles"
);
}
return null;
}
module.exports = getUserBrowser

View File

@@ -0,0 +1,12 @@
/**
* Determines whether a URL targets a playlist.
*
* The detection is based on the presence of the YouTube
* playlist query parameter (`list`).
*
* @param {string} url - URL to inspect.
* @returns {boolean} True if the URL appears to be a playlist.
*/
export function isUrlPlaylist(url) {
return url.includes("?list=") || url.includes("&list=") || url.includes("@");
}

View File

@@ -7,6 +7,7 @@ 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 {reloadFeatures} = require("../../config"); const {reloadFeatures} = require("../../config");
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
/** /**
* Reference to the currently running yt-dlp process. * Reference to the currently running yt-dlp process.
@@ -18,19 +19,6 @@ const {reloadFeatures} = require("../../config");
*/ */
let currentDownloadProcess = null; let currentDownloadProcess = null;
/**
* Determines whether a URL targets a playlist.
*
* The detection is based on the presence of the YouTube
* playlist query parameter (`list`).
*
* @param {string} url - URL to inspect.
* @returns {boolean} True if the URL appears to be a playlist.
*/
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
/** /**
* Creates a dedicated download folder for a playlist. * Creates a dedicated download folder for a playlist.
* *
@@ -49,23 +37,23 @@ function isPlaylistUrl(url) {
*/ */
function createPlaylistFolder(basePath, playlistTitle) { function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '') .replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim() .trim()
.substring(0, 200); .substring(0, 200);
const playlistPath = path.join(basePath, sanitizedTitle); const playlistPath = path.join(basePath, sanitizedTitle);
try { try {
if (!fs.existsSync(playlistPath)) { if (!fs.existsSync(playlistPath)) {
fs.mkdirSync(playlistPath, { recursive: true }); fs.mkdirSync(playlistPath, { recursive: true });
logger.info(`Playlist folder created: ${playlistPath}`); logger.info(`Playlist folder created: ${playlistPath}`);
return playlistPath; return playlistPath;
} }
let counter = 1; let counter = 1;
let newPath; let newPath;
while (counter <= 1000) { while (counter <= 1000) {
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`); newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
if (!fs.existsSync(newPath)) { if (!fs.existsSync(newPath)) {
@@ -75,17 +63,17 @@ function createPlaylistFolder(basePath, playlistTitle) {
} }
counter++; counter++;
} }
logger.error(`Could not find available playlist folder after 1000 attempts`); logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder"); throw new Error("Unable to create playlist folder");
} catch (err) { } catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`); logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
// Fallback: create folder "Untitled Playlist X" // Fallback: create folder "Untitled Playlist X"
let counter = 1; let counter = 1;
let fallbackPath; let fallbackPath;
while (counter <= 1000) { while (counter <= 1000) {
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`); fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
if (!fs.existsSync(fallbackPath)) { if (!fs.existsSync(fallbackPath)) {
@@ -100,7 +88,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
} }
counter++; counter++;
} }
logger.error(`Could not create playlist folder after 1000 attempts`); logger.error(`Could not create playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder"); throw new Error("Unable to create playlist folder");
} }
@@ -139,17 +127,17 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
const userConfig = reloadFeatures(); const userConfig = reloadFeatures();
logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`); logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`);
let outputFolder = options.outputFolder || defaultDownloadFolder; 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);
if (!isSafePath(safeOutputFolder)) { if (!isSafePath(safeOutputFolder)) {
logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`); logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`);
safeOutputFolder = path.resolve(defaultDownloadFolder); safeOutputFolder = path.resolve(defaultDownloadFolder);
} }
// Create download folder if it doesn't exist // Create download folder if it doesn't exist
try { try {
fs.mkdirSync(safeOutputFolder, { recursive: true }); fs.mkdirSync(safeOutputFolder, { recursive: true });
@@ -160,7 +148,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
} }
// Détecte si c'est une playlist et crée un dossier approprié // Détecte si c'est une playlist et crée un dossier approprié
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url); const isPlaylist = options.playlistTitle || isUrlPlaylist(options.url);
if (isPlaylist && userConfig.createPlaylistFolders) { if (isPlaylist && userConfig.createPlaylistFolders) {
try { try {
@@ -196,13 +184,13 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
else reject(new Error(`YT-DLP failed with code : ${code}`)); else reject(new Error(`YT-DLP failed with code : ${code}`));
}); });
child.stdout.on("data", data => { child.stdout.on("data", data => {
data.toString().split("\n").forEach(line => { data.toString().split("\n").forEach(line => {
if (!line.trim()) return; if (!line.trim()) return;
logger.info(`[yt-dlp] ${line}`); logger.info(`[yt-dlp] ${line}`);
// Progress Bar // Progress Bar
if (line.startsWith("[download] Destination:")) { if (line.startsWith("[download] Destination:")) {
progressListeners.forEach(fn => fn("reset")); progressListeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading...")); stageListeners.forEach(fn => fn("Downloading..."));
@@ -253,7 +241,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`)); data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`));
}); });
}); });
} }
@@ -270,7 +258,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
function cancelDownload() { function cancelDownload() {
if (currentDownloadProcess) { if (currentDownloadProcess) {
logger.info("Cancelling download and killing all child processes..."); logger.info("Cancelling download and killing all child processes...");
// Force kill the process and all its children with SIGKILL // Force kill the process and all its children with SIGKILL
try { try {
// Try to kill with SIGKILL on Windows (process group) or Unix // Try to kill with SIGKILL on Windows (process group) or Unix
@@ -286,7 +274,7 @@ function cancelDownload() {
// Fallback to regular kill // Fallback to regular kill
currentDownloadProcess.kill('SIGKILL'); currentDownloadProcess.kill('SIGKILL');
} }
currentDownloadProcess = null; currentDownloadProcess = null;
logger.info("Download cancelled successfully"); logger.info("Download cancelled successfully");
return true; return true;

View File

@@ -1,6 +1,6 @@
const { execFile } = require("child_process"); const { execFile } = require("child_process");
const { userYtDlp, denoPath } = require("../helpers/path.helpers"); const { userYtDlp, denoPath } = require("../helpers/path.helpers");
const getUserBrowser = require("../helpers/getBrowser.helpers"); const {getUserBrowser} = require("../helpers/getBrowser.helpers");
const { logger } = require("../logger"); const { logger } = require("../logger");
/** /**
@@ -61,6 +61,7 @@ function fetchInfo(url) {
try { try {
const data = JSON.parse(stdout); const data = JSON.parse(stdout);
resolve(data); resolve(data);
} catch (e) { } catch (e) {
logger.error(`JSON Parsing Error: ${e.message}`); logger.error(`JSON Parsing Error: ${e.message}`);