Merge branch 'current' into feat/twitch
This commit is contained in:
@@ -6,36 +6,25 @@ import { cleanURL, apiJSON } from "./sub/utils.js";
|
||||
import { errorUnsupported } from "./sub/errors.js";
|
||||
import loc from "../localization/manager.js";
|
||||
import match from "./processing/match.js";
|
||||
import hostOverrides from "./processing/hostOverrides.js";
|
||||
|
||||
export async function getJSON(originalURL, lang, obj) {
|
||||
try {
|
||||
let patternMatch, url = decodeURIComponent(originalURL),
|
||||
hostname = new URL(url).hostname.split('.'),
|
||||
host = hostname[hostname.length - 2];
|
||||
|
||||
if (!url.startsWith('https://')) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
|
||||
switch(host) {
|
||||
case "youtu":
|
||||
host = "youtube";
|
||||
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
|
||||
break;
|
||||
case "goo":
|
||||
if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/") {
|
||||
host = "soundcloud";
|
||||
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
|
||||
}
|
||||
break;
|
||||
case "tumblr":
|
||||
if (!url.includes("blog/view")) {
|
||||
if (url.slice(-1) === '/') url = url.slice(0, -1);
|
||||
url = url.replace(url.split('/')[5], '')
|
||||
}
|
||||
break;
|
||||
}
|
||||
let overrides = hostOverrides(host, url);
|
||||
host = overrides.host;
|
||||
url = overrides.url;
|
||||
|
||||
if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
|
||||
let pathToMatch = cleanURL(url, host).split(`.${patterns[host]['tld'] ? patterns[host]['tld'] : "com"}/`)[1].replace('.', '');
|
||||
for (let i in patterns[host]["patterns"]) {
|
||||
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(`.${patterns[host]['tld'] ? patterns[host]['tld'] : "com"}/`)[1].replace('.', ''));
|
||||
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(pathToMatch);
|
||||
if (patternMatch) break
|
||||
}
|
||||
if (!patternMatch) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import * as fs from "fs";
|
||||
import { languageList } from "../localization/manager.js";
|
||||
import { loadLoc, languageList } from "../localization/manager.js";
|
||||
import { cleanHTML } from "./sub/utils.js";
|
||||
|
||||
import page from "./pageRender/page.js";
|
||||
|
||||
function cleanHTML(html) {
|
||||
let clean = html.replace(/ {4}/g, '');
|
||||
clean = clean.replace(/\n/g, '');
|
||||
return clean
|
||||
}
|
||||
export async function buildFront(commitHash, branch) {
|
||||
try {
|
||||
// preload localization files
|
||||
await loadLoc();
|
||||
|
||||
// build html
|
||||
if (!fs.existsSync('./build/')){
|
||||
fs.mkdirSync('./build/');
|
||||
@@ -17,6 +17,10 @@ export async function buildFront(commitHash, branch) {
|
||||
fs.mkdirSync('./build/pc/');
|
||||
fs.mkdirSync('./build/mob/');
|
||||
}
|
||||
// get rid of old build path
|
||||
if (fs.existsSync('./min')) {
|
||||
fs.rmSync('./min', { recursive: true, force: true });
|
||||
}
|
||||
for (let i in languageList) {
|
||||
i = languageList[i];
|
||||
let params = {
|
||||
@@ -36,7 +40,7 @@ export async function buildFront(commitHash, branch) {
|
||||
// build js & css
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'],
|
||||
outdir: 'min/',
|
||||
outdir: 'build/min/',
|
||||
minify: true,
|
||||
loader: { '.js': 'js', '.css': 'css', },
|
||||
charset: 'utf8'
|
||||
|
||||
7
src/modules/buildStatic.js
Normal file
7
src/modules/buildStatic.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { buildFront } from "./build.js";
|
||||
import { getCurrentBranch, shortCommit } from "./sub/currentCommit.js";
|
||||
|
||||
const commitHash = shortCommit();
|
||||
const branch = getCurrentBranch();
|
||||
|
||||
await buildFront(commitHash, branch);
|
||||
@@ -1,54 +1,154 @@
|
||||
{
|
||||
"current": {
|
||||
"version": "5.4",
|
||||
"title": "instagram support, hop, docker, and more!",
|
||||
"banner": "catphonestand.webp",
|
||||
"content": "something many of you've been waiting for is finally here! try it out and let me know what you think :)\n\n<span class='text-backdrop'>tl;dr:</span>\n*; added experimental instagram support! download any reels or videos you like, and make sure to report any issues you encounter. yes, you can convert either to audio.\n*; fixed support for on.soundcloud links.\n*; added share button to \"how to save?\" popup.\n*; added docker support.\n*; main instance is now powered by <a class=\"text-backdrop italic\" href=\"https://hop.io/\" target=\"_blank\">hop.io</a>.\n\nservice improvements:\n*; added experimental support for videos from instagram. currently only reels and post videos are downloadable, but i'm looking into ways to save high resolution photos too. if you experience any issues, please report them on either of support platforms.\n*; fixed support for on.soundcloud share links. should work just as well as other versions!\n*; fixed an issue that made some youtube videos impossible to download.\n\ninterface improvements:\n*; new css-only checkmark! yes, i can't stop tinkering with it because slight flashing on svg load annoyed me. now it loads instantly (and also looks slightly better).\n*; fixed copy animation.\n*; minor localization improvements.\n*; fixed the embed logo that i broke somewhere in between 5.3 and 5.4.\n\ninternal improvements:\n*; now using nanoid for live render stream ids.\n*; added support for docker. it's kind of clumsy because of how i get .git folder inside the container, but if you know how to do it better, feel free to make a pr.\n*; cobalt now checks only for existence of environment variables, not exactly the .env file.\n*; changed the way user ip address is retrieved for instances using cloudflare.\n*; added ability to disable cors, both to setup script and environment variables.\n*; moved main instance to <a class=\"text-backdrop italic\" href=\"https://hop.io/\" target=\"_blank\">hop.io</a> infra. there should no longer be random downtimes. huge shout out to the hop team for being so nice and helping me out :D\n\ni can't believe how diverse and widespread cobalt has become. it's used in all fields: music production, education, content creation, and even game development. <span class='text-backdrop'>thank you</span>. this is absolutely nuts.\nif you don't mind sharing, please tell me about your use case. i'd really love to hear how you use cobalt and how i could make it even more useful for you."
|
||||
"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"
|
||||
},
|
||||
"history": [{
|
||||
"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!",
|
||||
"banner": {
|
||||
"file": "meowthproductions.webp",
|
||||
"width": 640,
|
||||
"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"
|
||||
}, {
|
||||
"version": "7.0",
|
||||
"date": "August 15, 2023",
|
||||
"title": "biggest ui refresh yet!",
|
||||
"banner": {
|
||||
"file": "meowthcooking.webp",
|
||||
"width": 640,
|
||||
"height": 360
|
||||
},
|
||||
"content": "hey! this update is huge and mostly aimed to refresh the ui, but there are also some other nice fixes/additions. read below for more info :)\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; entirety of web app has been refreshed. it's more prettier and optimized than ever, both on phone and desktop.\n*; if you're on ios, try adding cobalt to home screen! it'll look and act like a native app.\n*; all soundcloud links are now supported and audio quality is higher than before.\n*; all x (previously twitter) links are now supported and work properly.\n*; newer reddit videos are downloadable now.\n*; added some sort of eula, list of keyboard shortcuts, updated privacy policy for more clarity. check it all in refreshed about tab!\n*; cobalt now lets you know if your browser doesn't support clipboard pasting and helps you fix it.\n\n<span class=\"text-backdrop\">accessibility notice:</span>\nthis update includes animations and transparency, if you'd like to disable any or all of them, head to settings > other > accessibility.\n\n<span class=\"text-backdrop\">[full changelog]</span>\n\nservice improvements:\n*; fixed unexpected 502 errors when downloading newer reddit videos.\n*; newer reddit videos (with audio) are downloadable now.\n*; upgraded soundcloud downloads to use higher audio quality than before.\n*; all soundcloud links are now supported.\n*; added support for x.com urls.\n*; changed twitter api once again. now everything works, again.\n\nweb improvements:\n*; all-new matte glass aesthetic, applied to revamped popup headers, tab selectors, and also small popups.\n*; rounded corners everywhere! cobalt is now safe for everyone who can't handle sharp objects.\n*; paddings everywhere are smaller, more content fits on the screen at once.\n*; optimized installed web app to look and act like a native app, especially on ios.\n*; added update release dates to changelogs.\n*; cobalt now lets you know if your browser doesn't support clipboard api and helps you fix it.\n*; refreshed all popups: less padding, more content.\n*; completely remade error and download popups, they're consistent with the rest of refreshed design.\n*; refreshed the look of entire changelog tab: separated title and version/commit, made title bigger, evened out all paddings.\n*; replaced close button with back button, moved it to left.\n*; added interaction animations.\n*; added more keyboard shorcuts.\n*; added a list of keyboard shortcuts to about tab.\n*; added eula to about tab. check it out.\n*; added more accessibility options, put them all into one category. you can disable animations and transparency if you want to.\n*; added a link to self-troubleshooting guide to about tab.\n*; renamed 2160p and 4320p to 4k and 8k respectfully for better clarity.\n*; popups now work without any weird workarounds, especially on mobile. they're clean and nice.\n*; home screen now also works without any weird workarounds. it is also clean and nice.\n*; optimized css of almost all ui elements. should be even more consistent across platforms now.\n*; added ability to translate \"cobalt\" more in-depth localization. for example, in russian \"cobalt\" is now \"кобальт\", that's the style i'll be going with from now on.\n*; updated many localization strings for more clarity.\n*; removed ability to change the app name dynamically in all locations. cobalt is a sustained app name.\n*; updated donation and privacy policy texts for more clarity in both english and russian.\n*; home screen now smoothly fades in instead of popping in.\n*; proper banner loading. no more jumping text!\n*; proper banner error handling. if banner wasn't loaded, it'll simply go grey instead of disappearing.\n*; links are no longer italic and are instead underlined.\n*; collapsible lists now have corresponding emoji.\n*; donate button is now highlighted with magenta instead of white.\n*; proper dropdown arrow.\n*; removed 6.0 api fallback.\n*; fixed celebrations emoji. again.\n*; cleaned up all related frontend modules, especially page.js.\n*; urgent notice is now a js element, not a static piece of text. can be updated easily.\n\napi improvements:\n*; now catching all json api related errors.\n*; moved on demand blocks to web server, now changelog can be updated independently from preferred api server.\n*; now sending standard rate limiting headers.\n*; better readability in source.\n\nother improvements:\n*; renamed docker-compose.yml.example to docker-compose.example.yml for linting in code editors.\n*; added a wiki with wip troubleshooting guide on github. more guides are coming soon!\n\nthat's a ton of changes! i really hope you like this update as much as i do.\n\nif you experience any issues, feel free to contact me on any platform listed in about tab! i'd love to hear back from you.\n\nthank you for sticking with me and cobalt, i hope you have THE best day :D"
|
||||
}, {
|
||||
"version": "6.2",
|
||||
"date": "June 27 2023",
|
||||
"title": "all network issues have been fixed!",
|
||||
"banner": {
|
||||
"file": "meowthhammer.webp",
|
||||
"width": 1280,
|
||||
"height": 827
|
||||
},
|
||||
"content": "hey! there have been some hiccups in cobalt's stability lately, i was going through finals while trying to scale up the infrastructure, and that didn't really work out, lol.\nBUT i'm happy to announce that i've optimized all nodes! <span class=\"text-backdrop\">there should no longer be any networking issues</span>.\n\nenjoy stable experience while i work in background to make cobalt even better :)\n\nhere's what's new in this update:\n*; better button contrast in both themes. \n*; button highlight in light theme now actually looks like a highlight.\n*; removed ip gate for streamables and updated privacy policy to reflect this change.\n*; streamable links now last for 20 seconds instead of 2 minutes.\n*; cleaned up stream verification algorithm. now the same function doesn't run 4 times in a row.\n*; removed deprecated way of hosting a cobalt instance.\n\nthank you for sticking with cobalt, and i hope you have a great day :D\n\nbanner photo is by <a class=\"text-backdrop link\" href=\"https://twitter.com/halftroller\" target=\"_blank\">@halftroller</a> on twitter, thank you so much!"
|
||||
}, {
|
||||
"version": "6.0",
|
||||
"date": "June 7, 2023",
|
||||
"title": "better reliability, new infrastructure, pinterest support, and way more!",
|
||||
"banner": {
|
||||
"file": "catswitchboxes.webp",
|
||||
"width": 600,
|
||||
"height": 314
|
||||
},
|
||||
"content": "hey! long time no see, hopefully over 40 changes will make up for it :)\n\ncobalt now has an official community discord server. you can go there for news, support, or just to chat. <a class=\"text-backdrop link\" href=\"https://discord.gg/pQPt8HBUPu\" target=\"_blank\">go check it out!</a>\n\n<span class='text-backdrop'>tl;dr</span>\n*; new infra, new hosting structure, new main instance api url. developers, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/blob/current/docs/API.md\" target=\"_blank\">get it here.</a>\n*; added support for pinterest, vine archive, tumblr audio, youtube vr videos.\n*; better web app performance and look.\n*; better stability thanks to load balancing.\n*; (hopefully) no more random video/audio download drops.\n\nservice improvements:\n*; added support for pinterest videos and stories (pr by <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/commit/40291c4d24cb5f441cdddfd26104f149bc4ee27c\" target=\"_blank\">@Snazzah</a>).\n*; added support for tumblr audio. sorry, tumblr.\n*; added support for youtube vr videos. please note that they're in youtube's proprietary ratio.\n*; added support for vine archive.\n*; added support for ancient vk videos in 240p.\n*; fixed an issue related to muted video downloads from tumblr.\n*; moved to twitter v2 api.\n*; soundcloud share links are now processed without errors.\n\nui improvements:\n*; lazy image loading. should significantly speed up the page load.\n*; fixed checkbox width on mobile devices.\n*; addition of a temporary urgent notice.\n*; added hover border to all buttons.\n*; less annoying donation button highlight.\n*; more consistent color scheme.\n*; added link to a discord server into about popup.\n*; remember celebratory emoji changes? they've been fixed, and are now dynamically loaded!\n*; changelog history now lets you try to load it again if first attempt failed for whatever reason.\n*; padding (everywhere) has been slightly reduced to fit in more content and be consistent across ui.\n*; added more info to the \"how to save\" popup for ios devices.\n*; crypto wallet press-to-copy buttons now look like buttons.\n*; improved ui layout for smallest screens (iphone 5, 5s, se, etc).\n*; removed partial translations for sake of clarity and consistency.\n\ninternal improvements:\n*; separated web and api servers. they're now completely independent and therefore more stress-resistant.\n*; added a dedicated script for building the web app if you don't want to reload the frontend server.\n*; web app building improvements.\n*; async localization preloading.\n*; consistent server start time reporting.\n*; dynamic stream and ip hashing salt generation.\n\ninfrastructure improvements:\n*; load balancing: your api requests are now sent to the least busy server. yes, there are now several of them with more to come in the future.\n*; when possible, server in closest region is used instead of a far-away one. this should help with download speeds.\n*; currently there are multiple servers in europe. i will let you know when (and if) i manage to get an american one.\n\nupdates for developers and instance hosters:\n*; server info api endpoint: you can now check up on the api server of choice. it reports all the basic info you may need. <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/blob/current/docs/API.md#get-apiserverinfo\" target=\"_blank\">check the api docs</a> for more info.\n*; api names: each and every api instance should have a distinctive name. this will be useful in the future :)\n*; added docker compose sample config.\n*; updated and more granular setup script.\n*; better api scalability and faster server start up thanks to web and api separation.\n*; added ability to specify ffmpeg threads. simply add ffmpegThreads to your environment variables!\n\ni'm still in awe from how popular cobalt has become. there are now over 200k of unique users monthly, and that number only keeps growing. i even had to come up with something to accommodate for larger traffic, it's absolutely insane.\n\nlove you all, have a great day :D"
|
||||
}, {
|
||||
"version": "5.4",
|
||||
"title": "instagram support, docker, and more!",
|
||||
"banner": {
|
||||
"file": "catphonestand.webp",
|
||||
"width": 451,
|
||||
"height": 272
|
||||
},
|
||||
"content": "something many of you've been waiting for is finally here! try it out and let me know what you think :)\n\n<span class='text-backdrop'>tl;dr:</span>\n*; added experimental instagram support! download any reels or videos you like, and make sure to report any issues you encounter. yes, you can convert either to audio.\n*; fixed support for on.soundcloud links.\n*; added share button to \"how to save?\" popup.\n*; added docker support.\n\nservice improvements:\n*; added experimental support for videos from instagram. currently only reels and post videos are downloadable, but i'm looking into ways to save high resolution photos too. if you experience any issues, please report them on either of support platforms.\n*; fixed support for on.soundcloud share links. should work just as well as other versions!\n*; fixed an issue that made some youtube videos impossible to download.\n\ninterface improvements:\n*; new css-only checkmark! yes, i can't stop tinkering with it because slight flashing on svg load annoyed me. now it loads instantly (and also looks slightly better).\n*; fixed copy animation.\n*; minor localization improvements.\n*; fixed the embed logo that i broke somewhere in between 5.3 and 5.4.\n\ninternal improvements:\n*; now using nanoid for live render stream ids.\n*; added support for docker. it's kind of clumsy because of how i get .git folder inside the container, but if you know how to do it better, feel free to make a pr.\n*; cobalt now checks only for existence of environment variables, not exactly the .env file.\n*; changed the way user ip address is retrieved for instances using cloudflare.\n*; added ability to disable cors, both to setup script and environment variables.\n\ni can't believe how diverse and widespread cobalt has become. it's used in all fields: music production, education, content creation, and even game development. <span class='text-backdrop'>thank you</span>. this is absolutely nuts.\nif you don't mind sharing, please tell me about your use case. i'd really love to hear how you use cobalt and how i could make it even more useful for you."
|
||||
}, {
|
||||
"version": "5.3",
|
||||
"title": "better looks, better feel",
|
||||
"banner": "cattired.webp",
|
||||
"banner": {
|
||||
"file": "cattired.webp",
|
||||
"width": 640,
|
||||
"height": 286
|
||||
},
|
||||
"content": "this update isn't as big as previous ones, but it still greatly enhances the cobalt experience.\n\nhere's what's up:\n*; new mode switcher! elegant and 100% clear. should no longer cause any confusion. let me know if you like it better this way :D\n*; wide paste button on mobile is back, but now it's even closer to your finger.\n*; removed the weird grey chin on changelog banners.\n*; removed left-handed layout toggle since it is no longer needed.\n*; fixed input area display in chromium 112+.\n*; centered the main action box.\n*; cleaned up css of main action box to get rid of tricks and ensure correct display on all devices.\n*; fixed a bug that'd cause notifications dots to disappear when an unrelated checkbox was checked.\n\nhopefully from now on i'll focus on adding support for more services.\nthank you for using cobalt. stay cool :)"
|
||||
}, {
|
||||
"version": "5.2",
|
||||
"title": "fastest one in the game",
|
||||
"banner": "catspeed.webp",
|
||||
"banner": {
|
||||
"file": "catspeed.webp",
|
||||
"width": 640,
|
||||
"height": 356
|
||||
},
|
||||
"content": "hey, notice anything different? well, at very least the page loaded way faster! this update includes many improvements and fixes, but also some new features.\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; twitter retweet links are now supported.\n*; all vimeo videos should now be possible to download.\n*; you now can download audio from vimeo.\n*; it's now possible to pick between preferred vimeo download method in settings.\n*; fixed issues related to tiktok, twitter, twitter spaces, and vimeo downloads.\n*; overall cobalt performance should be MUCH better.\n\nservice improvements:\n*; added support for twitter retweet links. now all kinds of tweet links are supported.\n*; fixed the issue related to periods in tiktok usernames (#96).\n*; fixed twitter spaces downloads.\n*; added support for audio downloads from vimeo.\n*; added ability to choose between \"progressive\" and \"dash\" vimeo downloads. go to settings > video to pick your preference.\n*; fixed the issue related to vimeo quality picking.\n*; fixed the issue when vimeo module wouldn't show appropriate errors and instead would fallback to default ones.\n*; improved audio only downloads for some edge cases.\n*; (hopefully) better youtube reliability.\n*; temporarily disabled douyin support due to api endpoint cut off.\n\ninterface improvements:\n*; merged clipboard and mode switcher rows into one for mobile view.\n*; added left-handed layout toggle for those who prefer to have the clipboard button on left.\n*; new custom-made clipboard icon. now it clearly indicates what it does.\n*; improved english and russian localization. both are way more direct and less bloaty.\n*; frontend page is now rendered once and is cached on disk instead of being rendered every time someone requests a page. this greatly improves page loading speeds and further reduces strain put on the server.\n*; frontend page is now minimized just like js and css files. this should minimize traffic wasted on loading the page, along with minor loading speed improvement.\n*; added proper checkbox icon for better clarity.\n*; checkboxes are now stretched edge-to-edge on phone to be easier to manage for right-handed people.\n*; removed button hover highlights on phones.\n*; fixed button press animations for safari on ios.\n*; fixed text selection on ios. previously you could select text or images anywhere, but now they're selectable in limited places, just like on other platforms.\n*; frontend platform is now marked in settings: p is for pc; m is for mobile; i is for ios. this is done for possible future debugging and issue-solving.\n*; better error messaging.\n\ninternal improvements:\n*; better rate limiting, there should be way less cases of accidental limits.\n*; added support for m3u8 playlists. this will be useful for future additions, and is currently used by vimeo module.\n*; added support for \"chop\" stream format for vimeo downloads.\n*; fixed vk user id extraction. i assumed the - in url was a separator, but it's actually a part of id.\n*; completely reworked the vimeo module. it's much cleaner and better performant now.\n*; minor clean ups across the board.\n\nnot really related to this update, but thank you for 50k monthly users! i really appreciate that you're still here, because that means i'm doing some things right :D"
|
||||
}, {
|
||||
"version": "5.1",
|
||||
"title": "the evil has been defeated",
|
||||
"banner": "happymeowth.webp",
|
||||
"banner": {
|
||||
"file": "happymeowth.webp",
|
||||
"width": 500,
|
||||
"height": 330
|
||||
},
|
||||
"content": "hey, ever wanted to download a youtube video without a hassle? cobalt is here to help. this update fixes all issues related to youtube downloads.\nnot only that, but it also introduces features never before seen in a downloader, such as youtube dub downloads! read below to see what's up :)\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; audio in youtube videos FINALLY no longer gets cut off.\n*; you now can pick any video resolution you want (from 360p to 8k) and any possible youtube video codec (h264/av1/vp9).\n*; you now can download youtube videos with dubs in your native language. just check settings > audio.\n*; youtube processing has been vastly sped up.\n\nok, now onto the nerdy part of changelog. this update is pretty huge and includes improvements across the board.\n\nservice improvements:\n*; all youtube functionality has been reworked. cobalt now relies on innertube apis, not web scraping.\n*; random audio cut off issue has been fixed, let me know if it ever occurs again. (closes #62, #66, #75, #88).\n*; added support for youtube dubs. currently it's using your browser's default language when enabled, but i have plans on making a picker. i'll ask people on twitter and mastodon if this feature is needed, and add a picker in next updates.\n*; instead of adding more quality presets, i added granular quality options. pick whatever you like, from 360p up to 4320p (for all services, not just youtube).\n*; replaced a format picker with codec picker for youtube. you can pick h264, av1, or vp9. all of them should work as expected (closes #88).\n*; youtube audio files are now properly matched to corresponding video files.\n*; it's now always possible to download pristine h264 720p/360p videos from youtube. these videos will work ANYWHERE, so they're default for mobile.\n*; youtube requests are no longer permanently cached, ram usage should drop even further.\n*; youtube video and audio file names now include codec and dub language when applicable.\n*; max video and audio duration limits have been bumped up to 3 hours.\n*; general performance of entire youtube download process has been greatly improved.\n*; vk module has been reworked to be more compact and not make use of outdated technique of quality picking. should also be way more reliable.\n\ninternal improvements:\n*; cleaned up services config, all constants have been moved directly to modules for quicker access.\n*; matching module has been slightly cleaned up.\n\ninterface improvements:\n*; many descriptions and error messages have been slightly tuned to be less wordy.\n*; unnecessary title duplications in settings have been merged into one.\n*; added more clarity to quality and codec descriptions.\n\nif you use cobalt api, please note that you have to update your creation to support new features.\n\nthis is the second batch of 5.x improvements, there's way more to come. thank you for being here, i really appreciate your support.\n\nif you want to thank me (the developer), there's a nice tab under this changelog that has \"donations\" text on it. anything helps me continue developing and hosting the friendliest media downloader :D"
|
||||
}, {
|
||||
"version": "5.0",
|
||||
"title": "it's all about attention to detail!",
|
||||
"banner": "valentines.webp",
|
||||
"banner": {
|
||||
"file": "valentines.webp",
|
||||
"width": 489,
|
||||
"height": 374
|
||||
},
|
||||
"content": "happy valentine's day! i have an update for you, as a gift :D\n\ntl;dr: added support for <span class=\"text-backdrop\">reddit gifs</span>, fixed douyin downloads, fixed vimeo quality picking, revamped entirety of codebase, and many other fixes.\n\nhere's more info:\n\nthis update is mostly about cleaning up and polishing the codebase, but it also has some new features. here's what's up:\n\nservice-related improvements:\n*; you now can download gifs from reddit!\n*; attempting to download a video from douyin no longer throws an error (bytedance changed the api endpoint, yet again).\n*; fixed quality picking for vimeo downloads.\n*; fixed length limit check in vimeo module.\n*; fixed support for \"user view\" vk clips links.\n*; various twitter errors are now displayed correctly instead of falling back to the default error.\n*; state of all services is now tested on each commit.\n\nui improvements:\n*; cobalt social links no longer disappear if you have an aggressive ad blocking extension installed.\n*; various localization improvements for both english and russian.\n*; changed some service aliases to display full list of supported downloads.\n*; added current branch information to version text (in settings).\n*; fixed typos in older changelogs.\n\ninternal improvements:\n*; <span class=\"text-backdrop\">everything</span> has been sanitized, improved, and refactored. code is now much easier to read and maintain.\n*; rewrote and/or optimized all modules that were messy or inefficient.\n*; all git interaction functions now store info in cache instead of fetching it every time the function is called.\n*; added a test script that checks functionality of all supported services.\n*; updated deepsource config. checks are more accurate now.\n*; requests from internet explorer are now dropped entirely instead of redirecting people stuck in 90s to a proper browser download page. this was done to avoid (my) personal bias towards browsers.\n\ni put a ton of effort into this version, and i hope you like it as much as i do.\n\nthank you for using cobalt. there's so much more to come :)"
|
||||
}, {
|
||||
"version": "4.8",
|
||||
"title": "prettier than ever",
|
||||
"banner": "catmakeup.webp",
|
||||
"content": "this version brings many visual improvements and a completely revamped \"about\" tab.\n\nwhat's new in \"about\" tab:\n*; all information is now split into collapsible sections, making it easier to navigate.\n*; added privacy policy to further prove that none of your data is collected.\n*; added emoji to the page title to make it look consistent with other pages.\n*; added mastodon account handle and link.\n*; there are now short notes at the end of each section.\n*; other changes that are too small to describe. just go check it out!\n\nvisual improvements:\n*; less wasted space: paddings and margins have been reduced and optimized for usability, consistency, and overall beauty.\n*; all <a class=\"text-backdrop italic\" href=\"https://youtu.be/dQw4w9WgXcQ\" target=\"_blank\">links</a> are now in italic. it's much easier to tell them apart from <span class=\"text-backdrop\">regular highlights</span>.\n*; error popup no longer looks broken and out of place.\n*; download popup now has a proper close button, not something from 2.x era.\n*; emoji are no longer selectable or draggable.\n*; better scalability: desktop layout for home screen is shown if device viewport is wide enough to fit in three action buttons.\n*; page shouldn't look broken on phones in landscape mode (i still highly recommend using cobalt in portrait mode).\n*; removed bulletpoint padding. it was unnecessary.\n*; updated some service names.\n\nas always, you can suggest features or report bugs on any platform listed in the \"support\" section of about tab.\n\nthank you for using cobalt. i hope you have a good day :)"
|
||||
"banner": {
|
||||
"file": "catmakeup.webp",
|
||||
"width": 394,
|
||||
"height": 266
|
||||
},
|
||||
"content": "this version brings many visual improvements and a completely revamped \"about\" tab.\n\nwhat's new in \"about\" tab:\n*; all information is now split into collapsible sections, making it easier to navigate.\n*; added privacy policy to further prove that none of your data is collected.\n*; added emoji to the page title to make it look consistent with other pages.\n*; added mastodon account handle and link.\n*; there are now short notes at the end of each section.\n*; other changes that are too small to describe. just go check it out!\n\nvisual improvements:\n*; less wasted space: paddings and margins have been reduced and optimized for usability, consistency, and overall beauty.\n*; all <a class=\"text-backdrop link\" href=\"https://youtu.be/dQw4w9WgXcQ\" target=\"_blank\">links</a> are now in italic. it's much easier to tell them apart from <span class=\"text-backdrop\">regular highlights</span>.\n*; error popup no longer looks broken and out of place.\n*; download popup now has a proper close button, not something from 2.x era.\n*; emoji are no longer selectable or draggable.\n*; better scalability: desktop layout for home screen is shown if device viewport is wide enough to fit in three action buttons.\n*; page shouldn't look broken on phones in landscape mode (i still highly recommend using cobalt in portrait mode).\n*; removed bulletpoint padding. it was unnecessary.\n*; updated some service names.\n\nas always, you can suggest features or report bugs on any platform listed in the \"support\" section of about tab.\n\nthank you for using cobalt. i hope you have a good day :)"
|
||||
}, {
|
||||
"version": "4.7",
|
||||
"title": "we're better together! thank you for bug reports.",
|
||||
"banner": "bettertogether.webp",
|
||||
"content": "this update includes a bunch of improvements, many of which were made thanks to the community :D\n\nservice-related improvements:\n*; private soundcloud links are now supported (#68);\n*; tiktok usernames with dots in them no longer confuse cobalt (#71);\n*; .ogg files no longer wrongfully include a video channel (#67);\n*; fixed an issue that caused cobalt to freak out when user attempted to download an audio from audio-only service with \"mute video\" option enabled.\n\nui improvements:\n*; popup padding has been evened out. popups are now able to fit in more information on scroll, especially on mobile;\n*; all buttons are now of even size and are displayed without any padding issues across all modern browsers and devices;\n*; checkbox is no longer crippled on ios;\n*; many explanation texts have been simplified to get rid of unnecessary bloat (no bullshit, remember?);\n*; moved tiktok section in video settings higher due to higher priority;\n*; fixed unexpectedly displayed scrollbars on switch rows in firefox.\n\nstability improvements:\n*; ffmpeg process now should end upon finishing the render;\n*; ffmpeg should also quit when download is abruptly cut off;\n*; fixed a memory leak that was caused by misconfigured stream information caching (#63).\n\ninternal improvements:\n*; requested streams are now stored in cache for 2 minutes instead of 1000 hours (yes, 1000 hours, i fucked up);\n*; cached data is now reused if user requests same content within 2 minutes;\n*; page render module is now even cleaner than before;\n*; proper support for bullet-points in loc strings.\n\nyou can suggest features or report bugs on <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">twitter</a>. both work just fine, use whichever you're more comfortable with.\n\nthank you for using cobalt, and thank you for reading this changelog.\n\nyou're amazing, keep it up :)"
|
||||
"banner": {
|
||||
"file": "bettertogether.webp",
|
||||
"width": 640,
|
||||
"height": 358
|
||||
},
|
||||
"content": "this update includes a bunch of improvements, many of which were made thanks to the community :D\n\nservice-related improvements:\n*; private soundcloud links are now supported (#68);\n*; tiktok usernames with dots in them no longer confuse cobalt (#71);\n*; .ogg files no longer wrongfully include a video channel (#67);\n*; fixed an issue that caused cobalt to freak out when user attempted to download an audio from audio-only service with \"mute video\" option enabled.\n\nui improvements:\n*; popup padding has been evened out. popups are now able to fit in more information on scroll, especially on mobile;\n*; all buttons are now of even size and are displayed without any padding issues across all modern browsers and devices;\n*; checkbox is no longer crippled on ios;\n*; many explanation texts have been simplified to get rid of unnecessary bloat (no bullshit, remember?);\n*; moved tiktok section in video settings higher due to higher priority;\n*; fixed unexpectedly displayed scrollbars on switch rows in firefox.\n\nstability improvements:\n*; ffmpeg process now should end upon finishing the render;\n*; ffmpeg should also quit when download is abruptly cut off;\n*; fixed a memory leak that was caused by misconfigured stream information caching (#63).\n\ninternal improvements:\n*; requested streams are now stored in cache for 2 minutes instead of 1000 hours (yes, 1000 hours, i fucked up);\n*; cached data is now reused if user requests same content within 2 minutes;\n*; page render module is now even cleaner than before;\n*; proper support for bullet-points in loc strings.\n\nyou can suggest features or report bugs on <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">github</a> or <a class=\"text-backdrop link\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">twitter</a>. both work just fine, use whichever you're more comfortable with.\n\nthank you for using cobalt, and thank you for reading this changelog.\n\nyou're amazing, keep it up :)"
|
||||
}, {
|
||||
"version": "4.6",
|
||||
"title": "mute videos and proper soundcloud support",
|
||||
"banner": "shutup.png",
|
||||
"banner": {
|
||||
"file": "shutup.webp",
|
||||
"width": 1024,
|
||||
"height": 665
|
||||
},
|
||||
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n*; you now can download videos with no audio! simply enable the \"mute audio\" option in settings > audio.\n*; soundcloud module has been updated, and downloads should no longer break after some time.\nvisual improvements:\n*; moved some things around in settings popup, and added separators where separation is needed.\n*; updated some texts in english and russian.\n*; version and commit hash have been joined together, now they're a single unit.\ninternal improvements:\n*; updated api documentation to include isAudioMuted.\n*; simplified the startup message.\n*; created render elements for separator and explanation due to high duplication of them in the page.\n*; fully deprecated GET method for API requests.\n*; fixed some code quirks.\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
|
||||
}, {
|
||||
"version": "4.5",
|
||||
"title": "better, faster, stronger, stable",
|
||||
"banner": "meowthstrong.webp",
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
"banner": {
|
||||
"file": "meowthstrong.webp",
|
||||
"width": 500,
|
||||
"height": 280
|
||||
},
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop link\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop link\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
}, {
|
||||
"version": "4.4",
|
||||
"title": "over 1 million monthly requests. thank you.",
|
||||
"banner": "onemillionr.webp",
|
||||
"banner": {
|
||||
"file": "onemillionr.webp",
|
||||
"width": 1441,
|
||||
"height": 1441
|
||||
},
|
||||
"content": "this is a huge milestone for me, i cannot express enough how grateful i am for each and every one of you.\nthank you for using cobalt, and thank you for showing that people love the web that's friendly and bullshit-free. i'm hoping to never disappoint you in the future and keep up the good work.\n\nthank you <3\n\nif you want to thank ME, check out the renovated donations tab, which now is also linked alongside bottom action buttons."
|
||||
}, {
|
||||
"version": "4.3.2",
|
||||
@@ -57,8 +157,12 @@
|
||||
}, {
|
||||
"version": "4.3",
|
||||
"title": "developers, developers, developers, developers",
|
||||
"banner": "developersdevelopersdevelopers.webp",
|
||||
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop italic\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
|
||||
"banner": {
|
||||
"file": "developers.webp",
|
||||
"width": 640,
|
||||
"height": 360
|
||||
},
|
||||
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop link\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop link\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
|
||||
}, {
|
||||
"version": "4.2",
|
||||
"title": "optimized quality picking and 8k video support",
|
||||
@@ -70,7 +174,7 @@
|
||||
}, {
|
||||
"version": "4.0",
|
||||
"title": "better and faster than ever",
|
||||
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
|
||||
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
|
||||
}, {
|
||||
"version": "3.7",
|
||||
"title": "support for multi media tweets is here!",
|
||||
@@ -86,7 +190,7 @@
|
||||
}, {
|
||||
"version": "3.5.4",
|
||||
"title": "tiktok support is back :D",
|
||||
"content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop italic\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works."
|
||||
"content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop link\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works."
|
||||
}, {
|
||||
"version": "3.5.2",
|
||||
"title": "vk clips support, improved changelog system, and less bugs",
|
||||
|
||||
@@ -6,19 +6,33 @@ let changelog = loadJSON('./src/modules/changelog/changelog.json')
|
||||
export default function(string) {
|
||||
try {
|
||||
switch (string) {
|
||||
case "version":
|
||||
return `<span class="text-backdrop changelog-tag-version">v.${changelog["current"]["version"]}</span>${
|
||||
changelog["current"]["date"] ? `<span class="changelog-tag-date">· ${changelog["current"]["date"]}</span>` : ''
|
||||
}`
|
||||
case "title":
|
||||
return `<span class="text-backdrop">${changelog["current"]["version"]}:</span> ${replaceBase(changelog["current"]["title"])}`;
|
||||
return replaceBase(changelog["current"]["title"]);
|
||||
case "banner":
|
||||
return changelog["current"]["banner"] ? `updateBanners/${changelog["current"]["banner"]}` : false;
|
||||
return changelog["current"]["banner"] ? {
|
||||
url: `updateBanners/${changelog["current"]["banner"]["file"]}`,
|
||||
width: changelog["current"]["banner"]["width"],
|
||||
height: changelog["current"]["banner"]["height"]
|
||||
} : false;
|
||||
case "content":
|
||||
return replaceBase(changelog["current"]["content"]);
|
||||
case "history":
|
||||
return changelog["history"].map((i) => {
|
||||
return {
|
||||
title: `<span class="text-backdrop">${i["version"]}:</span> ${replaceBase(i["title"])}`,
|
||||
title: replaceBase(i["title"]),
|
||||
version: `<span class="text-backdrop changelog-tag-version">v.${i["version"]}</span>${
|
||||
i["date"] ? `<span class="changelog-tag-date">· ${i["date"]}</span>` : ''
|
||||
}`,
|
||||
content: replaceBase(i["content"]),
|
||||
version: i["version"],
|
||||
banner: i["banner"] ? `updateBanners/${i["banner"]}` : false,
|
||||
banner: i["banner"] ? {
|
||||
url: `updateBanners/${i["banner"]["file"]}`,
|
||||
width: i["banner"]["width"],
|
||||
height: i["banner"]["height"]
|
||||
} : false,
|
||||
}
|
||||
});
|
||||
default:
|
||||
|
||||
@@ -6,7 +6,6 @@ const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.jso
|
||||
export const
|
||||
services = servicesConfigJson.config,
|
||||
audioIgnore = servicesConfigJson.audioIgnore,
|
||||
appName = packageJson.name,
|
||||
version = packageJson.version,
|
||||
streamLifespan = config.streamLifespan,
|
||||
maxVideoDuration = config.maxVideoDuration,
|
||||
|
||||
@@ -10,7 +10,6 @@ const names = {
|
||||
"🀄": "dragon_face_wukko",
|
||||
"💸": "money_with_wings",
|
||||
"⚙️": "gear",
|
||||
"☹️": "frowning_face",
|
||||
"📋": "clipboard",
|
||||
"🎃": "pumpkin",
|
||||
"🎄": "christmas_tree",
|
||||
@@ -23,18 +22,36 @@ const names = {
|
||||
"🐙": "octopus",
|
||||
"🔮": "crystal_ball",
|
||||
"💪": "biceps",
|
||||
"💖": "sparkling_heart"
|
||||
"💖": "sparkling_heart",
|
||||
"👾": "alien_monster",
|
||||
"😿": "cat_crying",
|
||||
"🙀": "cat_flabbergasted",
|
||||
"🐱": "cat_smile",
|
||||
"❤️🩹": "mending_heart",
|
||||
"🔒": "locked",
|
||||
"🔍": "magnifying_glass",
|
||||
"🔗": "link",
|
||||
"⌨": "keyboard",
|
||||
"📑": "boring_document",
|
||||
"🧮": "abacus",
|
||||
"😸": "cat_grin"
|
||||
}
|
||||
let sizing = {
|
||||
18: 0.8,
|
||||
22: 0.4,
|
||||
30: 0.7,
|
||||
48: 0.9,
|
||||
64: 0.9
|
||||
64: 0.9,
|
||||
78: 0.9
|
||||
}
|
||||
export default function(emoji, size, disablePadding) {
|
||||
export default function(emoji, size, disablePadding, fluent) {
|
||||
if (!size) size = 22;
|
||||
let padding = size !== 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : false;
|
||||
if (disablePadding) padding = 'margin-right:0!important;';
|
||||
|
||||
if (!names[emoji]) emoji = "❓";
|
||||
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}"` : ''}alt="${emoji}" src="emoji/${names[emoji]}.svg">`
|
||||
|
||||
let filePath = `emoji/${names[emoji]}.svg`;
|
||||
if (fluent) filePath = `emoji/3d/${names[emoji]}.svg`;
|
||||
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}"` : ''}alt="${emoji}" src="${filePath}" loading="lazy">`
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { 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">
|
||||
<path d="M14.1905 28.5L2 16L14.1905 3.5L16.2857 5.62054L7.65986 14.4654H30V17.5346H7.65986L16.2857 26.3516L14.1905 28.5Z" fill="#E1E1E1"/>
|
||||
</svg>`
|
||||
|
||||
export const dropdownSVG = `<svg width="18" height="18" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 12.0533L16 24L4 12.0533L6.03571 10L14.7188 18.4104L16.25 19.9348L17.7813 18.4104L25.9375 10L28 12.0533Z" fill="#E1E1E1"/>
|
||||
</svg>`
|
||||
|
||||
export function switcher(obj) {
|
||||
let items = ``;
|
||||
if (obj.name === "download") {
|
||||
items = obj.items;
|
||||
} else {
|
||||
for (let i = 0; i < obj.items.length; i++) {
|
||||
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []
|
||||
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : [];
|
||||
if (i === 0) classes.push("first");
|
||||
if (i === (obj.items.length - 1)) classes.push("last");
|
||||
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>`
|
||||
}
|
||||
}
|
||||
@@ -19,26 +29,18 @@ export function switcher(obj) {
|
||||
${obj.explanation ? `<div class="explanation">${obj.explanation}</div>` : ``}
|
||||
</div>`
|
||||
}
|
||||
export function checkbox(obj) {
|
||||
let paddings = ["bottom-margin", "top-margin", "no-margin", "top-margin-only"];
|
||||
let checkboxes = ``;
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
let paddingClass = obj[i].padding && paddings.includes(obj[i].padding) ? ` ${obj[i].padding}` : '';
|
||||
|
||||
export function checkbox(action, text, paddingType, aria) {
|
||||
let paddingClass = ` `
|
||||
switch (paddingType) {
|
||||
case 1:
|
||||
paddingClass += "bottom-margin"
|
||||
break;
|
||||
case 2:
|
||||
paddingClass += "top-margin"
|
||||
break;
|
||||
case 3:
|
||||
paddingClass += "no-margin"
|
||||
break;
|
||||
case 4:
|
||||
paddingClass += "top-margin-only"
|
||||
checkboxes += `<label id="${obj[i].action}-chkbx" class="checkbox${paddingClass}">
|
||||
<input id="${obj[i].action}" type="checkbox" aria-label="${obj[i].aria ? obj[i].aria : obj[i].name}" onclick="checkbox('${obj[i].action}')">
|
||||
<span>${obj[i].name}</span>
|
||||
</label>`
|
||||
}
|
||||
return `<label id="${action}-chkbx" class="checkbox${paddingClass}">
|
||||
<input id="${action}" type="checkbox" ${aria ? `aria-label="${aria}"` : `aria-label="${text}"`} onclick="checkbox('${action}')">
|
||||
<span>${text}</span>
|
||||
</label>`
|
||||
return checkboxes
|
||||
}
|
||||
export function sep(paddingType) {
|
||||
let paddingClass = ``
|
||||
@@ -50,7 +52,7 @@ export function sep(paddingType) {
|
||||
return `<div class="separator${paddingClass}"></div>`
|
||||
}
|
||||
export function popup(obj) {
|
||||
let classes = obj.classes ? obj.classes : []
|
||||
let classes = obj.classes ? obj.classes : [];
|
||||
let body = obj.body;
|
||||
if (Array.isArray(obj.body)) {
|
||||
body = ``
|
||||
@@ -65,47 +67,63 @@ export function popup(obj) {
|
||||
}
|
||||
}
|
||||
return `
|
||||
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">` : ''}
|
||||
${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.standalone && !obj.buttonOnly ? `<button id="close-button" class="switch up" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>` : ''}
|
||||
${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 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"${obj.footer ? ' class="with-footer"' : ''}>
|
||||
<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>
|
||||
${obj.footer ? `<div id="popup-footer" class="popup-footer">
|
||||
<a id="popup-bottom" class="popup-footer-content" target="_blank" href="${obj.footer.url}">${obj.footer.text}</a>
|
||||
</div>` : ''}
|
||||
${classes.includes("small") ? `<div class="glass-bkg small"></div>`: ''}
|
||||
${obj.standalone ? `</div>` : ''}`
|
||||
}
|
||||
|
||||
export function multiPagePopup(obj) {
|
||||
let tabs = ``
|
||||
let tabContent = ``
|
||||
let tabs = `
|
||||
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
|
||||
${backButtonSVG}
|
||||
</button>`;
|
||||
|
||||
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-button" 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" 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>` : ''}${tabContent}</div>
|
||||
<div id="popup-tabs" class="switches popup-tabs">${tabs}</div>
|
||||
<div id="popup-${obj.name}" class="popup center box scrollable">
|
||||
<div id="popup-content">
|
||||
${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">
|
||||
<div class="switches popup-tabs-child">${tabs}</div>
|
||||
<div class="glass-bkg alone"></div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
export function collapsibleList(arr) {
|
||||
let items = ``
|
||||
let items = ``;
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
items += `<div id="${arr[i]["name"]}-collapse" class="collapse-list">
|
||||
let classes = arr[i]["classes"] ? arr[i]["classes"] : [];
|
||||
if (i === 0) classes.push("first");
|
||||
if (i === (arr.length - 1)) classes.push("last");
|
||||
items += `<div id="${arr[i]["name"]}-collapse" class="collapse-list${classes.length > 0 ? ' ' + classes.join(' ') : ''}">
|
||||
<div class="collapse-header" onclick="expandCollapsible(event)">
|
||||
<div class="collapse-title">${arr[i]["title"]}</div>
|
||||
<div class="collapse-indicator">^</div>
|
||||
<div class="collapse-indicator">${dropdownSVG}</div>
|
||||
</div>
|
||||
<div id="${arr[i]["name"]}-body" class="collapse-body">${arr[i]["body"]}</div>
|
||||
</div>`
|
||||
@@ -113,26 +131,34 @@ export function collapsibleList(arr) {
|
||||
return items;
|
||||
}
|
||||
export function popupWithBottomButtons(obj) {
|
||||
let tabs = ``
|
||||
let tabs = `
|
||||
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
|
||||
${backButtonSVG}
|
||||
</button>`
|
||||
|
||||
for (let i = 0; i < obj.buttons.length; i++) {
|
||||
tabs += obj.buttons[i]
|
||||
}
|
||||
tabs += `<button id="close-button" 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" 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>` : ''}${obj.content}</div>
|
||||
<div id="popup-buttons" class="switches popup-tabs">${tabs}</div>
|
||||
<div id="popup-${obj.name}" class="popup center box scrollable">
|
||||
<div id="popup-content">
|
||||
${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">
|
||||
<div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div>
|
||||
<div class="glass-bkg alone"></div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
export function backdropLink(link, text) {
|
||||
return `<a class="text-backdrop italic" href="${link}" target="_blank">${text}</a>`
|
||||
}
|
||||
export function socialLink(emji, name, handle, url) {
|
||||
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop italic" href="${url}" target="_blank">${handle}</a></div>`
|
||||
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop link" href="${url}" target="_blank">${handle}</a></div>`
|
||||
}
|
||||
export function settingsCategory(obj) {
|
||||
return `<div id="settings-${obj.name}" class="settings-category">
|
||||
@@ -187,3 +213,35 @@ export function celebrationsEmoji() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export function urgentNotice(obj) {
|
||||
if (obj.visible) {
|
||||
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 ``
|
||||
}
|
||||
export function keyboardShortcuts(arr) {
|
||||
let base = `<div id="keyboard-shortcuts" class="explanation">`;
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
base += `<div class="shortcut-category">`;
|
||||
for (let c = 0; c < arr[i].items.length; c++) {
|
||||
let combo = arr[i].items[c].combo.split('+').map(
|
||||
key => `<span class="text-backdrop key">${key}</span>`
|
||||
).join("+")
|
||||
base += `<div class="shortcut">${combo}: ${arr[i].items[c].name}</div>`
|
||||
}
|
||||
base += `</div>`
|
||||
}
|
||||
base += `</div>`;
|
||||
|
||||
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,5 @@
|
||||
import changelogManager from "../changelog/changelogManager.js"
|
||||
import { cleanHTML } from "../sub/utils.js";
|
||||
|
||||
let cache = {}
|
||||
|
||||
@@ -10,8 +11,22 @@ export function changelogHistory() { // blockId 0
|
||||
let historyLen = history.length;
|
||||
for (let i in history) {
|
||||
let separator = (i !== 0 && i !== historyLen) ? '<div class="separator"></div>' : '';
|
||||
render += `${separator}${history[i]["banner"] ? `<div class="changelog-banner"><img class="changelog-img" src="${history[i]["banner"]}" onerror="this.style.display='none'"></img></div>` : ''}<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div><div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
|
||||
|
||||
render += `
|
||||
${separator}${history[i]["banner"] ?
|
||||
`<div class="changelog-banner">
|
||||
<img class="changelog-img" ` +
|
||||
`src="${history[i]["banner"]["url"]}" ` +
|
||||
`width="${history[i]["banner"]["width"]}" ` +
|
||||
`height="${history[i]["banner"]["height"]}" ` +
|
||||
`onerror="this.style.opacity=0" loading="lazy">`+
|
||||
`</img>
|
||||
</div>` : ''}
|
||||
<div id="popup-desc" class="changelog-tags">${history[i]["version"]}</div>
|
||||
<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div>
|
||||
<div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
|
||||
}
|
||||
render = cleanHTML(render);
|
||||
cache['0'] = render;
|
||||
return render;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { backdropLink, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js";
|
||||
import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
|
||||
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, 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";
|
||||
import emoji from "../emoji.js";
|
||||
@@ -7,9 +7,7 @@ import changelogManager from "../changelog/changelogManager.js";
|
||||
|
||||
let com = getCommitInfo();
|
||||
|
||||
let enabledServices = Object.keys(s).filter((p) => {
|
||||
if (s[p].enabled) return true;
|
||||
}).sort().map((p) => {
|
||||
let enabledServices = Object.keys(s).filter(p => s[p].enabled).sort().map((p) => {
|
||||
return `<br>• ${s[p].alias ? s[p].alias : p}`
|
||||
}).join('').substring(4)
|
||||
|
||||
@@ -30,48 +28,58 @@ for (let i in donations["crypto"]) {
|
||||
|
||||
export default function(obj) {
|
||||
const t = (str, replace) => { return loc(obj.lang, str, replace) };
|
||||
|
||||
let ua = obj.useragent.toLowerCase();
|
||||
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');
|
||||
|
||||
try {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${obj.lang}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
|
||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
|
||||
|
||||
<title>${appName}</title>
|
||||
<title>${t("AppTitleCobalt")}</title>
|
||||
|
||||
<meta property="og:url" content="${process.env.selfURL}" />
|
||||
<meta property="og:title" content="${appName}" />
|
||||
<meta property="og:url" content="${process.env.webURL || process.env.selfURL}" />
|
||||
<meta property="og:title" content="${t("AppTitleCobalt")}" />
|
||||
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
|
||||
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
|
||||
<meta name="title" content="${appName}" />
|
||||
<meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" />
|
||||
<meta name="title" content="${t("AppTitleCobalt")}" />
|
||||
<meta name="description" content="${t('AboutSummary')}" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="${t("AppTitleCobalt")}">
|
||||
|
||||
<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="fonts/notosansmono.css" rel="preload" />
|
||||
<link rel="stylesheet" href="cobalt.css" />
|
||||
<link rel="stylesheet" href="fonts/notosansmono.css" />
|
||||
|
||||
<link rel="me" href="${authorInfo.support.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",
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
tabs: [{
|
||||
name: "about",
|
||||
title: `${emoji("🐲")} ${t('AboutTab')}`,
|
||||
@@ -82,31 +90,84 @@ export default function(obj) {
|
||||
text: t('MadeWithLove'),
|
||||
url: authorInfo.link
|
||||
},
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
|
||||
},
|
||||
body: [{
|
||||
text: t('AboutSummary')
|
||||
}, {
|
||||
text: collapsibleList([{
|
||||
"name": "services",
|
||||
"title": t("CollapseServices"),
|
||||
"body": `${enabledServices}<br/><br/>${t("ServicesNote")}`
|
||||
name: "services",
|
||||
title: `${emoji("🔗")} ${t("CollapseServices")}`,
|
||||
body: `${enabledServices}<br/><br/>${t("ServicesNote")}`
|
||||
}, {
|
||||
"name": "support",
|
||||
"title": t("CollapseSupport"),
|
||||
"body": `${t("FollowSupport")}<br/>
|
||||
${socialLink(emoji("🐘"), "mastodon", authorInfo.support.mastodon.handle, authorInfo.support.mastodon.url)}
|
||||
${socialLink(emoji("🐦"), "twitter", authorInfo.support.twitter.handle, authorInfo.support.twitter.url)}<br/>
|
||||
name: "keyboard",
|
||||
title: `${emoji("⌨")} ${t("CollapseKeyboard")}`,
|
||||
body:
|
||||
`${t("KeyboardShortcutsIntro")}
|
||||
${keyboardShortcuts([{
|
||||
items: [{
|
||||
combo: "Shift+D",
|
||||
name: t("PasteFromClipboard")
|
||||
}, {
|
||||
combo: "Shift+K",
|
||||
name: t("ModeToggleAuto")
|
||||
}, {
|
||||
combo: "Shift+L",
|
||||
name: t("ModeToggleAudio")
|
||||
}]
|
||||
}, {
|
||||
items: [{
|
||||
combo: "Ctrl+V",
|
||||
name: t("KeyboardShortcutQuickPaste")
|
||||
}, {
|
||||
combo: "Esc",
|
||||
name: t("KeyboardShortcutClear")
|
||||
}, {
|
||||
combo: "Esc",
|
||||
name: t("KeyboardShortcutClosePopup")
|
||||
}]
|
||||
}, {
|
||||
items: [{
|
||||
combo: "Shift+B",
|
||||
name: t("AboutTab")
|
||||
}, {
|
||||
combo: "Shift+N",
|
||||
name: t("ChangelogTab")
|
||||
}, {
|
||||
combo: "Shift+M",
|
||||
name: t("TitlePopupSettings")
|
||||
}]
|
||||
}])}`
|
||||
}, {
|
||||
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(emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo)}<br/>
|
||||
${socialLink(
|
||||
emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo
|
||||
)}<br/>
|
||||
${t("SupportNote")}`
|
||||
}, {
|
||||
"name": "privacy",
|
||||
"title": t("CollapsePrivacy"),
|
||||
"body": t("PrivacyPolicy")
|
||||
name: "privacy",
|
||||
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
|
||||
body: t("PrivacyPolicy")
|
||||
}, {
|
||||
name: "legal",
|
||||
title: `${emoji("📑")} ${t("CollapseLegal")}`,
|
||||
body: t("FairUse")
|
||||
}])
|
||||
+ `${process.env.DEPLOYMENT_ID && process.env.INTERNAL_IP ? '<a id="hop-attribution" class="explanation" href="https://hop.io/" target="_blank">powered by hop.io</a>' : ''}`
|
||||
}]
|
||||
})
|
||||
}, {
|
||||
@@ -115,15 +176,27 @@ export default function(obj) {
|
||||
content: popup({
|
||||
name: "changelog",
|
||||
header: {
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
title: `${emoji("🪄", 30)} ${t('TitlePopupChangelog')}`
|
||||
},
|
||||
body: [{
|
||||
text: `<div class="category-title">${t('ChangelogLastMajor')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: changelogManager("banner") ? `<div class="changelog-banner"><img class="changelog-img" src="${changelogManager("banner")}" onerror="this.style.display='none'"></img></div>`: '',
|
||||
text: changelogManager("banner") ?
|
||||
`<div class="changelog-banner">
|
||||
<img class="changelog-img" ` +
|
||||
`src="${changelogManager("banner")["url"]}" ` +
|
||||
`width="${changelogManager("banner")["width"]}" ` +
|
||||
`height="${changelogManager("banner")["height"]}" ` +
|
||||
`onerror="this.style.opacity=0" loading="lazy">`+
|
||||
`</img>
|
||||
</div>`: '',
|
||||
raw: true
|
||||
}, {
|
||||
text: changelogManager("version"),
|
||||
classes: ["changelog-tags"],
|
||||
nopadding: true
|
||||
}, {
|
||||
text: changelogManager("title"),
|
||||
classes: ["changelog-subtitle"],
|
||||
@@ -131,19 +204,26 @@ export default function(obj) {
|
||||
}, {
|
||||
text: changelogManager("content")
|
||||
}, {
|
||||
text: `${sep()}<span class="text-backdrop">${obj.hash}:</span> ${com[0]}`,
|
||||
text: sep(),
|
||||
raw: true
|
||||
},{
|
||||
text: `<a class="text-backdrop changelog-tag-version" href="${repo}/commit/${obj.hash}">#${obj.hash}</a>`,
|
||||
classes: ["changelog-tags"],
|
||||
nopadding: true
|
||||
}, {
|
||||
text: com[0],
|
||||
classes: ["changelog-subtitle"],
|
||||
nopadding: true
|
||||
}, {
|
||||
text: com[1]
|
||||
}, {
|
||||
text: backdropLink(`${repo}/commits`, t('LinkGitHubChanges')),
|
||||
classes: ["bottom-link"]
|
||||
}, {
|
||||
text: `<div class="category-title">${t('ChangelogOlder')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: `<div id="changelog-history"><button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${t("ChangelogPressToExpand")}</button></div>`,
|
||||
text: `
|
||||
<div id="changelog-history">
|
||||
<button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${t("ChangelogPressToExpand")}</button>
|
||||
</div>`,
|
||||
raw: true
|
||||
}]
|
||||
})
|
||||
@@ -153,14 +233,21 @@ export default function(obj) {
|
||||
content: popup({
|
||||
name: "donate",
|
||||
header: {
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
title: emoji("💸", 30) + t('TitlePopupDonate')
|
||||
},
|
||||
body: [{
|
||||
text: `<div class="category-title">${t('DonateSub')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: `<div class="changelog-banner"><img class="changelog-img" src="updateBanners/catsleep.webp" onerror="this.style.display='none'"></img></div>`,
|
||||
text: `<div class="changelog-banner">
|
||||
<img class="changelog-img" ` +
|
||||
`src="updateBanners/catsleep.webp"` +
|
||||
`width="480" ` +
|
||||
`height="270" ` +
|
||||
`onerror="this.style.opacity=0" loading="lazy">`+
|
||||
`</img>
|
||||
</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: t('DonateExplanation')
|
||||
@@ -188,7 +275,7 @@ export default function(obj) {
|
||||
})}
|
||||
${multiPagePopup({
|
||||
name: "settings",
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
header: {
|
||||
aboveTitle: {
|
||||
text: `v.${version}-${obj.hash}${platform} (${obj.branch})`,
|
||||
@@ -206,33 +293,37 @@ export default function(obj) {
|
||||
name: "vQuality",
|
||||
explanation: t('SettingsQualityDescription'),
|
||||
items: [{
|
||||
"action": "max",
|
||||
"text": "4320p+"
|
||||
action: "max",
|
||||
text: "8k+"
|
||||
}, {
|
||||
"action": "2160",
|
||||
"text": "2160p"
|
||||
action: "2160",
|
||||
text: "4k"
|
||||
}, {
|
||||
"action": "1440",
|
||||
"text": "1440p"
|
||||
action: "1440",
|
||||
text: "1440p"
|
||||
}, {
|
||||
"action": "1080",
|
||||
"text": "1080p"
|
||||
action: "1080",
|
||||
text: "1080p"
|
||||
}, {
|
||||
"action": "720",
|
||||
"text": "720p"
|
||||
action: "720",
|
||||
text: "720p"
|
||||
}, {
|
||||
"action": "480",
|
||||
"text": "480p"
|
||||
action: "480",
|
||||
text: "480p"
|
||||
}, {
|
||||
"action": "360",
|
||||
"text": "360p"
|
||||
action: "360",
|
||||
text: "360p"
|
||||
}]
|
||||
})
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3)
|
||||
title: "tiktok",
|
||||
body: checkbox([{
|
||||
action: "disableTikTokWatermark",
|
||||
name: t("SettingsRemoveWatermark"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: t('SettingsCodecSubtitle'),
|
||||
@@ -240,14 +331,14 @@ export default function(obj) {
|
||||
name: "vCodec",
|
||||
explanation: t('SettingsCodecDescription'),
|
||||
items: [{
|
||||
"action": "h264",
|
||||
"text": "h264 (mp4)"
|
||||
action: "h264",
|
||||
text: "h264 (mp4)"
|
||||
}, {
|
||||
"action": "av1",
|
||||
"text": "av1 (mp4)"
|
||||
action: "av1",
|
||||
text: "av1 (mp4)"
|
||||
}, {
|
||||
"action": "vp9",
|
||||
"text": "vp9 (webm)"
|
||||
action: "vp9",
|
||||
text: "vp9 (webm)"
|
||||
}]
|
||||
})
|
||||
})
|
||||
@@ -257,11 +348,11 @@ export default function(obj) {
|
||||
name: "vimeoDash",
|
||||
explanation: t('SettingsVimeoPreferDescription'),
|
||||
items: [{
|
||||
"action": "false",
|
||||
"text": "progressive"
|
||||
action: "false",
|
||||
text: "progressive"
|
||||
}, {
|
||||
"action": "true",
|
||||
"text": "dash"
|
||||
action: "true",
|
||||
text: "dash"
|
||||
}]
|
||||
})
|
||||
})
|
||||
@@ -271,31 +362,44 @@ export default function(obj) {
|
||||
content: settingsCategory({
|
||||
name: "general",
|
||||
title: t('SettingsFormatSubtitle'),
|
||||
body:
|
||||
switcher({
|
||||
name: "aFormat",
|
||||
explanation: t('SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
|
||||
}) + settingsCategory({
|
||||
name: "dub",
|
||||
title: t("SettingsAudioDub"),
|
||||
body: switcher({
|
||||
name: "dubLang",
|
||||
explanation: t('SettingsAudioDubDescription'),
|
||||
items: [{
|
||||
"action": "original",
|
||||
"text": t('SettingsDubDefault')
|
||||
}, {
|
||||
"action": "auto",
|
||||
"text": t('SettingsDubAuto')
|
||||
}]
|
||||
})
|
||||
}) + settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription'))
|
||||
})
|
||||
body: switcher({
|
||||
name: "aFormat",
|
||||
explanation: t('SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
})
|
||||
+ sep(0)
|
||||
+ checkbox([{
|
||||
action: "muteAudio",
|
||||
name: t("SettingsVideoMute"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
+ explanation(t('SettingsVideoMuteExplanation'))
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "dub",
|
||||
title: t("SettingsAudioDub"),
|
||||
body: switcher({
|
||||
name: "dubLang",
|
||||
explanation: t('SettingsAudioDubDescription'),
|
||||
items: [{
|
||||
action: "original",
|
||||
text: t('SettingsDubDefault')
|
||||
}, {
|
||||
action: "auto",
|
||||
text: t('SettingsDubAuto')
|
||||
}]
|
||||
})
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok",
|
||||
body: checkbox([{
|
||||
action: "fullTikTokAudio",
|
||||
name: t("SettingsAudioFullTikTok"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
+ explanation(t('SettingsAudioFullTikTokDescription'))
|
||||
})
|
||||
}, {
|
||||
name: "other",
|
||||
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
|
||||
@@ -304,122 +408,189 @@ export default function(obj) {
|
||||
title: t('SettingsAppearanceSubtitle'),
|
||||
body: switcher({
|
||||
name: "theme",
|
||||
subtitle: t('SettingsThemeSubtitle'),
|
||||
items: [{
|
||||
"action": "auto",
|
||||
"text": t('SettingsThemeAuto')
|
||||
action: "auto",
|
||||
text: t('SettingsThemeAuto')
|
||||
}, {
|
||||
"action": "dark",
|
||||
"text": t('SettingsThemeDark')
|
||||
action: "dark",
|
||||
text: t('SettingsThemeDark')
|
||||
}, {
|
||||
"action": "light",
|
||||
"text": t('SettingsThemeLight')
|
||||
action: "light",
|
||||
text: t('SettingsThemeLight')
|
||||
}]
|
||||
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), 4, t('AccessibilityKeepDownloadButton'))
|
||||
}) + settingsCategory({
|
||||
})
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "accessibility",
|
||||
title: t('Accessibility'),
|
||||
body: checkbox([{
|
||||
action: "alwaysVisibleButton",
|
||||
name: t("SettingsKeepDownloadButton"),
|
||||
aria: t("AccessibilityKeepDownloadButton")
|
||||
}, {
|
||||
action: "reduceTransparency",
|
||||
name: t("SettingsReduceTransparency")
|
||||
}, {
|
||||
action: "disableAnimations",
|
||||
name: t("SettingsDisableAnimations"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
})
|
||||
+ settingsCategory({
|
||||
name: "miscellaneous",
|
||||
title: t('Miscellaneous'),
|
||||
body: checkbox("disableChangelog", t('SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", t('SettingsEnableDownloadPopup'), 1, t('AccessibilityEnableDownloadPopup')) : ''}`
|
||||
body: checkbox([{
|
||||
action: "downloadPopup",
|
||||
name: t("SettingsEnableDownloadPopup"),
|
||||
aria: t("AccessibilityEnableDownloadPopup")
|
||||
}, {
|
||||
action: "disableMetadata",
|
||||
name: t("SettingsDisableMetadata")
|
||||
}, {
|
||||
action: "disableChangelog",
|
||||
name: t("SettingsDisableNotifications"),
|
||||
padding: "no-margin"
|
||||
}])
|
||||
})
|
||||
}],
|
||||
})}
|
||||
${popup({
|
||||
name: "download",
|
||||
standalone: true,
|
||||
header: {
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
subtitle: t('TitlePopupDownload')
|
||||
},
|
||||
body: switcher({
|
||||
name: "download",
|
||||
subtitle: t('DownloadPopupWayToSave'),
|
||||
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
|
||||
items: `<a id="pd-download" class="switch full" target="_blank" href="/">${t('Download')}</a>
|
||||
<div id="pd-share" class="switch full">${t('ShareURL')}</div>
|
||||
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
|
||||
})
|
||||
}]
|
||||
})}
|
||||
${popupWithBottomButtons({
|
||||
name: "picker",
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
header: {
|
||||
title: `<div id="picker-title"></div>`,
|
||||
title: `${emoji("🧮", 30)} <div id="picker-title"></div>`,
|
||||
explanation: `<div id="picker-subtitle"></div>`,
|
||||
},
|
||||
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${t('ImagePickerDownloadAudio')}</a>`],
|
||||
content: '<div id="picker-holder"></div>'
|
||||
})}
|
||||
${popup({
|
||||
name: "error",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small"],
|
||||
buttonText: t('ErrorPopupCloseButton'),
|
||||
header: {
|
||||
closeAria: t('AccessibilityClosePopup'),
|
||||
title: t('TitlePopupError'),
|
||||
emoji: emoji("☹️", 64, 1),
|
||||
},
|
||||
body: `<div id="desc-error" class="desc-padding subtext"></div>`
|
||||
})}
|
||||
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
|
||||
<div id="cobalt-main-box" class="center" style="visibility: hidden;">
|
||||
<div id="logo">${appName}</div>
|
||||
<div id="download-area">
|
||||
<div id="top">
|
||||
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
|
||||
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
|
||||
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
|
||||
</div>
|
||||
<div id="bottom">
|
||||
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
|
||||
${switcher({
|
||||
name: "audioMode",
|
||||
noParent: true,
|
||||
items: [{
|
||||
"action": "false",
|
||||
"text": `${emoji("✨")} ${t("ModeToggleAuto")}`
|
||||
}, {
|
||||
"action": "true",
|
||||
"text": `${emoji("🎶")} ${t("ModeToggleAudio")}`
|
||||
}]
|
||||
})}
|
||||
<div id="popup-download-container" class="popup-from-bottom">
|
||||
${popup({
|
||||
name: "download",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small"],
|
||||
header: {
|
||||
closeAria: t('AccessibilityGoBack'),
|
||||
emoji: emoji("🐱", 78, 1, 1),
|
||||
title: t('TitlePopupDownload')
|
||||
},
|
||||
body: switcher({
|
||||
name: "download",
|
||||
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
|
||||
items: `<a id="pd-download" class="switch full" target="_blank" href="/"><span>${t('Download')}</span></a>
|
||||
<div id="pd-share" class="switch full">${t('ShareURL')}</div>
|
||||
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
|
||||
}),
|
||||
buttonText: t('PopupCloseDone')
|
||||
})}
|
||||
</div>
|
||||
<div id="popup-error-container" class="popup-from-bottom">
|
||||
${popup({
|
||||
name: "error",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
classes: ["small"],
|
||||
header: {
|
||||
title: t('TitlePopupError'),
|
||||
emoji: emoji("😿", 78, 1, 1),
|
||||
},
|
||||
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: "✨",
|
||||
text: t("UrgentNewDomain"),
|
||||
visible: true,
|
||||
action: "popup('about', 1, 'changelog')"
|
||||
})}
|
||||
<div id="cobalt-main-box" class="center">
|
||||
<div id="logo">${t("AppTitleCobalt")}</div>
|
||||
<div id="download-area">
|
||||
<div id="top">
|
||||
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
|
||||
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
|
||||
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
|
||||
</div>
|
||||
<div id="bottom">
|
||||
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
|
||||
${switcher({
|
||||
name: "audioMode",
|
||||
noParent: true,
|
||||
items: [{
|
||||
action: "false",
|
||||
text: `${emoji("✨")} ${t("ModeToggleAuto")}`
|
||||
}, {
|
||||
action: "true",
|
||||
text: `${emoji("🎶")} ${t("ModeToggleAudio")}`
|
||||
}]
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer id="footer">
|
||||
${footerButtons([{
|
||||
name: "about",
|
||||
type: "popup",
|
||||
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
|
||||
aria: t('AccessibilityOpenAbout')
|
||||
}, {
|
||||
name: "about",
|
||||
type: "popup",
|
||||
context: "donate",
|
||||
text: `${emoji("💖", 22)} ${t('Donate')}`,
|
||||
aria: t('AccessibilityOpenDonate')
|
||||
}, {
|
||||
name: "settings",
|
||||
type: "popup",
|
||||
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
|
||||
aria: t('AccessibilityOpenSettings')
|
||||
}])}
|
||||
</footer>
|
||||
</div>
|
||||
<footer id="footer" style="visibility: hidden;">
|
||||
${/* big action buttons are ALWAYS either first or last, because usual buttons are bundled in pairs and are sandwiched between bigger buttons for mobile view */
|
||||
footerButtons([{
|
||||
name: "about",
|
||||
type: "popup",
|
||||
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
|
||||
aria: t('AccessibilityOpenAbout')
|
||||
}, {
|
||||
name: "about",
|
||||
type: "popup",
|
||||
context: "donate",
|
||||
text: `${emoji("💖", 22)} ${t('Donate')}`,
|
||||
aria: t('AccessibilityOpenDonate')
|
||||
}, {
|
||||
name: "settings",
|
||||
type: "popup",
|
||||
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
|
||||
aria: t('AccessibilityOpenSettings')
|
||||
}])}
|
||||
</footer>
|
||||
</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${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
|
||||
};</script>
|
||||
<script type="text/javascript">
|
||||
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>`;
|
||||
</html>
|
||||
`
|
||||
} catch (err) {
|
||||
return `${t('ErrorPageRenderFail', obj.hash)}`;
|
||||
}
|
||||
|
||||
37
src/modules/processing/cookie/cookie.js
Normal file
37
src/modules/processing/cookie/cookie.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
||||
export default class Cookie {
|
||||
constructor(input) {
|
||||
assert(typeof input === 'object');
|
||||
this._values = {};
|
||||
this.set(input)
|
||||
}
|
||||
set(values) {
|
||||
Object.entries(values).forEach(
|
||||
([ key, value ]) => this._values[key] = value
|
||||
)
|
||||
}
|
||||
unset(keys) {
|
||||
for (const key of keys) delete this._values[key]
|
||||
}
|
||||
static fromString(str) {
|
||||
const obj = {};
|
||||
|
||||
str.split('; ').forEach(cookie => {
|
||||
const key = cookie.split('=')[0];
|
||||
const value = cookie.split('=').splice(1).join('=');
|
||||
obj[key] = value
|
||||
})
|
||||
|
||||
return new Cookie(obj)
|
||||
}
|
||||
toString() {
|
||||
return Object.entries(this._values).map(([ name, value ]) => `${name}=${value}`).join('; ')
|
||||
}
|
||||
toJSON() {
|
||||
return this.toString()
|
||||
}
|
||||
values() {
|
||||
return Object.freeze({ ...this._values })
|
||||
}
|
||||
}
|
||||
5
src/modules/processing/cookie/cookies_example.json
Normal file
5
src/modules/processing/cookie/cookies_example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"instagram": [
|
||||
"mid=replace; ig_did=this; csrftoken=cookie"
|
||||
]
|
||||
}
|
||||
62
src/modules/processing/cookie/manager.js
Normal file
62
src/modules/processing/cookie/manager.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import Cookie from './cookie.js';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
|
||||
|
||||
const WRITE_INTERVAL = 60000,
|
||||
cookiePath = process.env.cookiePath,
|
||||
COUNTER = Symbol('counter');
|
||||
|
||||
let cookies = {}, dirty = false, intervalId;
|
||||
|
||||
const setup = async () => {
|
||||
try {
|
||||
if (!cookiePath) return;
|
||||
|
||||
cookies = await readFile(cookiePath, 'utf8');
|
||||
cookies = JSON.parse(cookies);
|
||||
intervalId = setInterval(writeChanges, WRITE_INTERVAL)
|
||||
} catch { /* no cookies for you */ }
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
function writeChanges() {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
|
||||
writeFile(cookiePath, JSON.stringify(cookies, null, 4)).catch(() => {
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
}
|
||||
|
||||
export function getCookie(service) {
|
||||
if (!cookies[service] || !cookies[service].length) return;
|
||||
|
||||
let n;
|
||||
if (cookies[service][COUNTER] === undefined) {
|
||||
n = cookies[service][COUNTER] = 0
|
||||
} else {
|
||||
++cookies[service][COUNTER]
|
||||
n = (cookies[service][COUNTER] %= cookies[service].length)
|
||||
}
|
||||
|
||||
const cookie = cookies[service][n];
|
||||
if (typeof cookie === 'string') cookies[service][n] = Cookie.fromString(cookie);
|
||||
|
||||
return cookies[service][n]
|
||||
}
|
||||
|
||||
export function updateCookie(cookie, headers) {
|
||||
if (!cookie) return;
|
||||
|
||||
const parsed = parseSetCookie(
|
||||
splitCookiesString(headers.get('set-cookie')),
|
||||
{ decodeValues: false }
|
||||
), values = {}
|
||||
|
||||
cookie.unset(parsed.filter(c => c.expires < new Date()).map(c => c.name));
|
||||
parsed.filter(c => !c.expires || c.expires > new Date()).forEach(c => values[c.name] = c.value);
|
||||
|
||||
cookie.set(values);
|
||||
if (Object.keys(values).length) dirty = true
|
||||
}
|
||||
40
src/modules/processing/hostOverrides.js
Normal file
40
src/modules/processing/hostOverrides.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export default function (inHost, inURL) {
|
||||
let host = String(inHost);
|
||||
let url = String(inURL);
|
||||
|
||||
switch(host) {
|
||||
case "youtube":
|
||||
if (url.startsWith("https://youtube.com/live/") || url.startsWith("https://www.youtube.com/live/")) {
|
||||
url = url.split("?")[0].replace("www.", "");
|
||||
url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
|
||||
}
|
||||
break;
|
||||
case "youtu":
|
||||
if (url.startsWith("https://youtu.be/")) {
|
||||
host = "youtube";
|
||||
url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}`
|
||||
}
|
||||
break;
|
||||
case "vxtwitter":
|
||||
case "x":
|
||||
if (url.startsWith("https://x.com/")) {
|
||||
host = "twitter";
|
||||
url = url.replace("https://x.com/", "https://twitter.com/")
|
||||
}
|
||||
if (url.startsWith("https://vxtwitter.com/")) {
|
||||
host = "twitter";
|
||||
url = url.replace("https://vxtwitter.com/", "https://twitter.com/")
|
||||
}
|
||||
break;
|
||||
case "tumblr":
|
||||
if (!url.includes("blog/view")) {
|
||||
if (url.slice(-1) === '/') url = url.slice(0, -1);
|
||||
url = url.replace(url.split('/')[5], '')
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
host: host,
|
||||
url: url
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,13 @@ import vimeo from "./services/vimeo.js";
|
||||
import soundcloud from "./services/soundcloud.js";
|
||||
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";
|
||||
|
||||
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) });
|
||||
@@ -111,6 +113,16 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
case "vine":
|
||||
r = await vine({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "pinterest":
|
||||
r = await pinterest({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "streamable":
|
||||
r = await streamable({
|
||||
id: patternMatch["id"],
|
||||
quality: obj.vQuality,
|
||||
isAudioOnly: isAudioOnly,
|
||||
});
|
||||
break;
|
||||
case "twitch":
|
||||
r = await twitch({
|
||||
vodId: patternMatch["video"] ? patternMatch["video"] : false,
|
||||
@@ -119,7 +131,6 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
isAudioOnly: obj.isAudioOnly,
|
||||
format: obj.vFormat
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
@@ -129,7 +140,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.ip, 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,22 +2,23 @@ import { audioIgnore, services, supportedAudio } from "../config.js";
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
|
||||
export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMuted) {
|
||||
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
|
||||
let action,
|
||||
responseType = 2,
|
||||
defaultParams = {
|
||||
u: r.urls,
|
||||
service: host,
|
||||
ip: ip,
|
||||
filename: r.filename,
|
||||
fileMetadata: !disableMetadata ? r.fileMetadata : false
|
||||
},
|
||||
params = {}
|
||||
|
||||
if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video";
|
||||
if (r.isM3U8) action = "singleM3U8";
|
||||
if (isAudioOnly && !r.picker) action = "audio";
|
||||
if (r.picker) action = "picker";
|
||||
if (isAudioMuted) action = "muteVideo";
|
||||
|
||||
if (r.isPhoto) action = "photo";
|
||||
else if (r.picker) action = "picker"
|
||||
else if (isAudioMuted) action = "muteVideo";
|
||||
else if (isAudioOnly) action = "audio";
|
||||
else if (r.isM3U8) action = "singleM3U8";
|
||||
else action = "video";
|
||||
|
||||
if (action === "picker" || action === "audio") {
|
||||
defaultParams.filename = r.audioFilename;
|
||||
@@ -26,13 +27,16 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "photo":
|
||||
responseType = 1;
|
||||
break;
|
||||
case "video":
|
||||
switch (host) {
|
||||
case "bilibili":
|
||||
params = { type: "render", time: r.time };
|
||||
params = { type: "render" };
|
||||
break;
|
||||
case "youtube":
|
||||
params = { type: r.type, time: r.time };
|
||||
params = { type: r.type };
|
||||
break;
|
||||
case "reddit":
|
||||
responseType = r.typeId;
|
||||
@@ -56,6 +60,8 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
case "instagram":
|
||||
case "tumblr":
|
||||
case "twitter":
|
||||
case "pinterest":
|
||||
case "streamable":
|
||||
responseType = 1;
|
||||
break;
|
||||
}
|
||||
@@ -69,6 +75,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
|
||||
mute: true
|
||||
}
|
||||
if (host === "reddit" && r.typeId === 1) responseType = 1;
|
||||
break;
|
||||
|
||||
case "picker":
|
||||
@@ -113,9 +120,11 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
processType = "bridge"
|
||||
}
|
||||
}
|
||||
|
||||
if ((audioFormat === "best" && services[host]["bestAudio"])
|
||||
|| services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
|
||||
if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) {
|
||||
audioFormat = "mp3";
|
||||
processType = "bridge"
|
||||
}
|
||||
if ((audioFormat === "best" && services[host]["bestAudio"]) || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) {
|
||||
audioFormat = services[host]["bestAudio"];
|
||||
processType = "bridge"
|
||||
} else if (audioFormat === "best") {
|
||||
@@ -135,8 +144,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
type: processType,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls,
|
||||
audioFormat: audioFormat,
|
||||
copy: copy,
|
||||
fileMetadata: r.fileMetadata ? r.fileMetadata : false
|
||||
copy: copy
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -11,17 +11,16 @@ export default async function(obj) {
|
||||
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
|
||||
if (streamData.data.timelength > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let video = streamData["data"]["dash"]["video"].filter((v) => {
|
||||
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
|
||||
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
|
||||
let video = streamData["data"]["dash"]["video"].filter(v =>
|
||||
!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
|
||||
).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
|
||||
|
||||
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));
|
||||
let audio = streamData["data"]["dash"]["audio"].filter(a =>
|
||||
!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
|
||||
).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`
|
||||
};
|
||||
|
||||
@@ -1,34 +1,100 @@
|
||||
import got from "got";
|
||||
import { createStream } from "../../stream/manage.js";
|
||||
import { genericUserAgent } from "../../config.js";
|
||||
import { getCookie, updateCookie } from '../cookie/manager.js';
|
||||
|
||||
export default async function(obj) {
|
||||
// i hate this implementation but fetch doesn't work here for some reason (i personally blame facebook)
|
||||
let html;
|
||||
let data;
|
||||
try {
|
||||
html = await got.get(`https://www.instagram.com/p/${obj.id}/`)
|
||||
html.on('error', () => {
|
||||
html = false;
|
||||
});
|
||||
html = html ? html.body : false;
|
||||
const url = new URL('https://www.instagram.com/graphql/query/');
|
||||
url.searchParams.set('query_hash', 'b3055c01b4b222b8a47dc12b090e4e64')
|
||||
url.searchParams.set('variables', JSON.stringify({
|
||||
child_comment_count: 3,
|
||||
fetch_comment_count: 40,
|
||||
has_threaded_comments: true,
|
||||
parent_comment_count: 24,
|
||||
shortcode: obj.id
|
||||
}))
|
||||
|
||||
const cookie = getCookie('instagram');
|
||||
|
||||
data = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||
'User-Agent': genericUserAgent,
|
||||
'X-Ig-App-Id': '936619743392459',
|
||||
'X-Asbd-Id': '129477',
|
||||
'x-ig-www-claim': cookie?._wwwClaim || '0',
|
||||
'x-csrftoken': cookie?.values()?.csrftoken,
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'accept-encoding': 'gzip, deflate, br',
|
||||
'accept-language': 'en-US,en;q=0.9,en;q=0.8',
|
||||
cookie
|
||||
}
|
||||
})
|
||||
|
||||
if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) {
|
||||
cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim');
|
||||
}
|
||||
|
||||
updateCookie(cookie, data.headers);
|
||||
data = (await data.json()).data;
|
||||
} catch (e) {
|
||||
html = false;
|
||||
data = false;
|
||||
}
|
||||
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes('application/ld+json')) return { error: 'ErrorEmptyDownload' };
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let single, multiple = [], postInfo = JSON.parse(html.split('script type="application/ld+json"')[1].split('">')[1].split('</script>')[0]);
|
||||
|
||||
if (postInfo.video.length > 1) {
|
||||
for (let i in postInfo.video) { multiple.push({type: "video", thumb: postInfo.video[i]["thumbnailUrl"], url: postInfo.video[i]["contentUrl"]}) }
|
||||
} else if (postInfo.video.length === 1) {
|
||||
single = postInfo.video[0]["contentUrl"]
|
||||
let single, multiple = [];
|
||||
const sidecar = data?.shortcode_media?.edge_sidecar_to_children;
|
||||
if (sidecar) {
|
||||
sidecar.edges.forEach(e => {
|
||||
if (e.node?.is_video) {
|
||||
multiple.push({
|
||||
type: "video",
|
||||
// thumbnails have `Cross-Origin-Resource-Policy` set to `same-origin`, so we need to proxy them
|
||||
thumb: createStream({
|
||||
service: "instagram",
|
||||
type: "default",
|
||||
u: e.node?.display_url,
|
||||
filename: "image.jpg"
|
||||
}),
|
||||
url: e.node?.video_url
|
||||
})
|
||||
} else {
|
||||
multiple.push({
|
||||
type: "photo",
|
||||
thumb: createStream({
|
||||
service: "instagram",
|
||||
type: "default",
|
||||
u: e.node?.display_url,
|
||||
filename: "image.jpg"
|
||||
}),
|
||||
url: e.node?.display_url
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (data?.shortcode_media?.video_url) {
|
||||
single = data.shortcode_media.video_url
|
||||
} else if (data?.shortcode_media?.display_url) {
|
||||
return {
|
||||
urls: data?.shortcode_media?.display_url,
|
||||
isPhoto: true
|
||||
}
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
||||
if (single) {
|
||||
return { urls: single, filename: `instagram_${obj.id}.mp4`, audioFilename: `instagram_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return {
|
||||
urls: single,
|
||||
filename: `instagram_${obj.id}.mp4`,
|
||||
audioFilename: `instagram_${obj.id}_audio`
|
||||
}
|
||||
} else if (multiple.length) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
|
||||
24
src/modules/processing/services/pinterest.js
Normal file
24
src/modules/processing/services/pinterest.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
const pinId = obj.id.split('--').reverse()[0];
|
||||
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' };
|
||||
let data = await fetch(`https://www.pinterest.com/resource/PinResource/get?data=${encodeURIComponent(JSON.stringify({
|
||||
options: {
|
||||
field_set_key: "unauth_react_main_pin",
|
||||
id: pinId
|
||||
}
|
||||
}))}`).then((r) => { return r.json() }).catch(() => { return false });
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
data = data["resource_response"]["data"];
|
||||
|
||||
let video = null;
|
||||
|
||||
if (data.videos !== null) video = data.videos.video_list.V_720P;
|
||||
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7;
|
||||
|
||||
if (!video) return { error: 'ErrorEmptyDownload' };
|
||||
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
return { urls: video.url, filename: `pinterest_${pinId}.mp4`, audioFilename: `pinterest_${pinId}_audio` }
|
||||
}
|
||||
@@ -11,17 +11,25 @@ export default async function(obj) {
|
||||
if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' };
|
||||
if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
|
||||
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
|
||||
await fetch(audio, { method: "HEAD" }).then((r) => {if (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''});
|
||||
let audio = false,
|
||||
video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
|
||||
audioFileLink = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
|
||||
|
||||
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3];
|
||||
if (!audio.length > 0) return { typeId: 1, urls: video };
|
||||
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
|
||||
|
||||
// fallback for videos with differentiating audio quality
|
||||
if (!audio) {
|
||||
audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4`
|
||||
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
|
||||
}
|
||||
|
||||
let id = video.split('/')[3];
|
||||
|
||||
if (!audio) return { typeId: 1, urls: video };
|
||||
return {
|
||||
typeId: 2,
|
||||
type: "render",
|
||||
urls: [video, audio],
|
||||
urls: [video, audioFileLink],
|
||||
audioFilename: `reddit_${id}_audio`,
|
||||
filename: `reddit_${id}.mp4`
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
import { cleanString } from "../../sub/utils.js";
|
||||
|
||||
let cachedID = {};
|
||||
|
||||
@@ -34,28 +35,33 @@ async function findClientID() {
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
let html;
|
||||
if (!obj.author && !obj.song && obj.shortLink) {
|
||||
html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (obj.author && obj.song) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!(html.includes('<script>window.__sc_hydration = ')
|
||||
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
|
||||
&& html.includes('{"hydratable":"sound","data":'))) {
|
||||
return { error: ['ErrorBrokenLink', 'soundcloud'] }
|
||||
}
|
||||
|
||||
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
|
||||
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let clientId = await findClientID();
|
||||
if (!clientId) return { error: 'ErrorSoundCloudNoClientId' };
|
||||
|
||||
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive"),
|
||||
let link;
|
||||
if (obj.shortLink && !obj.author && !obj.song) {
|
||||
link = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`, { redirect: "manual" }).then((r) => {
|
||||
if (r.status === 302 && r.headers.get("location").startsWith("https://soundcloud.com/")) {
|
||||
return r.headers.get("location").split('?', 1)[0]
|
||||
}
|
||||
return false
|
||||
}).catch(() => { return false });
|
||||
}
|
||||
if (!link && obj.author && obj.song) {
|
||||
link = `https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`
|
||||
}
|
||||
if (!link) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => {
|
||||
return r.status === 200 ? r.json() : false
|
||||
}).catch(() => { return false });
|
||||
if (!json) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let fileUrlBase = json.media.transcodings.filter(v => v.preset === "opus_0_0")[0]["url"],
|
||||
fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
|
||||
|
||||
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
if (json.duration > maxVideoDuration) return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };
|
||||
@@ -67,8 +73,8 @@ export default async function(obj) {
|
||||
urls: file,
|
||||
audioFilename: `soundcloud_${json.id}`,
|
||||
fileMetadata: {
|
||||
title: json.title,
|
||||
artist: json.user.username,
|
||||
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
|
||||
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/modules/processing/services/streamable.js
Normal file
19
src/modules/processing/services/streamable.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default async function(obj) {
|
||||
let video = await fetch(`https://api.streamable.com/videos/${obj.id}`).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (!video) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let best = video.files['mp4-mobile'];
|
||||
if (video.files.mp4 && (obj.isAudioOnly || obj.quality === "max" || obj.quality >= Number("720"))) {
|
||||
best = video.files.mp4;
|
||||
}
|
||||
|
||||
if (best) return {
|
||||
urls: best.url,
|
||||
filename: `streamable_${obj.id}_${best.width}x${best.height}.mp4`,
|
||||
audioFilename: `streamable_${obj.id}_audio`,
|
||||
fileMetadata: {
|
||||
title: video.title
|
||||
}
|
||||
}
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
@@ -17,7 +17,7 @@ function selector(j, h, id) {
|
||||
let t;
|
||||
switch (h) {
|
||||
case "tiktok":
|
||||
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true })[0];
|
||||
t = j["aweme_list"].filter(v => v["aweme_id"] === id)[0];
|
||||
break;
|
||||
case "douyin":
|
||||
t = j['aweme_detail'];
|
||||
@@ -92,7 +92,7 @@ export default async function(obj) {
|
||||
let imageLinks = [];
|
||||
for (let i in images) {
|
||||
let sel = obj.host === "tiktok" ? images[i]["display_image"]["url_list"] : images[i]["url_list"];
|
||||
sel = sel.filter((p) => { if (p.includes(".jpeg?")) return true; })
|
||||
sel = sel.filter(p => p.includes(".jpeg?"))
|
||||
imageLinks.push({url: sel[0]})
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,21 @@ export default async function(obj) {
|
||||
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, filename: `tumblr_${obj.id}.mp4`, audioFilename: `tumblr_${obj.id}_audio` }
|
||||
let r;
|
||||
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
|
||||
r = {
|
||||
urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`,
|
||||
filename: `tumblr_${obj.id}.mp4`,
|
||||
audioFilename: `tumblr_${obj.id}_audio`
|
||||
}
|
||||
} else if (html.includes('property="og:audio" content="https://a.tumblr.com/')) {
|
||||
r = {
|
||||
urls: `https://a.tumblr.com/${html.split('property="og:audio" content="https://a.tumblr.com/')[1].split('"')[0]}`,
|
||||
audioFilename: `tumblr_${obj.id}`,
|
||||
isAudioOnly: true
|
||||
}
|
||||
} else r = { error: 'ErrorEmptyDownload' };
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { genericUserAgent } from "../../config.js";
|
||||
|
||||
function bestQuality(arr) {
|
||||
return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
|
||||
return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"]
|
||||
}
|
||||
const apiURL = "https://api.twitter.com"
|
||||
|
||||
export default async function(obj) {
|
||||
let _headers = {
|
||||
"user-agent": genericUserAgent,
|
||||
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
|
||||
// ^ no explicit content, but with multi media support
|
||||
"host": "api.twitter.com",
|
||||
"x-twitter-client-language": "en",
|
||||
"x-twitter-active-user": "yes",
|
||||
"Accept-Language": "en"
|
||||
"accept-language": "en"
|
||||
};
|
||||
let conversationURL = `${apiURL}/2/timeline/conversation/${obj.id}.json?cards_platform=Web-12&tweet_mode=extended&include_cards=1&include_ext_media_availability=true&include_ext_sensitive_media_warning=true&simple_quoted_tweet=true&trim_user=1`;
|
||||
let activateURL = `${apiURL}/1.1/guest/activate.json`;
|
||||
|
||||
let activateURL = `https://api.twitter.com/1.1/guest/activate.json`;
|
||||
let graphqlTweetURL = `https://twitter.com/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId`;
|
||||
let graphqlSpaceURL = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById`;
|
||||
|
||||
let req_act = await fetch(activateURL, {
|
||||
method: "POST",
|
||||
@@ -24,40 +24,39 @@ export default async function(obj) {
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["host"] = "twitter.com";
|
||||
_headers["content-type"] = "application/json";
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]}`;
|
||||
|
||||
if (!obj.spaceId) {
|
||||
let conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) {
|
||||
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
|
||||
// ^ explicit content, but no multi media support
|
||||
delete _headers["x-guest-token"];
|
||||
delete _headers["cookie"];
|
||||
|
||||
req_act = await fetch(activateURL, {
|
||||
method: "POST",
|
||||
headers: _headers
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false });
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
_headers['cookie'] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
|
||||
conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (obj.id) {
|
||||
let query = {
|
||||
variables: {"tweetId": obj.id, "withCommunity": false, "includePromotedContent": false, "withVoice": false},
|
||||
features: {"creator_subscriptions_tweet_preview_api_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_media_download_video_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false}
|
||||
}
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) return { error: 'ErrorTweetUnavailable' };
|
||||
query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1);
|
||||
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
|
||||
query = `${graphqlTweetURL}?variables=${query.variables}&features=${query.features}`;
|
||||
|
||||
let baseMedia, baseTweet = conversation.globalObjects.tweets[obj.id];
|
||||
if (baseTweet.retweeted_status_id_str && conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities) {
|
||||
baseMedia = conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities
|
||||
let TweetResultByRestId = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
|
||||
|
||||
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
|
||||
if (!TweetResultByRestId || TweetResultByRestId.data.tweetResult.result.__typename !== "Tweet") return { error: 'ErrorTweetUnavailable' };
|
||||
|
||||
let baseMedia,
|
||||
baseTweet = TweetResultByRestId.data.tweetResult.result.legacy;
|
||||
|
||||
if (baseTweet.retweeted_status_result && baseTweet.retweeted_status_result.result.legacy.extended_entities.media) {
|
||||
baseMedia = baseTweet.retweeted_status_result.result.legacy.extended_entities
|
||||
} else if (baseTweet.extended_entities && baseTweet.extended_entities.media) {
|
||||
baseMedia = baseTweet.extended_entities
|
||||
}
|
||||
if (!baseMedia) return { error: 'ErrorNoVideosInTweet' };
|
||||
|
||||
let single, multiple = [], media = baseMedia["media"];
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true });
|
||||
|
||||
if (media.length > 1) {
|
||||
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
|
||||
} else if (media.length === 1) {
|
||||
@@ -73,7 +72,9 @@ export default async function(obj) {
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
} else {
|
||||
}
|
||||
// spaces no longer work with guest authorization
|
||||
if (obj.spaceId) {
|
||||
_headers["host"] = "twitter.com";
|
||||
_headers["content-type"] = "application/json";
|
||||
|
||||
@@ -83,7 +84,7 @@ export default async function(obj) {
|
||||
}
|
||||
query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1);
|
||||
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
|
||||
query = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById?variables=${query.variables}&features=${query.features}`;
|
||||
query = `${graphqlSpaceURL}?variables=${query.variables}&features=${query.features}`;
|
||||
|
||||
let AudioSpaceById = await fetch(query, { headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false });
|
||||
if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { maxVideoDuration } from "../../config.js";
|
||||
|
||||
// vimeo you're fucked in the head for this
|
||||
const resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"2732": "1440",
|
||||
@@ -11,7 +12,6 @@ const resolutionMatch = {
|
||||
"640": "360",
|
||||
"426": "240"
|
||||
}
|
||||
// ^ vimeo you're fucked in the head for this ^
|
||||
|
||||
const qualityMatch = {
|
||||
"2160": "4K",
|
||||
@@ -64,7 +64,7 @@ export default async function(obj) {
|
||||
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
|
||||
switch (type) {
|
||||
case "parcel":
|
||||
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a) => { if (a['mime_type'] === "audio/mp4") return true }),
|
||||
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter(a => a['mime_type'] === "audio/mp4"),
|
||||
bestAudio = masterJSON_Audio[0];
|
||||
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
|
||||
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
|
||||
|
||||
@@ -1,59 +1,38 @@
|
||||
import { xml2json } from "xml-js";
|
||||
import { genericUserAgent, maxVideoDuration } from "../../config.js";
|
||||
|
||||
const representationMatch = {
|
||||
"2160": 7,
|
||||
"1440": 6,
|
||||
"1080": 5,
|
||||
"720": 4,
|
||||
"480": 3,
|
||||
"360": 2,
|
||||
"240": 1,
|
||||
"144": 0
|
||||
}, resolutionMatch = {
|
||||
"3840": "2160",
|
||||
"2560": "1440",
|
||||
"1920": "1080",
|
||||
"1280": "720",
|
||||
"852": "480",
|
||||
"640": "360",
|
||||
"426": "240",
|
||||
// "256": "144"
|
||||
}
|
||||
const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"];
|
||||
|
||||
export default async function(o) {
|
||||
let html, url, filename = `vk_${o.userId}_${o.videoId}_`;
|
||||
let html, url,
|
||||
quality = o.quality === "max" ? 2160 : o.quality,
|
||||
filename = `vk_${o.userId}_${o.videoId}_`;
|
||||
|
||||
html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, {
|
||||
headers: { "user-agent": genericUserAgent }
|
||||
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let quality = o.quality === "max" ? 7 : representationMatch[o.quality],
|
||||
js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
|
||||
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
|
||||
|
||||
if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' };
|
||||
if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
if (js.player.params[0]["manifest"]) {
|
||||
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })),
|
||||
repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"],
|
||||
bestQuality = repr[repr.length - 1],
|
||||
resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height';
|
||||
|
||||
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
|
||||
|
||||
url = js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`];
|
||||
filename += `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4`
|
||||
|
||||
} else if (js.player.params[0]["url240"]) { // fallback for when video is too old
|
||||
url = js.player.params[0]["url240"];
|
||||
filename += `320x240.mp4`
|
||||
for (let i in resolutions) {
|
||||
if (js.player.params[0][`url${resolutions[i]}`]) {
|
||||
quality = resolutions[i];
|
||||
break
|
||||
}
|
||||
}
|
||||
if (Number(quality) > Number(o.quality)) quality = o.quality;
|
||||
|
||||
url = js.player.params[0][`url${quality}`];
|
||||
filename += `${quality}p.mp4`
|
||||
|
||||
if (url && filename) return {
|
||||
urls: url,
|
||||
filename: filename
|
||||
};
|
||||
}
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import { maxVideoDuration } from '../../config.js';
|
||||
import { cleanString } from '../../sub/utils.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
|
||||
@@ -23,6 +24,10 @@ const c = {
|
||||
|
||||
export default async function(o) {
|
||||
let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max
|
||||
function qual(i) {
|
||||
return i['quality_label'].split('p')[0].split('s')[0]
|
||||
}
|
||||
|
||||
try {
|
||||
info = await yt.getBasicInfo(o.id, 'ANDROID');
|
||||
} catch (e) {
|
||||
@@ -30,22 +35,23 @@ export default async function(o) {
|
||||
}
|
||||
|
||||
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
|
||||
|
||||
if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' };
|
||||
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
|
||||
|
||||
let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter((e) => {
|
||||
if (e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)) return true
|
||||
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter(e =>
|
||||
e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)
|
||||
).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
|
||||
bestQuality = adaptive_formats.find(i => i["has_video"]);
|
||||
hasAudio = adaptive_formats.find(i => i["has_audio"]);
|
||||
|
||||
if (bestQuality) bestQuality = bestQuality['quality_label'].split('p')[0];
|
||||
if (bestQuality) bestQuality = qual(bestQuality);
|
||||
if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' };
|
||||
if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
|
||||
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]),
|
||||
audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
|
||||
audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]);
|
||||
|
||||
if (o.dubLang) {
|
||||
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
|
||||
@@ -54,35 +60,38 @@ export default async function(o) {
|
||||
isDubbed = true
|
||||
}
|
||||
}
|
||||
if (hasAudio && o.isAudioOnly) {
|
||||
let r = {
|
||||
type: "render",
|
||||
isAudioOnly: true,
|
||||
urls: audio.url,
|
||||
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
|
||||
fileMetadata: {
|
||||
title: info.basic_info.title,
|
||||
artist: info.basic_info.author.replace("- Topic", "").trim(),
|
||||
}
|
||||
};
|
||||
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
|
||||
let descItems = info.basic_info.short_description.split("\n\n")
|
||||
r.fileMetadata.album = descItems[2]
|
||||
r.fileMetadata.copyright = descItems[3]
|
||||
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
||||
};
|
||||
return r
|
||||
|
||||
let fileMetadata = {
|
||||
title: cleanString(info.basic_info.title.replace(/\p{Emoji}/gu, '').trim()),
|
||||
artist: cleanString(info.basic_info.author.replace("- Topic", "").replace(/\p{Emoji}/gu, '').trim()),
|
||||
}
|
||||
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
|
||||
checkBestVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === bestQuality),
|
||||
checkRightVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === quality);
|
||||
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
|
||||
let descItems = info.basic_info.short_description.split("\n\n");
|
||||
fileMetadata.album = descItems[2];
|
||||
fileMetadata.copyright = descItems[3];
|
||||
if (descItems[4].startsWith("Released on:")) {
|
||||
fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
|
||||
}
|
||||
};
|
||||
|
||||
if (hasAudio && o.isAudioOnly) return {
|
||||
type: "render",
|
||||
isAudioOnly: true,
|
||||
urls: audio.url,
|
||||
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
|
||||
fileMetadata: fileMetadata
|
||||
}
|
||||
let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
|
||||
checkBestVideo = (i) => (i["has_video"] && !i["has_audio"] && qual(i) === bestQuality),
|
||||
checkRightVideo = (i) => (i["has_video"] && !i["has_audio"] && qual(i) === quality);
|
||||
|
||||
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
|
||||
let single = info.streaming_data.formats.find(i => checkSingle(i));
|
||||
if (single) return {
|
||||
type: "bridge",
|
||||
urls: single.url,
|
||||
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`
|
||||
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`,
|
||||
fileMetadata: fileMetadata
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +99,8 @@ export default async function(o) {
|
||||
if (video && audio) return {
|
||||
type: "render",
|
||||
urls: [video.url, audio.url],
|
||||
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`
|
||||
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`,
|
||||
fileMetadata: fileMetadata
|
||||
};
|
||||
|
||||
return { error: 'ErrorYTTryOtherCodec' }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"audioIgnore": ["vk"],
|
||||
"config": {
|
||||
"bilibili": {
|
||||
"alias": "bilibili (.com only)",
|
||||
"alias": "bilibili.com videos",
|
||||
"patterns": ["video/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"twitter": {
|
||||
"alias": "twitter posts & spaces & voice",
|
||||
"alias": "twitter videos & voice",
|
||||
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
|
||||
"enabled": true
|
||||
},
|
||||
@@ -22,8 +22,8 @@
|
||||
"enabled": true
|
||||
},
|
||||
"youtube": {
|
||||
"alias": "youtube videos & shorts & music",
|
||||
"patterns": ["watch?v=:id"],
|
||||
"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
|
||||
@@ -43,7 +43,7 @@
|
||||
"enabled": false
|
||||
},
|
||||
"vimeo": {
|
||||
"patterns": [":id"],
|
||||
"patterns": [":id", "video/:id"],
|
||||
"enabled": true,
|
||||
"bestAudio": "mp3"
|
||||
},
|
||||
@@ -53,7 +53,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"instagram": {
|
||||
"alias": "instagram reels & video posts",
|
||||
"alias": "instagram reels & posts",
|
||||
"patterns": ["reels/:id", "reel/:id", "p/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
@@ -63,7 +63,17 @@
|
||||
"patterns": ["v/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"pinterest": {
|
||||
"alias": "pinterest videos & stories",
|
||||
"patterns": ["pin/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"streamable": {
|
||||
"alias": "streamable.com",
|
||||
"patterns": [":id", "o/:id", "e/:id", "s/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"twitch": {
|
||||
"alias": "twitch vods & videos & clips",
|
||||
"tld": "tv",
|
||||
"patterns": ["videos/:video", ":channel/clip/:clip"],
|
||||
|
||||
@@ -23,12 +23,16 @@ export const testers = {
|
||||
|
||||
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
|
||||
|
||||
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"]
|
||||
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32)),
|
||||
"soundcloud": (patternMatch) => (patternMatch["author"]?.length <= 25 && patternMatch["song"]?.length <= 255)
|
||||
|| (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32),
|
||||
|
||||
"instagram": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128),
|
||||
|
||||
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6),
|
||||
|
||||
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100 || patternMatch["video"] && patternMatch["video"].length <= 10)),
|
||||
}
|
||||
|
||||
@@ -3,50 +3,105 @@ import { createInterface } from "readline";
|
||||
import { Cyan, Bright } from "./sub/consoleText.js";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import { version } from "../modules/config.js";
|
||||
|
||||
let envPath = './.env';
|
||||
let q = `${Cyan('?')} \x1b[1m`;
|
||||
let ob = {}
|
||||
let ob = {};
|
||||
let rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let final = () => {
|
||||
if (existsSync(envPath)) {
|
||||
unlinkSync(envPath)
|
||||
}
|
||||
if (existsSync(envPath)) unlinkSync(envPath);
|
||||
|
||||
for (let i in ob) {
|
||||
appendFileSync(envPath, `${i}=${ob[i]}\n`)
|
||||
}
|
||||
console.log(Bright("\nAwesome! I've created a fresh .env file for you."))
|
||||
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("\nAwesome! I've created a fresh .env file for you."));
|
||||
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] });
|
||||
console.log(`\n\n${Cyan("All done!\n")}`)
|
||||
console.log(Bright("You can re-run this script at any time to update the configuration."))
|
||||
console.log(Bright("\nYou're now ready to start cobalt. Simply run ") + Cyan("npm start") + Bright('!\nHave fun :)'))
|
||||
console.log(`\n\n${Cyan("All done!\n")}`);
|
||||
console.log(Bright("You can re-run this script at any time to update the configuration."));
|
||||
console.log(Bright("\nYou're now ready to start cobalt. Simply run ") + Cyan("npm start") + Bright('!\nHave fun :)'));
|
||||
rl.close()
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${Cyan("Welcome to cobalt!")}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
|
||||
`${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
|
||||
)
|
||||
|
||||
console.log(
|
||||
Bright("\nWhat's the domain this instance will be running on? (localhost)\nExample: co.wukko.me")
|
||||
`\n${Bright("⚠️ Please notice that since v.6.0 cobalt is hosted in two parts. API and web app are now separate.\nMerged hosting is no longer available.")}`
|
||||
)
|
||||
function setup() {
|
||||
console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web."));
|
||||
|
||||
rl.question(q, r1 => {
|
||||
ob['selfURL'] = `http://localhost:9000/`
|
||||
ob['port'] = 9000
|
||||
if (r1) ob['selfURL'] = `https://${r1}/`
|
||||
rl.question(q, r1 => {
|
||||
switch (r1.toLowerCase()) {
|
||||
case 'api':
|
||||
console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: co.wuk.sh"));
|
||||
|
||||
console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
|
||||
rl.question(q, apiURL => {
|
||||
ob['apiURL'] = `http://localhost:9000/`;
|
||||
ob['apiPort'] = 9000;
|
||||
if (apiURL && apiURL !== "localhost") ob['apiURL'] = `https://${apiURL.toLowerCase()}/`;
|
||||
|
||||
rl.question(q, r2 => {
|
||||
if (r2) ob['port'] = r2
|
||||
if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
|
||||
console.log(Bright("\nGreat! Now, what port will it be running on? (9000)"));
|
||||
|
||||
console.log(Bright("\nWould you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)"))
|
||||
rl.question(q, apiPort => {
|
||||
if (apiPort) ob['apiPort'] = apiPort;
|
||||
if (apiPort && (apiURL === "localhost" || !apiURL)) ob['apiURL'] = `http://localhost:${apiPort}/`;
|
||||
|
||||
rl.question(q, r3 => {
|
||||
if (r3.toLowerCase() !== 'y') ob['cors'] = '0'
|
||||
final()
|
||||
})
|
||||
});
|
||||
})
|
||||
console.log(Bright("\nWhat will your instance's name be? Usually it's something like eu-nl aka region-country. (local)"));
|
||||
|
||||
rl.question(q, apiName => {
|
||||
ob['apiName'] = apiName.toLowerCase();
|
||||
if (!apiName || apiName === "local") ob['apiName'] = "local";
|
||||
|
||||
console.log(Bright("\nOne last thing: would you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)"));
|
||||
|
||||
rl.question(q, apiCors => {
|
||||
let answCors = apiCors.toLowerCase().trim();
|
||||
if (answCors !== "y" && answCors !== "yes") ob['cors'] = '0'
|
||||
final()
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
})
|
||||
break;
|
||||
case 'web':
|
||||
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/`;
|
||||
ob['webPort'] = 9001;
|
||||
if (webURL && webURL !== "localhost") ob['webURL'] = `https://${webURL.toLowerCase()}/`;
|
||||
|
||||
console.log(
|
||||
Bright("\nGreat! Now, what port will it be running on? (9001)")
|
||||
)
|
||||
rl.question(q, webPort => {
|
||||
if (webPort) ob['webPort'] = webPort;
|
||||
if (webPort && (webURL === "localhost" || !webURL)) ob['webURL'] = `http://localhost:${webPort}/`;
|
||||
|
||||
console.log(
|
||||
Bright("\nOne last thing: what default API domain should be used? (co.wuk.sh)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000")
|
||||
);
|
||||
|
||||
rl.question(q, apiURL => {
|
||||
ob['apiURL'] = `https://${apiURL.toLowerCase()}/`;
|
||||
if (apiURL.includes(':')) ob['apiURL'] = `http://${apiURL.toLowerCase()}/`;
|
||||
if (!apiURL) ob['apiURL'] = "https://co.wuk.sh/";
|
||||
final()
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log(Bright("\nThis is not an option. Try again."));
|
||||
setup()
|
||||
}
|
||||
})
|
||||
}
|
||||
setup()
|
||||
|
||||
@@ -15,7 +15,7 @@ streamCache.on("expired", (key) => {
|
||||
export function createStream(obj) {
|
||||
let streamID = nanoid(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, streamSalt);
|
||||
ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt);
|
||||
|
||||
if (!streamCache.has(streamID)) {
|
||||
streamCache.set(streamID, {
|
||||
@@ -25,7 +25,6 @@ export function createStream(obj) {
|
||||
urls: obj.u,
|
||||
filename: obj.filename,
|
||||
hmac: ghmac,
|
||||
ip: obj.ip,
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
@@ -39,22 +38,20 @@ export function createStream(obj) {
|
||||
exp = streamInfo.exp;
|
||||
ghmac = streamInfo.hmac;
|
||||
}
|
||||
return `${process.env.selfURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
|
||||
return `${process.env.apiURL || process.env.selfURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
|
||||
}
|
||||
|
||||
export function verifyStream(ip, id, hmac, exp) {
|
||||
export function verifyStream(id, hmac, exp) {
|
||||
try {
|
||||
if (id.length === 21) {
|
||||
let streamInfo = streamCache.get(id);
|
||||
if (!streamInfo) return { error: 'this stream token does not exist', status: 400 };
|
||||
|
||||
let ghmac = sha256(`${id},${ip},${streamInfo.service},${exp}`, streamSalt);
|
||||
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
|
||||
&& String(ip) === streamInfo.ip && Number(exp) > Math.floor(new Date().getTime())) {
|
||||
return streamInfo;
|
||||
}
|
||||
let streamInfo = streamCache.get(id.toString());
|
||||
if (!streamInfo) return { error: "this download link has expired or doesn't exist. go back and try again!", status: 400 };
|
||||
|
||||
let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt);
|
||||
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
|
||||
&& Number(exp) > Math.floor(new Date().getTime())) {
|
||||
return streamInfo;
|
||||
}
|
||||
return { error: 'Unauthorized', status: 401 };
|
||||
return { error: "i couldn't verify if you have access to this download. go back and try again!", status: 401 };
|
||||
} catch (e) {
|
||||
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
|
||||
}
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import { verifyStream } from "./manage.js";
|
||||
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js";
|
||||
|
||||
export default function(res, ip, id, hmac, exp) {
|
||||
export default async function(res, streamInfo) {
|
||||
try {
|
||||
let streamInfo = verifyStream(ip, id, hmac, exp);
|
||||
if (streamInfo.error) {
|
||||
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
|
||||
return;
|
||||
}
|
||||
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
|
||||
streamAudioOnly(streamInfo, res);
|
||||
return;
|
||||
}
|
||||
switch (streamInfo.type) {
|
||||
case "render":
|
||||
streamLiveRender(streamInfo, res);
|
||||
await streamLiveRender(streamInfo, res);
|
||||
break;
|
||||
case "videoM3U8":
|
||||
case "mute":
|
||||
streamVideoOnly(streamInfo, res);
|
||||
break;
|
||||
default:
|
||||
streamDefault(streamInfo, res);
|
||||
await streamDefault(streamInfo, res);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import { spawn } from "child_process";
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
import got from "got";
|
||||
import { ffmpegArgs, genericUserAgent } from "../config.js";
|
||||
import { getThreads, metadataManager, msToTime } from "../sub/utils.js";
|
||||
import { getThreads, metadataManager } from "../sub/utils.js";
|
||||
import { request } from 'undici';
|
||||
|
||||
export function streamDefault(streamInfo, res) {
|
||||
function fail(res) {
|
||||
if (!res.headersSent) res.sendStatus(500);
|
||||
return res.destroy();
|
||||
}
|
||||
|
||||
export async function streamDefault(streamInfo, res) {
|
||||
try {
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
|
||||
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`;
|
||||
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
|
||||
const stream = got.get(streamInfo.urls, {
|
||||
headers: {
|
||||
"user-agent": genericUserAgent
|
||||
},
|
||||
isStream: true
|
||||
});
|
||||
stream.pipe(res).on('error', () => {
|
||||
res.destroy();
|
||||
});
|
||||
stream.on('error', () => {
|
||||
res.destroy();
|
||||
});
|
||||
stream.on('aborted', () => {
|
||||
res.destroy();
|
||||
|
||||
const { body: stream, headers } = await request(streamInfo.urls, {
|
||||
headers: { 'user-agent': genericUserAgent },
|
||||
maxRedirections: 16
|
||||
});
|
||||
|
||||
res.setHeader('content-type', headers['content-type']);
|
||||
res.setHeader('content-length', headers['content-length']);
|
||||
|
||||
stream.pipe(res).on('error', () => fail(res));
|
||||
stream.on('error', () => fail(res));
|
||||
stream.on('aborted', () => fail(res));
|
||||
} catch (e) {
|
||||
res.destroy();
|
||||
fail(res);
|
||||
}
|
||||
}
|
||||
export function streamLiveRender(streamInfo, res) {
|
||||
export async function streamLiveRender(streamInfo, res) {
|
||||
try {
|
||||
if (streamInfo.urls.length !== 2) {
|
||||
res.destroy();
|
||||
return;
|
||||
}
|
||||
let audio = got.get(streamInfo.urls[1], { isStream: true });
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
|
||||
if (streamInfo.urls.length !== 2) return fail(res);
|
||||
|
||||
let { body: audio } = await request(streamInfo.urls[1], {
|
||||
maxRedirections: 16
|
||||
});
|
||||
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1],
|
||||
args = [
|
||||
'-loglevel', '-8',
|
||||
'-threads', `${getThreads()}`,
|
||||
'-i', streamInfo.urls[0],
|
||||
@@ -43,8 +47,9 @@ export function streamLiveRender(streamInfo, res) {
|
||||
'-map', '0:v',
|
||||
'-map', '1:a',
|
||||
];
|
||||
args = args.concat(ffmpegArgs[format])
|
||||
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
|
||||
|
||||
args = args.concat(ffmpegArgs[format]);
|
||||
if (streamInfo.metadata) args = args.concat(metadataManager(streamInfo.metadata));
|
||||
args.push('-f', format, 'pipe:4');
|
||||
let ffmpegProcess = spawn(ffmpeg, args, {
|
||||
windowsHide: true,
|
||||
@@ -57,24 +62,24 @@ export function streamLiveRender(streamInfo, res) {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
|
||||
res.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
ffmpegProcess.stdio[4].pipe(res).on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
audio.pipe(ffmpegProcess.stdio[3]).on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
|
||||
audio.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
audio.on('aborted', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
|
||||
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
|
||||
@@ -84,11 +89,11 @@ export function streamLiveRender(streamInfo, res) {
|
||||
res.on('close', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
res.destroy();
|
||||
fail(res);
|
||||
}
|
||||
}
|
||||
export function streamAudioOnly(streamInfo, res) {
|
||||
@@ -132,10 +137,10 @@ export function streamAudioOnly(streamInfo, res) {
|
||||
res.on('close', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
} catch (e) {
|
||||
res.destroy();
|
||||
fail(res);
|
||||
}
|
||||
}
|
||||
export function streamVideoOnly(streamInfo, res) {
|
||||
@@ -168,9 +173,9 @@ export function streamVideoOnly(streamInfo, res) {
|
||||
res.on('close', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('error', () => {
|
||||
ffmpegProcess.kill();
|
||||
res.destroy();
|
||||
fail(res);
|
||||
});
|
||||
} catch (e) {
|
||||
res.destroy();
|
||||
fail(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { createStream } from "../stream/manage.js";
|
||||
|
||||
let apiVar = {
|
||||
const apiVar = {
|
||||
allowed: {
|
||||
vCodec: ["h264", "av1", "vp9"],
|
||||
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 = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||
|
||||
export function apiJSON(type, obj) {
|
||||
try {
|
||||
@@ -27,7 +29,7 @@ export function apiJSON(type, obj) {
|
||||
switch (obj.service) {
|
||||
case "douyin":
|
||||
case "tiktok":
|
||||
audio = createStream(obj)
|
||||
audio = obj.u
|
||||
pickerType = "images"
|
||||
break;
|
||||
}
|
||||
@@ -47,22 +49,7 @@ export function metadataManager(obj) {
|
||||
for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) }
|
||||
return commands;
|
||||
}
|
||||
export function msToTime(d) {
|
||||
let milliseconds = parseInt((d % 1000) / 100, 10),
|
||||
seconds = parseInt((d / 1000) % 60, 10),
|
||||
minutes = parseInt((d / (1000 * 60)) % 60, 10),
|
||||
hours = parseInt((d / (1000 * 60 * 60)) % 24, 10),
|
||||
r;
|
||||
|
||||
hours = (hours < 10) ? `0${hours}` : hours;
|
||||
minutes = (minutes < 10) ? `0${minutes}` : minutes;
|
||||
seconds = (seconds < 10) ? `0${seconds}` : seconds;
|
||||
r = `${hours}:${minutes}:${seconds}`;
|
||||
if (milliseconds) r += `.${milliseconds}`;
|
||||
return r;
|
||||
}
|
||||
export function cleanURL(url, host) {
|
||||
let forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"]
|
||||
switch(host) {
|
||||
case "vk":
|
||||
url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0];
|
||||
@@ -72,6 +59,8 @@ export function cleanURL(url, host) {
|
||||
break;
|
||||
case "tiktok":
|
||||
url = url.replace(/@([a-zA-Z]+(\.[a-zA-Z]+)+)/, "@a")
|
||||
case "pinterest":
|
||||
url = url.replace(/:\/\/(?:www.)pinterest(?:\.[a-z.]+)/, "://pinterest.com")
|
||||
default:
|
||||
url = url.split('?')[0];
|
||||
if (url.substring(url.length - 1) === "/") url = url.substring(0, url.length - 1);
|
||||
@@ -89,6 +78,12 @@ export function cleanURL(url, host) {
|
||||
}
|
||||
return url.slice(0, 128)
|
||||
}
|
||||
export function cleanString(string) {
|
||||
for (let i in forbiddenCharsString) {
|
||||
string = string.replaceAll(forbiddenCharsString[i], '')
|
||||
}
|
||||
return string;
|
||||
}
|
||||
export function verifyLanguageCode(code) {
|
||||
return RegExp(/[a-z]{2}/).test(String(code.slice(0, 2).toLowerCase())) ? String(code.slice(0, 2).toLowerCase()) : "en"
|
||||
}
|
||||
@@ -109,13 +104,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])) {
|
||||
@@ -154,3 +150,8 @@ export function getThreads() {
|
||||
return '0'
|
||||
}
|
||||
}
|
||||
export function cleanHTML(html) {
|
||||
let clean = html.replace(/ {4}/g, '');
|
||||
clean = clean.replace(/\n/g, '');
|
||||
return clean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user