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:
134
server/server.js
134
server/server.js
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user