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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user