Merge branch 'current' into instagram-stories
This commit is contained in:
@@ -1,5 +1,36 @@
|
||||
{
|
||||
"current": {
|
||||
"version": "7.5",
|
||||
"date": "September 16, 2023",
|
||||
"title": "support for twitch clips and rutube!",
|
||||
"banner": {
|
||||
"file": "twitchupdate.webp",
|
||||
"width": 851,
|
||||
"height": 640
|
||||
},
|
||||
"content": "hey! this update (finally) adds support for twitch clips and rutube, among other smaller changes.\n\nservice improvements:\n*; added support for twitch clips. no vods, they're unnecessary. just clip whatever you want to download!\n*; added support for rutube in case you ever wanted to download something russian.\n\ninterface improvements:\n*; added a note about cobalt not being affiliated with any supported services.\n*; added a note about meta (the company) in russian.\n*; better russian localization. will keep improving it to make it sound not so robotic over time.\n\nother improvements:\n*; all official servers are now using the docker package. and so should you!\n*; moved the load balancer to poland. requests should be slightly faster now.\n*; minor codebase clean up.\n\nif you're confused about the new domain, read the older changelog! just scroll lower and press \"expand\".\n\ni hope you find this update useful and have a wonderful day :)\n\nbtw, cobalt has a pretty active community server on discord. go to about > support & source code to join!"
|
||||
},
|
||||
"history": [{
|
||||
"version": "7.4",
|
||||
"date": "September 9, 2023",
|
||||
"title": "new domain, what's coming in future, bug fixes, and more!",
|
||||
"banner": {
|
||||
"file": "newdomain.webp",
|
||||
"width": 960,
|
||||
"height": 540
|
||||
},
|
||||
"content": "cobalt is finally moving to its own domain! many of you have been anticipating this, and many kept forgetting the link due to how cryptic it was.\n\nwell, worry no more - <span class=\"text-backdrop\">cobalt.tools</span> is here.\n\nif you haven't yet, open <a class=\"text-backdrop link\" href=\"https://co.wukko.me\" target=\"_blank\">co.wukko.me</a> to transfer your settings here! no additional action from you is required. just open the old link and cobalt will do everything for you :)\n\nmake sure to <span class=\"text-backdrop\">update your bookmarks</span> and reinstall the web app!\n\nhere's what domain change means:\n*; still no ads, same owner, same features, same reliability. just a way more rememberable link (it's literally two words).\n*; cobalt.tools makes it clear that cobalt is a tool and that it's \"cobalt\", not \"wukko\".\n*; i can host various versions of cobalt on subdomains without links looking awkward.\n*; i can host cobalt-related websites without polluting my personal domain's dns (such as crowdin).\n*; i stand by same privacy policies (and in fact am using the same exact server as before).\n\nthe domain change is required for the future of cobalt.\n\nhere's what's coming soon:\n*; support for many top-requested sites, such as (but not limited to) twitch and niconico.\n*; education version of cobalt, as often requested by students and educators.\n*; major localization system upgrade, allowing for simpler community contributions.\n*; region-specific versions with 100% translations and tweaks.\n*; native clients for desktop and mobile (not sure about this one, i'm no superman).\n*; ...and more!\n\nnow, here's what's new in 7.4:\n*; tabs in popups now scroll to top on tab bar tap.\n*; padding across web app was tuned.\n*; (obviously) a migration agent. soon will be used for importing and exporting settings.\n*; some minor clean ups in codebase.\n\nif you want to help cobalt achieve goals listed above, consider donating! donations are the only way i can keep cobalt ad-less, powerful, (basically) limitless, and also 100% free.\n\nin fact, donations have helped me grow cobalt more than i've ever anticipated. just imagine how much better it will be in a year.\n\ngo to donations down below to find ways to donate!\n\nthank you for reading through all of this. i hope you enjoy this update and have a great day :D"
|
||||
}, {
|
||||
"version": "7.2 & 7.3",
|
||||
"date": "September 6, 2023",
|
||||
"title": "extended video length limit, metadata toggle, ui improvements, and more!",
|
||||
"banner": {
|
||||
"file": "meowthsnap.webp",
|
||||
"width": 500,
|
||||
"height": 280
|
||||
},
|
||||
"content": "this update gives cobalt a sharp look in chromium browsers and makes it even more useful than before. check out the full changelog below!\n\nservice improvements:\n*; increased video length limit from 3 hours to 5 hours. feel free to download lectures you need :)\n*; you can now disable file metadata in settings.\n*; fixed a bug which previously caused some downloads to end up being 0 bytes.\n\nui improvements:\n*; fixed clickable area for urgent notice (text on top).\n*; fixed blurry header in chrome.\n*; fixed blurry tab bar in chrome.\n*; fixed blurry switches in chrome.\n*; fixed weirdly rounded corners in popups.\n*; fixed 1px gap on edges of various elements in popup in chrome.\n*; fixed overscrolling in other settings tab on ios.\n*; fixed unexpected button highlight effect on phones.\n*; removed outdated fixes for tiny screens.\n\nother improvements:\n*; cobalt web & api start faster than before, additional preparation functions aren't unexpectedly run anymore.\n*; cobalt is now available as a docker package. check it out on <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pkgs/container/cobalt\" target=\"_blank\">github</a>.\n\nthank you for being here. i hope you have a great day :D"
|
||||
}, {
|
||||
"version": "7.1",
|
||||
"date": "August 20, 2023",
|
||||
"title": "instagram, streamable, video metadata, and more!",
|
||||
@@ -9,8 +40,7 @@
|
||||
"height": 358
|
||||
},
|
||||
"content": "service improvements:\n*; extended instagram support: high quality photos, videos, reels. everything should work without any issues, enjoy! :)\n*; added support for streamable.com (thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/179\" target=\"_blank\">#179</a>)\n*; added video metadata to youtube videos.\n*; fixed vk video downloads.\n*; vxtwitter links are now supported.\n*; fixed support for youtube audio dubs.\n\nui improvements:\n*; fixed picker popup: it's now scrollable in all cases and clickable areas don't overlap each other.\n\nbackend improvements:\n*; cobalt will now let you know if something goes wrong during video download instead of nuking the stream.\n*; added support for cookies (thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/177\" target=\"_blank\">#177</a>)\n*; replaced got with undici (thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/182\" target=\"_blank\">#182</a>). downloads should be slightly faster and clean of garbage in headers.\n\ninternal improvements:\n*; moved host overrides into its own module.\n*; minor clean ups.\n\neven more cool stuff is coming in future updates! thank you for using cobalt :D"
|
||||
},
|
||||
"history": [{
|
||||
}, {
|
||||
"version": "7.0",
|
||||
"date": "August 15, 2023",
|
||||
"title": "biggest ui refresh yet!",
|
||||
|
||||
@@ -33,7 +33,9 @@ const names = {
|
||||
"🔗": "link",
|
||||
"⌨": "keyboard",
|
||||
"📑": "boring_document",
|
||||
"🧮": "abacus"
|
||||
"🧮": "abacus",
|
||||
"😸": "cat_grin",
|
||||
"📰": "newspaper"
|
||||
}
|
||||
let sizing = {
|
||||
18: 0.8,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { celebrations } from "../config.js";
|
||||
import { authorInfo, celebrations } from "../config.js";
|
||||
import emoji from "../emoji.js";
|
||||
|
||||
export const backButtonSVG = `<svg width="22" height="22" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -68,17 +68,19 @@ export function popup(obj) {
|
||||
}
|
||||
return `
|
||||
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box": ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''}
|
||||
<div id="popup-header" class="popup-header${!obj.buttonOnly ? " glass-bkg": ''}">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<div id="popup-header-contents">
|
||||
${obj.buttonOnly ? obj.header.emoji : ``}
|
||||
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" 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>
|
||||
${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>`: ''}
|
||||
</div>
|
||||
<div id="popup-content" class="popup-content-inner">
|
||||
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
|
||||
</div>
|
||||
${classes.includes("small") ? `<div class="glass-bkg small"></div>`: ''}
|
||||
${obj.standalone ? `</div>` : ''}`
|
||||
}
|
||||
|
||||
@@ -97,14 +99,18 @@ export function multiPagePopup(obj) {
|
||||
return `
|
||||
<div id="popup-${obj.name}" class="popup center box scrollable">
|
||||
<div id="popup-content">
|
||||
${obj.header ? `<div id="popup-header" class="popup-header glass-bkg">
|
||||
${obj.header ? `<div id="popup-header" class="popup-header">
|
||||
<div id="popup-header-contents">
|
||||
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" 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 class="glass-bkg alone"></div>
|
||||
</div>` : ''}${tabContent}</div>
|
||||
<div id="popup-tabs" class="switches popup-tabs glass-bkg"><div class="switches popup-tabs-child">${tabs}</div></div>
|
||||
<div id="popup-tabs" class="switches popup-tabs">
|
||||
<div class="switches popup-tabs-child">${tabs}</div>
|
||||
<div class="glass-bkg alone"></div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
export function collapsibleList(arr) {
|
||||
@@ -136,20 +142,34 @@ export function popupWithBottomButtons(obj) {
|
||||
return `
|
||||
<div id="popup-${obj.name}" class="popup center box scrollable">
|
||||
<div id="popup-content">
|
||||
${obj.header ? `<div id="popup-header" class="popup-header glass-bkg">
|
||||
${obj.header ? `<div id="popup-header" class="popup-header">
|
||||
<div id="popup-header-contents">
|
||||
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" 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>` : ''}
|
||||
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}
|
||||
</div>
|
||||
<div class="glass-bkg alone"></div>
|
||||
</div>` : ''}${obj.content}</div>
|
||||
<div id="popup-tabs" class="switches popup-tabs glass-bkg"><div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div></div>
|
||||
<div id="popup-tabs" class="switches popup-tabs">
|
||||
<div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div>
|
||||
<div class="glass-bkg alone"></div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
export function socialLink(emji, name, handle, url) {
|
||||
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop link" href="${url}" target="_blank">${handle}</a></div>`
|
||||
}
|
||||
export function socialLinks(lang) {
|
||||
let links = authorInfo.support[lang] ? authorInfo.support[lang] : authorInfo.support.default;
|
||||
let r = ``;
|
||||
for (let i in links) {
|
||||
r += socialLink(
|
||||
emoji(links[i].emoji), i, links[i].handle, links[i].url
|
||||
)
|
||||
}
|
||||
return r
|
||||
}
|
||||
export function settingsCategory(obj) {
|
||||
return `<div id="settings-${obj.name}" class="settings-category">
|
||||
<div class="category-title">${obj.title ? obj.title : obj.name}</div>
|
||||
@@ -205,7 +225,9 @@ export function celebrationsEmoji() {
|
||||
}
|
||||
export function urgentNotice(obj) {
|
||||
if (obj.visible) {
|
||||
return `<div id="urgent-notice" class="urgent-notice explanation" onclick="${obj.action}">${emoji(obj.emoji, 18)} ${obj.text}</div>`
|
||||
return `<div id="urgent-notice" class="urgent-notice explanation">` +
|
||||
`<span class="urgent-text" onclick="${obj.action}">${emoji(obj.emoji, 18)} ${obj.text}</span>` +
|
||||
`</div>`
|
||||
}
|
||||
return ``
|
||||
}
|
||||
@@ -226,3 +248,10 @@ export function keyboardShortcuts(arr) {
|
||||
|
||||
return base;
|
||||
}
|
||||
export function webLoc(t, arr) {
|
||||
let base = ``;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
base += `${arr[i]}:` + "`" + t(arr[i]) + "`" + `,`
|
||||
}
|
||||
return `{${base}};`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, urgentNotice, keyboardShortcuts } from "./elements.js";
|
||||
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc } from "./elements.js";
|
||||
import { services as s, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
|
||||
import { getCommitInfo } from "../sub/currentCommit.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
@@ -33,7 +33,7 @@ export default function(obj) {
|
||||
let isIOS = ua.match("iphone os");
|
||||
let isMobile = ua.match("android") || ua.match("iphone os");
|
||||
|
||||
let platform = isMobile ? "m" : "p";
|
||||
let platform = isMobile ? "m" : "d";
|
||||
if (isMobile && isIOS) platform = "i";
|
||||
|
||||
audioFormats[0]["text"] = t('SettingsAudioFormatBest');
|
||||
@@ -71,11 +71,11 @@ export default function(obj) {
|
||||
<link rel="stylesheet" href="fonts/notosansmono.css" rel="preload" />
|
||||
<link rel="stylesheet" href="cobalt.css" />
|
||||
|
||||
<link rel="me" href="${authorInfo.support.mastodon.url}">
|
||||
<link rel="me" href="${authorInfo.support.default.mastodon.url}">
|
||||
|
||||
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
|
||||
</head>
|
||||
<body id="cobalt-body" ${platform === "p" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
|
||||
<body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
|
||||
<body id="notification-area"></div>
|
||||
${multiPagePopup({
|
||||
name: "about",
|
||||
@@ -99,7 +99,11 @@ export default function(obj) {
|
||||
text: collapsibleList([{
|
||||
name: "services",
|
||||
title: `${emoji("🔗")} ${t("CollapseServices")}`,
|
||||
body: `${enabledServices}<br/><br/>${t("ServicesNote")}`
|
||||
body: `${enabledServices}`
|
||||
+ `<div class="explanation embedded">${t("SupportNotAffiliated")}`
|
||||
+ `${obj.lang === "ru" ? `<br>${t("SupportMetaNoticeRU")}` : ''}`
|
||||
+ `</div>`
|
||||
+ `${t("ServicesNote")}`
|
||||
}, {
|
||||
name: "keyboard",
|
||||
title: `${emoji("⌨")} ${t("CollapseKeyboard")}`,
|
||||
@@ -143,19 +147,11 @@ export default function(obj) {
|
||||
name: "support",
|
||||
title: `${emoji("❤️🩹")} ${t("CollapseSupport")}`,
|
||||
body:
|
||||
`${t("SupportSelfTroubleshooting")}<br/><br/>
|
||||
${t("FollowSupport")}<br/>
|
||||
${socialLink(
|
||||
emoji("🐦"), "twitter", authorInfo.support.twitter.handle, authorInfo.support.twitter.url
|
||||
)}
|
||||
${socialLink(
|
||||
emoji("👾"), "discord", authorInfo.support.discord.handle, authorInfo.support.discord.url
|
||||
)}
|
||||
${socialLink(
|
||||
emoji("🐘"), "mastodon", authorInfo.support.mastodon.handle, authorInfo.support.mastodon.url
|
||||
)}<br/>
|
||||
${t("SourceCode")}<br/>
|
||||
${socialLink(
|
||||
`${t("SupportSelfTroubleshooting")}<br/><br/>`
|
||||
+ `${t("FollowSupport")}<br/>`
|
||||
+ `${socialLinks(obj.lang)}<br/>`
|
||||
+ `${t("SourceCode")}<br/>`
|
||||
+ `${socialLink(
|
||||
emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo
|
||||
)}<br/>
|
||||
${t("SupportNote")}`
|
||||
@@ -440,16 +436,19 @@ export default function(obj) {
|
||||
name: "miscellaneous",
|
||||
title: t('Miscellaneous'),
|
||||
body: checkbox([{
|
||||
action: "disableChangelog",
|
||||
name: t("SettingsDisableNotifications")
|
||||
}, {
|
||||
action: "downloadPopup",
|
||||
name: t("SettingsEnableDownloadPopup"),
|
||||
padding: "no-margin",
|
||||
aria: t("AccessibilityEnableDownloadPopup")
|
||||
}, {
|
||||
action: "disableMetadata",
|
||||
name: t("SettingsDisableMetadata")
|
||||
}, {
|
||||
action: "disableChangelog",
|
||||
name: t("SettingsDisableNotifications"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
})
|
||||
}],
|
||||
}]
|
||||
})}
|
||||
${popupWithBottomButtons({
|
||||
name: "picker",
|
||||
@@ -466,7 +465,7 @@ export default function(obj) {
|
||||
name: "download",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small", "glass-bkg"],
|
||||
classes: ["small"],
|
||||
header: {
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
emoji: emoji("🐱", 78, 1, 1),
|
||||
@@ -487,20 +486,34 @@ export default function(obj) {
|
||||
name: "error",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small", "glass-bkg"],
|
||||
classes: ["small"],
|
||||
header: {
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
title: t('TitlePopupError'),
|
||||
emoji: emoji("😿", 78, 1, 1),
|
||||
},
|
||||
body: `<div id="desc-error" class="desc-padding subtext"></div>`,
|
||||
body: `<div id="desc-error" class="desc-padding subtext desc-error"></div>`,
|
||||
buttonText: t('ErrorPopupCloseButton')
|
||||
})}
|
||||
</div>
|
||||
<div id="popup-migration-container" class="popup-from-bottom">
|
||||
${popup({
|
||||
name: "migration",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small"],
|
||||
header: {
|
||||
title: t('NewDomainWelcomeTitle'),
|
||||
emoji: emoji("😸", 78, 1, 1),
|
||||
},
|
||||
body: `<div id="desc-migration" class="desc-padding subtext desc-error">${t('NewDomainWelcome')}</div>`,
|
||||
buttonText: t('ErrorPopupCloseButton')
|
||||
})}
|
||||
<div id="popup-backdrop-message" onclick="popup('message', 0)"></div>
|
||||
</div>
|
||||
<div id="popup-backdrop" onclick="hideAllPopups()"></div>
|
||||
<div id="home" style="visibility:hidden">
|
||||
${urgentNotice({
|
||||
emoji: "🔗",
|
||||
emoji: "👾",
|
||||
text: t("UrgentFeatureUpdate71"),
|
||||
visible: true,
|
||||
action: "popup('about', 1, 'changelog')"
|
||||
@@ -551,20 +564,25 @@ export default function(obj) {
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
const loc = {
|
||||
noInternet: ` + "`" + t('ErrorNoInternet') + "`" + `,
|
||||
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
|
||||
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
|
||||
collapseHistory: ` + "`" + t('ChangelogPressToHide') + "`" + `,
|
||||
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
|
||||
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
|
||||
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
|
||||
pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
|
||||
featureErrorGeneric: ` + "`" + t('FeatureErrorGeneric') + "`" + `,
|
||||
clipboardErrorNoPermission: ` + "`" + t('ClipboardErrorNoPermission') + "`" + `,
|
||||
clipboardErrorFirefox: ` + "`" + t('ClipboardErrorFirefox') + "`" + `,
|
||||
};
|
||||
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
|
||||
const loc = ${webLoc(t,
|
||||
[
|
||||
'ErrorNoInternet',
|
||||
'ErrorNoUrlReturned',
|
||||
'ErrorUnknownStatus',
|
||||
'ChangelogPressToHide',
|
||||
'MediaPickerTitle',
|
||||
'MediaPickerExplanationPhone',
|
||||
'MediaPickerExplanationPC',
|
||||
'ImagePickerTitle',
|
||||
'ImagePickerExplanationPhone',
|
||||
'ImagePickerExplanationPC',
|
||||
'FeatureErrorGeneric',
|
||||
'ClipboardErrorNoPermission',
|
||||
'ClipboardErrorFirefox',
|
||||
'DataTransferSuccess',
|
||||
'DataTransferError'
|
||||
])}
|
||||
</script>
|
||||
<script type="text/javascript" src="cobalt.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -8,6 +8,9 @@ export default function (inHost, inURL) {
|
||||
url = url.split("?")[0].replace("www.", "");
|
||||
url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
|
||||
}
|
||||
if (url.includes('youtube.com/shorts/')) {
|
||||
url = url.split('?')[0].replace('shorts/', 'watch?v=');
|
||||
}
|
||||
break;
|
||||
case "youtu":
|
||||
if (url.startsWith("https://youtu.be/")) {
|
||||
@@ -32,6 +35,11 @@ export default function (inHost, inURL) {
|
||||
url = url.replace(url.split('/')[5], '')
|
||||
}
|
||||
break;
|
||||
case "twitch":
|
||||
if (url.includes('clips.twitch.tv')) {
|
||||
url = url.split('?')[0].replace('clips.twitch.tv/', 'twitch.tv/_/clip/');
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
host: host,
|
||||
|
||||
@@ -19,10 +19,12 @@ import instagram from "./services/instagram.js";
|
||||
import vine from "./services/vine.js";
|
||||
import pinterest from "./services/pinterest.js";
|
||||
import streamable from "./services/streamable.js";
|
||||
import twitch from "./services/twitch.js";
|
||||
import rutube from "./services/rutube.js";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
let r, isAudioOnly = !!obj.isAudioOnly;
|
||||
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
|
||||
|
||||
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) });
|
||||
@@ -125,6 +127,20 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
isAudioOnly: isAudioOnly,
|
||||
});
|
||||
break;
|
||||
case "twitch":
|
||||
r = await twitch({
|
||||
clipId: patternMatch["clip"] ? patternMatch["clip"] : false,
|
||||
quality: obj.vQuality,
|
||||
isAudioOnly: obj.isAudioOnly
|
||||
});
|
||||
break;
|
||||
case "rutube":
|
||||
r = await rutube({
|
||||
id: patternMatch["id"],
|
||||
quality: obj.vQuality,
|
||||
isAudioOnly: isAudioOnly
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
@@ -134,7 +150,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
|
||||
if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) });
|
||||
|
||||
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted);
|
||||
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata);
|
||||
} catch (e) {
|
||||
return apiJSON(0, { t: genericError(lang, host) })
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@ import { audioIgnore, services, supportedAudio } from "../config.js";
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
|
||||
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted) {
|
||||
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
|
||||
let action,
|
||||
responseType = 2,
|
||||
defaultParams = {
|
||||
u: r.urls,
|
||||
service: host,
|
||||
filename: r.filename,
|
||||
fileMetadata: r.fileMetadata ? r.fileMetadata : false
|
||||
fileMetadata: !disableMetadata ? r.fileMetadata : false
|
||||
},
|
||||
params = {}
|
||||
|
||||
|
||||
if (r.isPhoto) action = "photo";
|
||||
else if (r.picker) action = "picker"
|
||||
else if (isAudioMuted) action = "muteVideo";
|
||||
@@ -55,7 +55,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted) {
|
||||
case "tiktok":
|
||||
params = { type: "bridge" };
|
||||
break;
|
||||
|
||||
|
||||
case "vine":
|
||||
case "instagram":
|
||||
case "tumblr":
|
||||
|
||||
30
src/modules/processing/services/rutube.js
Normal file
30
src/modules/processing/services/rutube.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import HLS from 'hls-parser';
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
let quality = obj.quality === "max" ? "9000" : obj.quality;
|
||||
let play = await fetch(`https://rutube.ru/api/play/options/${obj.id}/?no_404=true&referer&pver=v2`).then((r) => { return r.json() }).catch(() => { return false });
|
||||
if (!play) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
if ("hls" in play.live_streams) return { error: 'ErrorLiveVideo' };
|
||||
if (!play.video_balancer || play.detail) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
if (play.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let m3u8 = await fetch(play.video_balancer.m3u8).then((r) => { return r.text() }).catch(() => { return false });
|
||||
if (!m3u8) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
m3u8 = HLS.parse(m3u8).variants.sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
|
||||
|
||||
let bestQuality = m3u8[0];
|
||||
if (Number(quality) < bestQuality.resolution.height) {
|
||||
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
|
||||
}
|
||||
|
||||
return {
|
||||
urls: bestQuality.uri,
|
||||
isM3U8: true,
|
||||
audioFilename: `rutube_${play.id}_audio`,
|
||||
filename: `rutube_${play.id}_${bestQuality.resolution.width}x${bestQuality.resolution.height}.mp4`
|
||||
}
|
||||
}
|
||||
76
src/modules/processing/services/twitch.js
Normal file
76
src/modules/processing/services/twitch.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
const gqlURL = "https://gql.twitch.tv/gql";
|
||||
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
|
||||
|
||||
export default async function (obj) {
|
||||
let req_metadata = await fetch(gqlURL, {
|
||||
method: "POST",
|
||||
headers: clientIdHead,
|
||||
body: JSON.stringify({
|
||||
query: `{
|
||||
clip(slug: "${obj.clipId}") {
|
||||
broadcaster {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
curator {
|
||||
login
|
||||
}
|
||||
durationSeconds
|
||||
id
|
||||
medium: thumbnailURL(width: 480, height: 272)
|
||||
title
|
||||
videoQualities {
|
||||
quality
|
||||
sourceURL
|
||||
}
|
||||
}
|
||||
}`
|
||||
})
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
|
||||
if (!req_metadata) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let clipMetadata = req_metadata.data.clip;
|
||||
|
||||
if (clipMetadata.durationSeconds > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
if (!clipMetadata.videoQualities || !clipMetadata.broadcaster) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let req_token = await fetch(gqlURL, {
|
||||
method: "POST",
|
||||
headers: clientIdHead,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
"operationName": "VideoAccessToken_Clip",
|
||||
"variables": {
|
||||
"slug": obj.clipId
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11"
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false; }).catch(() => { return false });
|
||||
|
||||
if (!req_token) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let formats = clipMetadata.videoQualities;
|
||||
let format = formats.find(f => f.quality === obj.quality) || formats[0];
|
||||
|
||||
return {
|
||||
type: "bridge",
|
||||
urls: `${format.sourceURL}?${new URLSearchParams({
|
||||
sig: req_token[0].data.clip.playbackAccessToken.signature,
|
||||
token: req_token[0].data.clip.playbackAccessToken.value
|
||||
})}`,
|
||||
fileMetadata: {
|
||||
title: clipMetadata.title,
|
||||
artist: `Twitch Clip by @${clipMetadata.broadcaster.login}, clipped by @${clipMetadata.curator.login}`,
|
||||
},
|
||||
filename: `twitchclip_${clipMetadata.id}_${format.quality}p.mp4`,
|
||||
audioFilename: `twitchclip_${clipMetadata.id}_audio`
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"youtube": {
|
||||
"alias": "youtube videos & shorts & music",
|
||||
"alias": "youtube videos, shorts & music",
|
||||
"patterns": ["watch?v=:id", "embed/:id"],
|
||||
"bestAudio": "opus",
|
||||
"enabled": true
|
||||
@@ -32,7 +32,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"tiktok": {
|
||||
"alias": "tiktok videos & photos & audio",
|
||||
"alias": "tiktok videos, photos & audio",
|
||||
"patterns": [":user/video/:postId", ":id", "t/:id"],
|
||||
"audioFormats": ["best", "m4a", "mp3"],
|
||||
"enabled": true
|
||||
@@ -75,6 +75,18 @@
|
||||
"alias": "streamable.com",
|
||||
"patterns": [":id", "o/:id", "e/:id", "s/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"alias": "twitch clips",
|
||||
"tld": "tv",
|
||||
"patterns": [":channel/clip/:clip"],
|
||||
"enabled": true
|
||||
},
|
||||
"rutube": {
|
||||
"alias": "rutube videos",
|
||||
"tld": "ru",
|
||||
"patterns": ["video/:id", "play/embed/:id"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,20 @@ export const testers = {
|
||||
|| (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
|
||||
|
||||
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
|
||||
|
||||
|
||||
"soundcloud": (patternMatch) => (patternMatch["author"]?.length <= 25 && patternMatch["song"]?.length <= 255)
|
||||
|| (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32),
|
||||
|
||||
"instagram": (patternMatch) => (patternMatch.postId?.length <= 12)
|
||||
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
|
||||
|
||||
|
||||
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
|
||||
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128),
|
||||
|
||||
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6)
|
||||
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6),
|
||||
|
||||
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100)),
|
||||
|
||||
"rutube": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length === 32)),
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ function setup() {
|
||||
})
|
||||
break;
|
||||
case 'web':
|
||||
console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: co.wukko.me"));
|
||||
console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: cobalt.tools"));
|
||||
|
||||
rl.question(q, webURL => {
|
||||
ob['webURL'] = `http://localhost:9001/`;
|
||||
|
||||
@@ -16,7 +16,8 @@ export async function streamDefault(streamInfo, res) {
|
||||
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
|
||||
|
||||
const { body: stream, headers } = await request(streamInfo.urls, {
|
||||
headers: { 'user-agent': genericUserAgent }
|
||||
headers: { 'user-agent': genericUserAgent },
|
||||
maxRedirections: 16
|
||||
});
|
||||
|
||||
res.setHeader('content-type', headers['content-type']);
|
||||
@@ -33,7 +34,9 @@ export async function streamLiveRender(streamInfo, res) {
|
||||
try {
|
||||
if (streamInfo.urls.length !== 2) return fail(res);
|
||||
|
||||
let { body: audio } = await request(streamInfo.urls[1]);
|
||||
let { body: audio } = await request(streamInfo.urls[1], {
|
||||
maxRedirections: 16
|
||||
});
|
||||
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1],
|
||||
args = [
|
||||
@@ -149,7 +152,7 @@ export function streamVideoOnly(streamInfo, res) {
|
||||
'-c', 'copy'
|
||||
]
|
||||
if (streamInfo.mute) args.push('-an');
|
||||
if (streamInfo.service === "vimeo") args.push('-bsf:a', 'aac_adtstoasc');
|
||||
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") args.push('-bsf:a', 'aac_adtstoasc');
|
||||
if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov');
|
||||
args.push('-f', format, 'pipe:3');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
|
||||
@@ -6,7 +6,7 @@ const apiVar = {
|
||||
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
|
||||
},
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash"]
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"]
|
||||
}
|
||||
const forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
|
||||
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||
@@ -50,7 +50,7 @@ export function metadataManager(obj) {
|
||||
return commands;
|
||||
}
|
||||
export function cleanURL(url, host) {
|
||||
switch(host) {
|
||||
switch (host) {
|
||||
case "vk":
|
||||
url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0];
|
||||
break;
|
||||
@@ -70,9 +70,6 @@ export function cleanURL(url, host) {
|
||||
url = url.replaceAll(forbiddenChars[i], '')
|
||||
}
|
||||
url = url.replace('https//', 'https://')
|
||||
if (url.includes('youtube.com/shorts/')) {
|
||||
url = url.split('?')[0].replace('shorts/', 'watch?v=');
|
||||
}
|
||||
return url.slice(0, 128)
|
||||
}
|
||||
export function cleanString(string) {
|
||||
@@ -101,13 +98,14 @@ export function checkJSONPost(obj) {
|
||||
isNoTTWatermark: false,
|
||||
isTTFullAudio: false,
|
||||
isAudioMuted: false,
|
||||
disableMetadata: false,
|
||||
dubLang: false,
|
||||
vimeoDash: false
|
||||
}
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
if (!(objKeys.length <= 9 && obj.url)) return false;
|
||||
let defKeys = Object.keys(def);
|
||||
if (objKeys.length > defKeys.length + 1 || !obj.url) return false;
|
||||
|
||||
for (let i in objKeys) {
|
||||
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
|
||||
|
||||
Reference in New Issue
Block a user