7.11: cache encryption, meowbalt, dailymotion, bilibili, and much more! (#381)

This commit is contained in:
wukko 2024-03-07 00:43:43 +06:00 committed by GitHub
commit 0a457adf62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 727 additions and 261 deletions

View file

@ -13,7 +13,8 @@ this list is not final and keeps expanding over time. if support for a service y
| service | video + audio | only audio | only video | metadata | rich file names | | service | video + audio | only audio | only video | metadata | rich file names |
| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: | | :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
| bilibili.com | ✅ | ✅ | ✅ | | | | bilibili.com & bilibili.tv | ✅ | ✅ | ✅ | | |
| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ |
| instagram posts & stories | ✅ | ✅ | ✅ | | | | instagram posts & stories | ✅ | ✅ | ✅ | | |
| instagram reels | ✅ | ✅ | ✅ | | | | instagram reels | ✅ | ✅ | ✅ | | |
| ok video | ✅ | ❌ | ❌ | ✅ | ✅ | | ok video | ✅ | ❌ | ❌ | ✅ | ✅ |
@ -68,14 +69,23 @@ cobalt is ***NOT*** a piracy tool and cannot be used as such. it can only downlo
cobalt is my passion project, update schedule depends solely on my free time, motivation, and mood. don't expect any consistency in update releases. cobalt is my passion project, update schedule depends solely on my free time, motivation, and mood. don't expect any consistency in update releases.
## cobalt licenses ## cobalt license
cobalt code is licensed under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE). cobalt code is licensed under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).
update banners and various assets of cobalt branding included within the repo are *not* covered by the AGPL-3.0 license and cannot be used using same terms. cobalt branding, mascots, and other related assets included in the repo are ***copyrighted*** and not covered by the AGPL-3.0 license. you ***cannot*** use them under same terms.
you are allowed to host an ***unmodified*** instance of cobalt with branding, but this ***does not*** give you permission to use it anywhere else, or make derivatives of it in any way.
### notes:
- mascots and other assets are a part of the branding.
- when making an alternative version of the project, please replace or remove all branding (including the name).
- you **must** link the original repo when using any parts of code (such as using separate processing modules in your project) or forking the project.
- if you make a modified version of cobalt, the codebase **must** be published under the same license (according to AGPL-3.0).
## 3rd party licenses ## 3rd party licenses
[Fluent Emoji by Microsoft](https://github.com/microsoft/fluentui-emoji) (used in cobalt) is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE) license. - [Fluent Emoji by Microsoft](https://github.com/microsoft/fluentui-emoji) (used in cobalt) is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE) license.
- [Noto Sans Mono](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/) fonts (used in cobalt) are licensed under the [OFL](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/about) license.
[Noto Sans Mono](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/) fonts (used in cobalt) are licensed under the [OFL](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/about) license. - many update banners were taken from [tenor.com](https://tenor.com/).
many update banners were taken from [tenor.com](https://tenor.com/).

View file

@ -54,15 +54,12 @@ item type: `object`
| `thumb` | `string` | item thumbnail that's displayed in the picker | used only for `video` type. | | `thumb` | `string` | item thumbnail that's displayed in the picker | used only for `video` type. |
## GET: `/api/stream` ## GET: `/api/stream`
cobalt's live render (or stream) endpoint. used for sending various media content over to the user. cobalt's live render (or stream) endpoint. usually, you will receive a url to this endpoint
from a successful call to `/api/json`. however, the parameters passed to it are **opaque**
and **unmodifiable** from your (the api client's) perspective, and can change between versions.
### request query variables therefore you don't need to worry about what they mean - but if you really want to know, you can
| key | variables | description | [read the source code](../src/modules/stream/manage.js).
|:-----|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------|
| `p` | `1` | used for probing whether user is rate limited. |
| `t` | stream token | unique stream id. used for retrieving cached stream info data. |
| `h` | hmac | hashed combination of: (hashed) ip address, stream token, expiry timestamp, and service name. used for verification of stream. |
| `e` | expiry timestamp | |
## GET: `/api/serverInfo` ## GET: `/api/serverInfo`
returns current basic server info. returns current basic server info.

View file

@ -1,5 +1,8 @@
{ {
"instagram": [ "instagram": [
"mid=replace; ig_did=this; csrftoken=cookie" "mid=<replace>; ig_did=<with>; csrftoken=<your>; ds_user_id=<own>; sessionid=<cookies>"
],
"reddit": [
"client_id=<replace_this>; client_secret=<replace_this>; refresh_token=<replace_this>"
] ]
} }

View file

@ -13,17 +13,17 @@ services:
ports: ports:
- 9000:9000/tcp - 9000:9000/tcp
# if you're using a reverse proxy, uncomment the next line: # if you're using a reverse proxy, uncomment the next line and remove the one above (9000:9000/tcp):
#- 127.0.0.1:9000:9000 #- 127.0.0.1:9000:9000
environment: environment:
# replace apiURL with your instance's target url in same format # replace https://co.wuk.sh/ with your instance's target url in same format
- apiURL=https://co.wuk.sh/ - API_URL=https://co.wuk.sh/
# replace apiName with your instance's distinctive name # replace eu-nl with your instance's distinctive name
- apiName=eu-nl - API_NAME=eu-nl
# if you want to use cookies when fetching data from services, uncomment the next line # if you want to use cookies when fetching data from services, uncomment the next line
#- cookiePath=/cookies.json #- COOKIE_PATH=/cookies.json
# see cookies_example.json for example file. # see cookies.example.json for example file.
labels: labels:
- com.centurylinklabs.watchtower.scope=cobalt - com.centurylinklabs.watchtower.scope=cobalt
@ -43,14 +43,14 @@ services:
ports: ports:
- 9001:9001/tcp - 9001:9001/tcp
# if you're using a reverse proxy, uncomment the next line: # if you're using a reverse proxy, uncomment the next line and remove the one above (9001:9001/tcp):
#- 127.0.0.1:9001:9001 #- 127.0.0.1:9001:9001
environment: environment:
# replace webURL with your instance's target url in same format # replace https://cobalt.tools/ with your instance's target url in same format
- webURL=https://cobalt.tools/ - WEB_URL=https://cobalt.tools/
# replace apiURL with preferred api instance url # replace https://co.wuk.sh/ with preferred api instance url
- apiURL=https://co.wuk.sh/ - API_URL=https://co.wuk.sh/
labels: labels:
- com.centurylinklabs.watchtower.scope=cobalt - com.centurylinklabs.watchtower.scope=cobalt

View file

@ -47,3 +47,25 @@ setup script installs all needed `npm` dependencies, but you have to install `no
sudo apt install nscd sudo apt install nscd
sudo service nscd start sudo service nscd start
``` ```
## list of all environment variables
### variables for api
| variable name | default | example | description |
|:----------------------|:----------|:------------------------|:------------|
| `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. |
| `API_URL` | | `https://co.wuk.sh/` | changes url from which api server is accessible. <br> ***REQUIRED TO RUN API***. |
| `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. |
| `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing. <br> `0`: disabled. `1`: enabled. |
| `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. |
| `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. |
| `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. |
\* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)).
### variables for web
| variable name | default | example | description |
|:--------------- |:--------|:------------------------|:--------------------------------------------------------------------------------------|
| `WEB_PORT` | `9001` | `9001` | changes port from which frontend server is accessible. |
| `WEB_URL` | | `https://cobalt.tools/` | changes url from which frontend server is accessible. <br> ***REQUIRED TO RUN WEB***. |
| `SHOW_SPONSORS` | `0` | `1` | toggles sponsor list in about popup. <br> `0`: disabled. `1`: enabled. |
| `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo. <br> `0`: disabled. `1`: enabled. |

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "7.10.4", "version": "7.11",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",
@ -37,9 +37,9 @@
"ipaddr.js": "2.1.0", "ipaddr.js": "2.1.0",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"psl": "1.9.0", "psl": "https://github.com/lupomontero/psl#5eadae91361d8289d582700f90582b0d0cb73155",
"set-cookie-parser": "2.6.0", "set-cookie-parser": "2.6.0",
"undici": "^5.19.1", "undici": "^6.7.0",
"url-pattern": "1.0.3", "url-pattern": "1.0.3",
"youtubei.js": "^9.1.0" "youtubei.js": "^9.1.0"
} }

View file

@ -1,4 +1,5 @@
import "dotenv/config"; import "dotenv/config";
import "./modules/sub/alias-envs.js";
import express from "express"; import express from "express";
@ -21,8 +22,8 @@ app.disable('x-powered-by');
await loadLoc(); await loadLoc();
const apiMode = process.env.apiURL && !process.env.webURL; const apiMode = process.env.API_URL && !process.env.WEB_URL;
const webMode = process.env.webURL && process.env.apiURL; const webMode = process.env.WEB_URL && process.env.API_URL;
if (apiMode) { if (apiMode) {
const { runAPI } = await import('./core/api.js'); const { runAPI } = await import('./core/api.js');

View file

@ -54,7 +54,8 @@
} }
}, },
"links": { "links": {
"saveToGalleryShortcut": "https://www.icloud.com/shortcuts/b401917928fd407daf1db0fd07eb7e78", "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/14e9aebf04b24156acc34ceccf7e6fcd",
"saveToFilesShortcut": "https://www.icloud.com/shortcuts/2134cd9d4d6b41448b2201f933542b2e",
"statusPage": "https://status.cobalt.tools/", "statusPage": "https://status.cobalt.tools/",
"troubleshootingGuide": "https://github.com/wukko/cobalt/blob/current/docs/troubleshooting.md" "troubleshootingGuide": "https://github.com/wukko/cobalt/blob/current/docs/troubleshooting.md"
}, },

View file

@ -10,11 +10,11 @@ import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/util
import { Bright, Cyan } from "../modules/sub/consoleText.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js"; import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js"; import loc from "../localization/manager.js";
import { sha256 } from "../modules/sub/crypto.js"; import { generateHmac } from "../modules/sub/crypto.js";
import { verifyStream } from "../modules/stream/manage.js"; import { verifyStream } from "../modules/stream/manage.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) { export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.cors === '0' ? { const corsConfig = process.env.CORS_WILDCARD === '0' ? {
origin: process.env.CORS_URL, origin: process.env.CORS_URL,
optionsSuccessStatus: 200 optionsSuccessStatus: 200
} : {}; } : {};
@ -24,7 +24,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
max: 20, max: 20,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: req => sha256(getIP(req), ipSalt), keyGenerator: req => generateHmac(getIP(req), ipSalt),
handler: (req, res, next, opt) => { handler: (req, res, next, opt) => {
return res.status(429).json({ return res.status(429).json({
"status": "rate-limit", "status": "rate-limit",
@ -37,7 +37,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
max: 25, max: 25,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: req => sha256(getIP(req), ipSalt), keyGenerator: req => generateHmac(getIP(req), ipSalt),
handler: (req, res, next, opt) => { handler: (req, res, next, opt) => {
return res.status(429).json({ return res.status(429).json({
"status": "rate-limit", "status": "rate-limit",
@ -47,11 +47,15 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
}); });
const startTime = new Date(); const startTime = new Date();
const startTimestamp = Math.floor(startTime.getTime()); const startTimestamp = startTime.getTime();
app.set('trust proxy', ['loopback', 'uniquelocal']); app.set('trust proxy', ['loopback', 'uniquelocal']);
app.use('/api/:type', cors(corsConfig)); app.use('/api/:type', cors({
methods: ['GET', 'POST'],
...corsConfig
}));
app.use('/api/json', apiLimiter); app.use('/api/json', apiLimiter);
app.use('/api/stream', apiLimiterStream); app.use('/api/stream', apiLimiterStream);
app.use('/api/onDemand', apiLimiter); app.use('/api/onDemand', apiLimiter);
@ -60,6 +64,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') } try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next(); next();
}); });
app.use('/api/json', express.json({ app.use('/api/json', express.json({
verify: (req, res, buf) => { verify: (req, res, buf) => {
let acceptCon = String(req.header('Accept')) === "application/json"; let acceptCon = String(req.header('Accept')) === "application/json";
@ -71,6 +76,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
} }
} }
})); }));
// handle express.json errors properly (https://github.com/expressjs/express/issues/4065) // handle express.json errors properly (https://github.com/expressjs/express/issues/4065)
app.use('/api/json', (err, req, res, next) => { app.use('/api/json', (err, req, res, next) => {
let errorText = "invalid json body"; let errorText = "invalid json body";
@ -86,6 +92,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
next(); next();
} }
}); });
app.post('/api/json', async (req, res) => { app.post('/api/json', async (req, res) => {
try { try {
let lang = languageCode(req); let lang = languageCode(req);
@ -118,13 +125,17 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
try { try {
switch (req.params.type) { switch (req.params.type) {
case 'stream': case 'stream':
if (req.query.t && req.query.h && req.query.e && req.query.t.toString().length === 21 const q = req.query;
&& req.query.h.toString().length === 64 && req.query.e.toString().length === 13) { const checkQueries = q.t && q.e && q.h && q.s && q.i;
let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e); const checkBaseLength = q.t.length === 21 && q.e.length === 13;
const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22;
if (checkQueries && checkBaseLength && checkSafeLength) {
let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i);
if (streamInfo.error) { if (streamInfo.error) {
return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
} }
if (req.query.p) { if (q.p) {
return res.status(200).json({ return res.status(200).json({
status: "continue" status: "continue"
}); });
@ -132,7 +143,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
return stream(res, streamInfo); return stream(res, streamInfo);
} else { } else {
let j = apiJSON(0, { let j = apiJSON(0, {
t: "stream token, hmac, or expiry timestamp is missing" t: "bad request. stream link may be incomplete or corrupted."
}) })
return res.status(j.status).json(j.body); return res.status(j.status).json(j.body);
} }
@ -141,9 +152,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
version: version, version: version,
commit: gitCommit, commit: gitCommit,
branch: gitBranch, branch: gitBranch,
name: process.env.apiName || "unknown", name: process.env.API_NAME || "unknown",
url: process.env.apiURL, url: process.env.API_URL,
cors: process.env?.cors === "0" ? 0 : 1, cors: process.env?.CORS_WILDCARD === "0" ? 0 : 1,
startTime: `${startTimestamp}` startTime: `${startTimestamp}`
}); });
default: default:
@ -159,22 +170,25 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
}); });
} }
}); });
app.get('/api/status', (req, res) => { app.get('/api/status', (req, res) => {
res.status(200).end() res.status(200).end()
}); });
app.get('/favicon.ico', (req, res) => { app.get('/favicon.ico', (req, res) => {
res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) res.sendFile(`${__dirname}/src/front/icons/favicon.ico`)
}); });
app.get('/*', (req, res) => { app.get('/*', (req, res) => {
res.redirect('/api/json') res.redirect('/api/json')
}); });
app.listen(process.env.apiPort || 9000, () => { app.listen(process.env.API_PORT || 9000, () => {
console.log(`\n` + console.log(`\n` +
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.apiURL}`)}\n` + `URL: ${Cyan(`${process.env.API_URL}`)}\n` +
`Port: ${process.env.apiPort || 9000}\n` `Port: ${process.env.API_PORT || 9000}\n`
) )
}); });
} }

View file

@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
return res.redirect('/') return res.redirect('/')
}); });
app.listen(process.env.webPort || 9001, () => { app.listen(process.env.WEB_PORT || 9001, () => {
console.log(`\n` + console.log(`\n` +
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.webURL}`)}\n` + `URL: ${Cyan(`${process.env.WEB_URL}`)}\n` +
`Port: ${process.env.webPort || 9001}\n` `Port: ${process.env.WEB_PORT || 9001}\n`
) )
}) })
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -192,7 +192,7 @@ input[type="text"],
z-index: -1; z-index: -1;
position: absolute; position: absolute;
border: var(--accent-highlight) solid 0.15rem; border: var(--accent-highlight) solid 0.15rem;
border-radius: 8px/9px; border-radius: 22px;
} }
.desktop button:hover, .desktop button:hover,
.desktop .switch:hover, .desktop .switch:hover,
@ -441,11 +441,17 @@ button:active,
-webkit-backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
} }
.popup.small { .popup.small {
width: 20%; width: 21rem;
box-shadow: 0px 0px 60px 0px var(--accent-hover); box-shadow: 0px 0px 60px 0px var(--accent-hover);
padding: 1.7rem; padding: 18px;
transform: translate(-50%,-50%)scale(.95); transform: translate(-50%,-50%)scale(.95);
pointer-events: all; pointer-events: all;
border-radius: 22px;
}
.popup.small .popup-content-inner {
display: flex;
flex-direction: column;
gap: 18px;
} }
.popup.small.visible { .popup.small.visible {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
@ -462,12 +468,30 @@ button:active,
.popup.small .popup-title { .popup.small .popup-title {
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
} }
.popup.small .explanation {
margin-bottom: 0.9rem;
}
.popup.small .close-error.switch { .popup.small .close-error.switch {
background: var(--accent)!important; background: var(--accent)!important;
color: var(--background); color: var(--background);
height: 2.5rem;
}
#popup-error,
#popup-download {
display: flex;
flex-direction: column;
padding-top: 4rem;
}
#popup-error {
justify-content: center;
align-items: center;
}
.popout-meowbalt {
position: absolute;
top: -7rem;
user-select: none;
-webkit-user-select: none;
pointer-events: none;
height: 180px;
width: 180px;
aspect-ratio: 1/1;
} }
.popup.scrollable { .popup.scrollable {
height: 95%; height: 95%;
@ -531,7 +555,8 @@ button:active,
-webkit-user-select: text; -webkit-user-select: text;
} }
.desc-error { .desc-error {
padding-bottom: 1.5rem; padding-bottom: 0rem;
text-align: center;
} }
.popup-title { .popup-title {
font-size: 1.5rem; font-size: 1.5rem;
@ -957,44 +982,43 @@ button:active,
.changelog-img, .changelog-img,
.changelog-banner, .changelog-banner,
.close-error, .close-error,
.changelog-tag-version,
#download-switcher .switch, #download-switcher .switch,
#popup-about .switch, #popup-about .switch,
.popup-tabs .switch, .popup-tabs .switch,
.text-to-copy, .text-to-copy,
.text-to-copy.text-backdrop, .text-to-copy.text-backdrop,
#filename-preview { #filename-preview {
border-radius: 6px / 7px; border-radius: 8px / 9px;
} }
[type=checkbox] { [type=checkbox] {
border-radius: 3px / 4px; border-radius: 3px / 4px;
} }
.popup, .popup,
.scrollable .popup-content { .scrollable .popup-content {
border-radius: 8px; border-radius: 12px;
} }
.popup-header .glass-bkg { .popup-header .glass-bkg {
border-top-left-radius: 8px 9px; border-top-left-radius: 11px 12px;
border-top-right-radius: 8px 9px; border-top-right-radius: 11px 12px;
border-bottom: var(--accent-highlight) solid 0.1rem; border-bottom: var(--accent-highlight) solid 0.1rem;
top: -1px; top: -1px;
} }
.popup-tabs .glass-bkg { .popup-tabs .glass-bkg {
border-bottom-left-radius: 8px 9px; border-bottom-left-radius: 11px 12px;
border-bottom-right-radius: 8px 9px; border-bottom-right-radius: 11px 12px;
border-top: var(--accent-highlight) solid 0.1rem; border-top: var(--accent-highlight) solid 0.1rem;
bottom: -1px; bottom: -1px;
} }
.switches :first-child { .switches .switch:first-child {
border-top-left-radius: 6px 7px; border-top-left-radius: 8px 9px;
border-bottom-left-radius: 6px 7px; border-bottom-left-radius: 8px 9px;
} }
.switches :last-child { .switches .switch:last-child {
border-top-right-radius: 6px 7px; border-top-right-radius: 8px 9px;
border-bottom-right-radius: 6px 7px; border-bottom-right-radius: 8px 9px;
} }
.text-backdrop { .text-backdrop {
border-radius: 3px / 4px; border-radius: 4px / 5px;
} }
.collapse-list:first-child, .collapse-list:first-child,
.collapse-list:first-child .collapse-header { .collapse-list:first-child .collapse-header {
@ -1017,17 +1041,11 @@ button:active,
} }
/* adapt the page according to screen size */ /* adapt the page according to screen size */
@media screen and (max-width: 1550px) { @media screen and (max-width: 1550px) {
.popup.small {
width: 25%
}
.popup { .popup {
width: 40%; width: 40%;
} }
} }
@media screen and (max-width: 1440px) { @media screen and (max-width: 1440px) {
.popup.small {
width: 30%
}
.popup { .popup {
width: 45%; width: 45%;
} }
@ -1038,17 +1056,11 @@ button:active,
} }
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.popup.small {
width: 35%
}
.popup { .popup {
width: 55%; width: 55%;
} }
} }
@media screen and (max-width: 1025px) { @media screen and (max-width: 1025px) {
.popup.small {
width: 40%
}
.popup { .popup {
width: 60%; width: 60%;
} }
@ -1058,6 +1070,16 @@ button:active,
width: 75%; width: 75%;
} }
} }
@media screen and (max-width: 680px) {
.popup {
width: 90%;
}
}
@media screen and (max-width: 660px) {
#cobalt-main-box {
width: calc(100% - (0.7rem * 2));
}
}
/* mobile page */ /* mobile page */
@media screen and (max-width: 499px) { @media screen and (max-width: 499px) {
.tab { .tab {
@ -1070,10 +1092,7 @@ button:active,
width: calc(100% - 1.3rem); width: calc(100% - 1.3rem);
} }
} }
@media screen and (max-width: 660px) { @media screen and (max-width: 535px) {
#cobalt-main-box {
width: calc(100% - (0.7rem * 2));
}
#cobalt-main-box #bottom { #cobalt-main-box #bottom {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
@ -1128,7 +1147,7 @@ button:active,
transform: unset; transform: unset;
} }
.popup.small { .popup.small {
width: calc(100% - 1.7rem * 2); width: calc(100% - 18px * 2);
height: auto; height: auto;
top: unset; top: unset;
bottom: 0; bottom: 0;
@ -1143,8 +1162,8 @@ button:active,
border-top: var(--accent-highlight) solid 0.15rem; border-top: var(--accent-highlight) solid 0.15rem;
} }
.popup.small.visible { .popup.small.visible {
transform: none; transform: translateY(0rem);
transition: transform 210ms cubic-bezier(0.062, 0.82, 0.165, 1), opacity 130ms ease-in-out; transition: transform 250ms cubic-bezier(0.075, 0.82, 0.165, 1), opacity 130ms ease-in-out;
} }
.popup.small .popup-header { .popup.small .popup-header {
background: none; background: none;

View file

@ -1,4 +1,4 @@
const version = 41; const version = 42;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isIOS = ua.match("iphone os"); const isIOS = ua.match("iphone os");
@ -8,7 +8,7 @@ const isFirefox = ua.match("firefox/");
const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103; const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103;
const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
const notification = `<div class="notification-dot"></div>`; const notification = `<span class="notification-dot"></span>`;
const switchers = { const switchers = {
"theme": ["auto", "light", "dark"], "theme": ["auto", "light", "dark"],
@ -600,15 +600,11 @@ window.onload = () => {
if (setUn !== null) { if (setUn !== null) {
if (setUn) { if (setUn) {
sSet("migrated", "true") sSet("migrated", "true")
eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferSuccess}`
} else {
eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferError}`
} }
} }
} }
loadSettings(); loadSettings();
detectColorScheme(); detectColorScheme();
popup("migration", 1);
} }
window.history.replaceState(null, '', window.location.pathname); window.history.replaceState(null, '', window.location.pathname);

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -16,7 +16,6 @@
"AccessibilityOpenDonate": "open donation popup", "AccessibilityOpenDonate": "open donation popup",
"TitlePopupAbout": "what's cobalt?", "TitlePopupAbout": "what's cobalt?",
"TitlePopupSettings": "settings", "TitlePopupSettings": "settings",
"TitlePopupError": "uh-oh...",
"TitlePopupChangelog": "what's new?", "TitlePopupChangelog": "what's new?",
"TitlePopupDonate": "support cobalt", "TitlePopupDonate": "support cobalt",
"TitlePopupDownload": "how to save?", "TitlePopupDownload": "how to save?",
@ -46,7 +45,7 @@
"AccessibilityEnableDownloadPopup": "ask what to do with downloads", "AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"NoScriptMessage": "cobalt uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", "NoScriptMessage": "cobalt 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 link\" 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\".", "DownloadPopupDescriptionIOS": "how to save to photos:\n1. add <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">save to photos shortcut</a>.\n2. press \"share\" button above this text.\n3. select \"save to photos\" in the share sheet.\n\nhow to save to files:\n1. add <a class=\"text-backdrop link\" href=\"{saveToFilesShortcut}\" target=\"_blank\">save to files shortcut</a>.\n2. press \"share\" button above this text.\n3. select \"save to files\" in the share sheet.\n4. select a folder to save the file to and press \"open\".\n\nboth shortcuts can only be used from the cobalt web app.",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"ClickToCopy": "press to copy", "ClickToCopy": "press to copy",
"Download": "download", "Download": "download",
@ -91,7 +90,7 @@
"ChangelogPressToHide": "collapse", "ChangelogPressToHide": "collapse",
"Donate": "donate", "Donate": "donate",
"DonateSub": "help it stay online", "DonateSub": "help it stay online",
"DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, and thus is <span class=\"text-backdrop\">completely free to use for everyone</span>. but development and maintenance of a media-heavy service used by over 350k people is quite costly. both in terms of time and money. as a student, it's rather difficult for me to handle such expenses on my own.\n\nif cobalt has helped you in the past and you want to keep it growing and evolving, you can do so by making a donation!\n\nby donating you're helping everyone who uses cobalt: teachers, students, musicians, content creators, artists, lecturers, and many, many more!\n\nin past few months donations have let me:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open cobalt api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; move to a reliable and trustworthy cloud infrastructure provider.\n*; separate frontend and api for resilience and future decentralization.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!", "DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, meaning that it's <span class=\"text-backdrop\">completely free to use</span> for everyone. but development and maintenance of a media-heavy service used by over 750k people is quite costly. both in terms of time and money.\n\nif cobalt helped you in the past and you want to keep it growing and evolving, you can return the favor by making a donation!\n\nyour donation will help all cobalt users: educators, students, content creators, artists, musicians, and many, many more!\n\nin past, donations have let cobalt:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open the api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; add resource-intensive features (such as gif conversion).\n*; continue improving our infrastructure.\n*; keep developers happy.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!\n\nif you can't donate, share cobalt with a friend! we don't get ads anywhere, so cobalt is spread by word of mouth.\nsharing is the easiest way to help achieve the goal of better internet for everyone.",
"DonateVia": "donate via", "DonateVia": "donate via",
"DonateHireMe": "...or you can <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">hire me</a> :)", "DonateHireMe": "...or you can <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
"SettingsVideoMute": "mute audio", "SettingsVideoMute": "mute audio",
@ -103,7 +102,7 @@
"ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!", "ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!",
"FollowSupport": "keep in touch with cobalt for news, support, and more:", "FollowSupport": "keep in touch with cobalt for news, support, and more:",
"SourceCode": "explore source code, report issues, star or fork the repo:", "SourceCode": "explore source code, report issues, star or fork the repo:",
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.", "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
"ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!",
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api acts unexpectedly. try again or try another settings.", "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api acts unexpectedly. try again or try another settings.",
"SettingsCodecSubtitle": "youtube codec", "SettingsCodecSubtitle": "youtube codec",
@ -155,8 +154,8 @@
"DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly", "DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly",
"SettingsTwitterGif": "convert gifs to .gif", "SettingsTwitterGif": "convert gifs to .gif",
"SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.", "SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.",
"UpdateTwitterGif": "twitter gifs and pinterest",
"ErrorTweetProtected": "this tweet is from a private account, so i can't see it. try another one!", "ErrorTweetProtected": "this tweet is from a private account, so i can't see it. try another one!",
"ErrorTweetNSFW": "this tweet contains sensitive content, so i can't see it. try another one!" "ErrorTweetNSFW": "this tweet contains sensitive content, so i can't see it. try another one!",
"UpdateEncryption": "encryption and new services"
} }
} }

View file

@ -16,7 +16,6 @@
"AccessibilityOpenDonate": "сделать пожертвование", "AccessibilityOpenDonate": "сделать пожертвование",
"TitlePopupAbout": "что за кобальт?", "TitlePopupAbout": "что за кобальт?",
"TitlePopupSettings": "настройки", "TitlePopupSettings": "настройки",
"TitlePopupError": "опаньки...",
"TitlePopupChangelog": "что нового?", "TitlePopupChangelog": "что нового?",
"TitlePopupDonate": "поддержи кобальт", "TitlePopupDonate": "поддержи кобальт",
"TitlePopupDownload": "как сохранить?", "TitlePopupDownload": "как сохранить?",
@ -46,7 +45,7 @@
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", "NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescriptionIOS": "как сохранить в фото:\n1. добавь этот сценарий siri: <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">save to photos</a>.\n2. нажми \"поделиться\" выше этого текста.\n3. выбери \"save to photos\" в открывшемся окне.\n\nкак сохранить в файлы:\n1. добавь этот сценарий siri: <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">save to files</a>.\n2. нажми \"поделиться\" выше этого текста.\n3. выбери \"save to files\" в открывшемся окне.\n4. выбери папку для сохранения файла и нажми \"открыть\".\n\nоба сценария работают только вместе с веб-приложением кобальта.",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
"ClickToCopy": "нажми, чтобы скопировать", "ClickToCopy": "нажми, чтобы скопировать",
"Download": "скачать", "Download": "скачать",
@ -92,7 +91,7 @@
"ChangelogPressToHide": "скрыть", "ChangelogPressToHide": "скрыть",
"Donate": "донаты", "Donate": "донаты",
"DonateSub": "ты можешь помочь!", "DonateSub": "ты можешь помочь!",
"DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно для всех</span>. но разработка и поддержка медиа сервиса, которым пользуются более 350 тысяч людей, обходится довольно затратно. мне, как студенту, оплачивать такое в одиночку довольно трудно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал работать и развиваться, то это можно сделать через донаты!\n\nделая донат ты помогаешь всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nза последние несколько месяцев благодаря донатам я смог:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api кобальта для свободного публичного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; перейти к надёжному поставщику облачной инфры.\n*; разделить фронтенд и api для обеспечения отказоустойчивости и децентрализации в будущем.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!", "DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span> для всех. но разработка и поддержка медиа сервиса, которым пользуются более 750 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
"DonateVia": "открыть", "DonateVia": "открыть",
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)", "DonateHireMe": "...или же ты можешь <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
"SettingsVideoMute": "убрать аудио", "SettingsVideoMute": "убрать аудио",
@ -104,7 +103,7 @@
"ServicesNote": "этот список далеко не финальный и постоянно пополняется, заглядывай сюда почаще!", "ServicesNote": "этот список далеко не финальный и постоянно пополняется, заглядывай сюда почаще!",
"FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:", "FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:",
"SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:", "SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">90 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется рендер, то зашифрованные данные о ней временно хранятся в ОЗУ сервера. это необходимо для работы данной функции.\n\nзашифрованные данные хранятся в течение <span class=\"text-backdrop\">90 секунд</span> и затем безвозвратно удаляются.\n\ncохранённые данные можно расшифровать только с помощью уникальных ключей шифрования из твоей ссылки на скачивание. кроме того, официальная кодовая база кобальта не предусматривает возможности чтения эти данные вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube", "SettingsCodecSubtitle": "кодек для видео с youtube",
@ -157,8 +156,8 @@
"DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы", "DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
"SettingsTwitterGif": "конвертировать гифки в .gif", "SettingsTwitterGif": "конвертировать гифки в .gif",
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.", "SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
"UpdateTwitterGif": "гифки с твиттера и одноклассники",
"ErrorTweetProtected": "этот твит из закрытого аккаунта, поэтому я не могу его увидеть. попробуй другой!", "ErrorTweetProtected": "этот твит из закрытого аккаунта, поэтому я не могу его увидеть. попробуй другой!",
"ErrorTweetNSFW": "этот твит содержит деликатный контент, поэтому я не могу его увидеть. попробуй другой!" "ErrorTweetNSFW": "этот твит содержит деликатный контент, поэтому я не могу его увидеть. попробуй другой!",
"UpdateEncryption": "шифрование и новые сервисы"
} }
} }

View file

@ -19,6 +19,7 @@ export function replaceBase(s) {
return s return s
.replace(/\n/g, '<br>') .replace(/\n/g, '<br>')
.replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut) .replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut)
.replace(/{saveToFilesShortcut}/g, links.saveToFilesShortcut)
.replace(/{repo}/g, repo) .replace(/{repo}/g, repo)
.replace(/{statusPage}/g, links.statusPage) .replace(/{statusPage}/g, links.statusPage)
.replace(/\*;/g, "&bull;"); .replace(/\*;/g, "&bull;");

View file

@ -1,5 +1,17 @@
{ {
"current": { "current": {
"version": "7.11",
"date": "March 6, 2024",
"title": "cache encryption, meowbalt, dailymotion, bilibili, and much more!",
"banner": {
"file": "meowth7eleven.webp",
"alt": "meowth plush in front of 7-eleven store",
"width": 850,
"height": 640
},
"content": "cobalt may not have as many groceries as 7-eleven, but it sure does have lots of big changes in this update!\n\n*; all cached stream info is now encrypted and can only be decrypted with a link you get from cobalt.\n*; new popup style featuring meowbalt, cobalt's speedy mascot. you will see him more often from now on!\n*; added support for dailymotion (including short links).\n*; added support for bilibili.tv, fixed support for bilibili.com, and added support for all related short links.\n*; added support for private vimeo links.\n*; added support for tumblr audio and revamped the entire module.\n*; added support for embed ok.ru links.\n\nwe also updated the privacy policy to reflect the addition of data encryption, go check it out.\n\nfor people with iphones:\n*; clearer ios saving tutorial.\n*; added \"save to files\" ios shortcut.\n*; updated save to photos shortcut.\n\nmake sure to save both shortcuts and read the updated tutorial!\n\nfor people who host a cobalt instance:\n*; updated all environment variables TO_BE_LIKE_THIS. time to update your configs! for now cobalt is backwards compatible with old variable names, but it won't last forever.\n*; added a list of all environment variables and their descriptions to <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/run-an-instance.md#list-of-all-environment-variables\" target=\"_blank\">run-an-instance doc</a>.\n*; updated <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/examples/cookies.example.json\" target=\"_blank\">cookie file example</a> with more services and improved examples.\n*; updated <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/examples/docker-compose.example.yml\" target=\"_blank\">docker compose example</a> with better explanations and up-to-date env variable samples.\n*; updated some packages to get rid of all unnecessary messages in console.\n\nwant to host an instance? <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/run-an-instance.md\" target=\"_blank\">learn how to do it here</a>.\n\nfrontend changes:\n*; removed migration popup.\n*; corners across ui are even more round now.\n*; bottom glass bkg in popups is no longer rounded on top right.\n*; small popup no longer stretches like gum, it's fixed in size on desktop.\n*; small popup animation no longer lags on mobile.\n*; better ui scaling across resolutions.\n*; updated donation text.\n\nthank you for using cobalt, all 750k of you. hope you like this update as much as we enjoyed making it :D"
},
"history": [{
"version": "7.9", "version": "7.9",
"date": "January 17, 2024", "date": "January 17, 2024",
"title": "twitter gifs, pinterest, ok.ru, and more!", "title": "twitter gifs, pinterest, ok.ru, and more!",
@ -10,8 +22,7 @@
"height": 350 "height": 350
}, },
"content": "yes, you read that right. cobalt now lets you convert any twitter gif to an actual .gif file! (finally)\njust go to settings and enable this feature :)\n\nservice improvements:\n*; added an option to <a class=\"text-backdrop link\" href=\"{repo}/issues/250\" target=\"_blank\">convert gifs from twitter</a> into actual .gif format. files will be bigger and lower quality, but maybe you want that.\n*; pinterest support has been completely redone, now all videos (<a class=\"text-backdrop link\" href=\"{repo}/issues/160\" target=\"_blank\">and even pin.it links</a>) are supported.\n*; added <a class=\"text-backdrop link\" href=\"{repo}/issues/322\" target=\"_blank\">support for ok.ru</a> in case you're a russian grandma.\n*; now processing <a class=\"text-backdrop link\" href=\"{repo}/issues/318\" target=\"_blank\">all reddit links</a> (including old.reddit.com).\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/316\" target=\"_blank\">instagram live vods</a> are now supported.\n*; fixed a <a class=\"text-backdrop link\" href=\"{repo}/issues/289\" target=\"_blank\">rare vimeo bug</a> related to 1440p videos.\n\nother improvements:\n*; ui fade in animation is no longer present if you've disabled animations.\n*; all images now have alt descriptions.\n*; cobalt html is now <a class=\"text-backdrop link\" href=\"{repo}/issues/317\" target=\"_blank\">biblically correct</a> and follows the html spec.\n*; lots of cleaning up.\n\npatches since 7.8:\n*; shift+key <a class=\"text-backdrop link\" href=\"{repo}/issues/288\" target=\"_blank\">shortcuts are now ignored</a> if url bar is focused.\n*; longer soundcloud links are now supported, also catching more tiktok-related errors.\n*; removed mastodon from support links as that account is no longer active.\n*; added ability to download a specific video from multi media tweets and support for /mediaViewer links.\n*; fixed <a class=\"text-backdrop link\" href=\"{repo}/issues/309\" target=\"_blank\">modal blurriness</a> in chromium.\n*; minor html changes (road to biblically correct one).\n\nlots of long-awaited updates (especially twitter gifs), hope you enjoy them and have a great day :D" "content": "yes, you read that right. cobalt now lets you convert any twitter gif to an actual .gif file! (finally)\njust go to settings and enable this feature :)\n\nservice improvements:\n*; added an option to <a class=\"text-backdrop link\" href=\"{repo}/issues/250\" target=\"_blank\">convert gifs from twitter</a> into actual .gif format. files will be bigger and lower quality, but maybe you want that.\n*; pinterest support has been completely redone, now all videos (<a class=\"text-backdrop link\" href=\"{repo}/issues/160\" target=\"_blank\">and even pin.it links</a>) are supported.\n*; added <a class=\"text-backdrop link\" href=\"{repo}/issues/322\" target=\"_blank\">support for ok.ru</a> in case you're a russian grandma.\n*; now processing <a class=\"text-backdrop link\" href=\"{repo}/issues/318\" target=\"_blank\">all reddit links</a> (including old.reddit.com).\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/316\" target=\"_blank\">instagram live vods</a> are now supported.\n*; fixed a <a class=\"text-backdrop link\" href=\"{repo}/issues/289\" target=\"_blank\">rare vimeo bug</a> related to 1440p videos.\n\nother improvements:\n*; ui fade in animation is no longer present if you've disabled animations.\n*; all images now have alt descriptions.\n*; cobalt html is now <a class=\"text-backdrop link\" href=\"{repo}/issues/317\" target=\"_blank\">biblically correct</a> and follows the html spec.\n*; lots of cleaning up.\n\npatches since 7.8:\n*; shift+key <a class=\"text-backdrop link\" href=\"{repo}/issues/288\" target=\"_blank\">shortcuts are now ignored</a> if url bar is focused.\n*; longer soundcloud links are now supported, also catching more tiktok-related errors.\n*; removed mastodon from support links as that account is no longer active.\n*; added ability to download a specific video from multi media tweets and support for /mediaViewer links.\n*; fixed <a class=\"text-backdrop link\" href=\"{repo}/issues/309\" target=\"_blank\">modal blurriness</a> in chromium.\n*; minor html changes (road to biblically correct one).\n\nlots of long-awaited updates (especially twitter gifs), hope you enjoy them and have a great day :D"
}, }, {
"history": [{
"version": "7.8", "version": "7.8",
"date": "December 25, 2023", "date": "December 25, 2023",
"title": "new years clean up! bug fixes and fresh look for the home page", "title": "new years clean up! bug fixes and fresh look for the home page",

View file

@ -69,15 +69,17 @@ export function popup(obj) {
} }
return ` return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box" : ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''} ${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box" : ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''}
<div class="popup-header"> ${obj.buttonOnly ? obj.header.emoji : ``}
<div class="popup-header-contents"> ${obj.name === "error" ? `` :
${obj.buttonOnly ? obj.header.emoji : ``} `<div class="popup-header">
${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} <div class="popup-header-contents">
${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''} ${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
</div> ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''} </div>
</div> ${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''}
</div>`
}
<div class="popup-content popup-content-inner"> <div class="popup-content popup-content-inner">
${body}${obj.buttonOnly ? `<button class="close-error switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''} ${body}${obj.buttonOnly ? `<button class="close-error switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div> </div>
@ -264,5 +266,5 @@ export function sponsoredList() {
} }
export function betaTag() { export function betaTag() {
return process.env.isBeta ? '<span class="logo-sub">β</span>' : '' return process.env.IS_BETA ? '<span class="logo-sub">β</span>' : ''
} }

View file

@ -48,10 +48,10 @@ export default function(obj) {
<title>${t("AppTitleCobalt")}</title> <title>${t("AppTitleCobalt")}</title>
<meta property="og:url" content="${process.env.webURL}"> <meta property="og:url" content="${process.env.WEB_URL}">
<meta property="og:title" content="${t("AppTitleCobalt")}"> <meta property="og:title" content="${t("AppTitleCobalt")}">
<meta property="og:description" content="${t('EmbedBriefDescription')}"> <meta property="og:description" content="${t('EmbedBriefDescription')}">
<meta property="og:image" content="${process.env.webURL}icons/generic.png"> <meta property="og:image" content="${process.env.WEB_URL}icons/generic.png">
<meta name="title" content="${t("AppTitleCobalt")}"> <meta name="title" content="${t("AppTitleCobalt")}">
<meta name="description" content="${t('AboutSummary')}"> <meta name="description" content="${t('AboutSummary')}">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
@ -68,10 +68,12 @@ export default function(obj) {
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">
<link rel="preload" href="fonts/notosansmono.css" as="style">
<link rel="stylesheet" href="fonts/notosansmono.css"> <link rel="stylesheet" href="fonts/notosansmono.css">
<link rel="stylesheet" href="cobalt.css"> <link rel="stylesheet" href="cobalt.css">
<link rel="preload" href="fonts/notosansmono.css" as="style">
<link rel="preload" href="assets/meowbalt/error.png" as="image">
<link rel="preload" href="assets/meowbalt/question.png" as="image">
</head> </head>
<body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''}> <body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''}>
<noscript> <noscript>
@ -165,7 +167,7 @@ export default function(obj) {
body: t("FairUse") body: t("FairUse")
}]) }])
}, },
...(process.env.showSponsors ? ...(process.env.SHOW_SPONSORS ?
[{ [{
text: t("SponsoredBy"), text: t("SponsoredBy"),
classes: ["sponsored-by-text"], classes: ["sponsored-by-text"],
@ -531,7 +533,9 @@ export default function(obj) {
classes: ["small"], classes: ["small"],
header: { header: {
closeAria: t('AccessibilityGoBack'), closeAria: t('AccessibilityGoBack'),
emoji: emoji("🐱", 78, 1, 1), emoji: `<img class="popout-meowbalt" `
+ `draggable="false" loading="lazy" `
+ `alt="😿" src="assets/meowbalt/question.png">`,
title: t('TitlePopupDownload') title: t('TitlePopupDownload')
}, },
body: switcher({ body: switcher({
@ -551,33 +555,19 @@ export default function(obj) {
buttonOnly: true, buttonOnly: true,
classes: ["small"], classes: ["small"],
header: { header: {
title: t('TitlePopupError'), emoji: `<img class="popout-meowbalt" `
emoji: emoji("😿", 78, 1, 1), + `draggable="false" loading="lazy" `
+ `alt="😿" src="assets/meowbalt/error.png">`,
}, },
body: `<div id="desc-error" class="desc-padding subtext desc-error"></div>`, body: `<div id="desc-error" class="desc-padding subtext desc-error"></div>`,
buttonText: t('ErrorPopupCloseButton') buttonText: t('ErrorPopupCloseButton')
})} })}
</div> </div>
<div id="popup-migration-container" class="popup-from-bottom">
${popup({
name: "migration",
standalone: true,
buttonOnly: true,
classes: ["small"],
header: {
title: t('NewDomainWelcomeTitle'),
emoji: emoji("😸", 78, 1, 1),
},
body: `<div id="desc-migration" class="desc-padding subtext desc-error">${t('NewDomainWelcome')}</div>`,
buttonText: t('ErrorPopupCloseButton')
})}
<div id="popup-backdrop-message" onclick="popup('message', 0)"></div>
</div>
<div id="popup-backdrop" onclick="hideAllPopups()"></div> <div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home" style="visibility:hidden"> <div id="home" style="visibility:hidden">
${urgentNotice({ ${urgentNotice({
emoji: "🎬", emoji: "🔒",
text: t("UpdateTwitterGif"), text: t("UpdateEncryption"),
visible: true, visible: true,
action: "popup('about', 1, 'changelog')" action: "popup('about', 1, 'changelog')"
})} })}
@ -627,7 +617,7 @@ export default function(obj) {
</footer> </footer>
</div> </div>
<script> <script>
let defaultApiUrl = '${process.env.apiURL ? process.env.apiURL : ''}'; let defaultApiUrl = '${process.env.API_URL || ''}';
const loc = ${webLoc(t, const loc = ${webLoc(t,
[ [
'ErrorNoInternet', 'ErrorNoInternet',

View file

@ -3,7 +3,7 @@ import { readFile, writeFile } from 'fs/promises';
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
const WRITE_INTERVAL = 60000, const WRITE_INTERVAL = 60000,
cookiePath = process.env.cookiePath, cookiePath = process.env.COOKIE_PATH,
COUNTER = Symbol('counter'); COUNTER = Symbol('counter');
let cookies = {}, dirty = false, intervalId; let cookies = {}, dirty = false, intervalId;

View file

@ -24,6 +24,7 @@ import pinterest from "./services/pinterest.js";
import streamable from "./services/streamable.js"; import streamable from "./services/streamable.js";
import twitch from "./services/twitch.js"; import twitch from "./services/twitch.js";
import rutube from "./services/rutube.js"; import rutube from "./services/rutube.js";
import dailymotion from "./services/dailymotion.js";
export default async function(host, patternMatch, url, lang, obj) { export default async function(host, patternMatch, url, lang, obj) {
assert(url instanceof URL); assert(url instanceof URL);
@ -56,9 +57,7 @@ export default async function(host, patternMatch, url, lang, obj) {
}); });
break; break;
case "bilibili": case "bilibili":
r = await bilibili({ r = await bilibili(patternMatch);
id: patternMatch.id.slice(0, 12)
});
break; break;
case "youtube": case "youtube":
let fetchInfo = { let fetchInfo = {
@ -105,6 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
case "vimeo": case "vimeo":
r = await vimeo({ r = await vimeo({
id: patternMatch.id.slice(0, 11), id: patternMatch.id.slice(0, 11),
password: patternMatch.password,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash forceDash: isAudioOnly ? true : obj.vimeoDash
@ -158,6 +158,9 @@ export default async function(host, patternMatch, url, lang, obj) {
isAudioOnly: isAudioOnly isAudioOnly: isAudioOnly
}); });
break; break;
case "dailymotion":
r = await dailymotion(patternMatch);
break;
default: default:
return apiJSON(0, { t: errorUnsupported(lang) }); return apiJSON(0, { t: errorUnsupported(lang) });
} }

View file

@ -143,7 +143,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]); const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]);
const isTikTok = host === "tiktok" || host === "douyin"; const isTikTok = host === "tiktok" || host === "douyin";
const isTumblr = host === "tumblr" && !r.filename; const isTumblrAudio = host === "tumblr" && !r.filename;
const isSoundCloud = host === "soundcloud"; const isSoundCloud = host === "soundcloud";
if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) { if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) {
@ -168,11 +168,6 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
} }
} }
if (isTumblr && isBestOrMp3) {
audioFormat = "mp3";
processType = "bridge"
}
if (isBestAudioDefined || isBestHostAudio) { if (isBestAudioDefined || isBestHostAudio) {
audioFormat = services[host]["bestAudio"]; audioFormat = services[host]["bestAudio"];
processType = "bridge"; processType = "bridge";
@ -181,6 +176,11 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
copy = true copy = true
} }
if (isTumblrAudio && isBestOrMp3) {
audioFormat = "mp3";
processType = "bridge"
}
if (r.isM3U8 || host === "vimeo") { if (r.isM3U8 || host === "vimeo") {
copy = false; copy = false;
processType = "render" processType = "render"

View file

@ -1,27 +1,105 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js"; import { genericUserAgent, maxVideoDuration } from "../../config.js";
// TO-DO: quality picking, bilibili.tv support, and higher quality downloads (currently requires an account) // TO-DO: higher quality downloads (currently requires an account)
export default async function(obj) {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, { function com_resolveShortlink(shortId) {
return fetch(`https://b23.tv/${shortId}`, { redirect: 'manual' })
.then(r => r.status > 300 && r.status < 400 && r.headers.get('location'))
.then(url => {
if (!url) return;
const path = new URL(url).pathname;
if (path.startsWith('/video/'))
return path.split('/')[2];
})
.catch(() => {})
}
function getBest(content) {
return content?.filter(v => v.baseUrl || v.url)
.map(v => (v.baseUrl = v.baseUrl || v.url, v))
.reduce((a, b) => a?.bandwidth > b?.bandwidth ? a : b);
}
function extractBestQuality(dashData) {
const bestVideo = getBest(dashData.video),
bestAudio = getBest(dashData.audio);
if (!bestVideo || !bestAudio) return [];
return [ bestVideo, bestAudio ];
}
async function com_download(id) {
let html = await fetch(`https://bilibili.com/video/${id}`, {
headers: { "user-agent": genericUserAgent } headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (!(html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"'))) return { error: 'ErrorEmptyDownload' };
if (!(html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"'))) {
return { error: 'ErrorEmptyDownload' };
}
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]); let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; if (streamData.data.timelength > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
let video = streamData["data"]["dash"]["video"].filter(v => const [ video, audio ] = extractBestQuality(streamData.data.dash);
!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/") if (!video || !audio) {
).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); return { error: 'ErrorEmptyDownload' };
}
let audio = streamData["data"]["dash"]["audio"].filter(a =>
!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
return { return {
urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], urls: [video.baseUrl, audio.baseUrl],
audioFilename: `bilibili_${obj.id}_audio`, audioFilename: `bilibili_${id}_audio`,
filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` filename: `bilibili_${id}_${video.width}x${video.height}.mp4`
}; };
} }
async function tv_download(id) {
const url = new URL(
'https://api.bilibili.tv/intl/gateway/web/playurl'
+ '?s_locale=en_US&platform=web&qn=64&type=0&device=wap'
+ '&tf=0&spm_id=bstar-web.ugc-video-detail.0.0&from_spm_id='
);
url.searchParams.set('aid', id);
const { data } = await fetch(url).then(a => a.json());
if (!data?.playurl?.video) {
return { error: 'ErrorEmptyDownload' };
}
const [ video, audio ] = extractBestQuality({
video: data.playurl.video.map(s => s.video_resource)
.filter(s => s.codecs.includes('avc1')),
audio: data.playurl.audio_resource
});
if (!video || !audio) {
return { error: 'ErrorEmptyDownload' };
}
if (video.duration > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
return {
urls: [video.url, audio.url],
audioFilename: `bilibili_tv_${id}_audio`,
filename: `bilibili_tv_${id}.mp4`
};
}
export default async function({ comId, tvId, comShortLink }) {
if (comShortLink) {
comId = await com_resolveShortlink(comShortLink);
}
if (comId) {
return com_download(comId);
} else if (tvId) {
return tv_download(tvId);
}
return { error: 'ErrorCouldntFetch' };
}

View file

@ -0,0 +1,107 @@
import HLSParser from 'hls-parser';
import { maxVideoDuration } from '../../config.js';
let _token;
function getExp(token) {
return JSON.parse(
Buffer.from(token.split('.')[1], 'base64')
).exp * 1000;
}
const getToken = async () => {
if (_token && getExp(_token) > new Date().getTime()) {
return _token;
}
const req = await fetch('https://graphql.api.dailymotion.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0',
'Authorization': 'Basic MGQyZDgyNjQwOWFmOWU3MmRiNWQ6ODcxNmJmYTVjYmEwMmUwMGJkYTVmYTg1NTliNDIwMzQ3NzIyYWMzYQ=='
},
body: 'traffic_segment=&grant_type=client_credentials'
}).then(r => r.json()).catch(() => {});
if (req.access_token) {
return _token = req.access_token;
}
}
export default async function({ id }) {
const token = await getToken();
if (!token) return { error: 'ErrorSomethingWentWrong' };
const req = await fetch('https://graphql.api.dailymotion.com/',
{
method: 'POST',
headers: {
'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-DM-AppInfo-Version': '7.16.0_240213162706',
'X-DM-AppInfo-Type': 'iosapp',
'X-DM-AppInfo-Id': 'com.dailymotion.dailymotion'
},
body: JSON.stringify({
operationName: "Media",
query: `
query Media($xid: String!, $password: String) {
media(xid: $xid, password: $password) {
__typename
... on Video {
xid
hlsURL
duration
title
channel {
displayName
}
}
}
}
`,
variables: { xid: id }
})
}
).then(r => r.status === 200 && r.json()).catch(() => {});
const media = req?.data?.media;
if (media?.__typename !== 'Video' || !media.hlsURL) {
return { error: 'ErrorEmptyDownload' }
}
if (media.duration * 1000 > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
const manifest = await fetch(media.hlsURL).then(r => r.text()).catch(() => {});
if (!manifest) return { error: 'ErrorSomethingWentWrong' };
const bestQuality = HLSParser.parse(manifest).variants
.filter(v => v.codecs.includes('avc1'))
.reduce((a, b) => a.bandwidth > b.bandwidth ? a : b);
if (!bestQuality) return { error: 'ErrorEmptyDownload' }
const fileMetadata = {
title: media.title,
artist: media.channel.displayName
}
return {
urls: bestQuality.uri,
isM3U8: true,
filenameAttributes: {
service: 'dailymotion',
id: media.xid,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
qualityLabel: `${bestQuality.resolution.height}p`,
extension: 'mp4'
},
fileMetadata
}
}

View file

@ -1,8 +1,26 @@
import psl from "psl"; import psl from "psl";
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
export default async function(obj) { const API_KEY = 'jrsCWX1XDuVxAFO4GkK147syAoN8BJZ5voz8tS80bPcj26Vc5Z';
let { subdomain } = psl.parse(obj.url.hostname); const API_BASE = 'https://api-http2.tumblr.com';
function request(domain, id) {
const url = new URL(`/v2/blog/${domain}/posts/${id}/permalink`, API_BASE);
url.searchParams.set('api_key', API_KEY);
url.searchParams.set('fields[blogs]', 'uuid,name,avatar,?description,?can_message,?can_be_followed,?is_adult,?reply_conditions,'
+ '?theme,?title,?url,?is_blocked_from_primary,?placement_id,?primary,?updated,?followed,'
+ '?ask,?can_subscribe,?paywall_access,?subscription_plan,?is_blogless_advertiser,?tumblrmart_accessories');
return fetch(url, {
headers: {
'User-Agent': 'Tumblr/iPhone/33.3/333010/17.3.1/tumblr',
'X-Version': 'iPhone/33.3/333010/17.3.1/tumblr'
}
}).then(a => a.json()).catch(() => {});
}
export default async function(input) {
let { subdomain } = psl.parse(input.url.hostname);
if (subdomain?.includes('.')) { if (subdomain?.includes('.')) {
return { error: ['ErrorBrokenLink', 'tumblr'] } return { error: ['ErrorBrokenLink', 'tumblr'] }
@ -10,26 +28,44 @@ export default async function(obj) {
subdomain = undefined subdomain = undefined
} }
let html = await fetch(`https://${subdomain ?? obj.user}.tumblr.com/post/${obj.id}`, { const domain = `${subdomain ?? input.user}.tumblr.com`;
headers: { "user-agent": genericUserAgent } const data = await request(domain, input.id);
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; const element = data?.response?.timeline?.elements?.[0];
if (!element) return { error: 'ErrorEmptyDownload' };
let r; const contents = [
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) { ...element.content,
r = { ...element?.trail?.map(t => t.content).flat()
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` const audio = contents.find(c => c.type === 'audio');
} if (audio && audio.provider === 'tumblr') {
} else if (html.includes('property="og:audio" content="https://a.tumblr.com/')) { const fileMetadata = {
r = { title: audio?.title,
urls: `https://a.tumblr.com/${html.split('property="og:audio" content="https://a.tumblr.com/')[1].split('"')[0]}`, artist: audio?.artist
audioFilename: `tumblr_${obj.id}`, };
return {
urls: audio.media.url,
filenameAttributes: {
service: 'tumblr',
id: input.id,
title: fileMetadata.title,
author: fileMetadata.artist
},
isAudioOnly: true isAudioOnly: true
} }
} else r = { error: 'ErrorEmptyDownload' }; }
return r const video = contents.find(c => c.type === 'video');
if (video && video.provider === 'tumblr') {
return {
urls: video.media.url,
filename: `tumblr_${input.id}.mp4`,
audioFilename: `tumblr_${input.id}_audio`
}
}
return { error: 'ErrorEmptyDownload' }
} }

View file

@ -104,7 +104,7 @@ export default async function({ id, index, toGif }) {
const baseTweet = tweet.data.tweetResult.result.legacy, const baseTweet = tweet.data.tweetResult.result.legacy,
repostedTweet = baseTweet.retweeted_status_result?.result.legacy.extended_entities; repostedTweet = baseTweet.retweeted_status_result?.result.legacy.extended_entities;
let media = (repostedTweet?.media || baseTweet.extended_entities.media); let media = (repostedTweet?.media || baseTweet?.extended_entities?.media);
media = media?.filter(m => m.video_info?.variants?.length); media = media?.filter(m => m.video_info?.variants?.length);
// check if there's a video at given index (/video/<index>) // check if there's a video at given index (/video/<index>)

View file

@ -28,7 +28,14 @@ export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality; let quality = obj.quality === "max" ? "9000" : obj.quality;
if (!quality || obj.isAudioOnly) quality = "9000"; if (!quality || obj.isAudioOnly) quality = "9000";
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false }); const url = new URL(`https://player.vimeo.com/video/${obj.id}/config`);
if (obj.password) {
url.searchParams.set('h', obj.password);
}
let api = await fetch(url)
.then(r => r.json())
.catch(() => {});
if (!api) return { error: 'ErrorCouldntFetch' }; if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "dash"; let downloadType = "dash";
@ -71,6 +78,7 @@ export default async function(obj) {
} }
let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`; let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`;
const fallbackResolution = bestVideo.height > bestVideo.width ? bestVideo.width : bestVideo.height;
return { return {
urls: masterM3U8, urls: masterM3U8,
@ -81,8 +89,8 @@ export default async function(obj) {
id: obj.id, id: obj.id,
title: fileMetadata.title, title: fileMetadata.title,
author: fileMetadata.artist, author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`, resolution: `${bestVideo.width}x${bestVideo.height}`,
qualityLabel: `${resolutionMatch[bestVideo["width"]]}p`, qualityLabel: `${resolutionMatch[bestVideo.width] || fallbackResolution}p`,
extension: "mp4" extension: "mp4"
} }
} }

View file

@ -2,8 +2,11 @@
"audioIgnore": ["vk", "ok"], "audioIgnore": ["vk", "ok"],
"config": { "config": {
"bilibili": { "bilibili": {
"alias": "bilibili.com videos", "alias": "bilibili.com & bilibili.tv",
"patterns": ["video/:id"], "patterns": [
"video/:comId", "_shortLink/:comShortLink",
"_tv/:lang/video/:tvId", "_tv/video/:tvId"
],
"enabled": true "enabled": true
}, },
"reddit": { "reddit": {
@ -32,7 +35,7 @@
"ok": { "ok": {
"alias": "ok video", "alias": "ok video",
"tld": "ru", "tld": "ru",
"patterns": ["video/:id"], "patterns": ["video/:id", "videoembed/:id"],
"enabled": true "enabled": true
}, },
"youtube": { "youtube": {
@ -43,6 +46,7 @@
"enabled": true "enabled": true
}, },
"tumblr": { "tumblr": {
"alias": "tumblr video & audio",
"patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"], "patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"],
"subdomains": "*", "subdomains": "*",
"enabled": true "enabled": true
@ -61,7 +65,7 @@
"enabled": false "enabled": false
}, },
"vimeo": { "vimeo": {
"patterns": [":id", "video/:id"], "patterns": [":id", "video/:id", ":id/:password"],
"enabled": true, "enabled": true,
"bestAudio": "mp3" "bestAudio": "mp3"
}, },
@ -106,6 +110,11 @@
"tld": "ru", "tld": "ru",
"patterns": ["video/:id", "play/embed/:id"], "patterns": ["video/:id", "play/embed/:id"],
"enabled": true "enabled": true
},
"dailymotion": {
"alias": "dailymotion videos",
"patterns": ["video/:id"],
"enabled": true
} }
} }
} }

View file

@ -1,6 +1,9 @@
export const testers = { export const testers = {
"bilibili": (patternMatch) => "bilibili": (patternMatch) =>
patternMatch.id?.length <= 12, patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16
|| patternMatch.tvId?.length <= 24,
"dailymotion": (patternMatch) => patternMatch.id?.length <= 32,
"instagram": (patternMatch) => "instagram": (patternMatch) =>
patternMatch.postId?.length <= 12 patternMatch.postId?.length <= 12
@ -39,7 +42,8 @@ export const testers = {
patternMatch.id?.length < 20, patternMatch.id?.length < 20,
"vimeo": (patternMatch) => "vimeo": (patternMatch) =>
patternMatch.id?.length <= 11, patternMatch.id?.length <= 11
&& (!patternMatch.password || patternMatch.password.length < 16),
"vine": (patternMatch) => "vine": (patternMatch) =>
patternMatch.id?.length <= 12, patternMatch.id?.length <= 12,

View file

@ -16,6 +16,7 @@ export function aliasURL(url) {
url.search = `?v=${encodeURIComponent(parts[2])}` url.search = `?v=${encodeURIComponent(parts[2])}`
} }
break; break;
case "youtu": case "youtu":
if (url.hostname === 'youtu.be' && parts.length >= 2) { if (url.hostname === 'youtu.be' && parts.length >= 2) {
/* youtu.be urls can be weird, e.g. https://youtu.be/<id>//asdasd// still works /* youtu.be urls can be weird, e.g. https://youtu.be/<id>//asdasd// still works
@ -25,6 +26,7 @@ export function aliasURL(url) {
}`) }`)
} }
break; break;
case "pin": case "pin":
if (url.hostname === 'pin.it' && parts.length === 2) { if (url.hostname === 'pin.it' && parts.length === 2) {
url = new URL(`https://pinterest.com/url_shortener/${ url = new URL(`https://pinterest.com/url_shortener/${
@ -46,6 +48,22 @@ export function aliasURL(url) {
url = new URL(`https://twitch.tv/_/clip/${parts[1]}`); url = new URL(`https://twitch.tv/_/clip/${parts[1]}`);
} }
break; break;
case "bilibili":
if (host.tld === 'tv') {
url = new URL(`https://bilibili.com/_tv${url.pathname}`);
}
break;
case "b23":
if (url.hostname === 'b23.tv' && parts.length === 2) {
url = new URL(`https://bilibili.com/_shortLink/${parts[1]}`)
}
break;
case "dai":
if (url.hostname === 'dai.ly' && parts.length === 2) {
url = new URL(`https://dailymotion.com/video/${parts[1]}`)
}
} }
return url return url

View file

@ -39,27 +39,27 @@ function setup() {
console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: co.wuk.sh")); console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: co.wuk.sh"));
rl.question(q, apiURL => { rl.question(q, apiURL => {
ob['apiURL'] = `http://localhost:9000/`; ob.API_URL = `http://localhost:9000/`;
ob['apiPort'] = 9000; ob.API_PORT = 9000;
if (apiURL && apiURL !== "localhost") ob['apiURL'] = `https://${apiURL.toLowerCase()}/`; if (apiURL && apiURL !== "localhost") ob.API_URL = `https://${apiURL.toLowerCase()}/`;
console.log(Bright("\nGreat! Now, what port will it be running on? (9000)")); console.log(Bright("\nGreat! Now, what port will it be running on? (9000)"));
rl.question(q, apiPort => { rl.question(q, apiPort => {
if (apiPort) ob['apiPort'] = apiPort; if (apiPort) ob.API_PORT = apiPort;
if (apiPort && (apiURL === "localhost" || !apiURL)) ob['apiURL'] = `http://localhost:${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)")); 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 => { rl.question(q, apiName => {
ob['apiName'] = apiName.toLowerCase(); ob.API_NAME = apiName.toLowerCase();
if (!apiName || apiName === "local") ob['apiName'] = "local"; 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)")); 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 => { rl.question(q, apiCors => {
let answCors = apiCors.toLowerCase().trim(); let answCors = apiCors.toLowerCase().trim();
if (answCors !== "y" && answCors !== "yes") ob['cors'] = '0' if (answCors !== "y" && answCors !== "yes") ob.CORS_WILDCARD = '0'
final() final()
}) })
}) })
@ -71,25 +71,25 @@ function setup() {
console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: cobalt.tools")); console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: cobalt.tools"));
rl.question(q, webURL => { rl.question(q, webURL => {
ob['webURL'] = `http://localhost:9001/`; ob.WEB_URL = `http://localhost:9001/`;
ob['webPort'] = 9001; ob.WEB_PORT = 9001;
if (webURL && webURL !== "localhost") ob['webURL'] = `https://${webURL.toLowerCase()}/`; if (webURL && webURL !== "localhost") ob.WEB_URL = `https://${webURL.toLowerCase()}/`;
console.log( console.log(
Bright("\nGreat! Now, what port will it be running on? (9001)") Bright("\nGreat! Now, what port will it be running on? (9001)")
) )
rl.question(q, webPort => { rl.question(q, webPort => {
if (webPort) ob['webPort'] = webPort; if (webPort) ob.WEB_PORT = webPort;
if (webPort && (webURL === "localhost" || !webURL)) ob['webURL'] = `http://localhost:${webPort}/`; if (webPort && (webURL === "localhost" || !webURL)) ob.WEB_URL = `http://localhost:${webPort}/`;
console.log( 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") 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 => { rl.question(q, apiURL => {
ob['apiURL'] = `https://${apiURL.toLowerCase()}/`; ob.API_URL = `https://${apiURL.toLowerCase()}/`;
if (apiURL.includes(':')) ob['apiURL'] = `http://${apiURL.toLowerCase()}/`; if (apiURL.includes(':')) ob.API_URL = `http://${apiURL.toLowerCase()}/`;
if (!apiURL) ob['apiURL'] = "https://co.wuk.sh/"; if (!apiURL) ob.API_URL = "https://co.wuk.sh/";
final() final()
}) })
}); });

View file

@ -2,7 +2,7 @@ import NodeCache from "node-cache";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { sha256 } from "../sub/crypto.js"; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
import { streamLifespan } from "../config.js"; import { streamLifespan } from "../config.js";
const streamCache = new NodeCache({ const streamCache = new NodeCache({
@ -15,48 +15,68 @@ streamCache.on("expired", (key) => {
streamCache.del(key); streamCache.del(key);
}) })
const streamSalt = randomBytes(64).toString('hex'); const hmacSalt = randomBytes(64).toString('hex');
export function createStream(obj) { export function createStream(obj) {
let streamID = nanoid(), const streamID = nanoid(),
exp = Math.floor(new Date().getTime()) + streamLifespan, iv = randomBytes(16).toString('base64url'),
ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt); secret = randomBytes(32).toString('base64url'),
exp = new Date().getTime() + streamLifespan,
if (!streamCache.has(streamID)) { hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt),
streamCache.set(streamID, { streamData = {
id: streamID, exp: exp,
service: obj.service,
type: obj.type, type: obj.type,
urls: obj.u, urls: obj.u,
service: obj.service,
filename: obj.filename, filename: obj.filename,
hmac: ghmac,
exp: exp,
isAudioOnly: !!obj.isAudioOnly,
audioFormat: obj.audioFormat, audioFormat: obj.audioFormat,
time: obj.time ? obj.time : false, isAudioOnly: !!obj.isAudioOnly,
copy: !!obj.copy, copy: !!obj.copy,
mute: !!obj.mute, mute: !!obj.mute,
metadata: obj.fileMetadata ? obj.fileMetadata : false metadata: obj.fileMetadata || false
}); };
} else {
let streamInfo = streamCache.get(streamID); streamCache.set(
exp = streamInfo.exp; streamID,
ghmac = streamInfo.hmac; encryptStream(streamData, iv, secret)
)
let streamLink = new URL('/api/stream', process.env.API_URL);
const params = {
't': streamID,
'e': exp,
'h': hmac,
's': secret,
'i': iv
} }
return `${process.env.apiURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
for (const [key, value] of Object.entries(params)) {
streamLink.searchParams.append(key, value);
}
return streamLink.toString();
} }
export function verifyStream(id, hmac, exp) { export function verifyStream(id, hmac, exp, secret, iv) {
try { try {
let streamInfo = streamCache.get(id.toString()); const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt);
if (ghmac !== String(hmac)) {
return {
error: "i couldn't verify if you have access to this stream. go back and try again!",
status: 401
}
}
const streamInfo = JSON.parse(decryptStream(streamCache.get(id.toString()), iv, secret));
if (!streamInfo) return { if (!streamInfo) return {
error: "this download link has expired or doesn't exist. go back and try again!", error: "this download link has expired or doesn't exist. go back and try again!",
status: 400 status: 400
} }
let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt); if (String(exp) === String(streamInfo.exp) && Number(exp) > new Date().getTime()) {
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
&& Number(exp) > Math.floor(new Date().getTime())) {
return streamInfo; return streamInfo;
} }
return { return {
@ -64,6 +84,6 @@ export function verifyStream(id, hmac, exp) {
status: 401 status: 401
} }
} catch (e) { } catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } }; return { status: 500, body: { status: "error", text: "couldn't verify this stream. request a new one!" } };
} }
} }

View file

@ -80,17 +80,33 @@ export async function streamLiveRender(streamInfo, res) {
if (streamInfo.urls.length !== 2) return shutdown(); if (streamInfo.urls.length !== 2) return shutdown();
const { body: audio } = await request(streamInfo.urls[1], { const { body: audio } = await request(streamInfo.urls[1], {
maxRedirections: 16, signal: abortController.signal maxRedirections: 16, signal: abortController.signal,
headers: {
'user-agent': genericUserAgent,
referer: streamInfo.service === 'bilibili'
? 'https://www.bilibili.com/'
: undefined,
}
}); });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
args = [ let args = [
'-loglevel', '-8', '-loglevel', '-8',
'-user_agent', genericUserAgent
];
if (streamInfo.service === 'bilibili') {
args.push(
'-headers', 'Referer: https://www.bilibili.com/\r\n',
)
}
args.push(
'-i', streamInfo.urls[0], '-i', streamInfo.urls[0],
'-i', 'pipe:3', '-i', 'pipe:3',
'-map', '0:v', '-map', '0:v',
'-map', '1:a', '-map', '1:a',
]; );
args = args.concat(ffmpegArgs[format]); args = args.concat(ffmpegArgs[format]);
if (streamInfo.metadata) { if (streamInfo.metadata) {
@ -129,11 +145,16 @@ export function streamAudioOnly(streamInfo, res) {
try { try {
let args = [ let args = [
'-loglevel', '-8' '-loglevel', '-8',
] '-user_agent', genericUserAgent
];
if (streamInfo.service === "twitter") { if (streamInfo.service === "twitter") {
args.push('-seekable', '0') args.push('-seekable', '0');
} else if (streamInfo.service === 'bilibili') {
args.push('-headers', 'Referer: https://www.bilibili.com/\r\n');
} }
args.push( args.push(
'-i', streamInfo.urls, '-i', streamInfo.urls,
'-vn' '-vn'
@ -178,17 +199,23 @@ export function streamVideoOnly(streamInfo, res) {
let args = [ let args = [
'-loglevel', '-8' '-loglevel', '-8'
] ]
if (streamInfo.service === "twitter") { if (streamInfo.service === "twitter") {
args.push('-seekable', '0') args.push('-seekable', '0')
} else if (streamInfo.service === 'bilibili') {
args.push('-headers', 'Referer: https://www.bilibili.com/\r\n')
} }
args.push( args.push(
'-i', streamInfo.urls, '-i', streamInfo.urls,
'-c', 'copy' '-c', 'copy'
) )
if (streamInfo.mute) { if (streamInfo.mute) {
args.push('-an') args.push('-an')
} }
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") {
if (["vimeo", "rutube", "dailymotion"].includes(streamInfo.service)) {
args.push('-bsf:a', 'aac_adtstoasc') args.push('-bsf:a', 'aac_adtstoasc')
} }

View file

@ -0,0 +1,23 @@
import { Red } from "./consoleText.js";
const mapping = {
apiPort: 'API_PORT',
apiURL: 'API_URL',
apiName: 'API_NAME',
cors: 'CORS_WILDCARD',
cookiePath: 'COOKIE_PATH',
webPort: 'WEB_PORT',
webURL: 'WEB_URL',
showSponsors: 'SHOW_SPONSORS',
isBeta: 'IS_BETA'
}
for (const [ oldEnv, newEnv ] of Object.entries(mapping)) {
if (process.env[oldEnv] && !process.env[newEnv]) {
process.env[newEnv] = process.env[oldEnv];
console.error(`${Red('[!]')} ${oldEnv} is deprecated and will be removed in a future version.`);
console.error(` You should use ${newEnv} instead.`);
console.error();
delete process.env[oldEnv];
}
}

View file

@ -1,5 +1,23 @@
import { createHmac } from "crypto"; import { createHmac, createCipheriv, createDecipheriv, scryptSync } from "crypto";
export function sha256(str, salt) { const algorithm = "aes256"
return createHmac("sha256", salt).update(str).digest("hex");
export function generateHmac(str, salt) {
return createHmac("sha256", salt).update(str).digest("base64url");
}
export function encryptStream(plaintext, iv, secret) {
const buff = Buffer.from(JSON.stringify(plaintext));
const key = Buffer.from(secret, "base64url");
const cipher = createCipheriv(algorithm, key, Buffer.from(iv, "base64url"));
return Buffer.concat([ cipher.update(buff), cipher.final() ])
}
export function decryptStream(ciphertext, iv, secret) {
const buff = Buffer.from(ciphertext);
const key = Buffer.from(secret, "base64url");
const decipher = createDecipheriv(algorithm, key, Buffer.from(iv, "base64url"));
return Buffer.concat([ decipher.update(buff), decipher.final() ])
} }

View file

@ -11,7 +11,6 @@ const apiVar = {
}, },
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"] booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
} }
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
export function apiJSON(type, obj) { export function apiJSON(type, obj) {

View file

@ -1,4 +1,5 @@
import "dotenv/config"; import "dotenv/config";
import "../modules/sub/alias-envs.js";
import { getJSON } from "../modules/api.js"; import { getJSON } from "../modules/api.js";
import { services } from "../modules/config.js"; import { services } from "../modules/config.js";

View file

@ -746,6 +746,23 @@
"code": 200, "code": 200,
"status": "stream" "status": "stream"
} }
}, {
"name": "b23.tv shortlink",
"url": "https://b23.tv/lbMyOI9",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
},
{
"name": "bilibili.tv link",
"url": "https://www.bilibili.tv/en/video/4789599404426256",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}], }],
"tumblr": [{ "tumblr": [{
"name": "at.tumblr link", "name": "at.tumblr link",
@ -773,7 +790,7 @@
} }
}, { }, {
"name": "tumblr audio", "name": "tumblr audio",
"url": "https://rf9weu8hjf789234hf9.tumblr.com/post/172006661342/everyone-thats-made-a-video-out-of-this-without", "url": "https://www.tumblr.com/zedneon/737815079301562368/zedneon-ft-mr-sauceman-tech-n9ne-speed-of?source=share",
"params": {}, "params": {},
"expected": { "expected": {
"code": 200, "code": 200,
@ -830,6 +847,14 @@
"code": 200, "code": 200,
"status": "stream" "status": "stream"
} }
}, {
"name": "private video",
"url": "https://vimeo.com/903115595/f14d06da38",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}], }],
"reddit": [{ "reddit": [{
"name": "video with audio", "name": "video with audio",
@ -1180,5 +1205,30 @@
"code": 200, "code": 200,
"status": "stream" "status": "stream"
} }
}],
"dailymotion": [{
"name": "regular video",
"url": "https://www.dailymotion.com/video/x8t1eho",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "private video",
"url": "https://www.dailymotion.com/video/k41fZWpx2TaAORA2nok",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "dai.ly shortened link",
"url": "https://dai.ly/k41fZWpx2TaAORA2nok",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}] }]
} }