feat(youtube): ses dosyası indirme ve hata yönetimini iyileştir

Sadece ses dosyası indirme özelliğini geliştir ve hata yönetimini güçlendir:

- Ses indirme formatını m4a/mp4/webm opus önceliğiyle optimize et
- İş bazında ayarları destekle ve varsayılan ayarlara geri düş
- Sadece ses indirmeleri için özel extractor argümanları kullan
- yt-dlp hata koduna rağmen medya dosyası bulunursa devam et
- Hem ses hem video dosyalarını tespit eden yeni findYoutubeMediaFile fonksiyonu ekle
- Müzik tespiti mantığını iyileştir ve ses bayrağını dikkate al
This commit is contained in:
2025-12-14 21:40:29 +03:00
parent 47fe6390cc
commit 60fb7b8bc6

View File

@@ -785,8 +785,11 @@ function saveYoutubeSettings({ resolution, onlyAudio }) {
} }
function buildYoutubeFormat({ resolution, onlyAudio }) { function buildYoutubeFormat({ resolution, onlyAudio }) {
if (onlyAudio) return "bestaudio/b"; if (onlyAudio) {
const match = String(resolution || YT_DEFAULT_RESOLUTION).match(/(\\d+)/); // Tarayıcıda çalınabilir bir ses dosyası (öncelik m4a/mp4/webm opus) indir
return "ba[ext=m4a]/ba[ext=mp4]/ba[acodec^=opus]/bestaudio";
}
const match = String(resolution || YT_DEFAULT_RESOLUTION).match(/(\d+)/);
const height = match ? Number(match[1]) : 1080; const height = match ? Number(match[1]) : 1080;
const safeHeight = Number.isFinite(height) && height > 0 ? height : 1080; const safeHeight = Number.isFinite(height) && height > 0 ? height : 1080;
return `bestvideo[height<=${safeHeight}]+bestaudio/best[height<=${safeHeight}]`; return `bestvideo[height<=${safeHeight}]+bestaudio/best[height<=${safeHeight}]`;
@@ -795,7 +798,14 @@ function buildYoutubeFormat({ resolution, onlyAudio }) {
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 fallbackSettings = loadYoutubeSettings();
const ytSettings = {
resolution: job?.resolution || fallbackSettings.resolution,
onlyAudio:
typeof job?.onlyAudio === "boolean"
? job.onlyAudio
: fallbackSettings.onlyAudio
};
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) ||
@@ -803,7 +813,11 @@ function launchYoutubeJob(job) {
const extractorArgValue = const extractorArgValue =
YT_EXTRACTOR_ARGS || YT_EXTRACTOR_ARGS ||
(cookieFile ? "youtube:player-client=web" : "youtube:player-client=android"); (ytSettings.onlyAudio
? "youtube:player-client=web_safari,web,ios,mweb"
: cookieFile
? "youtube:player-client=web"
: "youtube:player-client=android");
const formatSelector = buildYoutubeFormat(ytSettings); const formatSelector = buildYoutubeFormat(ytSettings);
@@ -818,6 +832,17 @@ function launchYoutubeJob(job) {
jsRuntimeArg, jsRuntimeArg,
"--extractor-args", "--extractor-args",
extractorArgValue, extractorArgValue,
...(ytSettings.onlyAudio
? [
"--extract-audio",
"--audio-format",
"m4a",
"--audio-quality",
"0",
"--remux-audio",
"m4a"
]
: []),
...(cookieFile && fs.existsSync(cookieFile) ...(cookieFile && fs.existsSync(cookieFile)
? ["--cookies", cookieFile] ? ["--cookies", cookieFile]
: []), : []),
@@ -941,7 +966,8 @@ function updateYoutubeProgress(job, match) {
async function finalizeYoutubeJob(job, exitCode) { async function finalizeYoutubeJob(job, exitCode) {
job.downloadSpeed = 0; job.downloadSpeed = 0;
if (exitCode !== 0) { const fallbackMedia = findYoutubeMediaFile(job.savePath, Boolean(job.onlyAudio));
if (exitCode !== 0 && !fallbackMedia) {
job.state = "error"; job.state = "error";
const tail = job.debug?.logs ? job.debug.logs.slice(-8) : []; const tail = job.debug?.logs ? job.debug.logs.slice(-8) : [];
job.error = `yt-dlp ${exitCode} kodu ile sonlandı`; job.error = `yt-dlp ${exitCode} kodu ile sonlandı`;
@@ -958,6 +984,11 @@ async function finalizeYoutubeJob(job, exitCode) {
broadcastSnapshot(); broadcastSnapshot();
return; return;
} }
if (exitCode !== 0 && fallbackMedia) {
console.warn(
`⚠️ yt-dlp çıkış kodu ${exitCode} ancak medya bulundu, devam ediliyor: ${fallbackMedia}`
);
}
try { try {
if (job.currentStage && !job.currentStage.done && job.currentStage.totalBytes) { if (job.currentStage && !job.currentStage.done && job.currentStage.totalBytes) {
@@ -966,8 +997,11 @@ async function finalizeYoutubeJob(job, exitCode) {
} }
const infoJson = findYoutubeInfoJson(job.savePath); const infoJson = findYoutubeInfoJson(job.savePath);
const videoFile = findYoutubeVideoFile(job.savePath); const mediaFile = fallbackMedia || findYoutubeMediaFile(
if (!videoFile) { job.savePath,
Boolean(job.onlyAudio)
);
if (!mediaFile) {
job.state = "error"; job.state = "error";
job.error = "Video dosyası bulunamadı"; job.error = "Video dosyası bulunamadı";
console.warn("❌ yt-dlp çıktı video bulunamadı:", { console.warn("❌ yt-dlp çıktı video bulunamadı:", {
@@ -979,10 +1013,10 @@ async function finalizeYoutubeJob(job, exitCode) {
return; return;
} }
const absVideo = path.join(job.savePath, videoFile); const absMedia = path.join(job.savePath, mediaFile);
const stats = fs.statSync(absVideo); const stats = fs.statSync(absMedia);
const mediaInfo = await extractMediaInfo(absVideo).catch(() => null); const mediaInfo = await extractMediaInfo(absMedia).catch(() => null);
const relativeName = videoFile.replace(/\\/g, "/"); const relativeName = mediaFile.replace(/\\/g, "/");
job.files = [ job.files = [
{ {
index: 0, index: 0,
@@ -991,15 +1025,16 @@ async function finalizeYoutubeJob(job, exitCode) {
} }
]; ];
job.selectedIndex = 0; job.selectedIndex = 0;
job.title = deriveYoutubeTitle(videoFile, job.videoId); job.title = deriveYoutubeTitle(mediaFile, job.videoId);
job.downloaded = stats.size; job.downloaded = stats.size;
job.totalBytes = stats.size; job.totalBytes = stats.size;
job.progress = 1; job.progress = 1;
job.state = "completed"; job.state = "completed";
job.error = null;
const metadataPayload = await writeYoutubeMetadata( const metadataPayload = await writeYoutubeMetadata(
job, job,
absVideo, absMedia,
mediaInfo, mediaInfo,
infoJson infoJson
); );
@@ -1042,12 +1077,30 @@ async function finalizeYoutubeJob(job, exitCode) {
} }
} }
function findYoutubeVideoFile(savePath) { function findYoutubeMediaFile(savePath, preferAudio = false) {
const entries = fs.readdirSync(savePath, { withFileTypes: true }); const entries = fs.readdirSync(savePath, { withFileTypes: true });
const videos = entries const files = entries
.filter((entry) => entry.isFile()) .filter((entry) => entry.isFile())
.map((entry) => entry.name) .map((entry) => entry.name);
.filter((name) => VIDEO_EXTS.includes(path.extname(name).toLowerCase()));
const audioExts = Array.from(MUSIC_EXTENSIONS);
if (preferAudio) {
const audios = files.filter((name) =>
audioExts.includes(path.extname(name).toLowerCase())
);
if (audios.length) {
audios.sort((a, b) => {
const aSize = fs.statSync(path.join(savePath, a)).size;
const bSize = fs.statSync(path.join(savePath, b)).size;
return bSize - aSize;
});
return audios[0];
}
}
const videos = files.filter((name) =>
VIDEO_EXTS.includes(path.extname(name).toLowerCase())
);
if (!videos.length) return null; if (!videos.length) return null;
videos.sort((a, b) => { videos.sort((a, b) => {
const aSize = fs.statSync(path.join(savePath, a)).size; const aSize = fs.statSync(path.join(savePath, a)).size;
@@ -1075,8 +1128,7 @@ function deriveYoutubeTitle(fileName, videoId) {
return cleaned.replace(/[-_.]+$/g, "").trim() || base; return cleaned.replace(/[-_.]+$/g, "").trim() || base;
} }
function isYoutubeMusic(infoJson, mediaInfo, audioOnlyFlag = false) { function isYoutubeMusic(infoJson, mediaInfo) {
if (audioOnlyFlag) return true;
const categories = Array.isArray(infoJson?.categories) const categories = Array.isArray(infoJson?.categories)
? infoJson.categories.map((c) => String(c).toLowerCase()) ? infoJson.categories.map((c) => String(c).toLowerCase())
: []; : [];
@@ -1108,7 +1160,7 @@ 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 isAudioOnly = isYoutubeMusic(infoJson, mediaInfo) || Boolean(job.onlyAudio);
const derivedType = determineMediaType({ const derivedType = determineMediaType({
tracker: "youtube", tracker: "youtube",
movieMatch: null, movieMatch: null,