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