From 707115758007fe80433a41026121554096ffcfb3 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Jul 2024 19:54:19 +0600 Subject: [PATCH] youtube: fix audio track selection for dubbed videos & clean up closes #642 --- src/modules/processing/services/youtube.js | 109 ++++++++++++++------- 1 file changed, 73 insertions(+), 36 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 7beca2b4..2a0d25c5 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -1,25 +1,27 @@ -import { Innertube, Session } from 'youtubei.js'; -import { env } from '../../config.js'; -import { cleanString } from '../../sub/utils.js'; -import { fetch } from 'undici' -import { getCookie, updateCookieValues } from '../cookie/manager.js' +import { fetch } from "undici"; + +import { Innertube, Session } from "youtubei.js"; + +import { env } from "../../config.js"; +import { cleanString } from "../../sub/utils.js"; +import { getCookie, updateCookieValues } from "../cookie/manager.js"; const ytBase = Innertube.create().catch(e => e); const codecMatch = { h264: { - codec: "avc1", - aCodec: "mp4a", + videoCodec: "avc1", + audioCodec: "mp4a", container: "mp4" }, av1: { - codec: "av01", - aCodec: "mp4a", + videoCodec: "av01", + audioCodec: "mp4a", container: "mp4" }, vp9: { - codec: "vp9", - aCodec: "opus", + videoCodec: "vp9", + audioCodec: "opus", container: "webm" } } @@ -94,11 +96,16 @@ const cloneInnertube = async (customFetch) => { export default async function(o) { const yt = await cloneInnertube( - (input, init) => fetch(input, { ...init, dispatcher: o.dispatcher }) + (input, init) => fetch(input, { + ...init, + dispatcher: o.dispatcher + }) ); - let info, isDubbed, format = o.format || "h264"; - let quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality + const quality = o.quality === "max" ? "9000" : o.quality; + + let info, isDubbed, + format = o.format || "h264"; function qual(i) { if (!i.quality_label) { @@ -121,6 +128,7 @@ export default async function(o) { if (!info) return { error: 'ErrorCantConnectToServiceAPI' }; const playability = info.playability_status; + const basicInfo = info.basic_info; if (playability.status === 'LOGIN_REQUIRED') { if (playability.reason.endsWith('bot')) { @@ -135,11 +143,11 @@ export default async function(o) { } if (playability.status !== 'OK') return { error: 'ErrorYTUnavailable' }; - if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; + if (basicInfo.is_live) return { error: 'ErrorLiveVideo' }; // return a critical error if returned video is "Video Not Available" // or a similar stub by youtube - if (info.basic_info.id !== o.id) { + if (basicInfo.id !== o.id) { return { error: 'ErrorCantConnectToServiceAPI', critical: true @@ -148,11 +156,16 @@ export default async function(o) { let bestQuality, hasAudio; - const filterByCodec = (formats) => formats.filter(e => - e.mime_type.includes(codecMatch[format].codec) || e.mime_type.includes(codecMatch[format].aCodec) - ).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + const filterByCodec = (formats) => + formats + .filter(e => + e.mime_type.includes(codecMatch[format].videoCodec) + || e.mime_type.includes(codecMatch[format].audioCodec) + ) + .sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); + if (adaptive_formats.length === 0 && format === "vp9") { format = "h264" adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats) @@ -162,27 +175,43 @@ export default async function(o) { hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); if (bestQuality) bestQuality = qual(bestQuality); - if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' }; - if (info.basic_info.duration > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; - let checkBestAudio = (i) => (i.has_audio && !i.has_video), - audio = adaptive_formats.find(i => checkBestAudio(i) && !i.is_dubbed); + if (!bestQuality && !o.isAudioOnly || !hasAudio) + return { error: 'ErrorYTTryOtherCodec' }; + + if (basicInfo.duration > env.durationLimit) + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; + + const checkBestAudio = (i) => (i.has_audio && !i.has_video); + + let audio = adaptive_formats.find(i => + checkBestAudio(i) && i.is_original + ); if (o.dubLang) { let dubbedAudio = adaptive_formats.find(i => - checkBestAudio(i) && i.language === o.dubLang && i.audio_track && !i.audio_track.audio_is_default - ); + checkBestAudio(i) + && i.language === o.dubLang + && i.audio_track + ) + if (dubbedAudio) { audio = dubbedAudio; - isDubbed = true + isDubbed = true; } } - let fileMetadata = { - title: cleanString(info.basic_info.title.trim()), - artist: cleanString(info.basic_info.author.replace("- Topic", "").trim()), + + if (!audio) { + audio = adaptive_formats.find(i => checkBestAudio(i)); } - if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) { - let descItems = info.basic_info.short_description.split("\n\n"); + + let fileMetadata = { + title: cleanString(basicInfo.title.trim()), + artist: cleanString(basicInfo.author.replace("- Topic", "").trim()), + } + + if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) { + let descItems = basicInfo.short_description.split("\n\n"); fileMetadata.album = descItems[2]; fileMetadata.copyright = descItems[3]; if (descItems[4].startsWith("Released on:")) { @@ -204,13 +233,17 @@ export default async function(o) { urls: audio.decipher(yt.session.player), filenameAttributes: filenameAttributes, fileMetadata: fileMetadata, - bestAudio: format === "h264" ? 'm4a' : 'opus' + bestAudio: format === "h264" ? "m4a" : "opus" } + const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality, - checkSingle = i => qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].codec), - checkRender = i => qual(i) === matchingQuality && i.has_video && !i.has_audio; + checkSingle = i => + qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].videoCodec), + checkRender = i => + qual(i) === matchingQuality && i.has_video && !i.has_audio; let match, type, urls; + if (!o.isAudioOnly && !o.isAudioMuted && format === 'h264') { match = info.streaming_data.formats.find(checkSingle); type = "bridge"; @@ -218,10 +251,14 @@ export default async function(o) { } const video = adaptive_formats.find(checkRender); + if (!match && video) { match = video; type = "render"; - urls = [video.decipher(yt.session.player), audio.decipher(yt.session.player)]; + urls = [ + video.decipher(yt.session.player), + audio.decipher(yt.session.player) + ] } if (match) { @@ -232,7 +269,7 @@ export default async function(o) { return { type, urls, - filenameAttributes, + filenameAttributes, fileMetadata } }