diff --git a/server/server.js b/server/server.js index c046648..23a2c8b 100644 --- a/server/server.js +++ b/server/server.js @@ -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,