feat(youtube): indirme ayarları ve müzik tespiti ekle

YouTube indirmeleri için çözünürlük ve ses-only seçenekleri içeren
ayarlar sistemi eklendi. Kullanıcılar artık tercih ettikleri video
kalitesini (1080p, 720p, 480p, 360p, 240p, 144p) ve ses-only
indirme seçeneğini kaydedebilir. Müzik içeriklerini daha iyi tespit
etmek için yeni algoritma ve API endpoint'leri eklendi.
This commit is contained in:
2025-12-14 21:05:38 +03:00
parent e3d0eaf8cf
commit 47fe6390cc

View File

@@ -76,6 +76,16 @@ const YT_COOKIES_PATH =
process.env.YT_DLP_COOKIES || process.env.YT_DLP_COOKIES ||
process.env.YT_DLP_COOKIE_FILE || process.env.YT_DLP_COOKIE_FILE ||
path.join(CACHE_DIR, "yt_cookies.txt"); path.join(CACHE_DIR, "yt_cookies.txt");
const YT_SETTINGS_PATH = path.join(CACHE_DIR, "yt_settings.json");
const YT_DEFAULT_RESOLUTION = "1080p";
const YT_ALLOWED_RESOLUTIONS = new Set([
"1080p",
"720p",
"480p",
"360p",
"240p",
"144p"
]);
const YT_EXTRACTOR_ARGS = const YT_EXTRACTOR_ARGS =
process.env.YT_DLP_EXTRACTOR_ARGS || null; process.env.YT_DLP_EXTRACTOR_ARGS || null;
let resolvedYtDlpBinary = null; let resolvedYtDlpBinary = null;
@@ -625,8 +635,10 @@ function determineMediaType({
movieMatch, movieMatch,
seriesEpisode, seriesEpisode,
categories, categories,
relPath relPath,
audioOnly = false
}) { }) {
if (audioOnly) return "music";
if (seriesEpisode) return "tv"; if (seriesEpisode) return "tv";
if (movieMatch) return "movie"; if (movieMatch) return "movie";
if ( if (
@@ -686,6 +698,7 @@ function normalizeYoutubeWatchUrl(value) {
function startYoutubeDownload(url) { function startYoutubeDownload(url) {
const normalized = normalizeYoutubeWatchUrl(url); const normalized = normalizeYoutubeWatchUrl(url);
if (!normalized) return null; if (!normalized) return null;
const ytSettings = loadYoutubeSettings();
const videoId = new URL(normalized).searchParams.get("v"); const videoId = new URL(normalized).searchParams.get("v");
const folderId = `yt_${videoId}_${Date.now().toString(36)}`; const folderId = `yt_${videoId}_${Date.now().toString(36)}`;
const savePath = path.join(DOWNLOAD_DIR, folderId); const savePath = path.join(DOWNLOAD_DIR, folderId);
@@ -716,6 +729,8 @@ function startYoutubeDownload(url) {
error: null, error: null,
debug: { binary: null, args: null, logs: [] } debug: { binary: null, args: null, logs: [] }
}; };
job.resolution = ytSettings.resolution;
job.onlyAudio = ytSettings.onlyAudio;
youtubeJobs.set(job.id, job); youtubeJobs.set(job.id, job);
launchYoutubeJob(job); launchYoutubeJob(job);
@@ -738,9 +753,49 @@ function appendYoutubeLog(job, line) {
job.debug.logs = lines; job.debug.logs = lines;
} }
function loadYoutubeSettings() {
const defaults = {
resolution: YT_DEFAULT_RESOLUTION,
onlyAudio: false
};
try {
if (!fs.existsSync(YT_SETTINGS_PATH)) return defaults;
const raw = fs.readFileSync(YT_SETTINGS_PATH, "utf-8");
const parsed = JSON.parse(raw);
const resolution = YT_ALLOWED_RESOLUTIONS.has(parsed?.resolution)
? parsed.resolution
: YT_DEFAULT_RESOLUTION;
const onlyAudio = Boolean(parsed?.onlyAudio);
return { resolution, onlyAudio };
} catch (err) {
console.warn("⚠️ YouTube ayarları okunamadı, varsayılan kullanılacak:", err.message);
return defaults;
}
}
function saveYoutubeSettings({ resolution, onlyAudio }) {
const resValue = YT_ALLOWED_RESOLUTIONS.has(resolution)
? resolution
: YT_DEFAULT_RESOLUTION;
const settings = { resolution: resValue, onlyAudio: Boolean(onlyAudio) };
ensureDirForFile(YT_SETTINGS_PATH);
fs.writeFileSync(YT_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
return settings;
}
function buildYoutubeFormat({ resolution, onlyAudio }) {
if (onlyAudio) return "bestaudio/b";
const match = String(resolution || YT_DEFAULT_RESOLUTION).match(/(\\d+)/);
const height = match ? Number(match[1]) : 1080;
const safeHeight = Number.isFinite(height) && height > 0 ? height : 1080;
return `bestvideo[height<=${safeHeight}]+bestaudio/best[height<=${safeHeight}]`;
}
function launchYoutubeJob(job) { function launchYoutubeJob(job) {
const binary = getYtDlpBinary(); const binary = getYtDlpBinary();
const jsRuntimeArg = process.env.YT_DLP_JS_RUNTIME || "node"; const jsRuntimeArg = process.env.YT_DLP_JS_RUNTIME || "node";
const ytSettings = loadYoutubeSettings();
const cookieFile = const cookieFile =
(YT_COOKIES_PATH && fs.existsSync(YT_COOKIES_PATH) && YT_COOKIES_PATH) || (YT_COOKIES_PATH && fs.existsSync(YT_COOKIES_PATH) && YT_COOKIES_PATH) ||
@@ -750,9 +805,11 @@ function launchYoutubeJob(job) {
YT_EXTRACTOR_ARGS || YT_EXTRACTOR_ARGS ||
(cookieFile ? "youtube:player-client=web" : "youtube:player-client=android"); (cookieFile ? "youtube:player-client=web" : "youtube:player-client=android");
const formatSelector = buildYoutubeFormat(ytSettings);
const args = [ const args = [
"-f", "-f",
"bv+ba/b", formatSelector,
"--write-thumbnail", "--write-thumbnail",
"--convert-thumbnails", "--convert-thumbnails",
"jpg", "jpg",
@@ -772,7 +829,10 @@ function launchYoutubeJob(job) {
logs: [], logs: [],
jsRuntime: jsRuntimeArg, jsRuntime: jsRuntimeArg,
cookies: cookieFile, cookies: cookieFile,
extractorArgs: extractorArgValue extractorArgs: extractorArgValue,
resolution: ytSettings.resolution,
onlyAudio: ytSettings.onlyAudio,
format: formatSelector
}; };
const child = spawn(binary, args, { const child = spawn(binary, args, {
cwd: job.savePath, cwd: job.savePath,
@@ -1015,6 +1075,23 @@ function deriveYoutubeTitle(fileName, videoId) {
return cleaned.replace(/[-_.]+$/g, "").trim() || base; return cleaned.replace(/[-_.]+$/g, "").trim() || base;
} }
function isYoutubeMusic(infoJson, mediaInfo, audioOnlyFlag = false) {
if (audioOnlyFlag) return true;
const categories = Array.isArray(infoJson?.categories)
? infoJson.categories.map((c) => String(c).toLowerCase())
: [];
if (categories.includes("music")) return true;
const tags = Array.isArray(infoJson?.tags)
? infoJson.tags.map((t) => String(t).toLowerCase())
: [];
if (tags.some((t) => t.includes("music"))) return true;
// Sadece ses akışı varsa müzik kabul et
if (!mediaInfo?.video && mediaInfo?.audio) return true;
return false;
}
async function writeYoutubeMetadata(job, videoPath, mediaInfo, infoJsonFile) { async function writeYoutubeMetadata(job, videoPath, mediaInfo, infoJsonFile) {
const targetDir = path.join(YT_DATA_ROOT, job.folderId); const targetDir = path.join(YT_DATA_ROOT, job.folderId);
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
@@ -1031,12 +1108,14 @@ async function writeYoutubeMetadata(job, videoPath, mediaInfo, infoJsonFile) {
const categories = Array.isArray(infoJson?.categories) const categories = Array.isArray(infoJson?.categories)
? infoJson.categories ? infoJson.categories
: null; : null;
const isAudioOnly = isYoutubeMusic(infoJson, mediaInfo, Boolean(job.onlyAudio));
const derivedType = determineMediaType({ const derivedType = determineMediaType({
tracker: "youtube", tracker: "youtube",
movieMatch: null, movieMatch: null,
seriesEpisode: null, seriesEpisode: null,
categories, categories,
relPath: job.files?.[0]?.name || null relPath: job.files?.[0]?.name || null,
audioOnly: isAudioOnly
}); });
const payload = { const payload = {
id: job.id, id: job.id,
@@ -4291,7 +4370,8 @@ async function onTorrentDone({ torrent }) {
movieMatch: null, movieMatch: null,
seriesEpisode: null, seriesEpisode: null,
categories: null, categories: null,
relPath: normalizedRelPath relPath: normalizedRelPath,
audioOnly: false
}) })
}; };
@@ -4346,7 +4426,8 @@ async function onTorrentDone({ torrent }) {
movieMatch: null, movieMatch: null,
seriesEpisode: seriesEpisodes[normalizedRelPath], seriesEpisode: seriesEpisodes[normalizedRelPath],
categories: null, categories: null,
relPath: normalizedRelPath relPath: normalizedRelPath,
audioOnly: false
}); });
} }
} catch (err) { } catch (err) {
@@ -4419,7 +4500,8 @@ async function onTorrentDone({ torrent }) {
movieMatch: ensuredMedia.metadata, movieMatch: ensuredMedia.metadata,
seriesEpisode: seriesEpisodes[bestVideoPath] || null, seriesEpisode: seriesEpisodes[bestVideoPath] || null,
categories: null, categories: null,
relPath: bestVideoPath relPath: bestVideoPath,
audioOnly: false
}); });
perFileMetadata[bestVideoPath] = { perFileMetadata[bestVideoPath] = {
...(perFileMetadata[bestVideoPath] || {}), ...(perFileMetadata[bestVideoPath] || {}),
@@ -4454,7 +4536,8 @@ async function onTorrentDone({ torrent }) {
movieMatch: infoUpdate.files[bestVideoPath].movieMatch || null, movieMatch: infoUpdate.files[bestVideoPath].movieMatch || null,
seriesEpisode: seriesEpisodes[bestVideoPath] || null, seriesEpisode: seriesEpisodes[bestVideoPath] || null,
categories: null, categories: null,
relPath: bestVideoPath relPath: bestVideoPath,
audioOnly: false
}); });
infoUpdate.type = rootType; infoUpdate.type = rootType;
infoUpdate.files[bestVideoPath].type = infoUpdate.files[bestVideoPath].type =
@@ -5920,6 +6003,41 @@ app.post("/api/youtube/cookies", requireAuth, (req, res) => {
} }
}); });
// --- 🎚️ YouTube kalite ayarları ---
app.get("/api/youtube/settings", requireAuth, (req, res) => {
try {
const settings = loadYoutubeSettings();
let updatedAt = null;
if (fs.existsSync(YT_SETTINGS_PATH)) {
updatedAt = fs.statSync(YT_SETTINGS_PATH).mtimeMs;
}
res.json({ ok: true, ...settings, updatedAt });
} catch (err) {
console.warn("⚠️ YouTube ayarları okunamadı:", err.message);
res.status(500).json({ error: "Ayarlar okunamadı." });
}
});
app.post("/api/youtube/settings", requireAuth, (req, res) => {
try {
const { resolution, onlyAudio } = req.body || {};
if (resolution && !YT_ALLOWED_RESOLUTIONS.has(resolution)) {
return res.status(400).json({ error: "Geçersiz çözünürlük." });
}
const saved = saveYoutubeSettings({
resolution: resolution || YT_DEFAULT_RESOLUTION,
onlyAudio: Boolean(onlyAudio)
});
const stat = fs.existsSync(YT_SETTINGS_PATH)
? fs.statSync(YT_SETTINGS_PATH).mtimeMs
: Date.now();
res.json({ ok: true, ...saved, updatedAt: stat });
} catch (err) {
console.error("❌ YouTube ayarları kaydedilemedi:", err.message);
res.status(500).json({ error: "Ayarlar kaydedilemedi." });
}
});
// --- 🎫 YouTube cookies yönetimi --- // --- 🎫 YouTube cookies yönetimi ---
app.get("/api/youtube/cookies", requireAuth, (req, res) => { app.get("/api/youtube/cookies", requireAuth, (req, res) => {
try { try {