mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-14 17:28:27 +00:00
youtube fix example
This commit is contained in:
parent
ecf8896697
commit
de6a02463d
@ -41,7 +41,8 @@
|
|||||||
"set-cookie-parser": "2.6.0",
|
"set-cookie-parser": "2.6.0",
|
||||||
"undici": "^5.19.1",
|
"undici": "^5.19.1",
|
||||||
"url-pattern": "1.0.3",
|
"url-pattern": "1.0.3",
|
||||||
"youtubei.js": "^11.0.1",
|
"youtube-dl-exec": "^3.0.12",
|
||||||
|
"youtubei.js": "^12.0.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@ -59,6 +59,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab
|
|||||||
type: Array.isArray(r.urls) ? "merge" : "remux",
|
type: Array.isArray(r.urls) ? "merge" : "remux",
|
||||||
isHLS: true,
|
isHLS: true,
|
||||||
}
|
}
|
||||||
|
responseType = "redirect";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "muteVideo":
|
case "muteVideo":
|
||||||
|
@ -295,7 +295,8 @@ export default async function({ host, patternMatch, params }) {
|
|||||||
audioBitrate: params.audioBitrate,
|
audioBitrate: params.audioBitrate,
|
||||||
alwaysProxy: params.alwaysProxy,
|
alwaysProxy: params.alwaysProxy,
|
||||||
})
|
})
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
return createResponse("error", {
|
return createResponse("error", {
|
||||||
code: "error.api.fetch.critical",
|
code: "error.api.fetch.critical",
|
||||||
context: {
|
context: {
|
||||||
|
@ -71,7 +71,8 @@ export function createResponse(responseType, responseData) {
|
|||||||
...response
|
...response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
return internalError()
|
return internalError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,487 +1,72 @@
|
|||||||
import HLS from "hls-parser";
|
import youtubedl from 'youtube-dl-exec';
|
||||||
|
import { env } from '../../config.js';
|
||||||
import { fetch } from "undici";
|
|
||||||
import { Innertube, Session } from "youtubei.js";
|
|
||||||
|
|
||||||
import { env } from "../../config.js";
|
|
||||||
import { getCookie, updateCookieValues } from "../cookie/manager.js";
|
|
||||||
|
|
||||||
const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms
|
|
||||||
|
|
||||||
let innertube, lastRefreshedAt;
|
|
||||||
|
|
||||||
const codecList = {
|
|
||||||
h264: {
|
|
||||||
videoCodec: "avc1",
|
|
||||||
audioCodec: "mp4a",
|
|
||||||
container: "mp4"
|
|
||||||
},
|
|
||||||
av1: {
|
|
||||||
videoCodec: "av01",
|
|
||||||
audioCodec: "opus",
|
|
||||||
container: "webm"
|
|
||||||
},
|
|
||||||
vp9: {
|
|
||||||
videoCodec: "vp9",
|
|
||||||
audioCodec: "opus",
|
|
||||||
container: "webm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hlsCodecList = {
|
|
||||||
h264: {
|
|
||||||
videoCodec: "avc1",
|
|
||||||
audioCodec: "mp4a",
|
|
||||||
container: "mp4"
|
|
||||||
},
|
|
||||||
vp9: {
|
|
||||||
videoCodec: "vp09",
|
|
||||||
audioCodec: "mp4a",
|
|
||||||
container: "webm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoQualities = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320];
|
|
||||||
|
|
||||||
const transformSessionData = (cookie) => {
|
|
||||||
if (!cookie)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const values = { ...cookie.values() };
|
|
||||||
const REQUIRED_VALUES = [ 'access_token', 'refresh_token' ];
|
|
||||||
|
|
||||||
if (REQUIRED_VALUES.some(x => typeof values[x] !== 'string')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.expires) {
|
|
||||||
values.expiry_date = values.expires;
|
|
||||||
delete values.expires;
|
|
||||||
} else if (!values.expiry_date) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloneInnertube = async (customFetch) => {
|
|
||||||
const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date();
|
|
||||||
if (!innertube || shouldRefreshPlayer) {
|
|
||||||
innertube = await Innertube.create({
|
|
||||||
fetch: customFetch
|
|
||||||
});
|
|
||||||
lastRefreshedAt = +new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = new Session(
|
|
||||||
innertube.session.context,
|
|
||||||
innertube.session.key,
|
|
||||||
innertube.session.api_version,
|
|
||||||
innertube.session.account_index,
|
|
||||||
innertube.session.player,
|
|
||||||
undefined,
|
|
||||||
customFetch ?? innertube.session.http.fetch,
|
|
||||||
innertube.session.cache
|
|
||||||
);
|
|
||||||
|
|
||||||
const cookie = getCookie('youtube_oauth');
|
|
||||||
const oauthData = transformSessionData(cookie);
|
|
||||||
|
|
||||||
if (!session.logged_in && oauthData) {
|
|
||||||
await session.oauth.init(oauthData);
|
|
||||||
session.logged_in = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.logged_in) {
|
|
||||||
if (session.oauth.shouldRefreshToken()) {
|
|
||||||
await session.oauth.refreshAccessToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieValues = cookie.values();
|
|
||||||
const oldExpiry = new Date(cookieValues.expiry_date);
|
|
||||||
const newExpiry = new Date(session.oauth.oauth2_tokens.expiry_date);
|
|
||||||
|
|
||||||
if (oldExpiry.getTime() !== newExpiry.getTime()) {
|
|
||||||
updateCookieValues(cookie, {
|
|
||||||
...session.oauth.client_id,
|
|
||||||
...session.oauth.oauth2_tokens,
|
|
||||||
expiry_date: newExpiry.toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const yt = new Innertube(session);
|
|
||||||
return yt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function(o) {
|
export default async function(o) {
|
||||||
let yt;
|
|
||||||
try {
|
try {
|
||||||
yt = await cloneInnertube(
|
const output = await youtubedl(`https://www.youtube.com/watch?v=${o.id}`, {
|
||||||
(input, init) => fetch(input, {
|
dumpSingleJson: true,
|
||||||
...init,
|
noCheckCertificates: true,
|
||||||
dispatcher: o.dispatcher
|
noWarnings: true,
|
||||||
})
|
addHeader: [
|
||||||
|
'referer:youtube.com',
|
||||||
|
'user-agent:googlebot'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { is_live, duration, formats, title } = output;
|
||||||
|
|
||||||
|
if (is_live) {
|
||||||
|
return { error: "content.video.live" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration > env.durationLimit) {
|
||||||
|
return { error: "content.too_long" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoFormats = formats.filter(format =>
|
||||||
|
format.vcodec !== 'none' &&
|
||||||
|
format.acodec !== 'none'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
if (e.message?.endsWith("decipher algorithm")) {
|
if (videoFormats.length === 0) {
|
||||||
return { error: "youtube.decipher" }
|
return { error: "youtube.no_matching_format" };
|
||||||
} else if (e.message?.includes("refresh access token")) {
|
|
||||||
return { error: "youtube.token_expired" }
|
|
||||||
} else throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let useHLS = o.youtubeHLS;
|
const qualityDict = videoFormats.reduce((acc, format) => {
|
||||||
|
const qualityLabel = format.height ? `${format.height}p` : "unknown";
|
||||||
|
acc[qualityLabel] = {
|
||||||
|
url: format.url,
|
||||||
|
extension: format.ext,
|
||||||
|
resolution: format.height ? `${format.height}p` : "unknown",
|
||||||
|
youtubeFormat: format.ext
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
// HLS playlists don't contain the av1 video format, at least with the iOS client
|
return {
|
||||||
if (useHLS && o.format === "av1") {
|
type: "merge",
|
||||||
useHLS = false;
|
urls: qualityDict,
|
||||||
|
filenameAttributes: {
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
fileMetadata: {},
|
||||||
|
isHLS: true,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
if (e.message.includes("This video is unavailable")) {
|
||||||
|
return { error: "content.video.unavailable" };
|
||||||
}
|
}
|
||||||
|
|
||||||
let info;
|
if (e.message.includes("Private video")) {
|
||||||
try {
|
|
||||||
info = await yt.getBasicInfo(o.id, useHLS ? 'IOS' : 'ANDROID');
|
|
||||||
} catch (e) {
|
|
||||||
if (e?.info) {
|
|
||||||
const errorInfo = JSON.parse(e?.info);
|
|
||||||
|
|
||||||
if (errorInfo?.reason === "This video is private") {
|
|
||||||
return { error: "content.video.private" };
|
return { error: "content.video.private" };
|
||||||
}
|
}
|
||||||
if (["INVALID_ARGUMENT", "UNAUTHENTICATED"].includes(errorInfo?.error?.status)) {
|
|
||||||
return { error: "youtube.api_error" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e?.message === "This video is unavailable") {
|
if (e.message.includes("age verification")) {
|
||||||
return { error: "content.video.unavailable" };
|
return { error: "content.video.age" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { error: "fetch.fail" };
|
return { error: "fetch.fail" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info) return { error: "fetch.fail" };
|
|
||||||
|
|
||||||
const playability = info.playability_status;
|
|
||||||
const basicInfo = info.basic_info;
|
|
||||||
|
|
||||||
switch(playability.status) {
|
|
||||||
case "LOGIN_REQUIRED":
|
|
||||||
if (playability.reason.endsWith("bot")) {
|
|
||||||
return { error: "youtube.login" }
|
|
||||||
}
|
|
||||||
if (playability.reason.endsWith("age")) {
|
|
||||||
return { error: "content.video.age" }
|
|
||||||
}
|
|
||||||
if (playability?.error_screen?.reason?.text === "Private video") {
|
|
||||||
return { error: "content.video.private" }
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "UNPLAYABLE":
|
|
||||||
if (playability?.reason?.endsWith("request limit.")) {
|
|
||||||
return { error: "fetch.rate" }
|
|
||||||
}
|
|
||||||
if (playability?.error_screen?.subreason?.text?.endsWith("in your country")) {
|
|
||||||
return { error: "content.video.region" }
|
|
||||||
}
|
|
||||||
if (playability?.error_screen?.reason?.text === "Private video") {
|
|
||||||
return { error: "content.video.private" }
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "AGE_VERIFICATION_REQUIRED":
|
|
||||||
return { error: "content.video.age" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playability.status !== "OK") {
|
|
||||||
return { error: "content.video.unavailable" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (basicInfo.is_live) {
|
|
||||||
return { error: "content.video.live" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (basicInfo.duration > env.durationLimit) {
|
|
||||||
return { error: "content.too_long" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a critical error if returned video is "Video Not Available"
|
|
||||||
// or a similar stub by youtube
|
|
||||||
if (basicInfo.id !== o.id) {
|
|
||||||
return {
|
|
||||||
error: "fetch.fail",
|
|
||||||
critical: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const quality = o.quality === "max" ? 9000 : Number(o.quality);
|
|
||||||
|
|
||||||
const normalizeQuality = res => {
|
|
||||||
const shortestSide = res.height > res.width ? res.width : res.height;
|
|
||||||
return videoQualities.find(qual => qual >= shortestSide);
|
|
||||||
}
|
|
||||||
|
|
||||||
let video, audio, dubbedLanguage,
|
|
||||||
codec = o.format || "h264";
|
|
||||||
|
|
||||||
if (useHLS) {
|
|
||||||
const hlsManifest = info.streaming_data.hls_manifest_url;
|
|
||||||
|
|
||||||
if (!hlsManifest) {
|
|
||||||
return { error: "youtube.no_hls_streams" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchedHlsManifest = await fetch(hlsManifest, {
|
|
||||||
dispatcher: o.dispatcher,
|
|
||||||
}).then(r => {
|
|
||||||
if (r.status === 200) {
|
|
||||||
return r.text();
|
|
||||||
} else {
|
|
||||||
throw new Error("couldn't fetch the HLS playlist");
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
if (!fetchedHlsManifest) {
|
|
||||||
return { error: "youtube.no_hls_streams" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const variants = HLS.parse(fetchedHlsManifest).variants.sort(
|
|
||||||
(a, b) => Number(b.bandwidth) - Number(a.bandwidth)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!variants || variants.length === 0) {
|
|
||||||
return { error: "youtube.no_hls_streams" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchHlsCodec = codecs => (
|
|
||||||
codecs.includes(hlsCodecList[codec].videoCodec)
|
|
||||||
);
|
|
||||||
|
|
||||||
const best = variants.find(i => matchHlsCodec(i.codecs));
|
|
||||||
|
|
||||||
const preferred = variants.find(i =>
|
|
||||||
matchHlsCodec(i.codecs) && normalizeQuality(i.resolution) === quality
|
|
||||||
);
|
|
||||||
|
|
||||||
let selected = preferred || best;
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
codec = "h264";
|
|
||||||
selected = variants.find(i => matchHlsCodec(i.codecs));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
return { error: "youtube.no_matching_format" };
|
|
||||||
}
|
|
||||||
|
|
||||||
audio = selected.audio.find(i => i.isDefault);
|
|
||||||
|
|
||||||
// some videos (mainly those with AI dubs) don't have any tracks marked as default
|
|
||||||
// why? god knows, but we assume that a default track is marked as such in the title
|
|
||||||
if (!audio) {
|
|
||||||
audio = selected.audio.find(i => i.name.endsWith("- original"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (o.dubLang) {
|
|
||||||
const dubbedAudio = selected.audio.find(i =>
|
|
||||||
i.language?.startsWith(o.dubLang)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dubbedAudio && !dubbedAudio.isDefault) {
|
|
||||||
dubbedLanguage = dubbedAudio.language;
|
|
||||||
audio = dubbedAudio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selected.audio = [];
|
|
||||||
selected.subtitles = [];
|
|
||||||
video = selected;
|
|
||||||
} else {
|
|
||||||
// i miss typescript so bad
|
|
||||||
const sorted_formats = {
|
|
||||||
h264: {
|
|
||||||
video: [],
|
|
||||||
audio: [],
|
|
||||||
bestVideo: undefined,
|
|
||||||
bestAudio: undefined,
|
|
||||||
},
|
|
||||||
vp9: {
|
|
||||||
video: [],
|
|
||||||
audio: [],
|
|
||||||
bestVideo: undefined,
|
|
||||||
bestAudio: undefined,
|
|
||||||
},
|
|
||||||
av1: {
|
|
||||||
video: [],
|
|
||||||
audio: [],
|
|
||||||
bestVideo: undefined,
|
|
||||||
bestAudio: undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkFormat = (format, pCodec) => format.content_length &&
|
|
||||||
(format.mime_type.includes(codecList[pCodec].videoCodec)
|
|
||||||
|| format.mime_type.includes(codecList[pCodec].audioCodec));
|
|
||||||
|
|
||||||
// sort formats & weed out bad ones
|
|
||||||
info.streaming_data.adaptive_formats.sort((a, b) =>
|
|
||||||
Number(b.bitrate) - Number(a.bitrate)
|
|
||||||
).forEach(format => {
|
|
||||||
Object.keys(codecList).forEach(yCodec => {
|
|
||||||
const sorted = sorted_formats[yCodec];
|
|
||||||
const goodFormat = checkFormat(format, yCodec);
|
|
||||||
if (!goodFormat) return;
|
|
||||||
|
|
||||||
if (format.has_video) {
|
|
||||||
sorted.video.push(format);
|
|
||||||
if (!sorted.bestVideo) sorted.bestVideo = format;
|
|
||||||
}
|
|
||||||
if (format.has_audio) {
|
|
||||||
sorted.audio.push(format);
|
|
||||||
if (!sorted.bestAudio) sorted.bestAudio = format;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const noBestMedia = () => {
|
|
||||||
const vid = sorted_formats[codec]?.bestVideo;
|
|
||||||
const aud = sorted_formats[codec]?.bestAudio;
|
|
||||||
return (!vid && !o.isAudioOnly) || (!aud && o.isAudioOnly)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (noBestMedia()) {
|
|
||||||
if (codec === "av1") codec = "vp9";
|
|
||||||
else if (codec === "vp9") codec = "av1";
|
|
||||||
|
|
||||||
// if there's no higher quality fallback, then use h264
|
|
||||||
if (noBestMedia()) codec = "h264";
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there's no proper combo of av1, vp9, or h264, then give up
|
|
||||||
if (noBestMedia()) {
|
|
||||||
return { error: "youtube.no_matching_format" };
|
|
||||||
}
|
|
||||||
|
|
||||||
audio = sorted_formats[codec].bestAudio;
|
|
||||||
|
|
||||||
if (audio?.audio_track && !audio?.audio_track?.audio_is_default) {
|
|
||||||
audio = sorted_formats[codec].audio.find(i =>
|
|
||||||
i?.audio_track?.audio_is_default
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (o.dubLang) {
|
|
||||||
const dubbedAudio = sorted_formats[codec].audio.find(i =>
|
|
||||||
i.language?.startsWith(o.dubLang) && i.audio_track
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) {
|
|
||||||
audio = dubbedAudio;
|
|
||||||
dubbedLanguage = dubbedAudio.language;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!o.isAudioOnly) {
|
|
||||||
const qual = (i) => {
|
|
||||||
return normalizeQuality({
|
|
||||||
width: i.width,
|
|
||||||
height: i.height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const bestQuality = qual(sorted_formats[codec].bestVideo);
|
|
||||||
const useBestQuality = quality >= bestQuality;
|
|
||||||
|
|
||||||
video = useBestQuality
|
|
||||||
? sorted_formats[codec].bestVideo
|
|
||||||
: sorted_formats[codec].video.find(i => qual(i) === quality);
|
|
||||||
|
|
||||||
if (!video) video = sorted_formats[codec].bestVideo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileMetadata = {
|
|
||||||
title: basicInfo.title.trim(),
|
|
||||||
artist: basicInfo.author.replace("- Topic", "").trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) {
|
|
||||||
const descItems = basicInfo.short_description.split("\n\n", 5);
|
|
||||||
|
|
||||||
if (descItems.length === 5) {
|
|
||||||
fileMetadata.album = descItems[2];
|
|
||||||
fileMetadata.copyright = descItems[3];
|
|
||||||
if (descItems[4].startsWith("Released on:")) {
|
|
||||||
fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filenameAttributes = {
|
|
||||||
service: "youtube",
|
|
||||||
id: o.id,
|
|
||||||
title: fileMetadata.title,
|
|
||||||
author: fileMetadata.artist,
|
|
||||||
youtubeDubName: dubbedLanguage || false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audio && o.isAudioOnly) {
|
|
||||||
let bestAudio = codec === "h264" ? "m4a" : "opus";
|
|
||||||
let urls = audio.url;
|
|
||||||
|
|
||||||
if (useHLS) {
|
|
||||||
bestAudio = "mp3";
|
|
||||||
urls = audio.uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "audio",
|
|
||||||
isAudioOnly: true,
|
|
||||||
urls,
|
|
||||||
filenameAttributes,
|
|
||||||
fileMetadata,
|
|
||||||
bestAudio,
|
|
||||||
isHLS: useHLS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video && audio) {
|
|
||||||
let resolution;
|
|
||||||
|
|
||||||
if (useHLS) {
|
|
||||||
resolution = normalizeQuality(video.resolution);
|
|
||||||
filenameAttributes.resolution = `${video.resolution.width}x${video.resolution.height}`;
|
|
||||||
filenameAttributes.extension = hlsCodecList[codec].container;
|
|
||||||
|
|
||||||
video = video.uri;
|
|
||||||
audio = audio.uri;
|
|
||||||
} else {
|
|
||||||
resolution = normalizeQuality({
|
|
||||||
width: video.width,
|
|
||||||
height: video.height,
|
|
||||||
});
|
|
||||||
filenameAttributes.resolution = `${video.width}x${video.height}`;
|
|
||||||
filenameAttributes.extension = codecList[codec].container;
|
|
||||||
|
|
||||||
video = video.url;
|
|
||||||
audio = audio.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
filenameAttributes.qualityLabel = `${resolution}p`;
|
|
||||||
filenameAttributes.youtubeFormat = codec;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "merge",
|
|
||||||
urls: [
|
|
||||||
video,
|
|
||||||
audio,
|
|
||||||
],
|
|
||||||
filenameAttributes,
|
|
||||||
fileMetadata,
|
|
||||||
isHLS: useHLS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { error: "youtube.no_matching_format" };
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user