probably the biggest update in history of cobalt
This commit is contained in:
wukko
2022-08-12 19:36:19 +06:00
parent 2fae43d890
commit 54c14232d5
42 changed files with 896 additions and 385 deletions

View File

@@ -7,7 +7,7 @@ import { errorUnsupported } from "./sub/errors.js";
import loc from "../localization/manager.js";
import match from "./match.js";
export async function getJSON(originalURL, ip, lang, format, quality) {
export async function getJSON(originalURL, ip, lang, format, quality, audioFormat, isAudioOnly) {
try {
let url = decodeURI(originalURL);
if (!url.includes('http://')) {
@@ -28,7 +28,7 @@ export async function getJSON(originalURL, ip, lang, format, quality) {
if (patternMatch) break;
}
if (patternMatch) {
return await match(host, patternMatch, url, ip, lang, format, quality);
return await match(host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly);
} return apiJSON(0, { t: errorUnsupported(lang) } )
} return apiJSON(0, { t: errorUnsupported(lang) } )
} else {

View File

@@ -15,4 +15,5 @@ supportedLanguages = config.supportedLanguages,
quality = config.quality,
internetExplorerRedirect = config.internetExplorerRedirect,
donations = config.donations,
ffmpegArgs = config.ffmpegArgs
ffmpegArgs = config.ffmpegArgs,
supportedAudio = config.supportedAudio

24
src/modules/emoji.js Normal file
View File

@@ -0,0 +1,24 @@
const names = {
"🎶": "musical_notes",
"🎬": "clapper_board",
"💰": "money_bag",
"🎉": "party_popper",
"❓": "red_question_mark",
"✨": "sparkles",
"🪅": "pinata",
"🪄": "magic_wand",
"🐲": "dragon_face",
"💸": "money_with_wings",
"⚙️": "gear"
}
let sizing = {
22: 0.4,
30: 0.7
}
export default function(emoji, size, disablePadding) {
if (!size) size = 22;
let padding = size != 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;`: ``;
if (disablePadding) padding = 'margin-right:0!important;';
if (!names[emoji]) emoji = "❓";
return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">`
}

View File

@@ -11,8 +11,9 @@ import vk from "./services/vk.js";
import tiktok from "./services/tiktok.js";
import douyin from "./services/douyin.js";
import tumblr from "./services/tumblr.js";
import matchActionDecider from "./sub/matchActionDecider.js";
export default async function (host, patternMatch, url, ip, lang, format, quality) {
export default async function (host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly) {
try {
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
if (!(testers[host](patternMatch))) throw Error();
@@ -24,99 +25,69 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
id: patternMatch["id"],
lang: lang
});
return (!r.error) ? apiJSON(1, { u: r }) : apiJSON(0, { t: r.error });
break;
case "vk":
r = await vk({
userId: patternMatch["userId"],
videoId: patternMatch["videoId"],
lang: lang, quality: quality
});
return (!r.error) ? apiJSON(2, { type: "bridge", lang: lang, u: r.url, filename: r.filename,
service: host, ip: ip, salt: process.env.streamSalt }) : apiJSON(0, { t: r.error });
break;
case "bilibili":
r = await bilibili({
id: patternMatch["id"].slice(0, 12),
lang: lang
});
return (!r.error) ? apiJSON(2, {
type: "render", u: r.urls, lang: lang,
service: host, ip: ip,
filename: r.filename,
salt: process.env.streamSalt, time: r.time
}) : apiJSON(0, { t: r.error });
break;
case "youtube":
let fetchInfo = {
id: patternMatch["id"].slice(0,11),
lang: lang, quality: quality,
format: "mp4"
format: "webm"
};
if (url.match('music.youtube.com')) {
format = "audio"
}
if (url.match('music.youtube.com') || isAudioOnly == true) format = "audio";
switch (format) {
case "webm":
fetchInfo["format"] = "webm";
case "mp4":
fetchInfo["format"] = "mp4";
break;
case "audio":
fetchInfo["format"] = "webm";
fetchInfo["isAudioOnly"] = true;
fetchInfo["quality"] = "max";
isAudioOnly = true;
break;
}
r = await youtube(fetchInfo);
return (!r.error) ? apiJSON(2, {
type: r.type, u: r.urls, lang: lang, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt,
isAudioOnly: fetchInfo["isAudioOnly"] ? fetchInfo["isAudioOnly"] : false,
time: r.time,
}) : apiJSON(0, { t: r.error });
break;
case "reddit":
r = await reddit({
sub: patternMatch["sub"],
id: patternMatch["id"],
title: patternMatch["title"], lang: lang,
});
return (!r.error) ? apiJSON(r.typeId, {
type: r.type, u: r.urls, lang: lang,
service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
}) : apiJSON(0, { t: r.error });
break;
case "tiktok":
r = await tiktok({
postId: patternMatch["postId"],
id: patternMatch["id"], lang: lang,
});
return (!r.error) ? apiJSON(2, {
type: "bridge", u: r.urls, lang: lang,
service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
}) : apiJSON(0, { t: r.error });
break;
case "douyin":
r = await douyin({
postId: patternMatch["postId"],
id: patternMatch["id"], lang: lang,
});
return (!r.error) ? apiJSON(2, {
type: "bridge", u: r.urls, lang: lang,
service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
}) : apiJSON(0, { t: r.error });
break;
case "tumblr":
r = await tumblr({
id: patternMatch["id"], url: url, user: patternMatch["user"] ? patternMatch["user"] : false,
lang: lang
});
return (!r.error) ? apiJSON(1, { u: r.split('?')[0] }) : apiJSON(0, { t: r.error });
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}
return matchActionDecider(r, host, ip, audioFormat, isAudioOnly)
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View File

@@ -1,205 +0,0 @@
import { services, appName, authorInfo, version, quality, repo, donations } from "./config.js";
import { getCommitInfo } from "./sub/currentCommit.js";
import loc from "../localization/manager.js";
let s = services;
let com = getCommitInfo();
let enabledServices = Object.keys(s).filter((p) => {
if (s[p].enabled) {
return true
}
}).sort().map((p) => {
if (s[p].alias) {
return s[p].alias
} else {
return p
}
}).join(', ')
let donate = ``
for (let i in donations) {
donate += `<div class="subtitle">${i} (REPLACEME)</div><div id="don-${i}" class="text-to-copy" onClick="copy('don-${i}')">${donations[i]}</div>`
}
export default function(obj) {
let isIOS = obj.useragent.toLowerCase().match("iphone os")
try {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
<title>${appName}</title>
<meta property="og:url" content="${process.env.selfURL}" />
<meta property="og:title" content="${appName}" />
<meta property="og:description" content="${loc(obj.lang, 'EmbedBriefDescription')}" />
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" />
<meta name="description" content="${loc(obj.lang, 'AboutSummary')}" />
<meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="summary" />
<link rel="icon" type="image/x-icon" href="icons/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="stylesheet" href="cobalt.css" />
<link rel="stylesheet" href="fonts/notosansmono.css" />
<noscript><div style="margin: 2rem;">${loc(obj.lang, 'NoScriptMessage')}</div></noscript>
</head>
<body id="cobalt-body" data-nosnippet>
<div id="popup-about" class="popup center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('about', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="title" class="popup-title">${loc(obj.lang, 'TitlePopupAbout')}</div>
</div>
<div id="content" class="popup-content with-footer">
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'AboutSummary')}</div>
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.</div>
<div id="desc" class="popup-desc bottom-link"><a class="text-backdrop" href="${repo}">${loc(obj.lang, 'LinkGitHubIssues')}</a></div>
</div>
<div id="popup-footer" class="popup-footer">
<a id="popup-bottom" class="popup-footer-content" href="${authorInfo.link}">${loc(obj.lang, 'MadeWithLove')}</a>
</div>
</div>
<div id="popup-changelog" class="popup center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('changelog', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="title" class="popup-title">${loc(obj.lang, 'TitlePopupChangelog')}</div>
<div id="desc" class="popup-subtitle">${com[0]} (${obj.hash})</div>
</div>
<div id="content" class="popup-content">
<div id="desc" class="popup-desc about-padding">${com[1]}</div>
<div id="desc" class="popup-desc bottom-link"><a class="text-backdrop" href="${repo}/commits">${loc(obj.lang, 'LinkGitHubChanges')}</a></div>
</div>
</div>
<div id="popup-donate" class="popup scrollable center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('donate', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="title" class="popup-title">${loc(obj.lang, 'TitlePopupDonate')}</div>
<div id="desc" class="little-subtitle">${loc(obj.lang, 'DonateSubtitle')}</div>
</div>
<div id="content" class="popup-content">
${donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy').trim())}
<div id="desc" class="explanation about-padding">${loc(obj.lang, 'DonateDescription')}</div>
<div id="desc" class="popup-desc bottom-link"><a class="text-backdrop" href="${authorInfo.contact}">${loc(obj.lang, 'LinkDonateContact')}</a></div>
</div>
</div>
<div id="popup-settings" class="popup scrollable center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('settings', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="version" class="popup-above-title">v.${version} ~ ${obj.hash}</div>
<div id="title" class="popup-title">${loc(obj.lang, 'TitlePopupSettings')}</div>
</div>
<div id="content" class="popup-content">
<div id="settings-appearance" class="settings-category">
<div class="title">${loc(obj.lang, 'SettingsAppearanceSubtitle')}</div>
<div class="settings-category-content">
<div id="theme-switcher" class="switch-container">
<div class="subtitle">${loc(obj.lang, 'SettingsThemeSubtitle')}</div>
<div class="switches">
<div id="theme-auto" class="switch full" onclick="changeSwitcher('theme', 'auto', 1)">${loc(obj.lang, 'SettingsThemeAuto')}</div>
<div id="theme-dark" class="switch" onclick="changeSwitcher('theme', 'dark', 1)">${loc(obj.lang, 'SettingsThemeDark')}</div>
<div id="theme-light" class="switch full" onclick="changeSwitcher('theme', 'light', 1)">${loc(obj.lang, 'SettingsThemeLight')}</div>
</div>
</div>
</div>
</div>
<div id="settings-downloads" class="settings-category">
<div class="title">${loc(obj.lang, 'SettingsDownloadsSubtitle')}</div>
<div class="settings-category-content">
<div id="quality-switcher" class="switch-container">
<div class="subtitle">${loc(obj.lang, 'SettingsQualitySubtitle')}</div>
<div class="switches">
<div id="quality-max" class="switch full" onclick="changeSwitcher('quality', 'max', 1)">${loc(obj.lang, 'SettingsQualitySwitchMax')}</div>
<div id="quality-hig" class="switch" onclick="changeSwitcher('quality', 'hig', 1)">${loc(obj.lang, 'SettingsQualitySwitchHigh')}(${quality.hig}p)</div>
<div id="quality-mid" class="switch full" onclick="changeSwitcher('quality', 'mid', 1)">${loc(obj.lang, 'SettingsQualitySwitchMedium')}(${quality.mid}p)</div>
<div id="quality-low" class="switch right" onclick="changeSwitcher('quality', 'low', 1)">${loc(obj.lang, 'SettingsQualitySwitchLow')}(${quality.low}p)</div>
</div>
<div class="explanation">${loc(obj.lang, 'SettingsQualityDescription')}</div>
</div>
</div>
</div>
<div id="settings-youtube" class="settings-category">
<div class="title">youtube</div>
<div class="settings-category-content">
<div id="youtube-switcher" class="switch-container">
<div class="subtitle">${loc(obj.lang, 'SettingsFormatSubtitle')}</div>
<div class="switches">
<div id="youtubeFormat-mp4" class="switch full" onclick="changeSwitcher('youtubeFormat', 'mp4', 1)">mp4</div>
<div id="youtubeFormat-webm" class="switch" onclick="changeSwitcher('youtubeFormat', 'webm', 1)">webm</div>
<div id="youtubeFormat-audio" class="switch full" onclick="changeSwitcher('youtubeFormat', 'audio', 1)">${loc(obj.lang, 'SettingsFormatSwitchAudio')}</div>
</div>
<div class="explanation">${loc(obj.lang, 'SettingsFormatDescription')}</div>
</div>
</div>
</div>
<div id="settings-misc" class="settings-category">
<div class="title bottom-margin">${loc(obj.lang, 'SettingsMiscSubtitle')}</div>
<div class="settings-category-content">
<label class="checkbox">
<input id="alwaysVisibleButton" type="checkbox" aria-label="${loc(obj.lang, 'AccessibilityKeepDownloadButton')}" onclick="checkbox('alwaysVisibleButton', 'always-visible-button')">
<span>${loc(obj.lang, 'SettingsKeepDownloadButton')}</span>
</label>${!isIOS ? `
<label class="checkbox">
<input id="downloadPopup" type="checkbox" aria-label="${loc(obj.lang, 'AccessibilityEnableDownloadPopup')}" onclick="checkbox('downloadPopup', 'always-visible-button')">
<span>${loc(obj.lang, 'SettingsEnableDownloadPopup')}</span>
</label>` : ``}
</div>
</div>
</div>
</div>
<div id="popup-download" class="popup center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('download', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="title" class="popup-subtitle">${loc(obj.lang, 'TitlePopupDownload')}</div>
</div>
<div id="content" class="popup-content">
<div id="theme-switcher" class="switch-container small-padding">
<div class="subtitle">${loc(obj.lang, 'DownloadPopupDescription')}</div>
<div class="switches">
<a id="pd-download" class="switch full space-right" target="_blank" href="/">${loc(obj.lang, 'Download')}</a>
<div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>
</div>
</div>
<div id="desc" class="explanation about-padding">${isIOS ? loc(obj.lang, 'DownloadPopupDescriptionIOS') : loc(obj.lang, 'DownloadPopupDescription')}</div>
</div>
</div>
<div id="popup-error" class="popup center box" style="visibility: hidden;">
<div id="popup-header" class="popup-header">
<button id="close" class="button mono" onclick="popup('error', 0)" aria-label="${loc(obj.lang, 'AccessibilityClosePopup')}">x</button>
<div id="title" class="popup-title">${loc(obj.lang, 'TitlePopupError')}</div>
</div>
<div id="content" class="popup-content">
<div id="desc-error" class="popup-desc"></div>
</div>
</div>
<div id="popup-backdrop" style="visibility: hidden;"></div>
<div id="cobalt-main-box" class="center box" style="visibility: hidden;">
<div id="logo-area">${appName}</div>
<div id="download-area" class="mobile-center">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="110" autocapitalize="off" placeholder="${loc(obj.lang, 'LinkInput')}" aria-label="${loc(obj.lang, 'AccessibilityInputArea')}" oninput="button()">
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${loc(obj.lang, 'AccessibilityDownloadButton')}">
</div>
</div>
<footer id="footer" style="visibility: hidden;">
<div id="footer-buttons">
<button id="about-open" class="button footer-button" onclick="popup('about', 1)" aria-label="${loc(obj.lang, 'AccessibilityOpenAbout')}">?</button>
<button id="changelog-open" class="button footer-button" onclick="popup('changelog', 1)" aria-label="${loc(obj.lang, 'AccessibilityOpenChangelog')}">!</button>
<button id="donate-open" class="button footer-button" onclick="popup('donate', 1)" aria-label="${loc(obj.lang, 'AccessibilityOpenDonate')}">$</button>
<button id="settings-open" class="button footer-button" onclick="popup('settings', 1)" aria-label="${loc(obj.lang, 'AccessibilityOpenSettings')}">+</button>
</div>
</footer>
</body>
<script type="text/javascript">const loc = {noInternet:"${loc(obj.lang, 'ErrorNoInternet')}", noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}"};</script>
<script type="text/javascript" src="cobalt.js"></script>
</html>`;
} catch (err) {
return `${loc(obj.lang, 'ErrorPageRenderFail', obj.hash)}`;
}
}

View File

@@ -0,0 +1,95 @@
export function switcher(obj) {
let items = ``;
switch(obj.name) {
case "download":
items = obj.items;
break;
default:
for (let i = 0; i < obj.items.length; i++) {
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []
items += `<button id="${obj.name}-${obj.items[i]["action"]}" class="switch${classes.length > 0 ? ' ' + classes.join(' ') : ''}" onclick="changeSwitcher('${obj.name}', '${obj.items[i]["action"]}')">${obj.items[i]["text"] ? obj.items[i]["text"] : obj.items[i]["action"]}</button>`
}
break;
}
return `
<div id="${obj.name}-switcher" class="switch-container">
${obj.subtitle ? `<div class="subtitle">${obj.subtitle}</div>` : ``}
<div class="switches">${items}</div>
${obj.explanation ? `<div class="explanation">${obj.explanation}</div>` : ``}
</div>`
}
export function checkbox(action, text, aria) {
return `<label class="checkbox">
<input id="${action}" type="checkbox" ${aria ? `aria-label="${aria}"` : ''} onclick="checkbox('${action}')">
<span>${text}</span>
</label>`
}
export function popup(obj) {
let classes = obj.classes ? obj.classes : []
let body = obj.body;
if (Array.isArray(obj.body)) {
body = ``
for (let i = 0; i < obj.body.length; i++) {
let classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
if (i != obj.body.length - 1 && !obj.body[i]["nopadding"]) {
classes.push("desc-padding")
}
body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>`
}
}
return `
${!obj.embed ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">`: ''}
<div id="popup-header" class="popup-header">
${!obj.embed ? `<button id="popup-close" class="button mono" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>`: ''}
${obj.header.aboveTitle ? `<a id="popup-above-title" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div>
<div id="popup-content"${obj.footer ? ' class="with-footer"' : ''}>${body}</div>
${obj.footer ? `<div id="popup-footer" class="popup-footer">
<a id="popup-bottom" class="popup-footer-content" href="${obj.footer.url}">${obj.footer.text}</a>
</div>` : ''}
${!obj.embed ? `</div>`: ''}`
}
export function multiPagePopup(obj) {
let tabs = ``
let tabContent = ``
for (let i = 0; i < obj.tabs.length; i++) {
tabs += `<button id="tab-button-${obj.name}-${obj.tabs[i]["name"]}" class="switch tab tab-${obj.name}" onclick="changeTab(event, 'tab-${obj.name}-${obj.tabs[i]["name"]}', '${obj.name}')">${obj.tabs[i]["title"]}</button>`
tabContent += `<div id="tab-${obj.name}-${obj.tabs[i]["name"]}" class="popup-tab-content tab-content-${obj.name}">${obj.tabs[i]["content"]}</div>`
}
tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${obj.header.aboveTitle ? `<a id="popup-above-title" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}</div>`: ''}${tabContent}</div>
<div id="popup-tabs" class="switches popup-tabs">${tabs}</div>
</div>`
}
export function backdropLink(link, text) {
return `<a class="text-backdrop" href="${link}" target="_blank">${text}</a>`
}
export function settingsCategory(obj) {
return `<div id="settings-${obj.name}" class="settings-category">
<div class="category-title">${obj.title ? obj.title : obj.name}</div>
<div class="settings-category-content">${obj.body}</div>
</div>`
}
export function footerButtons(obj) {
let items = ``
for (let i = 0; i < obj.length; i++) {
let func = `${obj[i]["type"] == "toggle" ? `toggle('${obj[i]["name"]}')` : `popup('${obj[i]["name"]}', 1)`}`
items += `<button id="${obj[i]["name"]}" class="button footer-button" onclick="${func}" aria-label="${obj[i]["aria"]}">${obj[i]["icon"]}</button> `
}
return `
<div id="footer-buttons">${items}</div>`
}

View File

@@ -0,0 +1,291 @@
import { services, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js";
import { backdropLink, checkbox, footerButtons, multiPagePopup, popup, settingsCategory, switcher } from "./elements.js";
import emoji from "../emoji.js";
let s = services;
let com = getCommitInfo();
let enabledServices = Object.keys(s).filter((p) => {
if (s[p].enabled) return true;
}).sort().map((p) => {
return s[p].alias ? s[p].alias : p
}).join(', ')
let donate = ``
let donateLinks = ``
let audioFormats = supportedAudio.map((p) => {
return {"action": p}
})
audioFormats.unshift({ "action": "best" })
for (let i in donations["other"]) {
donateLinks += `<a id="don-${i}" class="switch autowidth" href="${donations["other"][i]}" target="_blank">${i}</a>`
}
let extr = ''
for (let i in donations["crypto"]) {
donate += `<div class="subtitle${extr}">${i} (REPLACEME)</div><div id="don-${i}" class="text-to-copy" onClick="copy('don-${i}')">${donations["crypto"][i]}</div>`
extr = ' extra'
}
export default function(obj) {
audioFormats[0]["text"] = loc(obj.lang, 'SettingsAudioFormatBest')
let isIOS = obj.useragent.toLowerCase().match("iphone os")
try {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
<title>${appName}</title>
<meta property="og:url" content="${process.env.selfURL}" />
<meta property="og:title" content="${appName}" />
<meta property="og:description" content="${loc(obj.lang, 'EmbedBriefDescription')}" />
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" />
<meta name="description" content="${loc(obj.lang, 'AboutSummary')}" />
<meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="summary" />
<link rel="icon" type="image/x-icon" href="icons/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="stylesheet" href="cobalt.css" />
<link rel="stylesheet" href="fonts/notosansmono.css" />
<noscript><div style="margin: 2rem;">${loc(obj.lang, 'NoScriptMessage')}</div></noscript>
</head>
<body id="cobalt-body" data-nosnippet>
${multiPagePopup({
name: "about",
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
tabs: [{
name: "about",
title: `${emoji("🐲")} ${loc(obj.lang, 'AboutTab')}`,
content: popup({
embed: true,
name: "about",
header: {
aboveTitle: {
text: loc(obj.lang, 'MadeWithLove'),
url: authorInfo.link
},
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
title: loc(obj.lang, 'TitlePopupAbout')
},
body: [{
text: loc(obj.lang, 'AboutSummary')
}, {
text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.`
}, {
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
classes: ["bottom-link"]
}]
})
}, {
name: "changelog",
title: `${emoji("🎉")} ${loc(obj.lang, 'ChangelogTab')}`,
content: popup({
embed: true,
name: "changelog",
header: {
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
title: `${emoji("🪄", 30)} ${loc(obj.lang, 'TitlePopupChangelog')}`
},
body: [{
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`,
raw: true
}, {
text: loc('en', 'ChangelogContentTitle'),
classes: ["changelog-subtitle"],
nopadding: true
}, {
text: loc('en', 'ChangelogContent')
}, {
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastCommit')}</div>`,
raw: true
}, {
text: `${com[0]} (${obj.hash})`,
classes: ["changelog-subtitle"],
nopadding: true
}, {
text: com[1]
}, {
text: backdropLink(`${repo}/commits`, loc(obj.lang, 'LinkGitHubChanges')),
classes: ["bottom-link"]
}]
})
}, {
name: "donate",
title: `${emoji("💰")} ${loc(obj.lang, 'DonationsTab')}`,
content: popup({
embed: true,
name: "donate",
header: {
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
title: emoji("💸", 30) + loc(obj.lang, 'TitlePopupDonate'),
subtitle: loc(obj.lang, 'DonateSubtitle')
},
body: [{
text: donateLinks,
raw: true
}, {
text: loc(obj.lang, 'DonateLinksDescription'),
classes: ["explanation"]
}, {
text: donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy'))
}, {
text: loc(obj.lang, 'DonateDescription'),
classes: ["explanation", "no-top-padding"]
}, {
text: backdropLink(authorInfo.contact, loc(obj.lang, 'LinkDonateContact')),
classes: ["bottom-link"]
}]
})
}],
})}
${multiPagePopup({
name: "settings",
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
header: {
aboveTitle: {
text: `v.${version} ~ ${obj.hash}`,
url: repo
},
title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}`
},
tabs: [{
name: "video",
title: `${emoji("🎬")} ${loc(obj.lang, 'SettingsVideoTab')}`,
content: settingsCategory({
name: "downloads",
title: loc(obj.lang, 'SettingsDownloadsSubtitle'),
body: switcher({
name: "quality",
subtitle: loc(obj.lang, 'SettingsQualitySubtitle'),
explanation: loc(obj.lang, 'SettingsQualityDescription'),
items: [{
"action": "max",
"text": loc(obj.lang, 'SettingsQualitySwitchMax')
}, {
"action": "hig",
"text": `${loc(obj.lang, 'SettingsQualitySwitchHigh')}(${quality.hig}p)`
}, {
"action": "mid",
"text": `${loc(obj.lang, 'SettingsQualitySwitchMedium')}(${quality.mid}p)`
}, {
"action": "low",
"text": `${loc(obj.lang, 'SettingsQualitySwitchLow')}(${quality.low}p)`
}]
})
}) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup')) : ''}`
+ settingsCategory({
name: "youtube",
body: switcher({
name: "ytFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsFormatDescription'),
items: [{
"action": "mp4"
}, {
"action": "webm"
}]
})
})
}, {
name: "audio",
title: `${emoji("🎶")} ${loc(obj.lang, 'SettingsAudioTab')}`,
content: settingsCategory({
name: "general",
title: loc(obj.lang, 'SettingsAudioTab'),
body: switcher({
name: "audioFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
items: audioFormats
})
})
}, {
name: "other",
title: `${emoji("🪅")} ${loc(obj.lang, 'SettingsOtherTab')}`,
content: settingsCategory({
name: "appearance",
title: loc(obj.lang, 'SettingsAppearanceSubtitle'),
body: switcher({
name: "theme",
subtitle: loc(obj.lang, 'SettingsThemeSubtitle'),
items: [{
"action": "auto",
"text": loc(obj.lang, 'SettingsThemeAuto')
},{
"action": "dark",
"text": loc(obj.lang, 'SettingsThemeDark')
},{
"action": "light",
"text": loc(obj.lang, 'SettingsThemeLight')
}]
})
}) + checkbox("alwaysVisibleButton", loc(obj.lang, 'SettingsKeepDownloadButton'), loc(obj.lang, 'AccessibilityKeepDownloadButton')) + checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableChangelogOnUpdate'), loc(obj.lang, 'SettingsDisableChangelogOnUpdate'))
}],
})}
${popup({
name: "download",
header: {
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
subtitle: loc(obj.lang, 'TitlePopupDownload')
},
body: switcher({
name: "download",
subtitle: loc(obj.lang, 'DownloadPopupWayToSave'),
explanation: `${!isIOS ? loc(obj.lang, 'DownloadPopupDescription') : loc(obj.lang, 'DownloadPopupDescriptionIOS')}`,
items: `<a id="pd-download" class="switch full space-right" target="_blank" href="/">${loc(obj.lang, 'Download')}</a>
<div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>`
})
})}
${popup({
name: "error",
header: {
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
title: loc(obj.lang, 'TitlePopupError')
},
body: `<div id="desc-error"></div>`
})}
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
<div id="cobalt-main-box" class="center box" style="visibility: hidden;">
<div id="logo-area">${appName}</div>
<div id="download-area" class="mobile-center">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="110" autocapitalize="off" placeholder="${loc(obj.lang, 'LinkInput')}" aria-label="${loc(obj.lang, 'AccessibilityInputArea')}" oninput="button()">
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${loc(obj.lang, 'AccessibilityDownloadButton')}">
</div>
</div>
<footer id="footer" style="visibility: hidden;">
${footerButtons([{
name: "about",
type: "popup",
icon: "?",
aria: loc(obj.lang, 'AccessibilityOpenAbout')
}, {
name: "settings",
type: "popup",
icon: "+",
aria: loc(obj.lang, 'AccessibilityOpenSettings')
}, {
name: "audioMode",
type: "toggle",
icon: emoji("✨", 22, 1),
aria: loc(obj.lang, 'AccessibilityModeToggle')
}]
)}
</footer>
</body>
<script type="text/javascript">const loc = {noInternet:"${loc(obj.lang, 'ErrorNoInternet')}", noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}", toggleDefault:'${emoji("✨")} ${loc(obj.lang, "ModeToggleDefault")}', toggleAudio:'${emoji("🎶")} ${loc(obj.lang, "SettingsFormatSwitchAudio")}'};</script>
<script type="text/javascript" src="cobalt.js"></script>
</html>`;
} catch (err) {
return `${loc(obj.lang, 'ErrorPageRenderFail', obj.hash)}`;
}
}

View File

@@ -20,7 +20,7 @@ export default async function(obj) {
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
return { urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], time: streamData.data.timelength, filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` };
return { urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], time: streamData.data.timelength, audioFilename: `bilibili_${obj.id}_audio`, filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` };
} else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) };
}

View File

@@ -28,7 +28,7 @@ export default async function(obj) {
});
iteminfo = JSON.parse(iteminfo.body);
if (iteminfo['item_list'][0]['video']['play_addr']['url_list'][0]) {
return { urls: iteminfo['item_list'][0]['video']['play_addr']['url_list'][0].replace("playwm", "play"), filename: `douyin_${obj.postId}.mp4` };
return { urls: iteminfo['item_list'][0]['video']['play_addr']['url_list'][0].replace("playwm", "play"), audioFilename: `douyin_${obj.postId}_audio`, filename: `douyin_${obj.postId}.mp4` };
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
}

View File

@@ -14,10 +14,11 @@ export default async function(obj) {
} catch (err) {
audio = ''
}
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]
if (audio.length > 0) {
return { typeId: 2, type: "render", urls: [video, audio], filename: `reddit_${data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]}.mp4` };
return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` };
} else {
return { typeId: 1, urls: video};
return { typeId: 1, urls: video, audioFilename: loc(obj.lang, 'ErrorEmptyDownload')};
}
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };

View File

@@ -23,7 +23,7 @@ export default async function(obj) {
});
html = html.body;
if (html.includes(',"preloadList":[{"url":"')) {
return { urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()), filename: `tiktok_${obj.postId}.mp4` };
return { urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()), audioFilename: `tiktok_${obj.postId}_audio`, filename: `tiktok_${obj.postId}.mp4` };
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
}

View File

@@ -14,7 +14,7 @@ export default async function(obj) {
if (html.includes('<!-- GOOGLE CAROUSEL --><script type="application/ld+json">')) {
let json = JSON.parse(html.split('<!-- GOOGLE CAROUSEL --><script type="application/ld+json">')[1].split('</script>')[0])
if (json["video"] && json["video"]["contentUrl"]) {
return json["video"]["contentUrl"]
return { urls: json["video"]["contentUrl"], audioFilename: `tumblr_${obj.id}_audio` }
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'tumblr') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'tumblr') }

View File

@@ -39,7 +39,7 @@ export default async function (obj) {
if (parsbod.hasOwnProperty("extended_entities") && parsbod["extended_entities"].hasOwnProperty("media")) {
if (parsbod["extended_entities"]["media"][0]["type"] === "video" || parsbod["extended_entities"]["media"][0]["type"] === "animated_gif") {
let variants = parsbod["extended_entities"]["media"][0]["video_info"]["variants"]
return variants.filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split('?')[0];
return { urls: variants.filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split('?')[0], audioFilename: `twitter_${obj.id}_audio` }
} else {
return nothing
}

View File

@@ -26,9 +26,9 @@ export default async function(obj) {
let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1)
let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r;})[maxQuality])
let id = js["player"]["params"][0][selectedQuality].split("id=")[1]
if (selectedQuality in js["player"]["params"][0]) {
return { url: js["player"]["params"][0][selectedQuality].replace(`type=${maxQuality}`, `type=${services.vk.quality_match[userQuality]}`), filename: `vk_${js["player"]["params"][0][selectedQuality].split("id=")[1]}_${attr['width']}x${attr['height']}.mp4` };
return { urls: js["player"]["params"][0][selectedQuality].replace(`type=${maxQuality}`, `type=${services.vk.quality_match[userQuality]}`), filename: `vk_${js["player"]["params"][0][selectedQuality].split("id=")[1]}_${attr['width']}x${attr['height']}.mp4`, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') };
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
}

View File

@@ -54,7 +54,7 @@ export default async function (obj) {
return { type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` };
} else if (audio.length > 0) {
return { type: "bridge", isAudioOnly: true, urls: audio[0]["url"], filename: `youtube_${obj.id}_${audio[0]["audioBitrate"]}kbps.${audio[0]["container"] == "webm" ? "opus" : "m4a"}` };
return { type: "bridge", isAudioOnly: true, urls: audio[0]["url"], audioFilename: `youtube_${obj.id}_audio` };
} else {
return { error: loc(obj.lang, 'ErrorBadFetch') };
}

View File

@@ -21,8 +21,9 @@ export function createStream(obj) {
ip: iphmac,
exp: exp,
isAudioOnly: obj.isAudioOnly ? true : false,
audioFormat: obj.audioFormat,
time: obj.time,
lang: obj.lang
copy: obj.copy
});
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
}

View File

@@ -2,19 +2,19 @@ import { apiJSON } from "../sub/utils.js";
import { verifyStream } from "./manage.js";
import { streamAudioOnly, streamDefault, streamLiveRender } from "./types.js";
export default function(res, ip, id, hmac, exp) {
export default function(res, ip, id, hmac, exp, lang) {
try {
let streamInfo = verifyStream(ip, id, hmac, exp, process.env.streamSalt);
if (!streamInfo.error) {
if (streamInfo.isAudioOnly && streamInfo.type == "render") {
if (streamInfo.isAudioOnly && streamInfo.type != "bridge") {
streamAudioOnly(streamInfo, res);
} else {
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
streamLiveRender(streamInfo, res, lang);
break;
default:
streamDefault(streamInfo, res);
streamDefault(streamInfo, res, lang);
break;
}
}

View File

@@ -8,7 +8,7 @@ import loc from "../../localization/manager.js";
export async function streamDefault(streamInfo, res) {
try {
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.filename}"`);
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename}"`);
const stream = got.get(streamInfo.urls, {
headers: {
"user-agent": genericUserAgent
@@ -25,7 +25,7 @@ export async function streamDefault(streamInfo, res) {
internalError(res);
}
}
export async function streamLiveRender(streamInfo, res) {
export async function streamLiveRender(streamInfo, res, lang) {
try {
if (streamInfo.urls.length == 2) {
let headers = {};
@@ -69,7 +69,7 @@ export async function streamLiveRender(streamInfo, res) {
ffmpegProcess.kill();
});
} else {
res.status(400).json({ status: "error", text: loc(streamInfo.lang, 'ErrorCorruptedStream') });
res.status(400).json({ status: "error", text: loc(lang, 'ErrorCorruptedStream') });
}
} catch (e) {
internalError(res);
@@ -82,15 +82,15 @@ export async function streamAudioOnly(streamInfo, res) {
headers = { "user-agent": genericUserAgent };
}
const audio = got.get(streamInfo.urls, { isStream: true, headers: headers });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
let args = [
'-loglevel', '-8',
'-i', 'pipe:3',
'-vn',
];
args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:4');
'-vn'
]
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]
args = args.concat(arg)
if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]);
args.push('-f', streamInfo.audioFormat == "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:4');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
@@ -104,7 +104,7 @@ export async function streamAudioOnly(streamInfo, res) {
audio.on('error', (err) => {
ffmpegProcess.kill();
});
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
ffmpegProcess.stdio[4].pipe(res);
audio.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill();

View File

@@ -0,0 +1,68 @@
import { supportedAudio } from "../config.js"
import { apiJSON } from "./utils.js"
export default function(r, host, ip, audioFormat, isAudioOnly) {
if (!r.error) {
if (!isAudioOnly) {
switch (host) {
case "twitter":
return apiJSON(1, { u: r.urls })
case "vk":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
})
case "bilibili":
return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt,
time: r.time
})
case "youtube":
return apiJSON(2, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt,
time: r.time,
})
case "reddit":
return apiJSON(r.typeId, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
})
case "tiktok":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
})
case "douyin":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
})
case "tumblr":
return apiJSON(1, { u: r.urls })
}
} else {
let type = "render"
let copy = false
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if (audioFormat == "best") {
if (host != "youtube") {
audioFormat = "m4a"
copy = true
} else {
audioFormat = "opus"
type = "bridge"
}
}
if (host == "reddit" && r.typeId == 1 || host == "vk") return apiJSON(0, { t: r.audioFilename });
return apiJSON(2, {
type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
})
}
} else {
return apiJSON(0, { t: r.error });
}
}