mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-11-13 23:19:21 +01:00
refactor(info): responsabilities is now better managed between controller and services.
Error is handled better. Normalize info
This commit is contained in:
@@ -1,129 +1,41 @@
|
|||||||
const { fetchInfo } = require("../services/info.services");
|
const {fetchNormalizedInfo} = require("../services/info.services");
|
||||||
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.
|
||||||
*
|
*
|
||||||
* This controller:
|
* Returns normalized JSON responses. Accepts URL via query (?url=...)
|
||||||
* - Validates incoming URL (query or body)
|
* or request body for flexibility.
|
||||||
* - Calls yt-dlp metadata extraction service
|
|
||||||
* - Detects playlist vs single video context
|
|
||||||
* - Applies fallback strategy if playlist extraction fails
|
|
||||||
* - Returns normalized parsed response (video or playlist)
|
|
||||||
*
|
*
|
||||||
* @param {import("express").Request} req
|
* @param {import("express").Request} req
|
||||||
* @param {import("express").Response} res
|
* @param {import("express").Response} res
|
||||||
*/
|
*/
|
||||||
async function infoController(req, res) {
|
async function infoController(req, res) {
|
||||||
|
try {
|
||||||
|
const url = req.query.url || (req.body && req.body.url);
|
||||||
|
const encodedUrl = typeof url === "string" ? encodeURIComponent(url) : undefined;
|
||||||
|
|
||||||
// Previously, i support both POST and GET element, but it's kinda weird. So now it only support GET.
|
|
||||||
const url = req.query.url;
|
|
||||||
const encodedUrl = encodeURIComponent(url)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic validation:
|
|
||||||
* - URL must exist
|
|
||||||
* - Must be a string
|
|
||||||
* - Must pass custom URL validation
|
|
||||||
*/
|
|
||||||
if (!url || typeof url !== "string" || !isValidUrl(url)) {
|
if (!url || typeof url !== "string" || !isValidUrl(url)) {
|
||||||
return res.status(400).send("Invalid URL Or Missing");
|
return res.status(400).json({
|
||||||
|
error: {code: "INVALID_URL", message: "Invalid or missing URL"},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Info request received. RAW URL: ${url}`);
|
logger.info(`Info request received. RAW URL: ${url}`);
|
||||||
|
if (encodedUrl) {
|
||||||
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
const result = await fetchNormalizedInfo(url);
|
||||||
const isPlaylistUrl = isUrlPlaylist(url);
|
return res.json(result);
|
||||||
|
|
||||||
logger.info(
|
|
||||||
isPlaylistUrl
|
|
||||||
? "Estimated Data Type: Playlist"
|
|
||||||
: "Estimated Data Type: Video"
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primary metadata fetch attempt
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
data = await fetchInfo(url);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback strategy:
|
|
||||||
* If playlist extraction fails, retry as single video
|
|
||||||
*/
|
|
||||||
if (isPlaylistUrl) {
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Playlist fetch failed: ${err.message}. Retrying as single video...`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove playlist query parameter (best-effort cleanup)
|
|
||||||
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = await fetchInfo(singleVideoUrl);
|
|
||||||
logger.info("Single video fetch succeeded after playlist failure");
|
|
||||||
} catch (retryErr) {
|
|
||||||
logger.error(
|
|
||||||
`Single video fetch also failed: ${retryErr.message}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Preserve original error context
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize response based on yt-dlp output type
|
|
||||||
*/
|
|
||||||
if (data._type === "playlist") {
|
|
||||||
|
|
||||||
// If the playlist got only one video, show it as a video.
|
|
||||||
if (data.entries && data.entries.length === 1) {
|
|
||||||
|
|
||||||
const singleVideo = parseVideo(data.entries[0]);
|
|
||||||
|
|
||||||
logger.info(`Playlist with 1 item converted to video: ${singleVideo.title}`);
|
|
||||||
|
|
||||||
return res.json(singleVideo);
|
|
||||||
}
|
|
||||||
|
|
||||||
const playlist = parsePlaylist(data);
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Playlist fetched: ${playlist.title} (${playlist.count} videos)`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json(playlist);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const video = parseVideo(data);
|
|
||||||
|
|
||||||
logger.info(`Video fetched: ${video.title}`);
|
|
||||||
|
|
||||||
return res.json(video);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Info controller error: ${err && err.message ? err.message : err}`
|
`Info controller error: ${err && err.message ? err.message : err}`
|
||||||
);
|
);
|
||||||
|
return res.status(500).json({
|
||||||
return res.status(500).send("Unable to fetch info");
|
error: {code: "FETCH_FAILED", message: "Unable to fetch info"},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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");
|
||||||
|
const {parseVideo, parsePlaylist} = require("../helpers/parseInfo.helpers");
|
||||||
|
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves metadata for a video or playlist using yt-dlp.
|
* Retrieves metadata for a video or playlist using yt-dlp.
|
||||||
@@ -21,33 +23,44 @@ const { logger } = require("../logger");
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function fetchInfo(url) {
|
function fetchInfo(url) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base yt-dlp arguments used to retrieve metadata
|
* Base yt-dlp arguments used to retrieve metadata
|
||||||
* without downloading media files.
|
* without downloading media files.
|
||||||
*/
|
*/
|
||||||
const metadataArgs = [
|
const metadataArgs = [
|
||||||
"--js-runtimes", `deno:${denoPath}`,
|
"--js-runtimes",
|
||||||
|
`deno:${denoPath}`,
|
||||||
"--dump-single-json",
|
"--dump-single-json",
|
||||||
"--ignore-errors",
|
"--ignore-errors",
|
||||||
"--yes-playlist",
|
"--yes-playlist",
|
||||||
"--cookies-from-browser",`${getUserBrowser()}`,
|
"--cookies-from-browser",
|
||||||
"--extractor-args","youtube:player_client=default",
|
`${getUserBrowser()}`,
|
||||||
|
"--extractor-args",
|
||||||
|
"youtube:player_client=default",
|
||||||
"--ignore-no-formats-error",
|
"--ignore-no-formats-error",
|
||||||
];
|
];
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile(
|
||||||
execFile(userYtDlp,[...metadataArgs , url],
|
userYtDlp,
|
||||||
{ timeout: 30_000, // 30s, if more timeout
|
[...metadataArgs, url],
|
||||||
maxBuffer: 10 * 1024 * 1024 }, // Maximum response size: 10 MB (Node.js default is much smaller)
|
{
|
||||||
|
timeout: 30_000, // 30s, if more timeout
|
||||||
|
maxBuffer: 10 * 1024 * 1024, // Maximum response size: 10 MB (Node.js default is much smaller)
|
||||||
|
},
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
const lower = stderr.toLowerCase();
|
const lower = stderr?.toLowerCase();
|
||||||
if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) {
|
if (
|
||||||
return reject(new Error(`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`));
|
lower.includes("sign in to confirm") ||
|
||||||
|
lower.includes("failed to decrypt") ||
|
||||||
|
lower.includes("could not copy")
|
||||||
|
) {
|
||||||
|
return reject(
|
||||||
|
new Error(
|
||||||
|
`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +74,66 @@ 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}`);
|
||||||
reject(new Error("JSON unreadable."));
|
reject(new Error("JSON unreadable."));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { fetchInfo };
|
/**
|
||||||
|
* Fetches and normalizes metadata for a video or playlist.
|
||||||
|
* Handles playlist fallback to single video when playlist extraction fails
|
||||||
|
* or when playlist contains only one entry.
|
||||||
|
*
|
||||||
|
* @param {string} url - Video or playlist URL
|
||||||
|
* @returns {Promise<Object>} Normalized metadata object
|
||||||
|
*/
|
||||||
|
async function fetchNormalizedInfo(url) {
|
||||||
|
if (!url || typeof url !== "string") {
|
||||||
|
throw new Error("Invalid URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPlaylistUrl = isUrlPlaylist(url);
|
||||||
|
let data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = await fetchInfo(url);
|
||||||
|
} catch (err) {
|
||||||
|
if (isPlaylistUrl) {
|
||||||
|
logger.info(
|
||||||
|
`Playlist fetch failed: ${err.message}. Retrying as single video...`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
|
||||||
|
data = await fetchInfo(singleVideoUrl);
|
||||||
|
logger.info("Single video fetch succeeded after playlist failure");
|
||||||
|
} catch (retryErr) {
|
||||||
|
logger.error(`Single video fetch also failed: ${retryErr.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data._type === "playlist") {
|
||||||
|
if (data.entries && data.entries.length === 1) {
|
||||||
|
const singleVideo = parseVideo(data.entries[0]);
|
||||||
|
logger.info(`Playlist with 1 item converted to video: ${singleVideo.title}`);
|
||||||
|
return singleVideo;
|
||||||
|
}
|
||||||
|
const playlist = parsePlaylist(data);
|
||||||
|
logger.info(`Playlist fetched: ${playlist.title} (${playlist.count} videos)`);
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = parseVideo(data);
|
||||||
|
logger.info(`Video fetched: ${video.title}`);
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {fetchInfo, fetchNormalizedInfo};
|
||||||
|
|||||||
Reference in New Issue
Block a user