mitosis (6.0)

Merge pull request #138 from wukko/mitosis
This commit is contained in:
wukko 2023-06-05 12:49:32 +06:00 committed by GitHub
commit ec62570067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 715 additions and 269 deletions

10
.gitignore vendored
View file

@ -5,11 +5,15 @@ package-lock.json
# secrets
.env
# esbuild
min
# page build
min
build
# stuff i already made but delayed
future
# docker
docker-compose.yml
# vscode
.vscode

View file

@ -1,6 +1,6 @@
# cobalt
Best way to save what you love.
Main instance: [co.wukko.me](https://co.wukko.me/)
Live web app: [co.wukko.me](https://co.wukko.me/)
![cobalt logo with repeated logo pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo pattern background")
@ -21,21 +21,22 @@ Paste the link, get the video, move on. It's that simple. Just how it should be.
| Reddit | ✅ | ✅ | ✅ | Support for GIFs and videos. |
| SoundCloud | | ✅ | | Audio metadata, downloads from private links. |
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. |
| Tumblr | ✅ | ✅ | ✅ | |
| Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. |
| Twitter | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. |
| Twitter Spaces | | ✅ | | Audio metadata with all participants and other info. |
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. |
| Vine Archive | ✅ | ✅ | ✅ | |
| VK Videos | ✅ | ❌ | ❌ | |
| VK Clips | ✅ | ❌ | ❌ | |
| YouTube Videos & Shorts | ✅ | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. |
| YouTube Videos & Shorts | ✅ | ✅ | ✅ | Support for 8K, 4K, HDR, VR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. |
| YouTube Music | | ✅ | | Audio metadata. |
This list is not final and keeps expanding over time, make sure to check it once in a while!
## cobalt API
cobalt has an open API that you can use in your projects for **free**.
It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
Feel free to use the main API instance ([co.wuk.sh](https://co.wuk.sh/)) in your projects.
## How to contribute translations
You can translate cobalt to any language you want on [cobalt's Crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to Crowdin.
@ -62,6 +63,8 @@ Setup script installs all needed `npm` dependencies, but you have to install `No
3. Run cobalt via `npm start`
4. Done.
You need to host API and web app separately ever since v.6.0. Setup script will help you with that!
### Ubuntu 22.04+ workaround
`nscd` needs to be installed and running so that the `ffmpeg-static` binary can resolve DNS ([#101](https://github.com/wukko/cobalt/issues/101#issuecomment-1494822258)):
@ -71,13 +74,8 @@ sudo service nscd start
```
### Docker
It's also possible to run cobalt via Docker, but you **need** to set all environment variables yourself:
| Variable | Description | Example |
| -------- | :--- | :--- |
| `selfURL` | Instance URL | `http://localhost:9000/` or `https://co.wukko.me/` or etc |
| `port` | Instance port | `9000` |
| `cors` | CORS toggle | `0` |
It's also possible to run cobalt via Docker. I *highly* recommend using Docker compose.
Check out the [example compose file](https://github.com/wukko/cobalt/blob/current/docker-compose.yml.example) and alter it for your needs.
## Disclaimer
cobalt is my passion project, so update schedule depends solely on my free time, motivation, and mood.

View file

@ -0,0 +1,32 @@
version: '3.5'
services:
cobalt-api:
build: .
restart: unless-stopped
container_name: cobalt-api
ports:
- 9000:9000/tcp
environment:
- apiPort=9000
- apiURL=https://co.wuk.sh/
- apiName=eu-nl
cobalt-web:
build: .
restart: unless-stopped
container_name: cobalt-web
ports:
- 9000:9000/tcp
environment:
- webPort=9000
- webURL=https://co.wukko.me/
- apiURL=https://co.wuk.sh/
cobalt-both:
build: .
restart: unless-stopped
container_name: cobalt-both
ports:
- 9000:9000/tcp
environment:
- port=9000
- selfURL=https://co.wukko.me/

View file

@ -1,5 +1,13 @@
# cobalt API Documentation
This document provides info about methods and acceptable variables for all cobalt API requests.<br>
```
⚠️ Main API instance has moved to https://co.wuk.sh/
Previous API domain will stop redirecting users to correct API instance after July 25th.
Make sure to update your projects in time.
```
## POST: ``/api/json``
Main processing endpoint.<br>

View file

@ -1,7 +1,7 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "5.7",
"version": "6.0-dev",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",
@ -11,7 +11,8 @@
"scripts": {
"start": "node src/cobalt",
"setup": "node src/modules/setup",
"test": "node src/test/test"
"test": "node src/test/test",
"build": "node src/modules/buildStatic"
},
"repository": {
"type": "git",

View file

@ -1,199 +1,37 @@
import "dotenv/config";
import express from "express";
import cors from "cors";
import rateLimit from "express-rate-limit";
import { randomBytes } from "crypto";
const ipSalt = randomBytes(64).toString('hex');
import { Bright, Green, Red } from "./modules/sub/consoleText.js";
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
import { loadLoc } from "./localization/manager.js";
import path from 'path';
import { fileURLToPath } from 'url';
import { runWeb } from "./core/web.js";
import { runAPI } from "./core/api.js";
import { runBoth } from "./core/both.js";
const app = express();
const gitCommit = shortCommit();
const gitBranch = getCurrentBranch();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename).slice(0, -4); // go up another level (get rid of src/)
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
import { appName, genericUserAgent, version } from "./modules/config.js";
import { getJSON } from "./modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "./modules/sub/utils.js";
import { Bright, Cyan, Green, Red } from "./modules/sub/consoleText.js";
import stream from "./modules/stream/stream.js";
import loc from "./localization/manager.js";
import { buildFront } from "./modules/build.js";
import { changelogHistory } from "./modules/pageRender/onDemand.js";
import { sha256 } from "./modules/sub/crypto.js";
import findRendered from "./modules/pageRender/findRendered.js";
import { celebrationsEmoji } from "./modules/pageRender/elements.js";
app.disable('x-powered-by');
if (process.env.selfURL && process.env.port) {
const commitHash = shortCommit();
const branch = getCurrentBranch();
const app = express();
await loadLoc(); // preload localization
app.disable('x-powered-by');
const corsConfig = process.env.cors === '0' ? { origin: process.env.selfURL, optionsSuccessStatus: 200 } : {};
const apiLimiter = rateLimit({
windowMs: 60000,
max: 25,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const apiLimiterStream = rateLimit({
windowMs: 60000,
max: 28,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
await buildFront(commitHash, branch);
app.use('/api/:type', cors(corsConfig));
app.use('/api/json', apiLimiter);
app.use('/api/stream', apiLimiterStream);
app.use('/api/onDemand', apiLimiter);
app.use('/', express.static('./min'));
app.use('/', express.static('./src/front'));
app.use((req, res, next) => {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.use((req, res, next) => {
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) res.destroy();
next();
});
app.use('/api/json', express.json({
verify: (req, res, buf) => {
try {
JSON.parse(buf);
if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' });
return;
}
if (String(req.header('Accept')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' });
return;
}
} catch(e) {
res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' });
return;
}
}
}));
app.post('/api/json', async (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" });
try {
let request = req.body;
if (request.url) {
request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request);
if (chck) chck["ip"] = ip;
j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
} else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') });
}
} catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
}
res.status(j.status).json(j.body);
return;
} catch (e) {
res.destroy();
return
}
});
app.get('/api/:type', (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
switch (req.params.type) {
case 'stream':
if (req.query.p) {
res.status(200).json({ "status": "continue" });
return;
} else if (req.query.t && req.query.h && req.query.e) {
stream(res, ip, req.query.t, req.query.h, req.query.e);
} else {
let j = apiJSON(0, { t: "no stream id" })
res.status(j.status).json(j.body);
return;
}
break;
case 'onDemand':
if (req.query.blockId) {
let blockId = req.query.blockId.slice(0, 3);
let r, j;
switch(blockId) {
case "0": // changelog history
r = changelogHistory();
j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" })
break;
case "1": // celebrations emoji
r = celebrationsEmoji();
j = r ? apiJSON(3, { t: r }) : false
break;
default:
j = apiJSON(0, { t: "couldn't find a block with this id" })
break;
}
if (j.body) {
res.status(j.status).json(j.body)
} else {
res.status(204).end()
}
} else {
let j = apiJSON(0, { t: "no block id" });
res.status(j.status).json(j.body)
}
break;
default:
let j = apiJSON(0, { t: "unknown response type" })
res.status(j.status).json(j.body);
break;
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
return;
}
});
app.get("/api", (req, res) => {
res.redirect('/api/json')
});
app.get("/status", (req, res) => {
res.status(200).end()
});
app.get("/", (req, res) => {
res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`);
});
app.get("/favicon.ico", (req, res) => {
res.redirect('/icons/favicon.ico');
});
app.get("/*", (req, res) => {
res.redirect('/')
});
app.listen(process.env.port, () => {
let startTime = new Date();
console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash} (${branch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`)
})
// i don't like this at all
if (process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port))) {
await runAPI(express, app, gitCommit, gitBranch, __dirname);
} else if (process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port))) {
await runWeb(express, app, gitCommit, gitBranch, __dirname);
} else if (process.env.selfURL && process.env.port && !((process.env.apiURL && process.env.apiPort) || (process.env.webURL && process.env.webPort))) {
await runBoth(express, app, gitCommit, gitBranch, __dirname)
} else {
console.log(Red(`cobalt hasn't been configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`));
}

View file

@ -14,6 +14,10 @@
"mastodon": {
"url": "https://wetdry.world/@cobalt",
"handle": "@cobalt@wetdry.world"
},
"discord": {
"url": "https://discord.gg/pQPt8HBUPu",
"handle": "cobalt community server"
}
}
},

185
src/core/api.js Normal file
View file

@ -0,0 +1,185 @@
import cors from "cors";
import rateLimit from "express-rate-limit";
import { randomBytes } from "crypto";
const ipSalt = randomBytes(64).toString('hex');
import { appName, version } from "../modules/config.js";
import { getJSON } from "../modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js";
import { changelogHistory } from "../modules/pageRender/onDemand.js";
import { sha256 } from "../modules/sub/crypto.js";
import { celebrationsEmoji } from "../modules/pageRender/elements.js";
import { verifyStream } from "../modules/stream/manage.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {};
const apiLimiter = rateLimit({
windowMs: 60000,
max: 25,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const apiLimiterStream = rateLimit({
windowMs: 60000,
max: 28,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const startTime = new Date();
const startTimestamp = Math.floor(startTime.getTime());
app.use('/api/:type', cors(corsConfig));
app.use('/api/json', apiLimiter);
app.use('/api/stream', apiLimiterStream);
app.use('/api/onDemand', apiLimiter);
app.use((req, res, next) => {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.use('/api/json', express.json({
verify: (req, res, buf) => {
try {
JSON.parse(buf);
if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' });
return;
}
if (String(req.header('Accept')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' });
return;
}
} catch(e) {
res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' });
return;
}
}
}));
app.post('/api/json', async (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" });
try {
let request = req.body;
if (request.url) {
request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request);
if (chck) chck["ip"] = ip;
j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
} else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') });
}
} catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
}
res.status(j.status).json(j.body);
return;
} catch (e) {
res.destroy();
return
}
});
app.get('/api/:type', (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
switch (req.params.type) {
case 'stream':
let streamInfo = verifyStream(ip, req.query.t, req.query.h, req.query.e);
if (streamInfo.error) {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
}
if (req.query.p) {
res.status(200).json({ "status": "continue" });
return;
} else if (req.query.t && req.query.h && req.query.e) {
stream(res, ip, req.query.t, req.query.h, req.query.e);
} else {
let j = apiJSON(0, { t: "no stream id" })
res.status(j.status).json(j.body);
return;
}
break;
case 'onDemand':
if (req.query.blockId) {
let blockId = req.query.blockId.slice(0, 3);
let r, j;
switch(blockId) {
case "0": // changelog history
r = changelogHistory();
j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" })
break;
case "1": // celebrations emoji
r = celebrationsEmoji();
j = r ? apiJSON(3, { t: r }) : false
break;
default:
j = apiJSON(0, { t: "couldn't find a block with this id" })
break;
}
if (j.body) {
res.status(j.status).json(j.body)
} else {
res.status(204).end()
}
} else {
let j = apiJSON(0, { t: "no block id" });
res.status(j.status).json(j.body)
}
break;
case 'serverInfo':
res.status(200).json({
version: version,
commit: gitCommit,
branch: gitBranch,
name: process.env.apiName ? process.env.apiName : "unknown",
url: process.env.apiURL,
cors: process.env.cors,
startTime: `${startTimestamp}`
});
break;
default:
let j = apiJSON(0, { t: "unknown response type" })
res.status(j.status).json(j.body);
break;
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
return;
}
});
app.get('/api/status', (req, res) => {
res.status(200).end()
});
app.get('/favicon.ico', (req, res) => {
res.sendFile(`${__dirname}/src/front/icons/favicon.ico`)
});
app.get('/*', (req, res) => {
res.redirect('/api/json')
});
app.listen(process.env.apiPort, () => {
console.log(`\n${Cyan(appName)} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\nURL: ${Cyan(`${process.env.apiURL}`)}\nPort: ${process.env.apiPort}\n`)
});
}

197
src/core/both.js Normal file
View file

@ -0,0 +1,197 @@
import cors from "cors";
import rateLimit from "express-rate-limit";
import { randomBytes } from "crypto";
const ipSalt = randomBytes(64).toString('hex');
import { appName, genericUserAgent, version } from "../modules/config.js";
import { getJSON } from "../modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan, Green, Red } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js";
import { buildFront } from "../modules/build.js";
import { changelogHistory } from "../modules/pageRender/onDemand.js";
import { sha256 } from "../modules/sub/crypto.js";
import findRendered from "../modules/pageRender/findRendered.js";
import { celebrationsEmoji } from "../modules/pageRender/elements.js";
export async function runBoth(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.cors === '0' ? { origin: process.env.selfURL, optionsSuccessStatus: 200 } : {};
const apiLimiter = rateLimit({
windowMs: 60000,
max: 25,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const apiLimiterStream = rateLimit({
windowMs: 60000,
max: 28,
standardHeaders: false,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const startTime = new Date();
const startTimestamp = Math.floor(startTime.getTime());
// preload localization files and build static pages
await buildFront(gitCommit, gitBranch);
app.use('/api/:type', cors(corsConfig));
app.use('/api/json', apiLimiter);
app.use('/api/stream', apiLimiterStream);
app.use('/api/onDemand', apiLimiter);
app.use('/', express.static('./build/min'));
app.use('/', express.static('./src/front'));
app.use((req, res, next) => {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.use((req, res, next) => {
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) res.destroy();
next();
});
app.use('/api/json', express.json({
verify: (req, res, buf) => {
try {
JSON.parse(buf);
if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' });
return;
}
if (String(req.header('Accept')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' });
return;
}
} catch(e) {
res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' });
return;
}
}
}));
app.post('/api/json', async (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" });
try {
let request = req.body;
if (request.url) {
request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request);
if (chck) chck["ip"] = ip;
j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
} else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') });
}
} catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
}
res.status(j.status).json(j.body);
return;
} catch (e) {
res.destroy();
return
}
});
app.get('/api/:type', (req, res) => {
try {
let ip = sha256(getIP(req), ipSalt);
switch (req.params.type) {
case 'stream':
if (req.query.p) {
res.status(200).json({ "status": "continue" });
return;
} else if (req.query.t && req.query.h && req.query.e) {
stream(res, ip, req.query.t, req.query.h, req.query.e);
} else {
let j = apiJSON(0, { t: "no stream id" })
res.status(j.status).json(j.body);
return;
}
break;
case 'onDemand':
if (req.query.blockId) {
let blockId = req.query.blockId.slice(0, 3);
let r, j;
switch(blockId) {
case "0": // changelog history
r = changelogHistory();
j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" })
break;
case "1": // celebrations emoji
r = celebrationsEmoji();
j = r ? apiJSON(3, { t: r }) : false
break;
default:
j = apiJSON(0, { t: "couldn't find a block with this id" })
break;
}
if (j.body) {
res.status(j.status).json(j.body)
} else {
res.status(204).end()
}
} else {
let j = apiJSON(0, { t: "no block id" });
res.status(j.status).json(j.body)
}
break;
case 'serverInfo':
res.status(200).json({
version: version,
commit: gitCommit,
branch: gitBranch,
name: process.env.apiName ? process.env.apiName : "unknown",
url: process.env.apiURL,
cors: process.env.cors,
startTime: `${startTimestamp}`
});
break;
default:
let j = apiJSON(0, { t: "unknown response type" })
res.status(j.status).json(j.body);
break;
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
return;
}
});
app.get("/api/status", (req, res) => {
res.status(200).end()
});
app.get("/api", (req, res) => {
res.redirect('/api/json')
});
app.get("/", (req, res) => {
res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`);
});
app.get("/favicon.ico", (req, res) => {
res.redirect('/icons/favicon.ico');
});
app.get("/*", (req, res) => {
res.redirect('/')
});
app.listen(process.env.port, () => {
console.log(`${Red("⚠️ This way of running cobalt has been deprecated and will be removed soon.\nCheck the docs and get ready: ")}${Green("WIP")}`)
console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(startTimestamp)})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`)
})
}

51
src/core/web.js Normal file
View file

@ -0,0 +1,51 @@
import { appName, genericUserAgent, version } from "../modules/config.js";
import { languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js";
import { buildFront } from "../modules/build.js";
import findRendered from "../modules/pageRender/findRendered.js";
// * will be removed in the future
import cors from "cors";
// *
export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
await buildFront(gitCommit, gitBranch);
// * will be removed in the future
const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {};
app.use('/api/:type', cors(corsConfig));
// *
app.use('/', express.static('./build/min'));
app.use('/', express.static('./src/front'));
app.use((req, res, next) => {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.get("/status", (req, res) => {
res.status(200).end()
});
app.get("/", (req, res) => {
res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`)
});
app.get("/favicon.ico", (req, res) => {
res.sendFile(`${__dirname}/src/front/icons/favicon.ico`)
});
// * will be removed in the future
app.get("/api/*", (req, res) => {
res.redirect(308, process.env.apiURL.slice(0, -1) + req.url)
});
app.post("/api/*", (req, res) => {
res.redirect(308, process.env.apiURL.slice(0, -1) + req.url)
});
// *
app.get("/*", (req, res) => {
res.redirect('/')
});
app.listen(process.env.webPort, () => {
let startTime = new Date();
console.log(`\n${Cyan(appName)} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.webURL}`)}\nPort: ${process.env.webPort}\n`)
})
}

View file

@ -156,7 +156,7 @@ button:active,
.text-to-copy:active {
background: var(--accent-press);
cursor: pointer;
transform: scale(0.95)
transform: scale(0.95);
}
.collapse-header:active {
background: var(--accent-press);
@ -664,10 +664,6 @@ button:active,
#pd-share {
display: none;
}
#hop-attribution {
display: block;
text-align: right;
}
#about-donate-footer::before {
content: "";
position: absolute;
@ -681,6 +677,9 @@ button:active,
#about-donate-footer:active::before {
opacity: 0;
}
.popup-tabs-child {
width: 100%;
}
/* adapt the page according to screen size */
@media screen and (min-width: 2300px) {
html {

View file

@ -19,10 +19,12 @@ const exceptions = { // used for mobile devices
"vQuality": "720"
};
const apiURL = '';
let store = {};
function changeAPI(url) {
apiURL = url;
return true
}
function eid(id) {
return document.getElementById(id)
}

View file

@ -0,0 +1,9 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3H10C10.55 3 11 3.45 11 4V5.43C11 5.74 10.74 6 10.43 6H9C8.45 6 8 5.55 8 5V4C8 3.45 8.45 3 9 3Z" fill="#635994" />
<path d="M11.99 29.03H13C13.55 29.03 14 28.58 14 28.03V27.03C14 26.48 13.55 26.03 13 26.03H10.57C10.26 26.03 10 26.29 10 26.6V27.04C10 28.14 10.89 29.03 11.99 29.03Z" fill="#635994" />
<path d="M18 27.03V28.03C18 28.58 18.45 29.03 19 29.03H20.03C21.12 29.03 22 28.15 22 27.06V26.6C22 26.28 21.74 26.03 21.43 26.03H19C18.45 26.03 18 26.48 18 27.03Z" fill="#635994" />
<path d="M24 5V4C24 3.45 23.55 3 23 3H22C21.45 3 21 3.45 21 4V5.43C21 5.74 21.26 6 21.57 6H23C23.55 6 24 5.55 24 5Z" fill="#635994" />
<path d="M28 11.03C28 10.48 28.45 10.03 29 10.03C29.55 10.03 30 10.48 30 11.03V15.03C30 15.58 29.55 16.03 29 16.03H28.57C28.26 16.03 28 16.28 28 16.6V17.06C28 18.15 27.12 19.03 26.03 19.03H25.57C25.26 19.03 25 19.28 25 19.6V24.04C25 25.14 24.11 26.03 23.01 26.03H22.57C22.26 26.03 22 25.78 22 25.46V22.6C22 22.29 21.75 22.03 21.43 22.03H10.57C10.26 22.03 10 22.28 10 22.6V25.46C10 25.77 9.75 26.03 9.43 26.03H9C7.9 26.03 7 25.13 7 24.03V19.6C7 19.29 6.74 19.03 6.43 19.03H6C4.9 19.03 4 18.13 4 17.03V16.6C4 16.29 3.74 16.03 3.43 16.03H3C2.45 16.03 2 15.58 2 15.03V11.03C2 10.48 2.45 10.03 3 10.03H3.03C3.58 10.03 4.03 10.48 4.03 11.03V12.46C4.03 12.78 4.28 13.03 4.6 13.03L6.4 13.02C6.7 13.01 6.96 12.8 7 12.51C7.24 10.7 8.71 9.29 10.53 9.06C10.8 9.03 11 8.78 11 8.5V6.57C11 6.26 11.26 6 11.58 6H11.88C13.05 6 14 6.95 14 8.12V8.46C14 8.78 14.26 9.03 14.57 9.03H17.43C17.74 9.03 18 8.78 18 8.46V8.07C18 6.93 18.93 6 20.07 6H20.43C20.74 6 21 6.26 21 6.57V8.5C21 8.78 21.2 9.03 21.47 9.06C23.29 9.28 24.74 10.7 24.97 12.52C25.01 12.82 25.27 13.03 25.57 13.03H27.43C27.74 13.03 28 12.78 28 12.46V11.03Z" fill="#635994" />
<path d="M10 15.9824C10 16.5466 10.4455 17 10.9999 17C11.5543 17 12.0097 16.5466 11.9998 15.9824V14.0176C11.9998 13.4534 11.5543 13 10.9999 13C10.4455 13 10 13.4534 10 14.0176V15.9824Z" fill="#402A32" />
<path d="M20 15.9824C20 16.5466 20.4455 17 21 17C21.5545 17 22 16.5365 22 15.9824V14.0176C22 13.4534 21.5545 13 21 13C20.4455 13 20 13.4534 20 14.0176V15.9824Z" fill="#402A32" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -47,7 +47,7 @@
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github",
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.",
"DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request popup on top, and press \"always allow\".\n\nalternative method: press and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".",
"DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request, and press \"always allow\".\n\nalternative method:\npress and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"DownloadPopupWayToSave": "pick a way to save",
"ClickToCopy": "press to copy",

View file

@ -47,7 +47,7 @@
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github",
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод: зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
"DownloadPopupWayToSave": "выбери, как сохранить",
"ClickToCopy": "нажми, чтобы скопировать",

View file

@ -7,16 +7,13 @@ const locPath = './src/localization/languages';
let loc = {}
let languages = [];
export function loadLoc() {
fs.readdir(locPath, (err, files) => {
if (err) return false;
files.forEach(file => {
loc[file.split('.')[0]] = loadJson(`${locPath}/${file}`);
languages.push(file.split('.')[0])
});
})
export async function loadLoc() {
const files = await fs.promises.readdir(locPath).catch((e) => { return [] });
files.forEach(file => {
loc[file.split('.')[0]] = loadJson(`${locPath}/${file}`);
languages.push(file.split('.')[0])
});
}
loadLoc();
export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "&bull;");

View file

@ -34,8 +34,9 @@ export async function getJSON(originalURL, lang, obj) {
}
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) });

View file

@ -1,6 +1,7 @@
import * as esbuild from "esbuild";
import * as fs from "fs";
import { languageList } from "../localization/manager.js";
import { loadLoc, languageList } from "../localization/manager.js";
import page from "./pageRender/page.js";
function cleanHTML(html) {
@ -10,6 +11,9 @@ function cleanHTML(html) {
}
export async function buildFront(commitHash, branch) {
try {
// preload localization files
await loadLoc();
// build html
if (!fs.existsSync('./build/')){
fs.mkdirSync('./build/');
@ -17,6 +21,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 +44,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'

View 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);

View file

@ -1,9 +1,9 @@
{
"current": {
"version": "5.4",
"title": "instagram support, hop, docker, and more!",
"title": "instagram support, 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."
"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."
},
"history": [{
"version": "5.3",

View file

@ -23,7 +23,8 @@ const names = {
"🐙": "octopus",
"🔮": "crystal_ball",
"💪": "biceps",
"💖": "sparkling_heart"
"💖": "sparkling_heart",
"👾": "alien_monster"
}
let sizing = {
22: 0.4,

View file

@ -89,14 +89,13 @@ export function multiPagePopup(obj) {
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-tabs" class="switches popup-tabs"><div class="switches popup-tabs-child">${tabs}</div><button id="close-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button></div>
</div>`
}
export function collapsibleList(arr) {

View file

@ -48,10 +48,10 @@ export default function(obj) {
<title>${appName}</title>
<meta property="og:url" content="${process.env.selfURL}" />
<meta property="og:url" content="${process.env.webURL || process.env.selfURL}" />
<meta property="og:title" content="${appName}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
<meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" />
<meta name="description" content="${t('AboutSummary')}" />
<meta name="theme-color" content="#000000" />
@ -96,8 +96,9 @@ export default function(obj) {
"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/>
${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/>
${t("SupportNote")}`
@ -106,7 +107,6 @@ export default function(obj) {
"title": t("CollapsePrivacy"),
"body": t("PrivacyPolicy")
}])
+ `${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>' : ''}`
}]
})
}, {
@ -408,7 +408,8 @@ export default function(obj) {
}])}
</footer>
</body>
<script type="text/javascript">const loc = {
<script type="text/javascript">
const loc = {
noInternet: ` + "`" + t('ErrorNoInternet') + "`" + `,
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
@ -417,7 +418,9 @@ export default function(obj) {
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
};</script>
};
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
</script>
<script type="text/javascript" src="cobalt.js"></script>
</html>`;
} catch (err) {

View file

@ -113,9 +113,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") {

View file

@ -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;
}

View file

@ -23,6 +23,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,6 +34,7 @@ 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' };
@ -40,7 +45,7 @@ export default async function(o) {
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] };
@ -73,9 +78,9 @@ export default async function(o) {
};
return r
}
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);
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));

View file

@ -5,48 +5,100 @@ import { execSync } from "child_process";
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.")}\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 deprecated and will be removed in the future.")}`
)
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 => {
if (apiCors.toLowerCase() !== 'y') 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: co.wukko.me"));
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()

View file

@ -39,7 +39,7 @@ 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) {

View file

@ -446,6 +446,17 @@
"code": 200,
"status": "stream"
}
}, {
"name": "vr 360, av1, max",
"url": "https://www.youtube.com/watch?v=hEdzv7D4CbQ",
"params": {
"vCodec": "vp9",
"vQuality": "max"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "inexistent video",
"url": "https://youtube.com/watch?v=gnjuHYWGEW",
@ -717,6 +728,24 @@
"code": 200,
"status": "redirect"
}
}, {
"name": "tumblr audio",
"url": "https://rf9weu8hjf789234hf9.tumblr.com/post/172006661342/everyone-thats-made-a-video-out-of-this-without",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "tumblr video converted to audio",
"url": "https://garfield-69.tumblr.com/post/696499862852780032",
"params": {
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}],
"vimeo": [{
"name": "4k progressive",