Merge branch 'main' into main-ua
|
@ -4,7 +4,7 @@ if you're reading this, you are probably interested in contributing to cobalt, w
|
|||
this document serves as a guide to help you make contributions that we can merge into the cobalt codebase.
|
||||
|
||||
## translations
|
||||
currently, we are **not accepting** translations of cobalt. this is because we are making significant changes to the frontend, and the currently used localization structure is being completely reworked. if this changes, this document will be updated.
|
||||
currently, we are **not accepting** translations of cobalt. we're working on changing this soon!
|
||||
|
||||
## adding features or support for services
|
||||
before putting in the effort to implement a feature, it's worth considering whether it would be appropriate to add it to cobalt. the cobalt api is built to assist people **only with downloading freely accessible content**. other functionality, such as:
|
||||
|
@ -22,9 +22,9 @@ when contributing code to cobalt, there are a few guidelines in place to ensure
|
|||
### clean commit messages
|
||||
internally, we use a format similar to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) - the first part signifies which part of the code you are changing (the *scope*), and the second part explains the change. for inspiration on how to write appropriate commit titles, you can take a look at the [commit history](https://github.com/imputnet/cobalt/commits/).
|
||||
|
||||
the scope is not strictly defined, you can write whatever you find most fitting for the particular change. suppose you are changing a small part of a more significant part of the codebase. in that case, you can specify both the larger and smaller scopes in the commit message for clarity (e.g., if you were changing something in internal streams, the commit could be something like `stream/internal: fix object not being handled properly`).
|
||||
the scope is not strictly defined, you can write whatever you find most fitting for the particular change. suppose you are changing a small part of a more significant part of the codebase. in that case, you can specify both the larger and smaller scopes in the commit message for clarity (e.g., if you were changing something in internal streams, the commit could be something like `api/stream: fix object not being handled properly`).
|
||||
|
||||
if you think a change deserves further explanation, we encourage you to write a short explanation in the commit message ([example](https://github.com/imputnet/cobalt/commit/d2e5b6542f71f3809ba94d56c26f382b5cb62762)), which will save both you and us time having to enquire about the change, and you explaining the reason behind it.
|
||||
if you think a change deserves further explanation, we encourage you to write a short explanation in the commit message ([example](https://github.com/imputnet/cobalt/commit/31be60484de8eaf63bba8a4f508e16438aa7ba6e)), which will save both you and us time having to enquire about the change, and you explaining the reason behind it.
|
||||
|
||||
if your contribution has uninformative commit titles, you may be asked to interactively rebase your branch and amend each commit to include a meaningful title.
|
||||
|
||||
|
|
115
README.md
|
@ -15,108 +15,43 @@
|
|||
💬 community discord server
|
||||
</a>
|
||||
<a href="https://x.com/justusecobalt">
|
||||
🐦 twitter/x
|
||||
🐦 twitter
|
||||
</a>
|
||||
</p>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
cobalt is a media downloader that doesn't piss you off. it's fast, friendly, and doesn't have any bullshit that modern web is filled with: ***no ads, trackers, or paywalls***.
|
||||
cobalt is a media downloader that doesn't piss you off. it's friendly, efficient, and doesn't have ads, trackers, paywalls or other nonsense.
|
||||
|
||||
paste the link, get the file, move on. it's that simple. just how it should be.
|
||||
paste the link, get the file, move on. that simple, just how it should be.
|
||||
|
||||
### supported services
|
||||
this list is not final and keeps expanding over time. if support for a service you want is missing, create an issue (or a pull request 👀).
|
||||
### cobalt monorepo
|
||||
this monorepo includes source code for api, frontend, and related packages:
|
||||
- [api tree](/api/)
|
||||
- [web tree](/web/)
|
||||
- [packages tree](/packages/)
|
||||
|
||||
| service | video + audio | only audio | only video | metadata | rich file names |
|
||||
| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
|
||||
| bilibili | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| bluesky | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| instagram | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| facebook | ✅ | ❌ | ✅ | ➖ | ➖ |
|
||||
| loom | ✅ | ❌ | ✅ | ✅ | ➖ |
|
||||
| ok.ru | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| pinterest | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| reddit | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| rutube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| snapchat | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ |
|
||||
| streamable | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| twitter/x | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| vimeo | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| vine | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| vk videos & clips | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| youtube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
| emoji | meaning |
|
||||
| :-----: | :---------------------- |
|
||||
| ✅ | supported |
|
||||
| ➖ | impossible/unreasonable |
|
||||
| ❌ | not supported |
|
||||
|
||||
### additional notes or features (per service)
|
||||
| service | notes or features |
|
||||
| :-------- | :----- |
|
||||
| instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. |
|
||||
| facebook | supports public accessible videos content only. |
|
||||
| pinterest | supports photos, gifs, videos and stories. |
|
||||
| reddit | supports gifs and videos. |
|
||||
| snapchat | supports spotlights and stories. lets you pick what to save from stories. |
|
||||
| rutube | supports yappy & private links. |
|
||||
| soundcloud | supports private links. |
|
||||
| tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. |
|
||||
| twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. |
|
||||
| vimeo | audio downloads are only available for dash. |
|
||||
| youtube | supports videos, music, and shorts. 8K, 4K, HDR, VR, and high FPS videos. rich metadata & dubs. h264/av1/vp9 codecs. |
|
||||
it also includes documentation in the [docs tree](/docs/):
|
||||
- [cobalt api documentation](/docs/api.md)
|
||||
- [how to run a cobalt instance](/docs/run-an-instance.md)
|
||||
- [how to protect a cobalt instance](/docs/protect-an-instance.md)
|
||||
- [how to configure a cobalt instance for youtube](/docs/configure-for-youtube.md)
|
||||
|
||||
### partners
|
||||
cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt), all main instances are currently hosted on their network :)
|
||||
cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt) and the main processing instance is hosted on their network. we really appreciate their kindness!
|
||||
|
||||
### ethics and disclaimer
|
||||
cobalt is a tool for easing content downloads from internet and takes ***zero liability***. you are responsible for what you download, how you use and distribute that content. please be mindful when using content of others and always credit original creators. fair use and credits benefit everyone.
|
||||
### ethics
|
||||
cobalt is a tool that makes downloading public content easier. it takes **zero liability**.
|
||||
the end user is responsible for what they download, how they use and distribute that content.
|
||||
cobalt never caches any content, it [works like a fancy proxy](/api/src/stream/).
|
||||
|
||||
cobalt is ***NOT*** a piracy tool and cannot be used as such. it can only download free, publicly accessible content. such content can be easily downloaded through any browser's dev tools. pressing one button is easier, so i made a convenient, ad-less tool for such repeated actions.
|
||||
cobalt is in no way a piracy tool and cannot be used as such.
|
||||
it can only download free & publicly accessible content.
|
||||
same content can be downloaded via dev tools of any modern web browser.
|
||||
|
||||
### cobalt license
|
||||
### contributing
|
||||
thank you for considering making a contribution to cobalt! please check the [contributing guidelines here](/CONTRIBUTING.md) before making a pull request.
|
||||
|
||||
### licenses
|
||||
for relevant licensing information, see the [api](api/README.md) and [web](web/README.md) READMEs.
|
||||
unless specified otherwise, the remainder of this repository is licensed under [AGPL-3.0](LICENSE).
|
||||
|
||||
## acknowledgements
|
||||
### ffmpeg
|
||||
cobalt heavily relies on ffmpeg for converting and merging media files. it's an absolutely amazing piece of software offered for anyone for free, yet doesn't receive as much credit as it should.
|
||||
|
||||
you can [support ffmpeg here](https://ffmpeg.org/donations.html)!
|
||||
|
||||
#### ffmpeg-static
|
||||
we use [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static) to get binaries for ffmpeg depending on the platform.
|
||||
|
||||
you can support the developer via various methods listed on their github page! (linked above)
|
||||
|
||||
### youtube.js
|
||||
cobalt relies on [youtube.js](https://github.com/LuanRT/YouTube.js) for interacting with the innertube api, it wouldn't have been possible without it.
|
||||
|
||||
you can support the developer via various methods listed on their github page! (linked above)
|
||||
|
||||
### many others
|
||||
cobalt also depends on:
|
||||
|
||||
- [content-disposition-header](https://www.npmjs.com/package/content-disposition-header) to simplify the provision of `content-disposition` headers.
|
||||
- [cors](https://www.npmjs.com/package/cors) to manage cross-origin resource sharing within expressjs.
|
||||
- [dotenv](https://www.npmjs.com/package/dotenv) to load environment variables from the `.env` file.
|
||||
- [esbuild](https://www.npmjs.com/package/esbuild) to minify the frontend files.
|
||||
- [express](https://www.npmjs.com/package/express) as the backbone of cobalt servers.
|
||||
- [express-rate-limit](https://www.npmjs.com/package/express-rate-limit) to rate limit api endpoints.
|
||||
- [hls-parser](https://www.npmjs.com/package/hls-parser) to parse `m3u8` playlists for certain services.
|
||||
- [ipaddr.js](https://www.npmjs.com/package/ipaddr.js) to parse ip addresses (for rate limiting).
|
||||
- [nanoid](https://www.npmjs.com/package/nanoid) to generate unique (temporary) identifiers for each requested stream.
|
||||
- [node-cache](https://www.npmjs.com/package/node-cache) to cache stream info in server ram for a limited amount of time.
|
||||
- [psl](https://www.npmjs.com/package/psl) as the domain name parser.
|
||||
- [set-cookie-parser](https://www.npmjs.com/package/set-cookie-parser) to parse cookies that cobalt receives from certain services.
|
||||
- [undici](https://www.npmjs.com/package/undici) for making http requests.
|
||||
- [url-pattern](https://www.npmjs.com/package/url-pattern) to match provided links with supported patterns.
|
||||
|
||||
...and many other packages that these packages rely on.
|
||||
|
|
105
api/README.md
|
@ -1,4 +1,67 @@
|
|||
# cobalt api
|
||||
this directory includes the source code for cobalt api. it's made with [express.js](https://www.npmjs.com/package/express) and love!
|
||||
|
||||
## running your own instance
|
||||
if you want to run your own instance for whatever purpose, [follow this guide](/docs/run-an-instance.md).
|
||||
we recommend to use docker compose unless you intend to run cobalt for developing/debugging purposes.
|
||||
|
||||
## accessing the api
|
||||
there is currently no publicly available pre-hosted api.
|
||||
we recommend [deploying your own instance](/docs/run-an-instance.md) if you wish to use the cobalt api.
|
||||
|
||||
you can read [the api documentation here](/docs/api.md).
|
||||
|
||||
> [!WARNING]
|
||||
> the v7 public api (/api/json) will be shut down on **november 11th, 2024**.
|
||||
> you can access documentation for it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md).
|
||||
|
||||
## supported services
|
||||
this list is not final and keeps expanding over time. if support for a service you want is missing, create an issue (or a pull request 👀).
|
||||
|
||||
| service | video + audio | only audio | only video | metadata | rich file names |
|
||||
| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
|
||||
| bilibili | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| bluesky | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| instagram | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| facebook | ✅ | ❌ | ✅ | ➖ | ➖ |
|
||||
| loom | ✅ | ❌ | ✅ | ✅ | ➖ |
|
||||
| ok.ru | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| pinterest | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| reddit | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| rutube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| snapchat | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ |
|
||||
| streamable | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| twitter/x | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| vimeo | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| vine | ✅ | ✅ | ✅ | ➖ | ➖ |
|
||||
| vk videos & clips | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| youtube | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
| emoji | meaning |
|
||||
| :-----: | :---------------------- |
|
||||
| ✅ | supported |
|
||||
| ➖ | impossible/unreasonable |
|
||||
| ❌ | not supported |
|
||||
|
||||
### additional notes or features (per service)
|
||||
| service | notes or features |
|
||||
| :-------- | :----- |
|
||||
| instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. |
|
||||
| facebook | supports public accessible videos content only. |
|
||||
| pinterest | supports photos, gifs, videos and stories. |
|
||||
| reddit | supports gifs and videos. |
|
||||
| snapchat | supports spotlights and stories. lets you pick what to save from stories. |
|
||||
| rutube | supports yappy & private links. |
|
||||
| soundcloud | supports private links. |
|
||||
| tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. |
|
||||
| twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. |
|
||||
| vimeo | audio downloads are only available for dash. |
|
||||
| youtube | supports videos, music, and shorts. 8K, 4K, HDR, VR, and high FPS videos. rich metadata & dubs. h264/av1/vp9 codecs. |
|
||||
|
||||
## license
|
||||
cobalt api code is licensed under [AGPL-3.0](LICENSE).
|
||||
|
@ -9,14 +72,38 @@ as long as you:
|
|||
- provide a link to the license and indicate if changes to the code were made, and
|
||||
- release the code under the **same license**
|
||||
|
||||
## running your own instance
|
||||
if you want to run your own instance for whatever purpose, [follow this guide](/docs/run-an-instance.md).
|
||||
it's *highly* recommended to use a docker compose method unless you run for developing/debugging purposes.
|
||||
## acknowledgements
|
||||
### ffmpeg
|
||||
cobalt heavily relies on ffmpeg for converting and merging media files. it's an absolutely amazing piece of software offered for anyone for free, yet doesn't receive as much credit as it should.
|
||||
|
||||
## accessing the api
|
||||
currently, there is no publicly accessible main api. we plan on providing a public api for
|
||||
cobalt 10 in some form in the future. we recommend deploying your own instance if you wish
|
||||
to use the latest api. you can access [the documentation](/docs/api.md) for it here.
|
||||
you can [support ffmpeg here](https://ffmpeg.org/donations.html)!
|
||||
|
||||
if you are looking for the documentation for the old (7.x) api, you can find
|
||||
it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md)
|
||||
#### ffmpeg-static
|
||||
we use [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static) to get binaries for ffmpeg depending on the platform.
|
||||
|
||||
you can support the developer via various methods listed on their github page! (linked above)
|
||||
|
||||
### youtube.js
|
||||
cobalt relies on [youtube.js](https://github.com/LuanRT/YouTube.js) for interacting with the innertube api, it wouldn't have been possible without it.
|
||||
|
||||
you can support the developer via various methods listed on their github page! (linked above)
|
||||
|
||||
### many others
|
||||
cobalt also depends on:
|
||||
|
||||
- [content-disposition-header](https://www.npmjs.com/package/content-disposition-header) to simplify the provision of `content-disposition` headers.
|
||||
- [cors](https://www.npmjs.com/package/cors) to manage cross-origin resource sharing within expressjs.
|
||||
- [dotenv](https://www.npmjs.com/package/dotenv) to load environment variables from the `.env` file.
|
||||
- [esbuild](https://www.npmjs.com/package/esbuild) to minify the frontend files.
|
||||
- [express](https://www.npmjs.com/package/express) as the backbone of cobalt servers.
|
||||
- [express-rate-limit](https://www.npmjs.com/package/express-rate-limit) to rate limit api endpoints.
|
||||
- [hls-parser](https://www.npmjs.com/package/hls-parser) to parse `m3u8` playlists for certain services.
|
||||
- [ipaddr.js](https://www.npmjs.com/package/ipaddr.js) to parse ip addresses (for rate limiting).
|
||||
- [nanoid](https://www.npmjs.com/package/nanoid) to generate unique (temporary) identifiers for each requested stream.
|
||||
- [node-cache](https://www.npmjs.com/package/node-cache) to cache stream info in server ram for a limited amount of time.
|
||||
- [psl](https://www.npmjs.com/package/psl) as the domain name parser.
|
||||
- [set-cookie-parser](https://www.npmjs.com/package/set-cookie-parser) to parse cookies that cobalt receives from certain services.
|
||||
- [undici](https://www.npmjs.com/package/undici) for making http requests.
|
||||
- [url-pattern](https://www.npmjs.com/package/url-pattern) to match provided links with supported patterns.
|
||||
|
||||
...and many other packages that these packages rely on.
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "node src/cobalt",
|
||||
"setup": "node src/util/setup",
|
||||
"test": "node src/util/test",
|
||||
"token:youtube": "node src/util/generate-youtube-tokens"
|
||||
"token:youtube": "node src/util/generate-youtube-tokens",
|
||||
"token:jwt": "node src/util/generate-jwt-secret"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -2,26 +2,24 @@ import "dotenv/config";
|
|||
|
||||
import express from "express";
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { env } from "./config.js"
|
||||
import { Bright, Green, Red } from "./misc/console-text.js";
|
||||
import { Red } from "./misc/console-text.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename).slice(0, -4);
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.disable("x-powered-by");
|
||||
|
||||
if (env.apiURL) {
|
||||
const { runAPI } = await import('./core/api.js');
|
||||
const { runAPI } = await import("./core/api.js");
|
||||
runAPI(express, app, __dirname)
|
||||
} else {
|
||||
console.log(
|
||||
Red(`cobalt wasn't configured yet or configuration is invalid.\n`)
|
||||
+ Bright(`please run the setup script to fix this: `)
|
||||
+ Green(`npm run setup`)
|
||||
Red("API_URL env variable is missing, cobalt api can't start.")
|
||||
)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,10 @@ const env = {
|
|||
const genericUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
|
||||
const cobaltUserAgent = `cobalt/${version} (+https://github.com/imputnet/cobalt)`;
|
||||
|
||||
if (env.sessionEnabled && env.jwtSecret.length < 16) {
|
||||
throw new Error("JWT_SECRET env is too short (must be at least 16 characters long)");
|
||||
}
|
||||
|
||||
export {
|
||||
env,
|
||||
genericUserAgent,
|
||||
|
|
|
@ -158,19 +158,20 @@ export const runAPI = (express, app, __dirname) => {
|
|||
return fail(res, "error.api.auth.jwt.missing");
|
||||
}
|
||||
|
||||
if (!authorization.startsWith("Bearer ") || authorization.length > 256) {
|
||||
if (authorization.length >= 256) {
|
||||
return fail(res, "error.api.auth.jwt.invalid");
|
||||
}
|
||||
|
||||
const verifyJwt = jwt.verify(
|
||||
authorization.split("Bearer ", 2)[1]
|
||||
);
|
||||
|
||||
if (!verifyJwt) {
|
||||
const [ type, token, ...rest ] = authorization.split(" ");
|
||||
if (!token || type.toLowerCase() !== 'bearer' || rest.length) {
|
||||
return fail(res, "error.api.auth.jwt.invalid");
|
||||
}
|
||||
|
||||
req.rateLimitKey = generateHmac(req.header("Authorization"), ipSalt);
|
||||
if (!jwt.verify(token)) {
|
||||
return fail(res, "error.api.auth.jwt.invalid");
|
||||
}
|
||||
|
||||
req.rateLimitKey = generateHmac(token, ipSalt);
|
||||
} catch {
|
||||
return fail(res, "error.api.generic");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { fetch } from "undici";
|
||||
|
||||
import { Innertube, Session } from "youtubei.js";
|
||||
|
||||
import { env } from "../../config.js";
|
||||
|
@ -10,7 +9,7 @@ const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms
|
|||
|
||||
let innertube, lastRefreshedAt;
|
||||
|
||||
const codecMatch = {
|
||||
const codecList = {
|
||||
h264: {
|
||||
videoCodec: "avc1",
|
||||
audioCodec: "mp4a",
|
||||
|
@ -116,19 +115,7 @@ export default async function(o) {
|
|||
} else throw e;
|
||||
}
|
||||
|
||||
const quality = o.quality === "max" ? "9000" : o.quality;
|
||||
|
||||
let info, isDubbed,
|
||||
format = o.format || "h264";
|
||||
|
||||
function qual(i) {
|
||||
if (!i.quality_label) {
|
||||
return;
|
||||
}
|
||||
|
||||
return i.quality_label.split('p')[0].split('s')[0]
|
||||
}
|
||||
|
||||
let info;
|
||||
try {
|
||||
info = await yt.getBasicInfo(o.id, yt.session.logged_in ? 'ANDROID' : 'IOS');
|
||||
} catch(e) {
|
||||
|
@ -146,7 +133,8 @@ export default async function(o) {
|
|||
const playability = info.playability_status;
|
||||
const basicInfo = info.basic_info;
|
||||
|
||||
if (playability.status === "LOGIN_REQUIRED") {
|
||||
switch(playability.status) {
|
||||
case "LOGIN_REQUIRED":
|
||||
if (playability.reason.endsWith("bot")) {
|
||||
return { error: "youtube.login" }
|
||||
}
|
||||
|
@ -156,9 +144,9 @@ export default async function(o) {
|
|||
if (playability?.error_screen?.reason?.text === "Private video") {
|
||||
return { error: "content.video.private" }
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
if (playability.status === "UNPLAYABLE") {
|
||||
case "UNPLAYABLE":
|
||||
if (playability?.reason?.endsWith("request limit.")) {
|
||||
return { error: "fetch.rate" }
|
||||
}
|
||||
|
@ -168,15 +156,24 @@ export default async function(o) {
|
|||
if (playability?.error_screen?.reason?.text === "Private video") {
|
||||
return { error: "content.video.private" }
|
||||
}
|
||||
break;
|
||||
|
||||
case "AGE_VERIFICATION_REQUIRED":
|
||||
return { error: "content.video.age" };
|
||||
}
|
||||
|
||||
if (playability.status !== "OK") {
|
||||
return { error: "content.video.unavailable" };
|
||||
}
|
||||
|
||||
if (basicInfo.is_live) {
|
||||
return { error: "content.video.live" };
|
||||
}
|
||||
|
||||
if (basicInfo.duration > env.durationLimit) {
|
||||
return { error: "content.too_long" };
|
||||
}
|
||||
|
||||
// return a critical error if returned video is "Video Not Available"
|
||||
// or a similar stub by youtube
|
||||
if (basicInfo.id !== o.id) {
|
||||
|
@ -186,45 +183,38 @@ export default async function(o) {
|
|||
}
|
||||
}
|
||||
|
||||
let format = o.format || "h264";
|
||||
|
||||
const filterByCodec = (formats) =>
|
||||
formats
|
||||
.filter(e =>
|
||||
e.mime_type.includes(codecMatch[format].videoCodec)
|
||||
|| e.mime_type.includes(codecMatch[format].audioCodec)
|
||||
)
|
||||
.sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||
formats.filter(e =>
|
||||
e.mime_type.includes(codecList[format].videoCodec)
|
||||
|| e.mime_type.includes(codecList[format].audioCodec)
|
||||
).sort((a, b) =>
|
||||
Number(b.bitrate) - Number(a.bitrate)
|
||||
);
|
||||
|
||||
let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats);
|
||||
|
||||
if (adaptive_formats.length === 0 && format === "vp9") {
|
||||
format = "h264"
|
||||
adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats)
|
||||
if (adaptive_formats.length === 0 && ["vp9", "av1"].includes(format)) {
|
||||
format = "h264";
|
||||
adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats);
|
||||
}
|
||||
|
||||
let bestQuality;
|
||||
|
||||
const bestVideo = adaptive_formats.find(i => i.has_video && i.content_length);
|
||||
const hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length);
|
||||
|
||||
if (bestVideo) bestQuality = qual(bestVideo);
|
||||
|
||||
if ((!bestQuality && !o.isAudioOnly) || !hasAudio)
|
||||
return { error: "youtube.codec" };
|
||||
|
||||
if (basicInfo.duration > env.durationLimit)
|
||||
return { error: "content.too_long" };
|
||||
if ((!bestVideo && !o.isAudioOnly) || (!hasAudio && o.isAudioOnly)) {
|
||||
return { error: "fetch.empty" };
|
||||
}
|
||||
|
||||
const checkBestAudio = (i) => (i.has_audio && !i.has_video);
|
||||
|
||||
let audio = adaptive_formats.find(i =>
|
||||
checkBestAudio(i) && i.is_original
|
||||
);
|
||||
let audio = adaptive_formats.find(i => checkBestAudio(i) && i.is_original);
|
||||
let isDubbed;
|
||||
|
||||
if (o.dubLang) {
|
||||
let dubbedAudio = adaptive_formats.find(i =>
|
||||
checkBestAudio(i)
|
||||
&& i.language === o.dubLang
|
||||
&& i.audio_track
|
||||
checkBestAudio(i) && i.language === o.dubLang && i.audio_track
|
||||
)
|
||||
|
||||
if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) {
|
||||
|
@ -237,13 +227,14 @@ export default async function(o) {
|
|||
audio = adaptive_formats.find(i => checkBestAudio(i));
|
||||
}
|
||||
|
||||
let fileMetadata = {
|
||||
const fileMetadata = {
|
||||
title: cleanString(basicInfo.title.trim()),
|
||||
artist: cleanString(basicInfo.author.replace("- Topic", "").trim()),
|
||||
artist: cleanString(basicInfo.author.replace("- Topic", "").trim())
|
||||
}
|
||||
|
||||
if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) {
|
||||
let descItems = basicInfo.short_description.split("\n\n", 5);
|
||||
const descItems = basicInfo.short_description.split("\n\n", 5);
|
||||
|
||||
if (descItems.length === 5) {
|
||||
fileMetadata.album = descItems[2];
|
||||
fileMetadata.copyright = descItems[3];
|
||||
|
@ -253,7 +244,7 @@ export default async function(o) {
|
|||
}
|
||||
}
|
||||
|
||||
let filenameAttributes = {
|
||||
const filenameAttributes = {
|
||||
service: "youtube",
|
||||
id: o.id,
|
||||
title: fileMetadata.title,
|
||||
|
@ -264,50 +255,44 @@ export default async function(o) {
|
|||
if (audio && o.isAudioOnly) return {
|
||||
type: "audio",
|
||||
isAudioOnly: true,
|
||||
urls: audio.decipher(yt.session.player),
|
||||
filenameAttributes: filenameAttributes,
|
||||
fileMetadata: fileMetadata,
|
||||
bestAudio: format === "h264" ? "m4a" : "opus"
|
||||
urls: audio.url,
|
||||
filenameAttributes,
|
||||
fileMetadata,
|
||||
bestAudio: format === "h264" ? "m4a" : "opus",
|
||||
}
|
||||
|
||||
const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality,
|
||||
checkSingle = i =>
|
||||
qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].videoCodec),
|
||||
checkRender = i =>
|
||||
qual(i) === matchingQuality && i.has_video && !i.has_audio;
|
||||
|
||||
let match, type, urls;
|
||||
|
||||
// prefer good premuxed videos if available
|
||||
if (!o.isAudioOnly && !o.isAudioMuted && format === "h264" && bestVideo.fps <= 30) {
|
||||
match = info.streaming_data.formats.find(checkSingle);
|
||||
type = "proxy";
|
||||
urls = match?.decipher(yt.session.player);
|
||||
const qual = (i) => {
|
||||
if (!i.quality_label) {
|
||||
return;
|
||||
}
|
||||
|
||||
const video = adaptive_formats.find(checkRender);
|
||||
|
||||
if (!match && video && audio) {
|
||||
match = video;
|
||||
type = "merge";
|
||||
urls = [
|
||||
video.decipher(yt.session.player),
|
||||
audio.decipher(yt.session.player)
|
||||
]
|
||||
return i.quality_label.split('p', 2)[0].split('s', 2)[0]
|
||||
}
|
||||
|
||||
if (match) {
|
||||
filenameAttributes.qualityLabel = match.quality_label;
|
||||
filenameAttributes.resolution = `${match.width}x${match.height}`;
|
||||
filenameAttributes.extension = codecMatch[format].container;
|
||||
const quality = o.quality === "max" ? "9000" : o.quality;
|
||||
const bestQuality = qual(bestVideo);
|
||||
const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality;
|
||||
|
||||
const video = adaptive_formats.find(i =>
|
||||
qual(i) === matchingQuality && i.has_video && !i.has_audio
|
||||
);
|
||||
|
||||
if (video && audio) {
|
||||
filenameAttributes.qualityLabel = video.quality_label;
|
||||
filenameAttributes.resolution = `${video.width}x${video.height}`;
|
||||
filenameAttributes.extension = codecList[format].container;
|
||||
filenameAttributes.youtubeFormat = format;
|
||||
|
||||
return {
|
||||
type,
|
||||
urls,
|
||||
type: "merge",
|
||||
urls: [
|
||||
video.url,
|
||||
audio.url
|
||||
],
|
||||
filenameAttributes,
|
||||
fileMetadata
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "fetch.fail" }
|
||||
return { error: "fetch.fail" };
|
||||
}
|
||||
|
|
22
api/src/util/generate-jwt-secret.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// run with `pnpm -r token:jwt`
|
||||
|
||||
const makeSecureString = (length = 64) => {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
||||
const out = [];
|
||||
|
||||
while (out.length < length) {
|
||||
for (const byte of crypto.getRandomValues(new Uint8Array(length))) {
|
||||
if (byte < alphabet.length) {
|
||||
out.push(alphabet[byte]);
|
||||
}
|
||||
|
||||
if (out.length === length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
console.log(`JWT_SECRET: ${JSON.stringify(makeSecureString(64))}`)
|
|
@ -1,105 +0,0 @@
|
|||
import { existsSync, unlinkSync, appendFileSync } from "fs";
|
||||
import { createInterface } from "readline";
|
||||
import { Cyan, Bright } from "../misc/console-text.js";
|
||||
import { loadJSON } from "../misc/load-from-fs.js";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const { version } = loadJSON("./package.json");
|
||||
|
||||
let envPath = './.env';
|
||||
let q = `${Cyan('?')} \x1b[1m`;
|
||||
let ob = {};
|
||||
let rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let final = () => {
|
||||
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")}`);
|
||||
execSync('pnpm 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 :)'));
|
||||
rl.close()
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${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.")}`
|
||||
)
|
||||
|
||||
function setup() {
|
||||
console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web."));
|
||||
|
||||
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: api.cobalt.tools"));
|
||||
|
||||
rl.question(q, apiURL => {
|
||||
ob.API_URL = `http://localhost:9000/`;
|
||||
ob.API_PORT = 9000;
|
||||
if (apiURL && apiURL !== "localhost") ob.API_URL = `https://${apiURL.toLowerCase()}/`;
|
||||
|
||||
console.log(Bright("\nGreat! Now, what port will it be running on? (9000)"));
|
||||
|
||||
rl.question(q, apiPort => {
|
||||
if (apiPort) ob.API_PORT = apiPort;
|
||||
if (apiPort && (apiURL === "localhost" || !apiURL)) ob.API_URL = `http://localhost:${apiPort}/`;
|
||||
|
||||
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.API_NAME = apiName.toLowerCase();
|
||||
if (!apiName || apiName === "local") ob.API_NAME = "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_WILDCARD = '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.WEB_URL = `http://localhost:9001/`;
|
||||
ob.WEB_PORT = 9001;
|
||||
if (webURL && webURL !== "localhost") ob.WEB_URL = `https://${webURL.toLowerCase()}/`;
|
||||
|
||||
console.log(
|
||||
Bright("\nGreat! Now, what port will it be running on? (9001)")
|
||||
)
|
||||
rl.question(q, webPort => {
|
||||
if (webPort) ob.WEB_PORT = webPort;
|
||||
if (webPort && (webURL === "localhost" || !webURL)) ob.WEB_URL = `http://localhost:${webPort}/`;
|
||||
|
||||
console.log(
|
||||
Bright("\nOne last thing: what default API domain should be used? (api.cobalt.tools)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000")
|
||||
);
|
||||
|
||||
rl.question(q, apiURL => {
|
||||
ob.API_URL = `https://${apiURL.toLowerCase()}/`;
|
||||
if (apiURL.includes(':')) ob.API_URL = `http://${apiURL.toLowerCase()}/`;
|
||||
if (!apiURL) ob.API_URL = "https://api.cobalt.tools/";
|
||||
final()
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log(Bright("\nThis is not an option. Try again."));
|
||||
setup()
|
||||
}
|
||||
})
|
||||
}
|
||||
setup()
|
|
@ -238,7 +238,7 @@
|
|||
},
|
||||
{
|
||||
"name": "private song",
|
||||
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
|
||||
"url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90",
|
||||
"params": {
|
||||
"audioFormat": "mp3"
|
||||
},
|
||||
|
@ -249,7 +249,7 @@
|
|||
},
|
||||
{
|
||||
"name": "private song (wav, isAudioMuted)",
|
||||
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
|
||||
"url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90",
|
||||
"params": {
|
||||
"downloadMode": "mute",
|
||||
"audioFormat": "wav"
|
||||
|
@ -261,7 +261,7 @@
|
|||
},
|
||||
{
|
||||
"name": "private song (ogg, isAudioMuted, isAudioOnly)",
|
||||
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
|
||||
"url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90",
|
||||
"params": {
|
||||
"downloadMode": "audio",
|
||||
"audioFormat": "ogg"
|
||||
|
@ -437,17 +437,6 @@
|
|||
"status": "tunnel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "audio bitrate higher than video, no vp9 video in response (vp9)",
|
||||
"url": "https://www.youtube.com/watch?v=t5nC_ucYBrc",
|
||||
"params": {
|
||||
"youtubeVideoCodec": "vp9"
|
||||
},
|
||||
"expected": {
|
||||
"code": 400,
|
||||
"status": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "short, defaults",
|
||||
"url": "https://www.youtube.com/shorts/r5FpeOJItbw",
|
||||
|
@ -486,6 +475,17 @@
|
|||
"code": 400,
|
||||
"status": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "broken audioOnly download",
|
||||
"url": "https://www.youtube.com/watch?v=ink80Al5nbw",
|
||||
"params": {
|
||||
"downloadMode": "audio"
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "tunnel"
|
||||
}
|
||||
}
|
||||
],
|
||||
"vk": [
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# cobalt api documentation
|
||||
this document provides info about methods and acceptable variables for all cobalt api requests.
|
||||
|
||||
> if you are looking for the documentation for the old (7.x) api, you can find
|
||||
> it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md)
|
||||
> [!IMPORTANT]
|
||||
> hosted api instances (such as `api.cobalt.tools`) use bot protection and are **not** intended to be used in other projects without explicit permission. if you want to access the cobalt api reliably, you should [host your own instance](/docs/run-an-instance.md) or ask an instance owner for access.
|
||||
|
||||
## authentication
|
||||
an api instance may be configured to require you to authenticate yourself.
|
||||
|
@ -46,9 +46,10 @@ cobalt's main processing endpoint.
|
|||
request body type: `application/json`
|
||||
response body type: `application/json`
|
||||
|
||||
```
|
||||
⚠️ you must include Accept and Content-Type headers with every `POST /` request.
|
||||
> [!IMPORTANT]
|
||||
> you must include `Accept` and `Content-Type` headers with every `POST /` request.
|
||||
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
|
33
docs/configure-for-youtube.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# how to configure a cobalt instance for youtube
|
||||
if you get various errors when attempting to download videos that are:
|
||||
publicly available, not region locked, and not age-restricted;
|
||||
then your instance's ip address may have bad reputation.
|
||||
|
||||
in this case you have to use disposable google accounts.
|
||||
there's no other known workaround as of time of writing this document.
|
||||
|
||||
> [!CAUTION]
|
||||
> **NEVER** use your personal google account for downloading videos via any means.
|
||||
> you can use any google accounts that you're willing to sacrifice,
|
||||
> but be prepared to have them **permanently suspended**.
|
||||
>
|
||||
> we recommend that you use accounts that don't link back to your personal google account or identity, just in case.
|
||||
>
|
||||
> use incognito mode when signing in.
|
||||
> we also recommend using vpn/proxy services (such as [mullvad](https://mullvad.net/)).
|
||||
|
||||
1. if you haven't done it already, clone the cobalt repo, go to the cloned directory, and run `pnpm install`
|
||||
|
||||
2. run `pnpm -C api token:youtube`
|
||||
|
||||
3. follow instructions, use incognito mode in your browser when signing in.
|
||||
i cannot stress this enough, but again, **DO NOT USE YOUR PERSONAL GOOGLE ACCOUNT**.
|
||||
|
||||
4. once you have the oauth token, add it to `youtube_oauth` in your cookies file.
|
||||
you can see an [example here](/docs/examples/cookies.example.json).
|
||||
you can have several account tokens in this file, if you like.
|
||||
|
||||
5. all done! enjoy freedom.
|
||||
|
||||
### liability
|
||||
you're responsible for any damage done to any of your google accounts or any other damages. you do this by yourself and at your own risk.
|
BIN
docs/images/protect-an-instance/add.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/images/protect-an-instance/created.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/protect-an-instance/domain.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/images/protect-an-instance/mode.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
docs/images/protect-an-instance/name.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/images/protect-an-instance/sidebar.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 17 KiB |
150
docs/protect-an-instance.md
Normal file
|
@ -0,0 +1,150 @@
|
|||
# how to protect your cobalt instance
|
||||
if you keep getting a ton of unknown traffic that hurts the performance of your instance, then it might be a good idea to enable bot protection.
|
||||
|
||||
> [!NOTE]
|
||||
> this tutorial will work reliably on the latest official version of cobalt 10.
|
||||
we can't promise full compatibility with anything else.
|
||||
|
||||
## configure cloudflare turnstile
|
||||
turnstile is a free, safe, and privacy-respecting alternative to captcha.
|
||||
cobalt uses it automatically to weed out bots and automated scripts.
|
||||
your instance doesn't have to be proxied by cloudflare to use turnstile.
|
||||
all you need is a free cloudflare account to get started.
|
||||
|
||||
cloudflare dashboard interface might change over time, but basics should stay the same.
|
||||
|
||||
> [!WARNING]
|
||||
> never share the turnstile secret key, always keep it private. if accidentally exposed, rotate it in widget settings.
|
||||
|
||||
1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account
|
||||
|
||||
2. once logged in, select `Turnstile` in the sidebar
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/sidebar.png" width="250" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
3. press `Add widget`
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/add.png" width="550" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
4. enter the widget name (can be anything, such as "cobalt")
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/name.png" width="450" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
5. add cobalt frontend domains you want the widget to work with, you can change this list later at any time
|
||||
- if you want to use your processing instance with [cobalt.tools](https://cobalt.tools/) frontend, then add `cobalt.tools` to the list
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/domain.png" width="450" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
6. select `invisible` widget mode
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/mode.png" width="450" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
7. press `create`
|
||||
|
||||
8. keep the page with sitekey and secret key open, you'll need them later.
|
||||
if you closed it, no worries!
|
||||
just open the same turnstile page and press "settings" on your freshly made turnstile widget.
|
||||
|
||||
<div align="left">
|
||||
<p>
|
||||
<img src="images/protect-an-instance/created.png" width="450" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
you've successfully created a turnstile widget!
|
||||
time to add it to your processing instance.
|
||||
|
||||
### enable turnstile on your processing instance
|
||||
this tutorial assumes that you only have `API_URL` in your `environment` variables list.
|
||||
if you have other variables there, just add new ones after existing ones.
|
||||
|
||||
> [!CAUTION]
|
||||
> never use any values from the tutorial, especially `JWT_SECRET`!
|
||||
|
||||
1. open your `docker-compose.yml` config file in any text editor of choice.
|
||||
2. copy the turnstile sitekey & secret key and paste them to their respective variables.
|
||||
`TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key:
|
||||
```yml
|
||||
environment:
|
||||
API_URL: "https://your.instance.url.here.local/"
|
||||
TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key
|
||||
TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key
|
||||
```
|
||||
3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters.
|
||||
this string will be used as salt for all JWT keys.
|
||||
|
||||
you can generate a random secret with `pnpm -r token:jwt` or use any other that you like.
|
||||
|
||||
```yml
|
||||
environment:
|
||||
API_URL: "https://your.instance.url.here.local/"
|
||||
TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key
|
||||
TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key
|
||||
JWT_SECRET: "bgBmF4efNCKPirD" # create a new secret, NEVER use this one
|
||||
```
|
||||
4. restart the docker container.
|
||||
|
||||
## configure api keys
|
||||
if you want to use your instance outside of web interface, you'll need an api key!
|
||||
|
||||
> [!NOTE]
|
||||
> this tutorial assumes that you'll keep your keys file locally, on the instance server.
|
||||
> if you wish to upload your file to a remote location,
|
||||
> replace the value for `API_KEYS_URL` with a direct url to the file
|
||||
> and skip the second step.
|
||||
|
||||
> [!WARNING]
|
||||
> when storing keys file remotely, make sure that it's not publicly accessible
|
||||
> and that link to it is either authenticated (via query) or impossible to guess.
|
||||
>
|
||||
> if api keys leak, you'll have to update/remove all UUIDs to revoke them.
|
||||
|
||||
1. create a `keys.json` file following [the schema and example here](/docs//run-an-instance.md#api-key-file-format).
|
||||
|
||||
2. expose the `keys.json` to the docker container:
|
||||
```yml
|
||||
volumes:
|
||||
- ./keys.json:/keys.json:ro # ro - read-only
|
||||
```
|
||||
|
||||
3. add a path to the keys file to container environment:
|
||||
```yml
|
||||
environment:
|
||||
# ... other variables here ...
|
||||
API_KEY_URL: "file:///keys.json"
|
||||
```
|
||||
|
||||
4. restart the docker container.
|
||||
|
||||
## limit access to an instance with api keys but no turnstile
|
||||
by default, api keys are additional, meaning that they're not *required*,
|
||||
but work alongside with turnstile or no auth (regular ip hash rate limiting).
|
||||
|
||||
to always require auth (via keys or turnstile, if configured), set `API_AUTH_REQUIRED` to 1:
|
||||
```yml
|
||||
environment:
|
||||
# ... other variables here ...
|
||||
API_AUTH_REQUIRED: 1
|
||||
```
|
||||
|
||||
- if both keys and turnstile are enabled, then nothing will change.
|
||||
- if only keys are configured, then all requests without a valid api key will be refused.
|
||||
|
||||
### why not make keys exclusive by default?
|
||||
keys may be useful for going around rate limiting,
|
||||
while keeping the rest of api rate limited, with no turnstile in place.
|
|
@ -1,4 +1,4 @@
|
|||
# how to host a cobalt instance yourself
|
||||
# how to run a cobalt instance
|
||||
## using docker compose and package from github (recommended)
|
||||
to run the cobalt docker package, you need to have `docker` and `docker-compose` installed and configured.
|
||||
|
||||
|
@ -54,8 +54,7 @@ sudo apt install nscd
|
|||
sudo service nscd start
|
||||
```
|
||||
|
||||
## list of all environment variables
|
||||
### variables for api
|
||||
## list of environment variables for api
|
||||
| variable name | default | example | description |
|
||||
|:----------------------|:----------|:------------------------|:------------|
|
||||
| `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. |
|
||||
|
@ -89,7 +88,7 @@ requests it makes for that particular download. to use freebind in cobalt, you n
|
|||
in a docker container, you also need to set the `API_LISTEN_ADDRESS` env to `127.0.0.1`, and set
|
||||
`network_mode` for the container to `host`.
|
||||
|
||||
#### api key file format
|
||||
## api key file format
|
||||
the file is a JSON-serialized object with the following structure:
|
||||
```typescript
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
# self-troubleshooting cobalt
|
||||
```
|
||||
🚧 this page is work-in-progress. expect more guides to be added in the future!
|
||||
```
|
||||
if any issues occur while using cobalt, you can fix many of them yourself. this document aims to provide guides on how to fix most complicated of them.
|
||||
use wiki navigation on right to jump between solutions.
|
||||
|
||||
## how to fix clipboard pasting in older versions of firefox
|
||||
```
|
||||
🎉 firefox finally supports pasting by default starting from version 125.
|
||||
|
||||
👍 you don't need to follow this tutorial if you're on the latest version of firefox.
|
||||
```
|
||||
you can fix this issue by changing a single preference in `about:config`.
|
||||
|
||||
### steps to enable clipboard functionality
|
||||
1. go to `about:config`:
|
||||
![screenshot showing about:config entered into address bar](images/troubleshooting/clipboard/config.png)
|
||||
|
||||
2. if asked, read what firefox has to say and press "accept the risk and continue".
|
||||
⚠ tinkering with other preferences may break your browser. **do not** edit them unless you know what you're doing.
|
||||
|
||||
![screenshot showing about:config security warning that reads: "proceed with caution. changing advanced configuration preferences can impact firefox performance or security." lower there's a pre-checked checkbox that says: "warn me when i attempt to access these preferences". lowest element is a blue button that says "accept the risk and continue"](images/troubleshooting/clipboard/risk.png)
|
||||
|
||||
3. search for `dom.events.asyncClipboard.readText`
|
||||
|
||||
![screenshot showing "dom.events.asyncclipboard.readtext" entered into search on about:config page](images/troubleshooting/clipboard/search.png)
|
||||
|
||||
4. press the toggle button on very right.
|
||||
|
||||
![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page with highlighted toggle button on very right](images/troubleshooting/clipboard/toggle.png)
|
||||
|
||||
5. "false" should change to "true".
|
||||
|
||||
![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](images/troubleshooting/clipboard/toggled.png)
|
||||
|
||||
6. go back to cobalt, reload the page, press `paste` button again. this time it works! enjoy simpler downloading experience :)
|
|
@ -48,9 +48,10 @@ fair use and credits benefit everyone.
|
|||
sectionId="abuse"
|
||||
/>
|
||||
|
||||
we have no way of detecting abusive behavior automatically, as cobalt is 100% anonymous.
|
||||
however, you can report such activities to us and we will do our best to comply manually: [safety@imput.net](mailto:safety@imput.net)
|
||||
we have no way of detecting abusive behavior automatically because cobalt is 100% anonymous.
|
||||
however, you can report such activities to us and we will do our best to comply manually: **safety@imput.net**
|
||||
|
||||
**this email is not intended for user support, you will not get a response if your concern is not related to abuse.**
|
||||
|
||||
please note that this email is not intended for user support.
|
||||
if you're experiencing issues, contact us via any preferred method on [the support page](/about/community).
|
||||
</section>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"video.quality.description": "if preferred video quality isn't available, next best is picked instead.",
|
||||
|
||||
"video.youtube.codec": "youtube video codec and container",
|
||||
"video.youtube.codec.description": "h264: best compatibility, average bitrate. max quality is 1080p. \nav1: best quality, efficiency, and bitrate. supports 8k & HDR. \nvp9: same quality & bitrate as av1, but file is approximately two times bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264.",
|
||||
"video.youtube.codec.description": "h264: best compatibility, average quality. max quality is 1080p. \nav1: best quality and efficiency. supports 8k & HDR. \nvp9: same quality as av1, but file is ~2x bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264. if av1 or vp9 isn't available, h264 is used instead.",
|
||||
|
||||
"video.twitter.gif": "twitter/x",
|
||||
"video.twitter.gif.title": "convert looping videos to GIF",
|
||||
|
@ -91,7 +91,7 @@
|
|||
"language.auto.title": "automatic selection",
|
||||
"language.auto.description": "cobalt will use your browser's default language if translation is available. if not, english will be used instead.",
|
||||
"language.preferred.title": "preferred language",
|
||||
"language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.\n\nwe use community-sourced translations for languages other than english, russian, and czech. they may be inaccurate or incomplete.",
|
||||
"language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.\n\nsome languages use community-sourced translations, they may be inaccurate or incomplete.",
|
||||
|
||||
"privacy.analytics": "anonymous traffic analytics",
|
||||
"privacy.analytics.title": "don't contribute to analytics",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
import { killDialog } from "$lib/dialogs";
|
||||
import { killDialog } from "$lib/state/dialogs";
|
||||
|
||||
import DialogBackdropClose from "$components/dialog/DialogBackdropClose.svelte";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import dialogs from "$lib/dialogs";
|
||||
import dialogs from "$lib/state/dialogs";
|
||||
|
||||
import SmallDialog from "$components/dialog/SmallDialog.svelte";
|
||||
import PickerDialog from "$components/dialog/PickerDialog.svelte";
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { t } from "$lib/i18n/translations";
|
||||
|
||||
import dialogs from "$lib/dialogs";
|
||||
import dialogs from "$lib/state/dialogs";
|
||||
|
||||
import { link } from "$lib/state/omnibox";
|
||||
import { cachedInfo } from "$lib/api/server-info";
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import API from "$lib/api/api";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
import { downloadFile } from "$lib/download";
|
||||
|
||||
import type { DialogInfo } from "$lib/types/dialog";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
import {
|
||||
storedSettings,
|
||||
updateSetting,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
import { resetSettings } from "$lib/state/settings";
|
||||
|
||||
import IconTrash from "@tabler/icons-svelte/IconTrash.svelte";
|
||||
|
|
|
@ -4,7 +4,7 @@ import env from "$lib/env";
|
|||
import { t } from "$lib/i18n/translations";
|
||||
import settings, { updateSetting } from "$lib/state/settings";
|
||||
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
|
||||
export const apiOverrideWarning = async () => {
|
||||
if (!env.DEFAULT_API || get(settings).processing.seenOverrideWarning) {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import turnstile from "$lib/api/turnstile";
|
||||
import { writable, get } from "svelte/store";
|
||||
import { currentApiURL } from "$lib/api/api-url";
|
||||
|
||||
import type { CobaltSession, CobaltErrorResponse, CobaltSessionResponse } from "$lib/types/api";
|
||||
|
||||
const cachedSession = writable<CobaltSession | undefined>();
|
||||
let cache: CobaltSession | undefined;
|
||||
|
||||
export const requestSession = async() => {
|
||||
export const requestSession = async () => {
|
||||
const apiEndpoint = `${currentApiURL()}/session`;
|
||||
|
||||
let requestHeaders = {};
|
||||
|
@ -43,7 +42,6 @@ export const requestSession = async() => {
|
|||
|
||||
export const getSession = async () => {
|
||||
const currentTime = () => Math.floor(new Date().getTime() / 1000);
|
||||
const cache = get(cachedSession);
|
||||
|
||||
if (cache?.token && cache?.exp - 2 > currentTime()) {
|
||||
return cache;
|
||||
|
@ -60,7 +58,7 @@ export const getSession = async () => {
|
|||
|
||||
if (!("status" in newSession)) {
|
||||
newSession.exp = currentTime() + newSession.exp;
|
||||
cachedSession.set(newSession);
|
||||
cache = newSession;
|
||||
}
|
||||
return newSession;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import settings from "$lib/state/settings";
|
|||
|
||||
import { device } from "$lib/device";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
|
||||
import type { DialogInfo } from "$lib/types/dialog";
|
||||
import type { CobaltFileUrlType } from "$lib/types/api";
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { beforeNavigate, goto } from "$app/navigation";
|
||||
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { createDialog } from "$lib/state/dialogs";
|
||||
import { downloadFile } from "$lib/download";
|
||||
|
||||
import Skeleton from "$components/misc/Skeleton.svelte";
|
||||
|
|
|
@ -1,25 +1,60 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { version } from "$lib/version";
|
||||
import { device, app } from "$lib/device";
|
||||
import { defaultNavPage } from "$lib/subnav";
|
||||
import settings, { storedSettings } from "$lib/state/settings";
|
||||
|
||||
import SectionHeading from "$components/misc/SectionHeading.svelte";
|
||||
import { type Readable, type Unsubscriber } from "svelte/store";
|
||||
|
||||
const stateSubscribers: Record<string, Unsubscriber> = {};
|
||||
let states: Record<string, unknown> = {};
|
||||
|
||||
$: sections = [
|
||||
{ title: "device", data: device },
|
||||
{ title: "app", data: app },
|
||||
{ title: "settings", data: $storedSettings },
|
||||
{ title: "version", data: $version },
|
||||
{ title: "states", data: states }
|
||||
];
|
||||
|
||||
const loadStates = () => {
|
||||
const modules = import.meta.glob("/src/lib/*/*.ts");
|
||||
const excluded = new Set(['translations.translations', 'settings']);
|
||||
|
||||
Object.entries(modules).map(async ([ name, _import ]) => {
|
||||
const moduleName = name.split('/').pop()?.split('.').shift();
|
||||
|
||||
const module = await _import() as Record<string, unknown>;
|
||||
for (const key in module) {
|
||||
const _export = module[key] as unknown as Readable<unknown>;
|
||||
if (typeof _export === 'object' && 'subscribe' in _export) {
|
||||
const name = moduleName + (key === 'default' ? '' : `.${key}`);
|
||||
if (excluded.has(name)) continue;
|
||||
|
||||
stateSubscribers[name] = _export.subscribe((value) => {
|
||||
states = {
|
||||
...states,
|
||||
[name]: value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!$settings.advanced.debug) {
|
||||
goto(defaultNavPage("settings"), { replaceState: true });
|
||||
}
|
||||
|
||||
loadStates();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
Object.values(stateSubscribers).map(unsub => unsub());
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $settings.advanced.debug}
|
||||
|
|