This commit is contained in:
wukko 2022-08-16 13:14:19 +06:00
parent a5e081e2bf
commit e11ee6fb62
24 changed files with 106 additions and 115 deletions

View file

@ -61,7 +61,7 @@ Take English or Russian localization from [this directory](https://github.com/wu
- [ ] Add an option to keep watermark on TikTok videos - [ ] Add an option to keep watermark on TikTok videos
### Other ### Other
- [ ] Remake video quality picking (do it more like I did in Vimeo module) - [ ] Remake video quality picking
- [ ] Add support for emoji in localization - [ ] Add support for emoji in localization
- [ ] Language picker in settings - [ ] Language picker in settings
- [ ] Make cobalt fully PWA compatible (add a service worker) - [ ] Make cobalt fully PWA compatible (add a service worker)
@ -96,8 +96,6 @@ Setup script installs all needed `npm` dependencies, but you have to install `No
## Disclaimer ## Disclaimer
This is my passion project, so update scheduele depends solely on my motivation. Don't expect any consistency in that. This is my passion project, so update scheduele depends solely on my motivation. Don't expect any consistency in that.
## Third party stuff
[Fluent Emoji](https://github.com/microsoft/fluentui-emoji) by Microsoft.
## License ## License
cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE). cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).
[Fluent Emoji](https://github.com/microsoft/fluentui-emoji) by Microsoft is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE).

View file

@ -50,8 +50,8 @@ if (fs.existsSync('./.env')) {
try { try {
decodeURIComponent(req.path) decodeURIComponent(req.path)
} }
catch(e) { catch (e) {
return res.redirect(process.env.selfURL); return res.redirect(process.env.selfURL);
} }
next(); next();
}); });

View file

@ -51,7 +51,7 @@ function changeDownloadButton(action, text) {
break; break;
} }
} }
document.addEventListener("keydown", function (event) { document.addEventListener("keydown", (event) => {
if (event.key == "Tab") { if (event.key == "Tab") {
eid("download-button").value = '>>' eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem' eid("download-button").style.padding = '0 1rem'
@ -257,7 +257,7 @@ async function download(url) {
} }
}).catch((error) => internetError()); }).catch((error) => internetError());
} }
window.onload = function () { window.onload = () => {
loadSettings(); loadSettings();
detectColorScheme(); detectColorScheme();
changeDownloadButton(0, '>>'); changeDownloadButton(0, '>>');
@ -274,6 +274,6 @@ window.onload = function () {
eid("url-input-area").addEventListener("keyup", (event) => { eid("url-input-area").addEventListener("keyup", (event) => {
if (event.key === 'Enter') eid("download-button").click(); if (event.key === 'Enter') eid("download-button").click();
}) })
document.onkeydown = function(event) { document.onkeydown = (event) => {
if (event.key === 'Escape') hideAllPopups(); if (event.key === 'Escape') hideAllPopups();
}; };

View file

@ -59,7 +59,7 @@
"SettingsEnableDownloadPopup": "спрашивать, как сохранять", "SettingsEnableDownloadPopup": "спрашивать, как сохранять",
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.", "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.",
"SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. такие видео твиттер обычно воспринимает намного лучше.", "SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. твиттер такие видео обычно воспринимает намного лучше.",
"DonateSubtitle": "помоги мне платить за хостинг", "DonateSubtitle": "помоги мне платить за хостинг",
"DonateDescription": "я не люблю крипто в его текущем состоянии, но у меня нет другого надёжного способа оплаты хостинга.", "DonateDescription": "я не люблю крипто в его текущем состоянии, но у меня нет другого надёжного способа оплаты хостинга.",
"LinkGitHubIssues": ">> сообщай о проблемах и смотри исходный код на github", "LinkGitHubIssues": ">> сообщай о проблемах и смотри исходный код на github",

View file

@ -29,10 +29,10 @@ export async function getJSON(originalURL, ip, lang, format, quality, audioForma
} }
if (patternMatch) { if (patternMatch) {
return await match(host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly); return await match(host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly);
} return apiJSON(0, { t: errorUnsupported(lang) } ) } return apiJSON(0, { t: errorUnsupported(lang) })
} return apiJSON(0, { t: errorUnsupported(lang) } ) } return apiJSON(0, { t: errorUnsupported(lang) })
} else { } else {
return apiJSON(0, { t: errorUnsupported(lang) } ) return apiJSON(0, { t: errorUnsupported(lang) })
} }
} catch (e) { } catch (e) {
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }); return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });

View file

@ -6,7 +6,7 @@ export async function buildFront() {
entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'], entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'],
outdir: `min/`, outdir: `min/`,
minify: true, minify: true,
loader: {".js": "js", ".css": "css"} loader: { ".js": "js", ".css": "css" }
}) })
} catch (e) { } catch (e) {
return; return;

View file

@ -3,17 +3,17 @@ const config = loadJson("./src/config.json");
const packageJson = loadJson("./package.json"); const packageJson = loadJson("./package.json");
export const export const
services = loadJson("./src/modules/servicesConfig.json"), services = loadJson("./src/modules/servicesConfig.json"),
appName = packageJson.name, appName = packageJson.name,
version = packageJson.version, version = packageJson.version,
streamLifespan = config.streamLifespan, streamLifespan = config.streamLifespan,
maxVideoDuration = config.maxVideoDuration, maxVideoDuration = config.maxVideoDuration,
genericUserAgent = config.genericUserAgent, genericUserAgent = config.genericUserAgent,
repo = packageJson["bugs"]["url"].replace('/issues', ''), repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo, authorInfo = config.authorInfo,
supportedLanguages = config.supportedLanguages, supportedLanguages = config.supportedLanguages,
quality = config.quality, quality = config.quality,
internetExplorerRedirect = config.internetExplorerRedirect, internetExplorerRedirect = config.internetExplorerRedirect,
donations = config.donations, donations = config.donations,
ffmpegArgs = config.ffmpegArgs, ffmpegArgs = config.ffmpegArgs,
supportedAudio = config.supportedAudio supportedAudio = config.supportedAudio

View file

@ -17,7 +17,7 @@ let sizing = {
} }
export default function(emoji, size, disablePadding) { export default function(emoji, size, disablePadding) {
if (!size) size = 22; if (!size) size = 22;
let padding = size != 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;`: ``; let padding = size != 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : ``;
if (disablePadding) padding = 'margin-right:0!important;'; if (disablePadding) padding = 'margin-right:0!important;';
if (!names[emoji]) emoji = "❓"; if (!names[emoji]) emoji = "❓";
return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">` return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">`

View file

@ -42,7 +42,7 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
break; break;
case "youtube": case "youtube":
let fetchInfo = { let fetchInfo = {
id: patternMatch["id"].slice(0,11), id: patternMatch["id"].slice(0, 11),
lang: lang, quality: quality, lang: lang, quality: quality,
format: "webm" format: "webm"
}; };
@ -87,7 +87,7 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
break; break;
case "vimeo": case "vimeo":
r = await vimeo({ r = await vimeo({
id: patternMatch["id"], quality: quality, id: patternMatch["id"].slice(0, 11), quality: quality,
lang: lang lang: lang
}); });
break; break;

View file

@ -40,9 +40,9 @@ export function popup(obj) {
} }
} }
return ` return `
${!obj.embed ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">`: ''} ${!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"> <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.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.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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
@ -51,7 +51,7 @@ export function popup(obj) {
${obj.footer ? `<div id="popup-footer" class="popup-footer"> ${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> <a id="popup-bottom" class="popup-footer-content" href="${obj.footer.url}">${obj.footer.text}</a>
</div>` : ''} </div>` : ''}
${!obj.embed ? `</div>`: ''}` ${!obj.embed ? `</div>` : ''}`
} }
export function multiPagePopup(obj) { export function multiPagePopup(obj) {
@ -68,7 +68,7 @@ export function multiPagePopup(obj) {
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header"> <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.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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}</div>`: ''}${tabContent}</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 id="popup-tabs" class="switches popup-tabs">${tabs}</div>
</div>` </div>`
} }

View file

@ -1,7 +1,7 @@
import { backdropLink, checkbox, footerButtons, multiPagePopup, popup, settingsCategory, switcher } from "./elements.js";
import { services, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js"; import { services, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js"; import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import { backdropLink, checkbox, footerButtons, multiPagePopup, popup, settingsCategory, switcher } from "./elements.js";
import emoji from "../emoji.js"; import emoji from "../emoji.js";
let s = services; let s = services;
@ -16,7 +16,7 @@ let enabledServices = Object.keys(s).filter((p) => {
let donate = `` let donate = ``
let donateLinks = `` let donateLinks = ``
let audioFormats = supportedAudio.map((p) => { let audioFormats = supportedAudio.map((p) => {
return {"action": p} return { "action": p }
}) })
audioFormats.unshift({ "action": "best" }) audioFormats.unshift({ "action": "best" })
for (let i in donations["other"]) { for (let i in donations["other"]) {
@ -183,19 +183,19 @@ export default function(obj) {
}] }]
}) })
}) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup')) : ''}` }) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup')) : ''}`
+ settingsCategory({ + settingsCategory({
name: "youtube", name: "youtube",
body: switcher({ body: switcher({
name: "ytFormat", name: "ytFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'), subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsFormatDescription'), explanation: loc(obj.lang, 'SettingsFormatDescription'),
items: [{ items: [{
"action": "mp4" "action": "mp4"
}, { }, {
"action": "webm" "action": "webm"
}] }]
})
}) })
})
}, { }, {
name: "audio", name: "audio",
title: `${emoji("🎶")} ${loc(obj.lang, 'SettingsAudioTab')}`, title: `${emoji("🎶")} ${loc(obj.lang, 'SettingsAudioTab')}`,
@ -221,10 +221,10 @@ export default function(obj) {
items: [{ items: [{
"action": "auto", "action": "auto",
"text": loc(obj.lang, 'SettingsThemeAuto') "text": loc(obj.lang, 'SettingsThemeAuto')
},{ }, {
"action": "dark", "action": "dark",
"text": loc(obj.lang, 'SettingsThemeDark') "text": loc(obj.lang, 'SettingsThemeDark')
},{ }, {
"action": "light", "action": "light",
"text": loc(obj.lang, 'SettingsThemeLight') "text": loc(obj.lang, 'SettingsThemeLight')
}] }]
@ -264,21 +264,21 @@ export default function(obj) {
</div> </div>
<footer id="footer" style="visibility: hidden;"> <footer id="footer" style="visibility: hidden;">
${footerButtons([{ ${footerButtons([{
name: "about", name: "about",
type: "popup", type: "popup",
icon: "?", icon: "?",
aria: loc(obj.lang, 'AccessibilityOpenAbout') aria: loc(obj.lang, 'AccessibilityOpenAbout')
}, { }, {
name: "settings", name: "settings",
type: "popup", type: "popup",
icon: "+", icon: "+",
aria: loc(obj.lang, 'AccessibilityOpenSettings') aria: loc(obj.lang, 'AccessibilityOpenSettings')
}, { }, {
name: "audioMode", name: "audioMode",
type: "toggle", type: "toggle",
icon: emoji("✨", 22, 1), icon: emoji("✨", 22, 1),
aria: loc(obj.lang, 'AccessibilityModeToggle') aria: loc(obj.lang, 'AccessibilityModeToggle')
}] }]
)} )}
</footer> </footer>
</body> </body>

View file

@ -20,7 +20,7 @@ export default async function(obj) {
let audio = streamData["data"]["dash"]["audio"].filter((a) => { let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
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` }; 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 { } else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) }; return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) };
} }

View file

@ -15,14 +15,16 @@ export default async function(obj) {
obj.postId = html.body.split('video/')[1].split('/?')[0] obj.postId = html.body.split('video/')[1].split('/?')[0]
} }
} }
let iteminfo = await got.get(`https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${obj.postId}`, {headers: { let iteminfo = await got.get(`https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${obj.postId}`, {
'authority': 'www.iesdouyin.com', headers: {
'user-agent': genericUserAgent, 'authority': 'www.iesdouyin.com',
'content-type': 'application/x-www-form-urlencoded', 'user-agent': genericUserAgent,
'accept': '*/*', 'content-type': 'application/x-www-form-urlencoded',
'referer': `https://www.iesdouyin.com/share/video/${obj.postId}/?region=CN&u_code=15b9142gf&titleType=title&utm_source=copy_link&utm_campaign=client_share&utm_medium=android&app=aweme`, 'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7' 'referer': `https://www.iesdouyin.com/share/video/${obj.postId}/?region=CN&u_code=15b9142gf&titleType=title&utm_source=copy_link&utm_campaign=client_share&utm_medium=android&app=aweme`,
}}); 'accept-language': 'zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7'
}
});
iteminfo.on('error', (err) => { iteminfo.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'douyin') }; return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'douyin') };
}); });

View file

@ -18,7 +18,7 @@ export default async function(obj) {
if (audio.length > 0) { if (audio.length > 0) {
return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` }; return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` };
} else { } else {
return { typeId: 1, urls: video, audioFilename: loc(obj.lang, 'ErrorEmptyDownload')}; return { typeId: 1, urls: video, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') };
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') }; return { error: loc(obj.lang, 'ErrorEmptyDownload') };

View file

@ -31,7 +31,7 @@ async function fetchTweetInfo(obj) {
return cantConnect; return cantConnect;
} }
} }
export default async function (obj) { export default async function(obj) {
let nothing = { error: loc(obj.lang, 'ErrorEmptyDownload') } let nothing = { error: loc(obj.lang, 'ErrorEmptyDownload') }
try { try {
let parsbod = await fetchTweetInfo(obj); let parsbod = await fetchTweetInfo(obj);

View file

@ -25,7 +25,7 @@ export default async function(obj) {
let selectedQuality = `url${attr["height"]}`; let selectedQuality = `url${attr["height"]}`;
let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1) 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 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] let id = js["player"]["params"][0][selectedQuality].split("id=")[1]
if (selectedQuality in js["player"]["params"][0]) { if (selectedQuality in js["player"]["params"][0]) {
return { urls: js["player"]["params"][0][selectedQuality].replace(`type=${maxQuality}`, `type=${services.vk.quality_match[userQuality]}`), filename: `vk_${id}_${attr['width']}x${attr['height']}.mp4`, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') }; return { urls: js["player"]["params"][0][selectedQuality].replace(`type=${maxQuality}`, `type=${services.vk.quality_match[userQuality]}`), filename: `vk_${id}_${attr['width']}x${attr['height']}.mp4`, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') };

View file

@ -3,7 +3,7 @@ import loc from "../../localization/manager.js";
import { maxVideoDuration, quality as mq } from "../config.js"; import { maxVideoDuration, quality as mq } from "../config.js";
import selectQuality from "../stream/selectQuality.js"; import selectQuality from "../stream/selectQuality.js";
export default async function (obj) { export default async function(obj) {
try { try {
let info = await ytdl.getInfo(obj.id); let info = await ytdl.getInfo(obj.id);
if (info) { if (info) {
@ -41,18 +41,24 @@ export default async function (obj) {
if (!obj.isAudioOnly && videoMatch.length > 0) { if (!obj.isAudioOnly && videoMatch.length > 0) {
if (video.length > 0 && audio.length > 0) { if (video.length > 0 && audio.length > 0) {
if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) { if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) {
return { type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"], return {
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` }; type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
} else { } else {
return { type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"], return {
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` }; type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };
} }
} else if (!obj.isAudioOnly) { } else if (!obj.isAudioOnly) {
return { type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"], return {
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` }; 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) { } else if (audio.length > 0) {
return { type: "bridge", isAudioOnly: true, urls: audio[0]["url"], audioFilename: `youtube_${obj.id}_audio` }; return { type: "bridge", isAudioOnly: true, urls: audio[0]["url"], audioFilename: `youtube_${obj.id}_audio` };
} else { } else {

View file

@ -40,6 +40,7 @@
"enabled": true "enabled": true
}, },
"youtube": { "youtube": {
"alias": "youtube, youtube music",
"patterns": ["watch?v=:id"], "patterns": ["watch?v=:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"], "quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"quality": { "quality": {
@ -49,18 +50,10 @@
}, },
"enabled": true "enabled": true
}, },
"youtube music": {
"patterns": ["watch?v=:id"],
"enabled": true
},
"tumblr": { "tumblr": {
"patterns": ["post/:id", "blog/view/:user/:id"], "patterns": ["post/:id", "blog/view/:user/:id"],
"enabled": true "enabled": true
}, },
"instagram": {
"patterns": [":type/:id"],
"enabled": false
},
"tiktok": { "tiktok": {
"patterns": [":user/video/:postId", ":id", "t/:id"], "patterns": [":user/video/:postId", ":id", "t/:id"],
"enabled": true "enabled": true

View file

@ -2,23 +2,23 @@ export let testers = {
"twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20), "twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20),
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] && "vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] &&
patternMatch["userId"].length <= 10 && patternMatch["videoId"].length == 9), patternMatch["userId"].length <= 10 && patternMatch["videoId"].length == 9),
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12), "bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12),
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11), "youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11),
"reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] && "reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] &&
patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96), patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96),
"tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) || "tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) ||
(patternMatch["id"] && patternMatch["id"].length <= 13)), (patternMatch["id"] && patternMatch["id"].length <= 13)),
"douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) || "douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) ||
(patternMatch["id"] && patternMatch["id"].length <= 13)), (patternMatch["id"] && patternMatch["id"].length <= 13)),
"tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) || "tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) ||
(patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)), (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)), "vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
}; };

View file

@ -7,7 +7,7 @@ import { execSync } from "child_process";
let envPath = './.env'; let envPath = './.env';
let q = `${Cyan('?')} \x1b[1m`; let q = `${Cyan('?')} \x1b[1m`;
let ob = { streamSalt: randomBytes(64).toString('hex') } let ob = { streamSalt: randomBytes(64).toString('hex') }
let rl = createInterface({ input: process.stdin,output: process.stdout }); let rl = createInterface({ input: process.stdin, output: process.stdout });
console.log( console.log(
`${Cyan("Welcome to cobalt!")}\n${Bright("We'll get you up and running in no time.\nLet's start by creating a ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}` `${Cyan("Welcome to cobalt!")}\n${Bright("We'll get you up and running in no time.\nLet's start by creating a ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
@ -46,7 +46,7 @@ let final = () => {
} }
console.log(Bright("\nI've created a .env file with selfURL, port, and stream salt.")) console.log(Bright("\nI've created a .env file with selfURL, port, and stream salt."))
console.log(`${Bright("Now I'll run")} ${Cyan("npm install")} ${Bright("to install all dependencies. It shouldn't take long.\n\n")}`) console.log(`${Bright("Now I'll run")} ${Cyan("npm install")} ${Bright("to install all dependencies. It shouldn't take long.\n\n")}`)
execSync('npm install',{stdio:[0,1,2]}); execSync('npm install', { stdio: [0, 1, 2] });
console.log(`\n\n${Green("All done!\n")}`) console.log(`\n\n${Green("All done!\n")}`)
console.log("You can re-run this script any time to update the configuration.") console.log("You can re-run this script any time to update the configuration.")
console.log("\nYou're now ready to start the main project.\nHave fun!") console.log("\nYou're now ready to start the main project.\nHave fun!")

View file

@ -1,22 +1,19 @@
import { services, quality as mq } from "../config.js"; import { services, quality as mq } from "../config.js";
function closest(goal, array) { function closest(goal, array) {
return array.sort().reduce(function(prev, curr) { return array.sort().reduce(function (prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
}); });
} }
export default function(service, quality, maxQuality) { export default function(service, quality, maxQuality) {
if (quality == "max") { if (quality == "max") return maxQuality;
return maxQuality
}
quality = parseInt(mq[quality]) quality = parseInt(mq[quality])
maxQuality = parseInt(maxQuality) maxQuality = parseInt(maxQuality)
if (quality >= maxQuality || quality == maxQuality) { if (quality >= maxQuality || quality == maxQuality) return maxQuality;
return maxQuality
}
if (quality < maxQuality) { if (quality < maxQuality) {
if (services[service]["quality"][quality]) { if (services[service]["quality"][quality]) {
return quality return quality

View file

@ -55,27 +55,22 @@ export async function streamLiveRender(streamInfo, res) {
ffmpegProcess.on('error', (err) => { ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
return;
}); });
video.pipe(ffmpegProcess.stdio[3]).on('error', (err) => { video.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
return;
}); });
audio.pipe(ffmpegProcess.stdio[4]).on('error', (err) => { audio.pipe(ffmpegProcess.stdio[4]).on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
return;
}); });
audio.on('error', (err) => { audio.on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
return;
}); });
video.on('error', (err) => { video.on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
return;
}); });
} else { } else {
res.end(); res.end();

View file

@ -40,9 +40,9 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
filename: r.filename, salt: process.env.streamSalt filename: r.filename, salt: process.env.streamSalt
}) })
case "tumblr": case "tumblr":
return apiJSON(1, { u: r.urls }) return apiJSON(1, { u: r.urls })
case "vimeo": case "vimeo":
return apiJSON(1, { u: r.urls }) return apiJSON(1, { u: r.urls })
} }
} else { } else {
let type = "render" let type = "render"

View file

@ -7,7 +7,7 @@ export function apiJSON(type, obj) {
return { status: 400, body: { status: "error", text: obj.t } }; return { status: 400, body: { status: "error", text: obj.t } };
case 1: case 1:
return { status: 200, body: { status: "redirect", url: obj.u } }; return { status: 200, body: { status: "redirect", url: obj.u } };
case 2: case 2:
return { status: 200, body: { status: "stream", url: createStream(obj) } }; return { status: 200, body: { status: "stream", url: createStream(obj) } };
case 3: case 3:
return { status: 200, body: { status: "success", text: obj.t } }; return { status: 200, body: { status: "success", text: obj.t } };