Compare commits

..

31 commits

Author SHA1 Message Date
wukko
7d3313b8bc New translations en.json (Polish) 2023-04-09 00:39:17 +06:00
wukko
5e4e72563b New translations en.json (Russian) 2023-04-08 23:18:06 +06:00
wukko
7d8237ae72 New translations en.json (Spanish) 2023-04-07 17:05:44 +06:00
wukko
008c2b7332 New translations en.json (Polish) 2023-04-04 19:57:59 +06:00
wukko
9ba3cb3381 New translations en.json (Polish) 2023-04-04 08:46:55 +06:00
wukko
826c36cf2a New translations en.json (German) 2023-04-04 04:08:42 +06:00
wukko
0afb81fe9b New translations en.json (Spanish) 2023-04-04 03:11:32 +06:00
wukko
88e2decd3a New translations en.json (Spanish) 2023-04-04 02:15:49 +06:00
wukko
57b7717e1e New translations en.json (Russian) 2023-04-03 22:40:13 +06:00
wukko
cd6c97d0b2 New translations en.json (Polish) 2023-03-30 05:06:06 +06:00
wukko
15203d2dc4 New translations en.json (German) 2023-03-29 23:37:24 +06:00
wukko
b03a77ce6c New translations en.json (Russian) 2023-03-29 22:26:45 +06:00
wukko
a19e46335b New translations en.json (Polish) 2023-03-29 01:50:34 +06:00
wukko
9c84e430e5 New translations en.json (Polish) 2023-03-29 00:40:34 +06:00
wukko
79d6cfe1b6 New translations en.json (German) 2023-03-27 22:13:13 +06:00
wukko
06d12511c9 New translations en.json (Russian) 2023-03-24 23:24:17 +06:00
wukko
0fb080bd27 New translations en.json (Dutch) 2023-02-27 20:09:45 +06:00
wukko
dd261c3438 New translations en.json (Romanian) 2023-02-27 18:53:46 +06:00
wukko
30c3baaed0 New translations en.json (Spanish) 2023-02-27 00:20:33 +06:00
wukko
9f6f7788fa New translations en.json (Russian) 2023-02-26 23:08:49 +06:00
wukko
ea0d46c1c2 New translations en.json (Dutch) 2023-02-22 02:49:26 +06:00
wukko
b0de86a89c New translations en.json (French) 2023-02-22 02:49:24 +06:00
wukko
5fd90dd4c8 New translations en.json (Spanish) 2023-02-22 00:06:00 +06:00
wukko
de7affe360 New translations en.json (Finnish) 2023-02-18 17:13:28 +06:00
wukko
5f10737416 New translations en.json (Romanian) 2023-02-18 04:58:28 +06:00
wukko
b6c0172a75 New translations en.json (German) 2023-02-14 02:05:57 +06:00
wukko
e05629014b New translations en.json (German) 2023-02-13 23:29:41 +06:00
wukko
5d2f789c6b New translations en.json (Russian) 2023-02-13 20:55:17 +06:00
wukko
21bc785046 New translations en.json (Russian) 2023-01-30 00:49:04 +06:00
wukko
59f8e1e4f1 New translations en.json (German) 2023-01-29 19:42:43 +06:00
wukko
5998b954bc New translations en.json (Russian) 2023-01-18 17:39:51 +06:00
79 changed files with 2081 additions and 2858 deletions

View file

@ -1,11 +1,5 @@
version = 1
test_patterns = [
"src/test/test.js"
]
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
environment = ["nodejs"]
enabled = true

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
custom: https://boosty.to/wukko

8
.gitignore vendored
View file

@ -6,10 +6,4 @@ package-lock.json
.env
# esbuild
min
# page build
build
# stuff i already made but delayed
future
min

View file

@ -1,19 +0,0 @@
workspace:
path: static-hoster
base: /build
pipeline:
build:
image: docker:23-cli
privileged: true
commands:
- apk add git
- docker buildx build --no-cache -t dev.cat-enby.club/nikurasu/cobalt:latest -t dev.cat-enby.club/nikurasu/cobalt:$(date "+%Y-%m-%d") -f Dockerfile .
- docker login -u $USER -p $PASSWORD dev.cat-enby.club
- docker push dev.cat-enby.club/nikurasu/cobalt:latest
- docker push dev.cat-enby.club/nikurasu/cobalt:$(date "+%Y-%m-%d")
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets: [ user, password ]
branches: current

View file

@ -1,15 +0,0 @@
FROM node:18-bullseye-slim
WORKDIR /app
RUN apt-get update
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm install
RUN git clone -n https://github.com/wukko/cobalt.git --depth 1 && mv cobalt/.git ./ && rm -rf cobalt
COPY . .
EXPOSE 9000
CMD [ "node", "src/cobalt" ]

View file

@ -1,55 +1,56 @@
# cobalt
Best way to save what you love.
Best way to save content you love.
Live: [co.wukko.me](https://co.wukko.me/)
[co.wukko.me](https://co.wukko.me/)
![cobalt logo with repeated logo pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo pattern background")
![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/wide.png "cobalt logo")
[![Crowdin](https://badges.crowdin.net/cobalt/localized.svg)](https://crowdin.com/project/cobalt) [![DeepSource](https://deepsource.io/gh/wukko/cobalt.svg/?label=active+issues&token=MsmsJ9zUOKwcQor0yaiFot84)](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge) [![DeepSource](https://deepsource.io/gh/wukko/cobalt.svg/?label=resolved+issues&token=MsmsJ9zUOKwcQor0yaiFot84)](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge)
## What's cobalt?
cobalt is a social and media platform downloader that doesn't piss you off.
cobalt is social media downloader with zero bullshit. It's friendly, accessible, efficient, and doesn't bother you with shock ads or privacy invasion "consent" popups.
It's fast, friendly, and doesn't have any bullshit that modern web is filled with: no ads, trackers, or analytics. Paste the link, get the video, move on. It's that simple. Just how it should be.
It preserves original media quality so you get best downloads possible (unless you change that in settings).
## Supported services
| Service | Video + Audio | Only audio | Additional features |
| -------- | :---: | :---: | :----- |
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
| Twitter Spaces | ❌️ | ✅ | Audio metadata. |
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. |
| YouTube Music | ❌ | ✅ | Audio metadata. |
| Reddit | ✅ | ✅ | GIFs and videos. |
| TikTok | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. Full audio downloads. |
| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links. |
| bilibili.com | ✅ | ✅ | |
| Tumblr | ✅ | ✅ | |
| Vimeo | ✅ | ❌️ | |
| VK Videos & Clips | ✅ | ❌️ | |
| Service | Video + Audio | Only audio | Additional features |
| -------- | :---: | :---: | :----- |
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
| Twitter Spaces | ❌️ | ✅ | Audio metadata. |
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. |
| YouTube Music | ❌ | ✅ | Audio metadata. |
| Reddit | ✅ | ✅ | |
| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermarks. |
| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links |
| bilibili.com | ✅ | ✅ | |
| Tumblr | ✅ | ✅ | |
| Vimeo | ✅ | ❌️ | |
| VK Videos & Clips | ✅ | ❌️ | |
## cobalt API
cobalt has an open API that you can use for free. It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
cobalt has an open API that you can use for free. It's pretty straightforward in use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
## How to contribute translations
You can translate cobalt to any language you want on [cobalt's Crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to Crowdin.
You can translate cobalt to any language you want on [cobalt's crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to crowdin.
### Translation guidelines:
- All text is **ALWAYS** stylized as **lowercase** unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}` or `{appName}`.
- Example: "`this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!`".
Notice how **everything is lowercase**, no matter the punctuation marks? Yes, that's cobalt's style and you have to follow it.
- Avoid formal language. Leave it for big and classy tech companies. Use informal language wherever possible.
- Keep translations lively, friendly, and fun. Translate strings as if the user was your buddy.
- You can (and should) rephrase sentences as long as they keep the same sense and send the same message as original.
- Avoid formal language. Leave it for boring big tech companies. Use informal language on all occasions.
- Strings are **ALWAYS** stylized as lowercase unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}`.
- Keep translations lively, friendly, and fun. Translate strings as if cobalt user was your buddy.
- Automatic translations from original language are not valid, and will be ignored.
- You can (and should) rephrase sentences as long as they keep the same point, if you think it'd be better that way.
- You can add wordplays or puns if it feels natural to do so.
- Do **NOT** use offensive or explicit vocabulary.
- Check if there are issues in UI with your localization, and optimize it accordingly. If impossible, open an issue.
- Even though I love cursing, keep that to minimum in translations, and do **NOT** use any offensive words.
- Check if there are issues in UI with your localization, and optimize it accordingly, or open an issue.
- Add "(in english)" translated to your language at the end of `ChangelogLastCommit`, `ChangelogLastMajor`, and `ChangelogOlder`. Those are always kept exclusively in English (for now), due to how often changelog changes.
- Sample translation to Russian: `"ChangelogLastCommit": "последний коммит (на английском)"`
- Be nice.
## Host an instance yourself
You might find cobalt's source code a bit messy, but I do my best to improve it with every commit.
### Requirements
- Node.js 17.5 or above
- Node.js 14.16 or above
- git
### npm modules
@ -63,7 +64,7 @@ You might find cobalt's source code a bit messy, but I do my best to improve it
- node-cache
- url-pattern
- xml-js
- youtubei.js
- ytdl-core
Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself.
@ -72,20 +73,10 @@ Setup script installs all needed `npm` dependencies, but you have to install `No
3. Run cobalt via `npm start`
4. Done.
### Docker
It's also possible to host cobalt via a Docker image, but in that case you'd need to set all environment variables by yourself.
That includes:
| Variable | Example |
| -------- | :--- |
| `selfURL` | `https://co.wukko.me/` |
| `port` | `9000` |
| `streamSalt` | `randomly generated sha512 hash` |
| `cors` | `0` |
## Disclaimer
cobalt is my passion project, so update release schedule depends solely on my motivation, free time, and mood. Don't expect any consistency in that.
cobalt is my passion project, so new feature release schedule depends solely on my motivation and mood. Don't expect any consistency in that.
## License
cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE) license.
cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).
[Fluent Emoji](https://github.com/microsoft/fluentui-emoji) used in the project is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE) license.
[Fluent Emoji](https://github.com/microsoft/fluentui-emoji) by Microsoft is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE).

View file

@ -2,22 +2,26 @@
This document provides info about methods and acceptable variables for all cobalt API requests.<br>
## POST: ``/api/json``
Main processing endpoint.<br>
```
⚠️ GET method for this endpoint is deprecated and will be removed entirely soon.
Make sure to update your shortcuts and scripts.
Only url query can be used with this method.
```
Request Body Type: ``application/json``<br>
Response Body Type: ``application/json``
### Request Body Variables
| key | type | variables | default | description |
|:----------------|:--------|:----------------------------------|:----------|:-------------------------------------------------------------------------------|
| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
| vCodec | string | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. |
| vQuality | string | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. |
| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
| isAudioOnly | boolean | ``true / false`` | ``false`` | |
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. |
| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. |
| dubLang | boolean | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. |
| key | type | variables | default | description |
|:----------------|:--------|:----------------------------------|:-----------|:----------------------------------------------------------------------|
| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
| vFormat | string | ``mp4 / webm`` | ``mp4`` | Applies only to YouTube downloads. ``mp4`` is recommended for phones. |
| vQuality | string | ``los / low / mid / hig / max`` | ``hig`` | ``mid`` quality is recommended for phones. |
| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
| isAudioOnly | boolean | ``true / false`` | ``false`` | |
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. |
| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. |
### Response Body Variables
| key | type | variables |
@ -43,8 +47,8 @@ Content live render streaming endpoint.<br>
### Request Query Variables
| key | variables | description |
|:----|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------|
| p | ``1`` | Used for probing the rate limit. |
| t | Stream token | Unique stream ID. Used for retrieving cached stream info data. |
| p | ``1`` | Used for checking the rate limit. |
| t | Stream token | Unique stream identificator which is 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 | |

View file

@ -1,17 +1,16 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "5.3.3",
"version": "4.7.4",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",
"engines": {
"node": ">=18"
"node": ">=17.5"
},
"scripts": {
"start": "node src/cobalt",
"setup": "node src/modules/setup",
"test": "node src/test/test"
"setup": "node src/modules/setup"
},
"repository": {
"type": "git",
@ -23,17 +22,16 @@
},
"homepage": "https://github.com/wukko/cobalt#readme",
"dependencies": {
"better-ytdl-core": "^1.0.1",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"esbuild": "^0.14.51",
"express": "^4.18.1",
"express": "^4.17.1",
"express-rate-limit": "^6.3.0",
"ffmpeg-static": "^5.1.0",
"got": "^12.1.0",
"nanoid": "^4.0.2",
"node-cache": "^5.1.2",
"url-pattern": "1.0.3",
"xml-js": "^1.6.11",
"youtubei.js": "4.1.1"
"xml-js": "^1.6.11"
}
}

View file

@ -1,74 +1,60 @@
import "dotenv/config";
import "dotenv/config"
import express from "express";
import cors from "cors";
import * as fs from "fs";
import rateLimit from "express-rate-limit";
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename).slice(0, -4); // go up another level (get rid of src/)
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
import { appName, genericUserAgent, version } from "./modules/config.js";
import { shortCommit } from "./modules/sub/currentCommit.js";
import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js";
import { getJSON } from "./modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "./modules/sub/utils.js";
import renderPage from "./modules/pageRender/page.js";
import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js";
import { Bright, Cyan, Green, Red } from "./modules/sub/consoleText.js";
import stream from "./modules/stream/stream.js";
import loc from "./localization/manager.js";
import { buildFront } from "./modules/build.js";
import { changelogHistory } from "./modules/pageRender/onDemand.js";
import { sha256 } from "./modules/sub/crypto.js";
import findRendered from "./modules/pageRender/findRendered.js";
if (process.env.selfURL && process.env.streamSalt && process.env.port) {
const commitHash = shortCommit();
const branch = getCurrentBranch();
const app = express();
const commitHash = shortCommit();
const app = express();
app.disable('x-powered-by');
const corsConfig = process.env.cors === '0' ? { origin: process.env.selfURL, optionsSuccessStatus: 200 } : {};
app.disable('x-powered-by');
if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && process.env.port) {
const apiLimiter = rateLimit({
windowMs: 60000,
max: 25,
standardHeaders: false,
windowMs: 1 * 60 * 1000,
max: 12,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), process.env.streamSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
const apiLimiterStream = rateLimit({
windowMs: 60000,
max: 28,
standardHeaders: false,
windowMs: 1 * 60 * 1000,
max: 12,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), process.env.streamSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
}
});
await buildFront(commitHash, branch);
app.use('/api/:type', cors(corsConfig));
app.use('/api/json', apiLimiter);
await buildFront();
app.use('/api/', apiLimiter);
app.use('/api/stream', apiLimiterStream);
app.use('/api/onDemand', apiLimiter);
app.use('/', express.static('./min'));
app.use('/', express.static('./src/front'));
app.use((req, res, next) => {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.use((req, res, next) => {
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) res.destroy();
try {
decodeURIComponent(req.path)
}
catch (e) {
return res.redirect(process.env.selfURL);
}
next();
});
app.use('/api/json', express.json({
@ -76,61 +62,60 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) {
try {
JSON.parse(buf);
if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' });
return;
}
if (String(req.header('Accept')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' });
return;
}
if (req.header('Content-Type') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid content type header' })
if (req.header('Accept') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid accept header' })
} catch(e) {
res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' });
return;
res.status(500).json({ 'status': 'error', 'text': 'invalid json body.' })
}
}
}));
app.post('/api/json', async (req, res) => {
app.post('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => {
try {
let ip = sha256(getIP(req), process.env.streamSalt);
let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" });
try {
let request = req.body;
if (request.url) {
request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request);
if (chck) chck["ip"] = ip;
j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
} else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') });
}
} catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
switch (req.params.type) {
case 'json':
try {
let request = req.body;
let chck = checkJSONPost(request);
if (request.url && chck) {
chck["ip"] = ip;
let j = await getJSON(chck["url"], languageCode(req), chck)
res.status(j.status).json(j.body);
} else if (request.url && !chck) {
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorCouldntFetch') });
res.status(j.status).json(j.body);
} else {
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink') })
res.status(j.status).json(j.body);
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
}
break;
default:
let j = apiJSON(0, { t: "unknown response type" })
res.status(j.status).json(j.body);
break;
}
res.status(j.status).json(j.body);
return;
} catch (e) {
res.destroy();
return
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
}
});
app.get('/api/:type', (req, res) => {
app.get('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), (req, res) => {
try {
let ip = sha256(getIP(req), process.env.streamSalt);
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
switch (req.params.type) {
case 'json':
res.status(405).json({ 'status': 'error', 'text': 'GET method for this request has been deprecated. see https://github.com/wukko/cobalt/blob/current/docs/API.md for up-to-date API documentation.' });
break;
case 'stream':
if (req.query.p) {
res.status(200).json({ "status": "continue" });
return;
} else if (req.query.t && req.query.h && req.query.e) {
stream(res, ip, req.query.t, req.query.h, req.query.e);
} else {
let j = apiJSON(0, { t: "no stream id" })
res.status(j.status).json(j.body);
return;
}
break;
case 'onDemand':
@ -158,18 +143,27 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) {
break;
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
return;
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
}
});
app.get("/api", (req, res) => {
res.redirect('/api/json')
});
app.get("/status", (req, res) => {
res.status(200).end()
});
app.get("/", (req, res) => {
res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`);
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) {
if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) {
res.redirect(internetExplorerRedirect.new)
} else {
res.redirect(internetExplorerRedirect.old)
}
} else {
res.send(renderPage({
"hash": commitHash,
"type": "default",
"lang": languageCode(req),
"useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent
}))
}
});
app.get("/favicon.ico", (req, res) => {
res.redirect('/icons/favicon.ico');
@ -177,11 +171,10 @@ if (process.env.selfURL && process.env.streamSalt && process.env.port) {
app.get("/*", (req, res) => {
res.redirect('/')
});
app.listen(process.env.port, () => {
let startTime = new Date();
console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash} (${branch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`)
})
console.log(`\n${Cyan(appName)} ${Bright(`v.${version}-${commitHash}`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.selfURL}`)}\nPort: ${process.env.port}\n`)
});
} else {
console.log(Red(`cobalt hasn't been configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`));
console.log(Red(`cobalt hasn't been configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`))
}

View file

@ -1,21 +1,17 @@
{
"streamLifespan": 120000,
"maxVideoDuration": 10800000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"maxVideoDuration": 7500000,
"maxAudioDuration": 7500000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"authorInfo": {
"name": "wukko",
"link": "https://wukko.me/",
"contact": "https://wukko.me/contacts",
"support": {
"twitter": {
"url": "https://twitter.com/justusecobalt",
"handle": "@justusecobalt"
},
"mastodon": {
"url": "https://wetdry.world/@cobalt",
"handle": "@cobalt@wetdry.world"
}
}
"contact": "https://wukko.me/contacts"
},
"internetExplorerRedirect": {
"newNT": ["6.1", "6.2", "6.3", "10.0"],
"old": "https://mypal-browser.org/",
"new": "https://www.mozilla.org/firefox/new/"
},
"donations": {
"crypto": {
@ -27,6 +23,11 @@
"boosty": "https://boosty.to/wukko"
}
},
"quality": {
"hig": "1440",
"mid": "720",
"low": "480"
},
"celebrations": {
"01-01": "🎄",
"02-17": "😺",
@ -51,15 +52,14 @@
"12-28": "🎄",
"12-29": "🎄",
"12-30": "🎄",
"12-31": "🎄",
"03-08": "💪"
"12-31": "🎄"
},
"supportedAudio": ["mp3", "ogg", "wav", "opus"],
"ffmpegArgs": {
"webm": ["-c:v", "copy", "-c:a", "copy"],
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
"audio": ["-vn", "-ar", "48000", "-ac", "2", "-b:a", "320k"],
"m4a": ["-movflags", "frag_keyframe+empty_moov"]
}
}

View file

@ -4,11 +4,7 @@
--border-15: 0.15rem solid var(--accent);
--border-10: 0.1rem solid var(--accent);
--font-mono: 'Noto Sans Mono', 'Consolas', 'SF Mono', monospace;
--padding-1: 0.75rem;
--line-height: 1.65rem;
--red: rgb(255, 0, 61);
--color: rgb(107, 67, 139);
--gap: 0.6rem;
}
@media (prefers-color-scheme: dark) {
:root {
@ -19,7 +15,6 @@
--accent-unhover: rgb(100, 100, 100);
--accent-unhover-2: rgb(110, 110, 110);
--background: rgb(0, 0, 0);
--checkmark: url(vectorIcons/checkmark_b.svg);
}
}
@media (prefers-color-scheme: light) {
@ -31,7 +26,6 @@
--accent-unhover: rgb(190, 190, 190);
--accent-unhover-2: rgb(110, 110, 110);
--background: rgb(255, 255, 255);
--checkmark: url(vectorIcons/checkmark.svg);
}
}
[data-theme="dark"] {
@ -42,7 +36,6 @@
--accent-unhover: rgb(100, 100, 100);
--accent-unhover-2: rgb(110, 110, 110);
--background: rgb(0, 0, 0);
--checkmark: url(vectorIcons/checkmark_b.svg);
}
[data-theme="light"] {
--accent: rgb(25, 25, 25);
@ -52,17 +45,15 @@
--accent-unhover: rgb(190, 190, 190);
--accent-unhover-2: rgb(110, 110, 110);
--background: rgb(255, 255, 255);
--checkmark: url(vectorIcons/checkmark.svg);
}
html,
body {
margin: 0;
background: var(--background);
color: var(--accent);
-webkit-tap-highlight-color: var(--transparent);
font-family: var(--font-mono);
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: var(--transparent);
overflow: hidden;
-ms-overflow-style: none;
scrollbar-width: none;
@ -71,7 +62,6 @@ a {
color: var(--accent);
text-decoration: none;
user-select: none;
-webkit-user-select: none;
}
::placeholder {
color: var(--accent-unhover-2);
@ -85,34 +75,27 @@ a {
[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
margin-right: var(--padding-1);
margin-right: 1rem;
z-index: 0;
border: 0;
height: 15px;
width: 15px;
}
[type="checkbox"]::before {
content: "";
width: 15px;
height: 15px;
border: 0.15rem solid var(--accent);
border: var(--border-15);
background-color: var(--accent-button-bg);
display: block;
z-index: 5;
position: relative;
}
[type="checkbox"]:checked::before {
background: var(--checkmark);
background-size: 90%;
background-position: center;
background-repeat: no-repeat;
}
[type="checkbox"]:checked::before {
box-shadow: inset 0 0 0 0.14rem var(--accent-button-bg);
background-color: var(--accent);
border: 0.15rem solid var(--accent);
}
.checkbox span {
margin-top: 0.21rem;
margin-left: 0.4rem;
}
button {
background: none;
@ -126,15 +109,20 @@ input[type="text"],
[type="text"] {
border-radius: 0;
}
.desktop button:hover,
.desktop .switch:hover,
.desktop .checkbox:hover,
.desktop .text-to-copy:hover,
.desktop .collapse-header:hover,
.desktop #close-button:hover {
button:hover,
.switch:hover,
.checkbox:hover,
.text-to-copy:hover {
background: var(--accent-hover);
cursor: pointer;
}
.switch.text-backdrop:hover,
.switch.text-backdrop:active,
.text-to-copy.text-backdrop:hover,
.text-to-copy.text-backdrop:active {
background: var(--accent);
color: var(--background);
}
button:active,
.switch:active,
.checkbox:active,
@ -143,19 +131,6 @@ button:active,
cursor: pointer;
transform: scale(0.95)
}
.collapse-header:active {
background: var(--accent-press);
cursor: pointer;
}
.switch.text-backdrop,
.switch.text-backdrop:hover,
.switch.text-backdrop:active,
.text-to-copy.text-backdrop,
.text-to-copy.text-backdrop:hover,
.text-to-copy.text-backdrop:active {
background: var(--accent);
color: var(--background);
}
.picker-image:active {
cursor: pointer;
transform: scale(0.95)
@ -182,17 +157,14 @@ input[type="checkbox"] {
position: fixed;
width: 60%;
height: auto;
display: flex;
flex-direction: row;
display: inline-flex;
}
#logo {
#logo-area {
padding-right: 3rem;
padding-top: 0.1rem;
text-align: left;
font-size: 1rem;
white-space: nowrap;
width: 7rem;
height: 2.5rem;
align-items: center;
display: flex;
}
#download-area {
display: flex;
@ -201,15 +173,20 @@ input[type="checkbox"] {
}
#cobalt-main-box #top {
display: inline-flex;
height: 2.5rem;
height: 2rem;
margin-top: -0.6rem;
flex-direction: row;
}
#cobalt-main-box #bottom {
padding-top: 1rem;
padding-top: 1.5rem;
display: flex;
flex-direction: row;
justify-content: space-between;
}
#cobalt-main-box #bottom button {
width: auto!important;
padding: 0.6rem 1.2rem!important;
}
.box {
background: var(--background);
border: var(--border-15);
@ -217,7 +194,7 @@ input[type="checkbox"] {
}
#url-input-area {
background: var(--background);
padding: 0 1rem;
padding: 1.2rem 1rem;
width: 100%;
color: var(--accent);
border: 0;
@ -227,11 +204,13 @@ input[type="checkbox"] {
font-size: 0.8rem;
}
#url-clear {
height: 100%;
background: none;
padding: 0 1rem 0.2rem;
transform: none;
padding: 0 1.1rem;
font-size: 1rem;
transform: none;
line-height: 0;
height: 1.6rem;
margin-top: .4rem;
}
#url-input-area:focus {
outline: none;
@ -263,7 +242,7 @@ input[type="checkbox"] {
#cobalt-main-box #bottom,
#footer-buttons,
#footer-buttons, .footer-pair {
gap: var(--gap);
gap: 0.8rem;
}
#footer-buttons, .footer-pair {
display: flex;
@ -273,7 +252,7 @@ input[type="checkbox"] {
.footer-button {
width: auto!important;
color: var(--accent-unhover-2);
padding: var(--gap) 1.2rem!important;
padding: 0.6rem 1.2rem!important;
align-content: center;
}
.notification-dot {
@ -286,18 +265,7 @@ input[type="checkbox"] {
.text-backdrop {
background: var(--accent);
color: var(--background);
}
.italic {
font-style: italic;
}
.cobalt-support-link {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 0.3rem;
margin-top: 0.5rem;
user-select: none;
-webkit-user-select: none;
padding: 0 0.1rem;
}
::-moz-selection {
background-color: var(--accent);
@ -342,8 +310,9 @@ input[type="checkbox"] {
}
.changelog-banner {
width: 100%;
background-color: var(--accent-button-bg);
max-height: 300px;
margin-bottom: 1.65rem;
margin-bottom: 2rem;
float: left;
}
.changelog-img {
@ -363,7 +332,7 @@ input[type="checkbox"] {
}
#popup-subtitle {
font-size: 1.1rem;
padding-bottom: var(--padding-1);
padding-bottom: 1rem;
}
#popup-desc,
#desc-error,
@ -371,9 +340,8 @@ input[type="checkbox"] {
width: 100%;
text-align: left;
float: left;
line-height: var(--line-height);
line-height: 1.7rem;
user-select: text;
-webkit-user-select: text;
}
#popup-title {
font-size: 1.5rem;
@ -391,7 +359,7 @@ input[type="checkbox"] {
}
.popup-footer-content {
font-size: 0.8rem;
line-height: var(--line-height);
line-height: 1.7rem;
color: var(--accent-unhover-2);
border-top: 0.05rem solid var(--accent-unhover-2);
padding-top: 0.4rem;
@ -418,8 +386,14 @@ input[type="checkbox"] {
#popup-content.with-footer {
margin-bottom: 3rem;
}
#popup-close {
cursor: pointer;
float: right;
right: 0;
position: absolute;
}
.settings-category {
padding-bottom: 1rem;
padding-bottom: 1.2rem;
}
.separator {
float: left;
@ -434,17 +408,13 @@ input[type="checkbox"] {
}
.category-title {
text-align: left;
line-height: var(--line-height);
line-height: 1.7rem;
}
.bottom-margin {
margin-bottom: var(--padding-1)!important;
margin-bottom: 1rem!important;
}
.top-margin {
margin-top: var(--padding-1)!important;
}
.top-margin-only {
margin-top: var(--padding-1)!important;
margin-bottom: 0!important;
margin-top: 1rem!important;
}
.no-margin {
margin: 0!important;
@ -454,10 +424,10 @@ input[type="checkbox"] {
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
padding: 0.55rem 1rem 0.8rem 0.7rem;
width: auto;
margin-right: var(--padding-1);
margin-bottom: var(--padding-1);
margin: 0 0.5rem 0.5rem 0;
background: var(--accent-button-bg);
}
.checkbox-label {
@ -469,7 +439,7 @@ input[type="checkbox"] {
.subtitle {
width: 100%;
text-align: left;
line-height: var(--line-height);
line-height: 1.7rem;
padding-bottom: 0.4rem;
color: var(--accent);
}
@ -490,7 +460,7 @@ input[type="checkbox"] {
.switch {
padding: 0.7rem;
width: 100%;
text-align: left;
text-align: center;
color: var(--accent);
background: var(--accent-button-bg);
display: flex;
@ -499,7 +469,7 @@ input[type="checkbox"] {
cursor: pointer;
}
.switch.space-right {
margin-right: var(--padding-1);
margin-right: 1rem
}
.switch[data-enabled="true"] {
color: var(--background);
@ -507,9 +477,6 @@ input[type="checkbox"] {
cursor: default;
z-index: 999
}
.switch[data-enabled="true"]:hover {
background: var(--accent);
}
.switches {
display: flex;
width: auto;
@ -526,25 +493,18 @@ input[type="checkbox"] {
}
.text-to-copy {
user-select: text;
-webkit-user-select: text;
border: var(--border-15);
padding: var(--padding-1);
padding: 1rem;
overflow: auto;
}
#close-button {
#close-bottom {
max-width: 2.8rem;
margin-left: var(--padding-1);
margin-left: 1rem;
background: var(--background);
border: var(--border-15);
color: var(--accent);
padding: 0.3rem 0.75rem 0.5rem;
}
#close-button.up {
float: right;
position: absolute;
right: 0;
height: 2.8rem;
}
.popup-tab-content {
display: none;
}
@ -559,9 +519,18 @@ input[type="checkbox"] {
}
.emoji {
margin-right: 0.4rem;
user-select: none;
-webkit-user-select: none;
}
.tooltip {
position: absolute;
margin-top: -6rem;
margin-left: -0.5rem;
line-height: 1.2;
text-align: left;
pointer-events: none;
color: var(--accent-unhover-2)!important;
}
.button:active .tooltip {
display: none;
}
.picker-image {
object-fit: cover;
@ -571,13 +540,13 @@ input[type="checkbox"] {
.picker-image-container {
width: 8rem;
height: 8rem;
margin-bottom: var(--padding-1);
margin-bottom: 1rem;
background-color: var(--accent-button-bg);
}
.picker-various-container {
height: 20rem;
width: 25rem;
margin-bottom: var(--padding-1);
margin-bottom: 1rem;
background-color: var(--accent-button-bg);
position: relative;
}
@ -603,64 +572,14 @@ input[type="checkbox"] {
position: absolute;
background: var(--background);
color: var(--accent);
padding: 0.3rem var(--gap);
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
opacity: 0.7;
margin: 0.4rem;
}
#popup-picker .explanation {
margin-top: 0!important;
margin-bottom: var(--padding-1);
}
#cobalt-main-box #bottom button {
width: auto;
padding: var(--gap) 1.2rem;
}
.collapse-list {
background: var(--accent-press);
user-select: none;
-webkit-user-select: none;
}
.collapse-header {
padding: var(--padding-1);
font-size: 1rem;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
background: var(--accent-button-bg);
}
.collapse-indicator {
transform: rotate(180deg);
}
.expanded .collapse-indicator {
transform: none;
}
.collapse-title {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.8rem;
}
.collapse-body {
display: none;
padding: var(--padding-1);
user-select: text;
-webkit-user-select: text;
}
.expanded .collapse-body {
display: block;
}
#download-switcher .switches {
gap: var(--gap);
}
#pd-share {
display: none;
}
#hop-attribution {
display: block;
text-align: right;
margin-bottom: 1rem;
}
/* adapt the page according to screen size */
@media screen and (min-width: 2300px) {
@ -736,12 +655,12 @@ input[type="checkbox"] {
}
}
/* mobile page */
@media screen and (max-width: 720px) {
@media screen and (max-width: 949px) {
#cobalt-main-box, #footer {
width: 90%;
width: 85%;
}
}
@media screen and (max-width: 499px) {
@media screen and (max-width: 475px) {
.tab {
font-size: 0!important;
}
@ -751,27 +670,15 @@ input[type="checkbox"] {
#cobalt-main-box, #footer {
width: 90%;
}
.checkbox {
width: 100%;
}
}
@media screen and (max-width: 320px) {
#popup-title {
font-size: 1.3rem;
line-height: 2rem;
}
.footer-button,
#audioMode-false,
#audioMode-true,
#paste {
.footer-button {
font-size: 0!important;
}
.footer-button .emoji,
#audioMode-false .emoji,
#audioMode-true .emoji,
#paste .emoji {
margin-right: 0;
}
.switch, .checkbox, .category-title, .subtitle, #popup-desc {
font-size: .75rem;
}
@ -788,36 +695,8 @@ input[type="checkbox"] {
.category-title {
margin-bottom: 0.8rem;
}
}
@media screen and (max-width: 720px) {
#cobalt-main-box #bottom {
flex-direction: column-reverse;
}
#cobalt-main-box #bottom button {
width: 100%;
}
#footer {
bottom: 4%;
transform: translate(-50%, 0%);
}
#footer-buttons {
flex-direction: column;
align-items: stretch;
}
.footer-pair .footer-button {
width: 100%!important;
}
#logo {
width: 100%;
height: auto;
justify-content: center;
}
#cobalt-main-box {
display: flex;
border: none;
padding: 0;
flex-direction: column;
gap: var(--gap);
.footer-button .emoji {
margin-right: 0;
}
}
@media screen and (max-width: 949px) {
@ -836,6 +715,23 @@ input[type="checkbox"] {
height: 20rem;
max-width: 100%;
}
#cobalt-main-box #bottom {
flex-direction: column;
}
#cobalt-main-box #bottom button {
width: 100%!important;
}
#footer {
bottom: 1.7rem;
transform: translate(-50%, 0%);
}
#footer-buttons {
flex-direction: column;
align-items: stretch;
}
.footer-pair .footer-button {
width: 100%!important;
}
#popup-header {
padding-top: 1.2rem;
}
@ -848,10 +744,24 @@ input[type="checkbox"] {
line-height: 7rem;
}
#close-error {
bottom: 3rem;
bottom: 5%;
position: absolute;
width: var(--without-padding);
}
#logo-area {
padding-right: 0;
padding-top: 0;
position: fixed;
line-height: 0;
margin-top: -2rem;
width: 100%;
text-align: center;
}
#cobalt-main-box {
display: flex;
border: none;
padding: 0;
}
.popup, .popup.scrollable, .popup.small {
border: none;
width: 90%;

View file

@ -1,7 +1,7 @@
let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os");
let version = 25;
let version = 21;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>`
@ -9,16 +9,13 @@ let store = {}
let switchers = {
"theme": ["auto", "light", "dark"],
"vCodec": ["h264", "av1", "vp9"],
"vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"],
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
"dubLang": ["original", "auto"],
"vimeoDash": ["false", "true"],
"audioMode": ["false", "true"]
"vFormat": ["mp4", "webm"],
"vQuality": ["hig", "max", "mid", "low"],
"aFormat": ["mp3", "best", "ogg", "wav", "opus"]
}
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
let exceptions = { // used for mobile devices
"vQuality": "720"
"vQuality": "mid"
}
function eid(id) {
@ -88,11 +85,8 @@ function clearInput() {
function copy(id, data) {
let e = document.getElementById(id);
e.classList.add("text-backdrop");
setTimeout(() => { e.classList.remove("text-backdrop") }, 600);
data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText);
}
async function share(url) {
try { await navigator.share({url: url}) } catch (e) {}
setTimeout(() => { e.classList.remove("text-backdrop") }, 600);
}
function detectColorScheme() {
let theme = "auto";
@ -118,11 +112,6 @@ function changeTab(evnt, tabId, tabClass) {
if (tabId === "tab-about-changelog" && sGet("changelogStatus") !== `${version}`) notificationCheck("changelog");
if (tabId === "tab-about-about" && !sGet("seenAbout")) notificationCheck("about");
}
function expandCollapsible(evnt) {
let classlist = evnt.currentTarget.parentNode.classList;
let c = "expanded";
!classlist.contains(c) ? classlist.add(c) : classlist.remove(c);
}
function notificationCheck(type) {
let changed = true;
switch (type) {
@ -134,6 +123,7 @@ function notificationCheck(type) {
break;
default:
changed = false;
break;
}
if (changed && sGet("changelogStatus") === `${version}` || type === "disable") {
setTimeout(() => {
@ -177,8 +167,6 @@ function popup(type, action, text) {
case "download":
eid("pd-download").href = text;
eid("pd-copy").setAttribute("onClick", `copy('pd-copy', '${text}')`);
eid("pd-share").setAttribute("onClick", `share('${text}')`);
if (navigator.canShare) eid("pd-share").style.display = "flex";
break;
case "picker":
switch (text.type) {
@ -224,14 +212,17 @@ function popup(type, action, text) {
eid("popup-backdrop").style.visibility = vis(action);
eid(`popup-${type}`).style.visibility = vis(action);
}
function updateMP4Text() {
eid("vFormat-mp4").innerHTML = sGet("vQuality") === "mid" ? "mp4 (h264/av1)" : "mp4 (av1)";
}
function changeSwitcher(li, b) {
if (b) {
if (!switchers[li].includes(b)) b = switchers[li][0];
sSet(li, b);
for (let i in switchers[li]) {
(switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
}
if (li === "theme") detectColorScheme();
if (li === "vQuality") updateMP4Text();
} else {
let pref = switchers[li][0];
if (isMobile && exceptions[li]) pref = exceptions[li];
@ -247,17 +238,40 @@ function internetError() {
popup("error", 1, loc.noInternet);
}
function checkbox(action) {
sSet(action, !!eid(action).checked);
switch(action) {
case "alwaysVisibleButton": button(); break;
if (eid(action).checked) {
sSet(action, "true");
if (action === "alwaysVisibleButton") button();
} else {
sSet(action, "false");
if (action === "alwaysVisibleButton") button();
}
action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
}
function updateToggle(toggl, state) {
switch(state) {
case "true":
eid(toggl).innerHTML = loc.toggleAudio;
break;
case "false":
eid(toggl).innerHTML = loc.toggleDefault;
break;
}
}
function toggle(toggl) {
let state = sGet(toggl);
if (state) {
sSet(toggl, opposite(state))
if (opposite(state) === "true") sSet(`${toggl}ToggledOnce`, "true");
} else {
sSet(toggl, "false")
}
updateToggle(toggl, sGet(toggl))
}
function loadSettings() {
try {
if (typeof(navigator.clipboard.readText) == "undefined") throw new Error();
} catch (err) {
eid("paste").style.display = "none";
eid("pasteFromClipboard").style.display = "none"
}
if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true;
@ -267,12 +281,17 @@ function loadSettings() {
if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true;
}
if (!sGet("audioMode")) {
toggle("audioMode")
}
for (let i = 0; i < checkboxes.length; i++) {
if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true;
}
updateToggle("audioMode", sGet("audioMode"));
for (let i in switchers) {
changeSwitcher(i, sGet(i))
}
updateMP4Text();
}
function changeButton(type, text) {
switch (type) {
@ -300,13 +319,11 @@ function resetSettings() {
window.location.reload();
}
async function pasteClipboard() {
try {
let t = await navigator.clipboard.readText();
if (regex.test(t)) {
eid("url-input-area").value = t;
download(eid("url-input-area").value);
}
} catch (e) {}
let t = await navigator.clipboard.readText();
if (regex.test(t)) {
eid("url-input-area").value = t;
download(eid("url-input-area").value);
}
}
async function download(url) {
changeDownloadButton(2, '...');
@ -315,23 +332,16 @@ async function download(url) {
let req = {
url: encodeURIComponent(url.split("&")[0].split('%')[0]),
aFormat: sGet("aFormat").slice(0, 4),
dubLang: false
}
if (sGet("dubLang") === "auto") {
req.dubLang = true
} else if (sGet("dubLang") === "custom") {
req.dubLang = true
}
if (sGet("vimeoDash") === "true") req.vimeoDash = true;
if (sGet("audioMode") === "true") {
req.isAudioOnly = true;
req.isNoTTWatermark = true; // video tiktok no watermark
if (sGet("fullTikTokAudio") === "true") req.isTTFullAudio = true; // audio tiktok full
req["isAudioOnly"] = true;
req["isNoTTWatermark"] = true; // video tiktok no watermark
if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full
} else {
req.vQuality = sGet("vQuality").slice(0, 4);
if (sGet("muteAudio") === "true") req.isAudioMuted = true;
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req.vCodec = sGet("vCodec").slice(0, 4);
if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req.isNoTTWatermark = true;
req["vQuality"] = sGet("vQuality").slice(0, 4);
if (sGet("muteAudio") === "true") req["isAudioMuted"] = true;
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req["vFormat"] = sGet("vFormat").slice(0, 4);
if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req["isNoTTWatermark"] = true;
}
await fetch('/api/json', { method: "POST", body: JSON.stringify(req), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(async (r) => {
let j = await r.json();
@ -448,4 +458,4 @@ eid("url-input-area").addEventListener("keyup", (event) => {
document.onkeydown = (event) => {
if (event.key === "Tab" || event.ctrlKey) eid("url-input-area").focus();
if (event.key === 'Escape') hideAllPopups();
}
};

View file

@ -1,7 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5131 29.9687H9.09374C4.34375 29.9687 2.01314 26.1101 2.0625 23C2.0625 19.5937 6.90625 7.12497 6.90625 7.12497C7.71875 5.09372 8.42131 3.42992 10.8125 3.46868C12.6563 3.46868 16.2031 4.9687 16.2031 7.96869C16.2031 9.5937 15.0625 10.0781 15.0625 10.0781C15.0625 11.0468 14.2656 11.9687 13.4219 11.9687H9.6875C9.6875 11.9687 12.8252 18.2666 13.0938 18.7656C13.3623 19.2645 13.625 18.7656 13.625 18.7656C14.6875 15.5312 18.25 12.994 21.4063 12.994C25.8947 12.9929 29.9994 17.015 30 20.7682C30 20.7685 30 20.7679 30 20.7682C29.9995 25.5319 26.8432 29.9687 20.5131 29.9687Z" fill="#FFC83D"/>
<path d="M10.25 6.43747L11.5469 8.08591H13.6406C13.1719 7.44531 11.525 6.21247 10.25 6.43747Z" fill="#D67D00"/>
<path d="M15.0764 10.0718C15.0674 10.0761 15.0625 10.0781 15.0625 10.0781C15.0625 11.0469 14.2656 11.9688 13.4219 11.9688H9.83165C9.83165 11.9688 10.2492 12.9355 10.771 14.1415C7.96197 13.9081 7 10.9922 7 10.9922H7.81449C7.82508 10.9972 7.83577 11.0024 7.84655 11.0078H13.1562C13.6875 11.0078 14.8438 10.5312 14.0156 9C14.3051 9 14.9137 9.43857 15.0764 10.0718Z" fill="#D67D00"/>
<path d="M14.6514 18.7018C14.4646 18.394 14.1945 18.1846 13.8945 18.0737C13.7922 18.3002 13.7021 18.5311 13.625 18.7656C13.625 18.7656 13.5309 18.9443 13.3978 18.9809C13.5531 18.9896 13.7049 19.0696 13.7966 19.2206L16.7601 24.1032C16.9034 24.3392 17.2109 24.4144 17.4469 24.2711C17.683 24.1279 17.7582 23.8203 17.6149 23.5843L14.6514 18.7018Z" fill="#D67D00"/>
<path d="M12.0916 18.6939C12.2604 18.4197 12.4952 18.2246 12.758 18.1084C12.9292 18.4473 13.0498 18.684 13.0938 18.7656C13.1693 18.906 13.2445 18.9674 13.3134 18.9831C13.1687 18.9991 13.0299 19.0773 12.9433 19.218L10.0508 23.9183C9.90612 24.1534 9.59814 24.2268 9.36296 24.0821C9.12778 23.9373 9.05446 23.6294 9.19918 23.3942L12.0916 18.6939Z" fill="#D67D00"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,6 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.38815 7.21997L3.31815 8.82997C2.95815 9.01997 2.88815 9.50997 3.18815 9.79997L5.79815 12.27L6.38815 7.21997Z" fill="#F9C23C"/>
<path d="M18.5582 28.5H16.7782L17.9782 22.5H16.4782L15.2782 28.5H11.7781L12.9781 22.5H11.4781L10.2781 28.5H8.47812C7.74812 28.5 7.14812 29.02 7.00812 29.71C6.96812 29.86 7.09812 30 7.24812 30H19.7782C19.9382 30 20.0582 29.86 20.0282 29.71C19.8882 29.02 19.2782 28.5 18.5582 28.5Z" fill="#F9C23C"/>
<path d="M17.5681 6.22C17.4381 5.8 17.2681 5.4 17.0481 5.03H17.6681C18.9581 5.03 19.9981 3.99 19.9981 2.7C19.9981 2.32 19.6781 2 19.2981 2H11.8381C8.65813 2 6.05813 4.48 5.84813 7.61L4.55813 18.79C4.17813 22.1 6.75813 25 10.0881 25H23.8381L23.8348 24.99H29.1181C29.5181 24.99 29.8381 24.67 29.8381 24.27V15.12C29.8381 14.6 29.2881 14.25 28.8081 14.47L21.4662 17.8893L19.1682 11H19.164L17.5681 6.22Z" fill="#00A6ED"/>
<path d="M10 10C10.5523 10 11 9.55228 11 9C11 8.44772 10.5523 8 10 8C9.44772 8 9 8.44772 9 9C9 9.55228 9.44772 10 10 10Z" fill="#212121"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,11 +1,10 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5 2H6.5C5.67157 2 5 2.67157 5 3.5V27.5C5 28.3284 5.67157 29 6.5 29H16.4244C16.795 29 17.1524 28.8628 17.4278 28.6149L26.5034 20.4469C26.8195 20.1624 27 19.7572 27 19.332V3.5C27 2.67157 26.3284 2 25.5 2Z" fill="#E19747"/>
<rect x="7" y="4" width="18" height="23" rx="1" fill="#F3EEF8"/>
<path d="M18 3C18 1.89543 17.1046 1 16 1C14.8954 1 14 1.89543 14 3H13C11.8954 3 11 3.89543 11 5V6.5C11 6.77614 11.2239 7 11.5 7H20.5C20.7761 7 21 6.77614 21 6.5V5C21 3.89543 20.1046 3 19 3H18ZM17 3C17 3.55228 16.5523 4 16 4C15.4477 4 15 3.55228 15 3C15 2.44772 15.4477 2 16 2C16.5523 2 17 2.44772 17 3Z" fill="#9B9B9B"/>
<path d="M28 11C28.5523 11 29 11.4477 29 12V26L28.5 26.5L24 31H14C13.4477 31 13 30.5523 13 30V12C13 11.4477 13.4477 11 14 11H28Z" fill="#D9D9D9"/>
<path d="M29 26H24.846C24.3788 26 24 26.3788 24 26.846V31C24.0914 30.9584 24.1755 30.9005 24.2478 30.8282L28.8282 26.2478C28.9005 26.1755 28.9584 26.0914 29 26Z" fill="#B9B9B9"/>
<path d="M15 15.5C15 15.2239 15.1919 15 15.4286 15H26.5714C26.8081 15 27 15.2239 27 15.5C27 15.7761 26.8081 16 26.5714 16H15.4286C15.1919 16 15 15.7761 15 15.5Z" fill="#9B9B9B"/>
<path d="M15 18.5C15 18.2239 15.1919 18 15.4286 18H26.5714C26.8081 18 27 18.2239 27 18.5C27 18.7761 26.8081 19 26.5714 19H15.4286C15.1919 19 15 18.7761 15 18.5Z" fill="#9B9B9B"/>
<path d="M15.4286 21C15.1919 21 15 21.2239 15 21.5C15 21.7761 15.1919 22 15.4286 22H26.5714C26.8081 22 27 21.7761 27 21.5C27 21.2239 26.8081 21 26.5714 21H15.4286Z" fill="#9B9B9B"/>
<path d="M15 24.5C15 24.2239 15.199 24 15.4444 24H22.5556C22.801 24 23 24.2239 23 24.5C23 24.7761 22.801 25 22.5556 25H15.4444C15.199 25 15 24.7761 15 24.5Z" fill="#9B9B9B"/>
<path d="M5 4.5C5 3.67157 5.67157 3 6.5 3H25.5C26.3284 3 27 3.67157 27 4.5V28.5C27 29.3284 26.3284 30 25.5 30H6.5C5.67157 30 5 29.3284 5 28.5V4.5Z" fill="#E19747"/>
<path d="M25 6C25 5.44772 24.5523 5 24 5H8C7.44772 5 7 5.44772 7 6V27C7 27.5523 7.44772 28 8 28H18.5858C18.7327 28 18.8764 27.9677 19.0071 27.9069L19.3282 27.1239L19.96 23.0379L24.4166 22.255L24.9064 22.0082C24.9675 21.8772 25 21.7332 25 21.5858V6Z" fill="#F3EEF8"/>
<path d="M24.9102 22H20C19.4477 22 19 22.4477 19 23V27.9102C19.108 27.861 19.2074 27.7926 19.2929 27.7071L24.7071 22.2929C24.7926 22.2074 24.861 22.108 24.9102 22Z" fill="#CDC4D6"/>
<path d="M18 4C18 2.89543 17.1046 2 16 2C14.8954 2 14 2.89543 14 4H13C11.8954 4 11 4.89543 11 6V7.5C11 7.77614 11.2239 8 11.5 8H20.5C20.7761 8 21 7.77614 21 7.5V6C21 4.89543 20.1046 4 19 4H18ZM17 4C17 4.55228 16.5523 5 16 5C15.4477 5 15 4.55228 15 4C15 3.44772 15.4477 3 16 3C16.5523 3 17 3.44772 17 4Z" fill="#9B9B9B"/>
<path d="M9 12.5C9 12.2239 9.22386 12 9.5 12H22.5C22.7761 12 23 12.2239 23 12.5C23 12.7761 22.7761 13 22.5 13H9.5C9.22386 13 9 12.7761 9 12.5Z" fill="#9B9B9B"/>
<path d="M9 15.5C9 15.2239 9.22386 15 9.5 15H22.5C22.7761 15 23 15.2239 23 15.5C23 15.7761 22.7761 16 22.5 16H9.5C9.22386 16 9 15.7761 9 15.5Z" fill="#9B9B9B"/>
<path d="M9.5 18C9.22386 18 9 18.2239 9 18.5C9 18.7761 9.22386 19 9.5 19H22.5C22.7761 19 23 18.7761 23 18.5C23 18.2239 22.7761 18 22.5 18H9.5Z" fill="#9B9B9B"/>
<path d="M9 21.5C9 21.2239 9.22386 21 9.5 21H17.5C17.7761 21 18 21.2239 18 21.5C18 21.7761 17.7761 22 17.5 22H9.5C9.22386 22 9 21.7761 9 21.5Z" fill="#9B9B9B"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,7 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 27C22.6274 27 28 21.6274 28 15C28 8.37258 22.6274 3 16 3C9.37257 3 4 8.37258 4 15C4 21.6274 9.37257 27 16 27Z" fill="#533566"/>
<path d="M24 24H8L7.07853 28.1805C6.7458 29.0769 7.51208 30 8.59093 30H23.4125C24.4913 30 25.2475 29.0769 24.9249 28.1805L24 24Z" fill="#B4ACBC"/>
<path d="M14.205 6.26449C14.085 6.21411 13.995 6.11335 13.945 6.00252L13.565 5.10579C13.495 4.96474 13.295 4.96474 13.225 5.10579L12.845 6.00252C12.795 6.12343 12.705 6.21411 12.585 6.26449L12.105 6.48615C11.965 6.55668 11.965 6.75819 12.105 6.82871L12.585 7.05038C12.705 7.10076 12.795 7.20151 12.845 7.31235L13.225 8.20907C13.295 8.35013 13.495 8.35013 13.565 8.20907L13.945 7.31235C13.995 7.19144 14.085 7.10076 14.205 7.05038L14.685 6.82871C14.825 6.75819 14.825 6.55668 14.685 6.48615L14.205 6.26449Z" fill="#FCD53F"/>
<path d="M24.12 10.8035C23.96 10.733 23.83 10.5919 23.76 10.4307L23.22 9.15113C23.12 8.94962 22.83 8.94962 22.73 9.15113L22.19 10.4307C22.12 10.5919 21.99 10.733 21.83 10.8035L21.15 11.1159C20.95 11.2166 20.95 11.5088 21.15 11.6096L21.83 11.9219C21.99 11.9924 22.12 12.1335 22.19 12.2947L22.73 13.5743C22.83 13.7758 23.12 13.7758 23.22 13.5743L23.76 12.2947C23.83 12.1335 23.96 11.9924 24.12 11.9219L24.8 11.6096C25 11.5088 25 11.2166 24.8 11.1159L24.12 10.8035Z" fill="#FCD53F"/>
<path d="M12.5861 14.0303C12.7249 14.3822 12.9838 14.6657 13.3168 14.8221L14.6948 15.477C15.1017 15.6921 15.1017 16.3079 14.6948 16.523L13.3168 17.1779C12.9931 17.3343 12.7249 17.6178 12.5861 17.9697L11.4948 20.6774C11.2913 21.1075 10.7087 21.1075 10.5052 20.6774L9.41387 17.9697C9.27515 17.6178 9.01618 17.3343 8.68323 17.1779L7.3052 16.523C6.89827 16.3079 6.89827 15.6921 7.3052 15.477L8.68323 14.8221C9.00693 14.6657 9.27515 14.3822 9.41387 14.0303L10.5052 11.3226C10.7087 10.8925 11.2913 10.8925 11.4948 11.3226L12.5861 14.0303Z" fill="#FCD53F"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,8 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 24.563C14 23.3163 13.02 22.3052 11.75 22.3052L19 16.7V27.7926C19 28.4601 18.43 29 17.75 29H15.25C14.57 29 14 28.4601 14 27.7926V24.563Z" fill="#636363"/>
<path d="M22.51 22.25L22 22H29V28.06C29 28.58 28.58 29 28.06 29H24.94C24.42 29 24 28.58 24 28.06V24.67C24 23.64 23.43 22.71 22.51 22.25Z" fill="#636363"/>
<path d="M19.35 6C25.23 6 30 10.723 29.99 16.5673V27.7796C29.99 28.4543 29.44 29 28.76 29H26.2339C25.5539 29 25.0039 28.4543 25.0039 27.7796V24.5151C25.0039 23.255 23.9739 22 22.7039 22H17.5C16.49 22 15.79 22.9573 15.32 23.7908C15.2356 23.9407 15.1457 24.1025 15.0509 24.273C14.0231 26.1221 12.4234 29 11.05 29H8.31C8.28361 29 8.26 28.9972 8.23771 28.9946C8.21777 28.9923 8.19889 28.9901 8.18 28.9901C7.35 28.9107 6.96 27.9284 7.45 27.2636L9.16 24.9318C9.88 23.9594 10.07 22.6795 9.59 21.5781C8.84797 19.8763 7.16017 19.1422 5 18.8549V22.5C5 22.7783 5.07227 22.8945 5.08948 22.9152C5.09336 22.9199 5.1032 22.9318 5.13954 22.9472C5.18248 22.9654 5.29076 23 5.5 23C6.32843 23 7 23.6716 7 24.5C7 25.3285 6.32843 26 5.5 26C4.38888 26 3.43171 25.6126 2.7841 24.8349C2.17679 24.1055 2 23.2217 2 22.5V10.9909C2 8.23253 4.25 6 7.03 6H19.35Z" fill="#9B9B9B"/>
<path d="M5.5 12.5C5.77614 12.5 6 12.7239 6 13V14C6 14.2761 5.77614 14.5 5.5 14.5C5.22386 14.5 5 14.2761 5 14V13C5 12.7239 5.22386 12.5 5.5 12.5Z" fill="#1C1C1C"/>
<path d="M10 6C14.4741 6 18.2026 9.18153 18.9773 13.3758C19.1323 14.2261 18.4737 15 17.6022 15H11.3945C10.6295 15 10 14.3885 10 13.6242V6Z" fill="#D3D3D3"/>
<path d="M3.46002 19.7C3.46002 20.2 3.86002 20.6 4.36002 20.6C6.36002 20.6 7.98002 19 7.99002 17H6.19002C6.19002 18.01 5.37002 18.8 4.36002 18.8C3.86002 18.8 3.46002 19.2 3.46002 19.7Z" fill="#D3D3D3"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,12 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31133 17.6539C7.42658 18.0654 7.50535 18.3466 6.71874 18.25C6.27534 18.1955 5.881 18.1308 5.56463 18.0788C5.24553 18.0264 5.00576 17.987 4.87499 17.9844C4.70832 17.9896 4.46249 18.2844 4.74999 18.9844C5.03749 19.6844 5.38541 20.3958 5.57811 20.7031L9.17187 19.8125L7.56249 17.3594L7.26561 17.4844C7.28021 17.5428 7.29609 17.5995 7.31133 17.6539Z" fill="#580A7C"/>
<path d="M24.7125 17.6539C24.5973 18.0654 24.5185 18.3466 25.3051 18.25C25.7485 18.1955 26.1428 18.1308 26.4592 18.0788C26.7783 18.0264 27.0181 17.987 27.1488 17.9844C27.3155 17.9896 27.5613 18.2844 27.2738 18.9844C26.9863 19.6844 26.6384 20.3958 26.4457 20.7031L22.852 19.8125L24.4613 17.3594L24.7582 17.4844C24.7436 17.5428 24.7277 17.5995 24.7125 17.6539Z" fill="#580A7C"/>
<path d="M8.17722 18.841C8.33838 19.1786 8.54222 19.6056 7.375 19.875L2.76562 20.9219C2.00783 21.1249 2.11991 21.4306 2.24244 21.765C2.2614 21.8167 2.2806 21.8691 2.29687 21.9219C2.74007 23.3599 6 25.8125 8.15625 25.5L12.3125 21.7812L8.42188 18.5938L8.15625 18.7969C8.16307 18.8114 8.17011 18.8261 8.17722 18.841Z" fill="#7D0AB3"/>
<path d="M23.977 18.841C23.8159 19.1786 23.612 19.6056 24.7792 19.875L29.3886 20.9219C30.1464 21.1249 30.0343 21.4306 29.9118 21.765C29.8928 21.8167 29.8736 21.8691 29.8574 21.9219C29.4142 23.3599 26.1542 25.8125 23.998 25.5L19.8417 21.7812L23.7324 18.5938L23.998 18.7969C23.9912 18.8114 23.9841 18.8261 23.977 18.841Z" fill="#7D0AB3"/>
<path d="M20.9627 20.7275L26.988 22.1259C27.1456 22.1176 27.4162 22.2503 27.238 22.8476C27.0152 23.5942 26.4704 25.1452 22.9577 25.1347C20.1475 25.1264 19.7181 22.5055 19.8547 21.196L20.9627 20.7275Z" fill="#9B2ECE"/>
<path d="M11.25 21.2812L5.12499 22.0937C4.96874 22.0728 4.68749 22.1749 4.81249 22.7499C4.96874 23.4687 5.37499 24.9687 8.87499 25.2499C11.675 25.4749 12.3333 23.052 12.3125 21.8124L11.25 21.2812Z" fill="#9B2ECE"/>
<path d="M10.9531 21.3594C11.1198 21.5521 11.3 22.0656 10.6875 22.5781C10.075 23.0906 6.94271 25.8542 5.45313 27.1719C5.16476 27.427 5.28134 27.5969 5.49493 27.9083C5.5117 27.9328 5.52906 27.9581 5.54688 27.9844C6.54939 29.4643 11.6719 31.9219 15.6875 26.7656C15.8022 26.6183 15.8926 26.5183 16 26.5163V26.5281C16.0118 26.524 16.0233 26.521 16.0345 26.519C16.0458 26.521 16.0573 26.524 16.0691 26.5281V26.5163C16.1765 26.5183 16.2669 26.6183 16.3816 26.7656C20.3972 31.9219 25.5197 29.4643 26.5222 27.9844C26.54 27.9581 26.5574 27.9328 26.5741 27.9083C26.7877 27.5969 26.9043 27.427 26.6159 27.1719C25.1264 25.8542 21.9941 23.0906 21.3816 22.5781C20.7691 22.0656 20.9493 21.5521 21.1159 21.3594L20.0144 20.9766L16.0345 22.2623L12.0547 20.9766L10.9531 21.3594Z" fill="#7D0AB3"/>
<path d="M6.375 12C6.375 7.02944 10.4044 3 15.375 3H17C21.9706 3 26 7.02944 26 12V13.625C26 18.5956 21.9706 22.625 17 22.625H15.375C10.4044 22.625 6.375 18.5956 6.375 13.625V12Z" fill="#952CC6"/>
<path d="M8.71875 15.25C9.66799 15.25 10.4375 14.4805 10.4375 13.5312C10.4375 12.582 9.66799 11.8125 8.71875 11.8125C7.76951 11.8125 7 12.582 7 13.5312C7 14.4805 7.76951 15.25 8.71875 15.25Z" fill="#1C1C1C"/>
<path d="M23.7187 15.25C24.6679 15.25 25.4374 14.4805 25.4374 13.5312C25.4374 12.582 24.6679 11.8125 23.7187 11.8125C22.7695 11.8125 22 12.582 22 13.5312C22 14.4805 22.7695 15.25 23.7187 15.25Z" fill="#1C1C1C"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,4 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0372 20.8626C13.0372 22.1648 14.1823 23.2221 15.5924 23.2221C17.0025 23.2221 18.1475 22.1648 18.1475 20.8528V19.1506C18.1475 19.0395 18.2212 18.9421 18.3271 18.9086C21.6766 17.8508 24 14.9188 24 11.5616V10.3084C24 6.0691 20.3104 2.53471 15.7726 2.4466C13.4931 2.39764 11.3409 3.19068 9.70813 4.65926C8.08598 6.12784 7.18478 8.10553 7.18478 10.2105C7.18478 11.5224 8.34043 12.5798 9.75054 12.5798C11.1606 12.5798 12.3057 11.5224 12.3057 10.2203C12.3057 9.39788 12.6556 8.62443 13.2917 8.04679C13.9278 7.46915 14.7654 7.15585 15.6666 7.17543C17.4478 7.21459 18.8897 8.62443 18.8897 10.3182V11.5616C18.8897 13.0302 17.7659 14.2932 16.2073 14.5575C14.3731 14.8708 13.0372 16.3492 13.0372 18.0723V20.8626Z" fill="#6B438B"/>
<path d="M15.5 30C16.8807 30 18 28.8807 18 27.5C18 26.1193 16.8807 25 15.5 25C14.1193 25 13 26.1193 13 27.5C13 28.8807 14.1193 30 15.5 30Z" fill="#6B438B"/>
<path d="M13.0372 20.8626C13.0372 22.1648 14.1823 23.2221 15.5924 23.2221C17.0025 23.2221 18.1475 22.1648 18.1475 20.8528V19.1506C18.1475 19.0395 18.2212 18.9421 18.3271 18.9086C21.6766 17.8508 24 14.9188 24 11.5616V10.3084C24 6.0691 20.3104 2.53471 15.7726 2.4466C13.4931 2.39764 11.3409 3.19068 9.70813 4.65926C8.08598 6.12784 7.18478 8.10553 7.18478 10.2105C7.18478 11.5224 8.34043 12.5798 9.75054 12.5798C11.1606 12.5798 12.3057 11.5224 12.3057 10.2203C12.3057 9.39788 12.6556 8.62443 13.2917 8.04679C13.9278 7.46915 14.7654 7.15585 15.6666 7.17543C17.4478 7.21459 18.8897 8.62443 18.8897 10.3182V11.5616C18.8897 13.0302 17.7659 14.2932 16.2073 14.5575C14.3731 14.8708 13.0372 16.3492 13.0372 18.0723V20.8626Z" fill="#F8312F"/>
<path d="M15.5 30C16.8807 30 18 28.8807 18 27.5C18 26.1193 16.8807 25 15.5 25C14.1193 25 13 26.1193 13 27.5C13 28.8807 14.1193 30 15.5 30Z" fill="#F8312F"/>
</svg>

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

BIN
src/front/icons/wide.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View file

@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.3871H2.1188L2.57078 14.1436L12.1982 30.2565L12.3437 30.5H12.6274H14.9529H15.2564L15.3965 30.2308L29.4436 3.23077L29.8238 2.5H29H25.6087H25.3024L25.1633 2.77281L13.875 24.903L6.45111 13.6124L6.30297 13.3871H6.03333H3Z" fill="white" stroke="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

View file

@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.3871H2.1188L2.57078 14.1436L12.1982 30.2565L12.3437 30.5H12.6274H14.9529H15.2564L15.3965 30.2308L29.4436 3.23077L29.8238 2.5H29H25.6087H25.3024L25.1633 2.77281L13.875 24.903L6.45111 13.6124L6.30297 13.3871H6.03333H3Z" fill="black" stroke="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

View file

@ -0,0 +1,122 @@
{
"name": "deutsch",
"substrings": {
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">erstelle ein ticket auf github</a>"
},
"strings": {
"LinkInput": "link hier einfügen",
"AboutSummary": "{appName} ist dein ort für downloads von sozialen medien und mehr. keine werbung, tracker oder anderer gruseliger bullshit. füge einfach einen link ein und es geht los!",
"EmbedBriefDescription": "speicher, was du liebst, ohne werbung, tracker oder anderen gruseligen bullshit.",
"MadeWithLove": "mit <3 von wukko gemacht",
"AccessibilityInputArea": "linkeingabefeld",
"AccessibilityOpenAbout": "Über-Popup öffnen",
"AccessibilityDownloadButton": "Downloadbutton",
"AccessibilityOpenSettings": "Einstellungen öffnen",
"AccessibilityClosePopup": "Pop-up schließen",
"AccessibilityOpenDonate": "Spenden-Popup öffnen",
"TitlePopupAbout": "was ist {appName}?",
"TitlePopupSettings": "einstellungen",
"TitlePopupError": "oh-oh...",
"TitlePopupChangelog": "was ist neu?",
"TitlePopupDonate": "{appName} unterstützen",
"TitlePopupDownload": "wie geht's weiter?",
"ErrorSomethingWentWrong": "etwas ist schief gelaufen und ich konnte nichts für dich bekommen. versuche es noch einmal, aber wenn das problem weiterhin besteht, {ContactLink}.",
"ErrorUnsupported": "es scheint, als ob dieser dienst noch nicht unterstützt wird oder dein link ungültig ist. hast du den richtigen link eingefügt?",
"ErrorBrokenLink": "{s} wird unterstützt, aber irgendwas stimmt mit dem link nicht. hast du ihn vielleicht nicht ganz kopiert?",
"ErrorNoLink": "ich kann nicht erraten, was du herunterladen möchtest! bitte gib mir einen link :(",
"ErrorPageRenderFail": "wenn du dies liest, dann stimmt etwas mit dem seitenrenderer nicht. bitte {ContactLink}. gib auch die domain an, auf der dieser fehler aufgetaucht ist und den aktuellen commit-hash ({s}). vielen dank im voraus :D",
"ErrorRateLimit": "du machst zu viele anfragen. versuch es in einer minute wieder!",
"ErrorCouldntFetch": "ich konnte nichts über diesen link finden. überprüfe ob er funktioniert und versuche es erneut! beachte, dass manche inhalte regional eingeschränkt sein können.",
"ErrorLengthLimit": "ich kann keine videos länger als {s} minuten verarbeiten, also wähle etwas kürzeres aus!",
"ErrorBadFetch": "irgendetwas ist schief gelaufen als ich versucht habe informationen über deinen link zu bekommen. überprüfe, ob der link funktioniert und versuche es erneut.",
"ErrorNoInternet": "Aktuell besteht keine Internetverbindung, oder {appName}s API ist nicht erreichbar. Überprüfe die Internetverbindung und probiere es nochmal.",
"ErrorCantConnectToServiceAPI": "ich konnte mich nicht mit der dienst-api verbinden. vielleicht ist sie ausgefallen oder {appName} wurde blockiert. versuche es noch einmal, aber wenn der fehler weiterhin besteht, {ContactLink}.",
"ErrorEmptyDownload": "Ich sehe nichts, was ich von deinem link runterladen könnte. Versuche einen anderen Link!",
"ErrorLiveVideo": "das ist ein livestream, ich muss noch lernen in die zukunft zu schauen. warte bis der stream zuende ist und versuchs nochmal!",
"SettingsAppearanceSubtitle": "Aussehen",
"SettingsThemeSubtitle": "Thema",
"SettingsFormatSubtitle": "format",
"SettingsQualitySubtitle": "qualität",
"SettingsThemeAuto": "Auto",
"SettingsThemeLight": "hell",
"SettingsThemeDark": "dunkel",
"SettingsKeepDownloadButton": "&gt;&gt; sichtbar halten",
"AccessibilityKeepDownloadButton": "den Download-Button immer sichtbar halten",
"SettingsEnableDownloadPopup": "frage, wie gespeichert werden soll",
"AccessibilityEnableDownloadPopup": "nach Verfahrensweise mit Downloads fragen",
"SettingsQualityDescription": "wenn die gewählte qualität nicht verfügbar ist, wird die nächstbeste verwendet.",
"LinkGitHubChanges": "&gt;&gt; vorherige Commits ansehen und auf GitHub beitragen",
"NoScriptMessage": "{appName} verwendet javascript für api-anfragen und die interaktive benutzeroberfläche. du musst javascript aktivieren, um die seite zu nutzen. es gibt keine nervigen skripte, ehrenwort.",
"DownloadPopupDescriptionIOS": "Drücke und halte den Download-Button, verstecke die Video-Vorschau, und wähle \"lade verlinkte Datei herunter\" um zu speichern.",
"DownloadPopupDescription": "Der Download-Button öffnet einen neuen Tab mit der angeforderten Datei. Sie können dieses Pop-up in den Einstellungen deaktivieren.",
"DownloadPopupWayToSave": "wähle einen Weg zum Speichern",
"ClickToCopy": "Drücken zum Kopieren",
"Download": "herunterladen",
"CopyURL": "URL kopieren",
"AboutTab": "Über",
"ChangelogTab": "Änderungsverlauf",
"DonationsTab": "Spenden",
"SettingsVideoTab": "Video",
"SettingsAudioTab": "Audio",
"SettingsOtherTab": "Anderes",
"ChangelogLastMajor": "aktuelle Version und Commit",
"AccessibilityModeToggle": "Download-Modus umschalten",
"DonateLinksDescription": "dies ist der beste weg zu spenden, wenn ich deine spende direkt erhalten soll.",
"SettingsAudioFormatBest": "beste",
"SettingsAudioFormatDescription": "wenn das \"best\" format ausgewählt ist, bekommst du audio in der bestmöglichen qualität, ohne konvertierung. alles andere wird konvertiert.",
"Keyphrase": "speicher, was dir gefällt",
"SettingsRemoveWatermark": "Wasserzeichen deaktivieren",
"ErrorPopupCloseButton": "Verstanden",
"ErrorLengthAudioConvert": "ich kann keinen audio länger als {s} minuten konvertieren. wähle das \"best\" format, wenn du einschränkungen vermeiden möchtest!",
"SettingsAudioFullTikTok": "ganzer ton",
"SettingsAudioFullTikTokDescription": "lädt den im video verwendeten originalton ohne zusätzliche änderungen durch den videoautor herunter.",
"ErrorCantGetID": "es konnten keine informationen aus dem gekürzten link extrahiert werden. bitte stelle sicher, dass der link funktioniert oder versuche einen ungekürzten link. wenn das problem weiterhin besteht, {ContactLink}",
"ErrorNoVideosInTweet": "ich konnte keine videos oder gifs in diesem tweet finden. probiere einen anderen!",
"ImagePickerTitle": "Bilder zum Herunterladen auswählen",
"ImagePickerDownloadAudio": "Audio herunterladen",
"ImagePickerExplanationPC": "Rechtsklick auf ein Bild, um es zu speichern.",
"ImagePickerExplanationPhone": "Drücken und halten Sie ein Bild, um es zu speichern.",
"ErrorNoUrlReturned": "ich habe keinen download-link vom server erhalten. versuche es bitte noch einmal. wenn das problem weiterhin besteht, {ContactLink}",
"ErrorUnknownStatus": "ich habe eine antwort erhalten, die ich nicht verarbeiten kann. das sollte niemals passieren. versuche es nochmal. falls es dann immer noch nicht geht, {ContactLink}",
"PasteFromClipboard": "einfügen und herunterladen",
"ChangelogOlder": "vorherige Version",
"ChangelogPressToExpand": "aufklappen",
"Miscellaneous": "Sonstiges",
"ModeToggleAuto": "Auto Modus",
"ModeToggleAudio": "Audio-Modus",
"SettingsDisableNotifications": "benachrichtigungen verbergen",
"MediaPickerTitle": "Wähle, was du speichern willst.",
"MediaPickerExplanationPC": "Klick oder Rechtsklick um herunterzuladen was du willst.",
"MediaPickerExplanationPhone": "Drücke oder halte gedrückt, um herunterzuladen was du willst.",
"MediaPickerExplanationPhoneIOS": "Drücke und halte den Download-Button, verstecke die Video-Vorschau, und wähle \"lade verlinkte Datei herunter\" um zu speichern.",
"TwitterSpaceWasntRecorded": "Dieser Twitter-Raum wurde nicht aufgezeichnet, also gibt es nichts zum herunterladen. Versuchen Sie einen anderen!",
"ErrorCantProcess": "Ich konnte deine Anfrage nicht bearbeiten :(\nDu kannst es erneut versuchen, aber wenn das Problem weiterhin besteht, bitte {ContactLink}.",
"ChangelogPressToHide": "einklappen",
"Donate": "Spende",
"DonateSub": "Hilf mir, es am laufen zu halten.",
"DonateExplanation": "{appName} zeigt keine Werbung und verkauft auch deine Daten nicht. Daher ist es <span class=\"text-backdrop\">völlig kostenlos</span> nutzbar. Aber es stellt sich heraus, dass der Betrieb eines Webdienstes, der von tausenden Menschen genutzt wird, ziemlich kostspielig ist.\n\nWenn du {appName} nützlich findest und es online halten willst, oder einfach dem Entwickler danken willst, kannst du etwas finanziell beitragen! Jeder cent hilft und wird SEHR geschätzt :D",
"DonateVia": "Spenden via",
"DonateHireMe": "oder du kannst <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">mich einstellen</a>",
"SettingsVideoMute": "Ton stummschalten",
"SettingsVideoMuteExplanation": "entfernt audio von video-downloads wenn möglich.",
"ErrorSoundCloudNoClientId": "ich konnte den temporären token nicht erhalten, der benötigt wird, um songs aus soundcloud herunterzuladen. versuche es noch einmal. falls das Problem weiterhin besteht, {ContactLink}.",
"CollapseServices": "unterstützte dienste",
"CollapseSupport": "support & quellcode",
"CollapsePrivacy": "datenschutzrichtlinie",
"ServicesNote": "diese liste ist nicht vollständig und erweitert sich im laufe der zeit, denke daran nochmal nachzuschauen!",
"FollowSupport": "folge {appName} auf mastodon oder twitter für support, umfragen, neuigkeiten, und mehr:",
"SupportNote": "bitte beachte, dass das beantworten von fragen und fehlermeldungen eine weile dauert, es gibt nur eine Person die alles managed.",
"SourceCode": "melde Probleme, erkundige den Quellcode, sterne oder forke das Repo:",
"PrivacyPolicy": "{appName}s datenschutzrichtlinie ist einfach: keine deiner daten werden gesammelt oder gespeichert. null, nada, niente, nichts.\nwas du herunterlädst ist dein ding, nicht meins.\n\neinige nicht zurückverfolgbare daten werden vorübergehend gespeichert, wenn ein angeforderter download live rendering erfordert. das ist notwendig, damit diese funktion funktioniert.\n\nin diesem fall werden, <span class=\"text-backdrop\">der salted sha256 hash deiner ip-adresse</span> und informationen über den angeforderten stream temporär im RAM des servers für <span class=\"text-backdrop\">2 minuten</span> gespeichert. nach 2 minuten werden alle zuvor gespeicherten informationen dauerhaft gelöscht. der hash deiner ip-adresse wird verwendet, <span class=\"text-backdrop\">um den zugang zum stream auf dich zu beschränken</span>.\nniemand (nichtmal ich) hat zugriff auf diese daten, da die offizielle {appName}-codebasis keine möglichkeit bietet, sie außerhalb der verarbeitungsfunktionen zu lesen.\n\ndu kannst {appName}s <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> selbst überprüfen und sehen, dass tatsächlich nichts dauerhaft gespeichert wird.",
"ErrorYTUnavailable": "Dieses YouTube-Video ist nicht verfügbar oder Altersbeschränkt. Ich kann derzeit keine Videos mit sensiblen Inhalten herunterladen. Versuche ein anderes...",
"ErrorYTTryOtherCodec": "Ich konnte nichts mit deinen Einstellungen zum Download finden. Versuche einen anderen Codec oder andere Qualitätseinstellungen!\n\nBitte beachte: Die YouTube API agiert manchmal unerwartet. Dafür kann ich nichts - das ist Googles schuld.",
"SettingsCodecSubtitle": "YouTube-Codec",
"SettingsCodecDescription": "h264: Im Allgemeinen bessere Unterstützung, aber die Qualität ist auf 1080p begrenzt.\nav1: geringe Unterstützung, unterstützt aber 8k & HDR.\nvp9: Normalerweise höchste Bitrate, bewahrt die meisten Details. Unterstützt 4k & HDR.\n\nFür die beste Kompatibilität wähle H264 aus.",
"SettingsAudioDub": "YouTube-Audiospur",
"SettingsAudioDubDescription": "Legt fest, welche Audiospur verwendet werden soll. Wenn die synchronisierte Spur nicht verfügbar ist, wird stattdessen die ursprüngliche Videosprache verwendet.\n\nOriginal: Originalsprache wird verwendet.\nAuto: Sprache wird aus Browser übernommen.",
"SettingsDubDefault": "German (de-de)",
"SettingsDubAuto": "Auto",
"SettingsVimeoPrefer": "Vimeo Download-Typ",
"SettingsVimeoPreferDescription": "progressive: direkter link zu vimeo's cdn. maximal 1080p.\ndash: video und audio werden durch {appName} zu einer datei kombiniert. maximal 4k.\n\nwähle \"progressive\" für bessere kompabilität mit editoren/wiedergabe/sozialen medien. falls progressive nicht funktioniert, wird automatisch dash genutzt. "
}
}

View file

@ -1,12 +1,13 @@
{
"name": "english",
"substrings": {
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>"
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">file an issue on github</a>"
},
"strings": {
"LinkInput": "paste the link here",
"AboutSummary": "{appName} is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
"EmbedBriefDescription": "save what you love without ads, trackers, or other creepy bullshit.",
"AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit. simply paste a share link and you're ready to rock!",
"AboutSupportedServices": "currently supported services:",
"EmbedBriefDescription": "save content from social media without annoyances",
"MadeWithLove": "made with <3 by wukko",
"AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup",
@ -19,40 +20,48 @@
"TitlePopupError": "uh-oh...",
"TitlePopupChangelog": "what's new?",
"TitlePopupDonate": "support {appName}",
"TitlePopupDownload": "how to continue?",
"ErrorSomethingWentWrong": "something went wrong and i couldn't get anything for you. try again, but if issue persists, {ContactLink}.",
"ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid. have you pasted the right link?",
"TitlePopupDownload": "download",
"ErrorSomethingWentWrong": "something went wrong and i couldn't get anything for you. you can try again, but if issue persists, please {ContactLink}.",
"ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid.",
"ErrorBrokenLink": "{s} is supported, but something is wrong with your link. maybe you didn't copy it fully?",
"ErrorNoLink": "i can't guess what you want to download! please give me a link :(",
"ErrorPageRenderFail": "if you're reading this, then there's something wrong with the page renderer. please {ContactLink}. make sure to provide the domain this error is present on and current commit hash ({s}). thank you in advance :D",
"ErrorRateLimit": "you're making too many requests. try again in a minute!",
"ErrorCouldntFetch": "i couldn't find anything about this link. check if it works and try again! some content may be region restricted, so keep that in mind.",
"ErrorLengthLimit": "i can't process videos longer than {s} minutes, so pick something shorter instead!",
"ErrorBadFetch": "something went wrong when i tried getting info about your link. are you sure it works? check if it does, and try again.",
"ErrorNoLink": "i can't guess what you want to download! please give me a link.",
"ErrorPageRenderFail": "something went wrong and page couldn't render. if it's a recurring or critical issue, please {ContactLink}. it'd be useful if you provided current commit hash ({s}) and error recreation steps. thank you in advance :D",
"ErrorRateLimit": "you're making too many requests. calm down and try again in a bit.",
"ErrorCouldntFetch": "couldn't get any info about your link. check if it's correct and try again.",
"ErrorLengthLimit": "current length limit is {s} minutes. video that you tried to download is longer than {s} minutes. pick something else!",
"ErrorBadFetch": "an error occurred when i tried to get info about your link. are you sure it works? check if it does, and try again.",
"ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.",
"ErrorNoInternet": "there's no internet or {appName} api is down. check your connection and try again.",
"ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. maybe it's down, or {appName} got blocked. try again, but if error persists, {ContactLink}.",
"ErrorEmptyDownload": "i don't see anything i could download by your link. try a different one!",
"ErrorLiveVideo": "this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!",
"ErrorCantConnectToServiceAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.",
"ErrorEmptyDownload": "i don't see anything i could download from here. try a different link.",
"ErrorLiveVideo": "i can't look into future and download a video live of which is ongoing. wait for the stream to finish and try again!",
"SettingsAppearanceSubtitle": "appearance",
"SettingsThemeSubtitle": "theme",
"SettingsFormatSubtitle": "format",
"SettingsFormatSubtitle": "download format",
"SettingsQualitySubtitle": "quality",
"SettingsThemeAuto": "auto",
"SettingsThemeLight": "light",
"SettingsThemeDark": "dark",
"SettingsQualitySwitchMax": "max",
"SettingsQualitySwitchHigh": "high",
"SettingsQualitySwitchMedium": "medium",
"SettingsQualitySwitchLow": "low",
"SettingsQualitySwitchLowest": "lowest",
"SettingsKeepDownloadButton": "keep &gt;&gt; visible",
"AccessibilityKeepDownloadButton": "keep the download button always visible",
"SettingsEnableDownloadPopup": "ask how to save",
"SettingsEnableDownloadPopup": "ask for a way to save",
"AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher bitrate, but ios devices can't play them natively.",
"SettingsQualityDescription": "if selected quality isn't available, closest one gets picked instead.\nif you want to post a youtube video on social media, select a combination of mp4 and 720p.",
"LinkGitHubIssues": "&gt;&gt; report issues and check out the source code on github",
"LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github",
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.",
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.",
"DownloadPopupDescriptionIOS": "press and hold the download button, hide the video preview, and then select \"download linked file\" to save.",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"DownloadPopupWayToSave": "pick a way to save",
"ClickToCopy": "press to copy",
"Download": "download",
"CopyURL": "copy",
"CopyURL": "copy url",
"AboutTab": "about",
"ChangelogTab": "changelog",
"DonationsTab": "donations",
@ -61,63 +70,46 @@
"SettingsOtherTab": "other",
"ChangelogLastMajor": "current version & commit",
"AccessibilityModeToggle": "toggle download mode",
"DonateLinksDescription": "this is the best way to donate if you want me to receive your donation directly.",
"DonateLinksDescription": "donation links open in a new tab. this is the best way to donate if you want me to receive your donation directly.",
"SettingsAudioFormatBest": "best",
"SettingsAudioFormatDescription": "when \"best\" format is selected, you get audio the way it is on service's side. it's not re-encoded. everything else will be re-encoded.",
"SettingsAudioFormatDescription": "when best format is selected, you get audio in best quality available, because it's not re-encoded. everything else will be re-encoded.",
"Keyphrase": "save what you love",
"SettingsRemoveWatermark": "disable watermark",
"ErrorPopupCloseButton": "got it",
"ErrorLengthAudioConvert": "i can't convert audio longer than {s} minutes. pick \"best\" format if you want to avoid limitations!",
"SettingsAudioFullTikTok": "full audio",
"SettingsAudioFullTikTokDescription": "downloads original sound used in the video without any additional changes by the post's author.",
"ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one! if issue persists, {ContactLink}.",
"ErrorNoVideosInTweet": "there are no videos or gifs in this tweet, try another one!",
"ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format if you want to avoid limitations.",
"SettingsAudioFullTikTok": "download full audio",
"SettingsAudioFullTikTokDescription": "downloads original audio or sound used in video without any additional changes by the video author.",
"ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one.",
"ErrorNoVideosInTweet": "i couldn't find any videos or gifs in this tweet. try another one!",
"ImagePickerTitle": "pick images to download",
"ImagePickerDownloadAudio": "download audio",
"ImagePickerExplanationPC": "right click an image to save it.",
"ImagePickerExplanationPhone": "press and hold an image to save it.",
"ErrorNoUrlReturned": "i didn't get a download link from the server. this should never happen. try again, but if it still doesn't work, {ContactLink}.",
"ErrorUnknownStatus": "i received a response i can't process. this should never happen. try again, but if it still doesn't work, {ContactLink}.",
"PasteFromClipboard": "paste and download",
"ErrorNoUrlReturned": "server didn't return a download link. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.",
"ErrorUnknownStatus": "i received a response i can't process. most likely something with status is wrong. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.",
"PasteFromClipboard": "paste from clipboard",
"FollowTwitter": "follow {appName}'s twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ChangelogOlder": "previous versions",
"ChangelogPressToExpand": "expand",
"ChangelogPressToExpand": "press to expand",
"Miscellaneous": "miscellaneous",
"ModeToggleAuto": "auto mode",
"ModeToggleAudio": "audio mode",
"SettingsDisableNotifications": "hide notifications",
"SettingsDisableNotifications": "hide notification dots",
"MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.",
"MediaPickerExplanationPhone": "press or press and hold to download what you want.",
"MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save.",
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!",
"ErrorCantProcess": "i couldn't process your request :(\nyou can try again, but if issue persists, please {ContactLink}.",
"ChangelogPressToHide": "collapse",
"ChangelogPressToHide": "press to collapse",
"Donate": "donate",
"DonateSub": "help me keep it up",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but turns out keeping up a web service used by over 40 thousand people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated :D",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but hey! turns out keeping up a web service used by hundreds of thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.",
"DonateVia": "donate via",
"DonateHireMe": "or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a>",
"DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">hire me</a>.",
"SettingsVideoMute": "mute audio",
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
"ErrorSoundCloudNoClientId": "i couldn't get the temporary token that's required to download songs from soundcloud. try again, but if issue persists, {ContactLink}.",
"CollapseServices": "supported services",
"CollapseSupport": "support & source code",
"CollapsePrivacy": "privacy policy",
"ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!",
"FollowSupport": "follow {appName} on mastodon or twitter for support, polls, news, and more:",
"SupportNote": "please note that questions and issues may take a while to respond to, there's only one person managing everything.",
"SourceCode": "report issues, explore source code, star or fork the repo:",
"PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, <span class=\"text-backdrop\">salted sha256 hash of your ip address</span> and information about requested stream are temporarily stored in server's RAM for <span class=\"text-backdrop\">2 minutes</span>. after 2 minutes all previously stored information is permanently removed. hash of your ip address is <span class=\"text-backdrop\">used for limiting stream access only to you</span>.\nno one (even me) has access to this data, because official {appName} codebase doesn't provide a way to read it outside of processing functions in the first place.\n\nyou can check {appName}'s <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> yourself and see that everything is as stated.",
"ErrorYTUnavailable": "this youtube video is unavailable or age restricted. i am currently unable to download videos with sensitive content. try another one!",
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.",
"SettingsCodecSubtitle": "youtube codec",
"SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.",
"SettingsAudioDub": "youtube audio track",
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and {appName}) language is used.",
"SettingsDubDefault": "original",
"SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "vimeo downloads type",
"SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by {appName} into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.",
"ShareURL": "share"
"SettingsVideoMuteExplanation": "disables audio in downloaded video when possible. ignored when audio mode is on or service only supports audio.",
"SettingsVideoGeneral": "general",
"ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}."
}
}

View file

@ -1,13 +1,12 @@
{
"name": "español",
"substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">presenta un problema en github</a>"
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">reportar un problema en github</a>"
},
"strings": {
"LinkInput": "pega tu enlace aquí",
"AboutSummary": "{appName} es tu lugar ideal para descargas de redes sociales. sin anuncios u otras mierdas sospechosas. ¡solo necesitas pegar un enlace y listo!",
"AboutSupportedServices": "servicios compatibles:",
"EmbedBriefDescription": "guarda contenido de redes sociales sin preocuparte por rastreadores",
"AboutSummary": "{appName} es tu lugar de confianza para descargar videos de redes sociales y sitios como youtube. sin anuncios, rastreadores, ni otras tonterías. ¡solo tienes que pegar el enlace de lo que quieras descargar y ya estás listo para arrasar!",
"EmbedBriefDescription": "guarda lo que amas sin anuncios, rastreadores ni otras tonterías.",
"MadeWithLove": "hecho con <3 por wukko",
"AccessibilityInputArea": "cuadro de captura",
"AccessibilityOpenAbout": "abrir ventana emergente de acerca de",
@ -17,45 +16,37 @@
"AccessibilityOpenDonate": "abrir ventana emergente de donación",
"TitlePopupAbout": "¿qué es {appName}?",
"TitlePopupSettings": "ajustes",
"TitlePopupError": "oh-no...",
"TitlePopupError": "oh no...",
"TitlePopupChangelog": "¿qué hay de nuevo?",
"TitlePopupDonate": "apoya a {appName}",
"TitlePopupDownload": "descargar",
"ErrorSomethingWentWrong": "algo salió mal y no pude encontrar nada para ti. puedes intentar de nuevo, pero si el problema persiste, por favor {ContactLink}.",
"ErrorUnsupported": "parece que este servicio aún no es compatible o tu enlace no es válido.",
"TitlePopupDownload": "¿cómo continuar?",
"ErrorSomethingWentWrong": "algo salió mal y no pude encontrar nada para ti. intenta de nuevo, pero si el problema persiste, {ContactLink}.",
"ErrorUnsupported": "parece que este servicio aún no está soportado o tu enlace es inválido. ¿has introducido el enlace correcto?",
"ErrorBrokenLink": "{s} es compatible con cobalt, pero algo está mal con tu enlace. ¿tal vez no lo copiaste completamente?",
"ErrorNoLink": "¡no puedo adivinar qué quieres descargar! por favor introduce un enlace.",
"ErrorPageRenderFail": "algo salió mal y la página no se pudo procesar. si quieres que solucione esto, por favor {ContactLink}. sería útil si proporcionas el commit hash ({s}) junto con pasos de recreación, gracias :D",
"ErrorRateLimit": "estás haciendo demasiadas solicitudes. cálmate y vuelve a intentarlo en unos minutos.",
"ErrorCouldntFetch": "no se pudo obtener ninguna información sobre tu enlace. comprueba si tu enlace es correcto e inténtalo de nuevo.",
"ErrorLengthLimit": "el limite de duración actual es de {s} minutos. lo que intentaste descargar es mas largo que eso. ¡escoge otra cosa que descargar!",
"ErrorBadFetch": "algo salió mal con la obtención de info. puedes probar con un formato y una resolución diferentes o simplemente intentarlo de nuevo más tarde.",
"ErrorCorruptedStream": "parece que esta descarga está corrupta. inténtalo de nuevo o intenta con otro formato o resolución.",
"ErrorNoLink": "¡no puedo adivinar qué quieres descargar! por favor introduce un enlace :(",
"ErrorPageRenderFail": "si estás leyendo esto, entonces hay algo mal con el renderizado de la página. por favor {ContactLink}. asegúrate de proporcionar el dominio en el que este error está presente y el hash del commit actual ({s}). gracias de antemano :D",
"ErrorRateLimit": "estás haciendo demasiadas solicitudes. ¡inténtalo de nuevo en un minuto!",
"ErrorCouldntFetch": "no he podido encontrar nada de este enlace. ¡comprueba que funcione e intenta de nuevo! algunos contenidos pueden estar restringidos por región, así que ten eso en cuenta.",
"ErrorLengthLimit": "no puedo procesar vídeos más de {s} minutos, ¡así que elige algo más corto!",
"ErrorBadFetch": "algo salió mal cuando trate de obtener información sobre tu enlace. ¿estás seguro de que funciona? asegurate de que sea así y prueba nuevamente.",
"ErrorNoInternet": "parece que no hay internet o la api de {appName} no está disponible. revisa tu conexión e intenta de nuevo.",
"ErrorCantConnectToServiceAPI": "no pude conectarme a la api de {s} . parace que {s} no está disponible o la ip del servidor de {appName} fue bloqueada. inténtalo de nuevo mas tarde.",
"ErrorEmptyDownload": "parece que no hay nada que descargar. ¡intentalo de nuevo con otro enlace!",
"ErrorLiveVideo": "no se puede descargar un video en vivo. espera que termine la transmisión y vuelve a intentarlo.",
"ErrorCantConnectToServiceAPI": "no he podido conectarme a la api del servicio. puede que este caída, o {appName} fue bloqueado. intenta nuevamente, pero si el error persiste, {ContactLink}.",
"ErrorEmptyDownload": "no veo nada que pueda descargar de tu enlace. ¡prueba con otro!",
"ErrorLiveVideo": "Esto es una transmisión en vivo, todavía no he aprendido a mirar al futuro. ¡Espera a que la transmisión finalice e inténtalo de nuevo!",
"SettingsAppearanceSubtitle": "apariencia",
"SettingsThemeSubtitle": "tema",
"SettingsFormatSubtitle": "formato de descarga",
"SettingsFormatSubtitle": "formato",
"SettingsQualitySubtitle": "calidad",
"SettingsThemeAuto": "auto",
"SettingsThemeLight": "claro",
"SettingsThemeDark": "oscuro",
"SettingsQualitySwitchMax": "max",
"SettingsQualitySwitchHigh": "alta",
"SettingsQualitySwitchMedium": "media",
"SettingsQualitySwitchLow": "baja",
"SettingsQualitySwitchLowest": "mas baja",
"SettingsKeepDownloadButton": "mantener &gt;&gt; visible",
"AccessibilityKeepDownloadButton": "mantener el botón de descarga siempre visible",
"SettingsEnableDownloadPopup": "pregunta por la forma de guardar",
"SettingsEnableDownloadPopup": "pregunta cómo guardar",
"AccessibilityEnableDownloadPopup": "preguntar qué hacer con las descargas",
"SettingsFormatDescription": "selecciona webm si necesitas la máxima calidad disponible. los videos webm suelen tener un mayor bitrate, pero los dispositivos ios no pueden reproducirlos de forma nativa.",
"SettingsQualityDescription": "si la calidad seleccionada no está disponible, la más cercana es elegida en su lugar.\nsi quieres publicar un vídeo de youtube en las redes sociales, selecciona una combinación de mp4 y 720p. esos videos normalmente no están en el códec av1, por lo que deberían reproducirse bien básicamente en todas partes.",
"LinkGitHubIssues": "&gt;&gt; informa sobre problemas y consulta el código fuente en github",
"SettingsQualityDescription": "si la calidad seleccionada no está disponible, la más cercana a esta se utilizará en su lugar.",
"LinkGitHubChanges": "&gt;&gt; mira los cambios anteriores y contribuye en github",
"NoScriptMessage": "{appName} usa javascript para las solicitudes de api y para la interfaz interactiva. tienes que permitir javascript en tu navegador para usar este sitio. no tenemos ningún anuncio ni rastreadores, lo prometo con el meñique.",
"NoScriptMessage": "{appName} utiliza javascript para hacer solicitudes hacia la api y la interfaz interactiva. necesitas activar javascript para usar esta página. no hay ningún script raro, lo prometo con el meñique.",
"DownloadPopupDescriptionIOS": "como tienes un dispositivo ios, debes mantener presionado el botón de descarga y luego seleccionar \"descargar video\" en la ventana emergente que aparece para guardar el video. esto será necesario mientras apple obligue a todos los desarrolladores de navegadores en ios a usar safari webview",
"DownloadPopupDescription": "el botón de descarga abre una nueva pestaña con el archivo solicitado. puedes desactivar esta ventana emergente en los ajustes.",
"DownloadPopupWayToSave": "elige una forma de guardar",
@ -63,53 +54,69 @@
"Download": "descargar",
"CopyURL": "copiar url",
"AboutTab": "acerca de",
"ChangelogTab": "changelog",
"ChangelogTab": "registro de cambios",
"DonationsTab": "donaciones",
"SettingsVideoTab": "vídeo",
"SettingsAudioTab": "audio",
"SettingsOtherTab": "otros",
"ChangelogLastMajor": "versión actual y commit",
"AccessibilityModeToggle": "cambiar el modo de descarga",
"DonateLinksDescription": "los enlaces de donación se abren en una nueva pestaña. esta es la mejor manera para donar dinero si quieres que lo reciba directamente.",
"DonateLinksDescription": "esta es la mejor manera de donar si quieres que reciba tu donación directamente.",
"SettingsAudioFormatBest": "mejor",
"SettingsAudioFormatDescription": "cuando seleccionas el formato mejor, obtienes audio en la mejor calidad disponible, porque el audio se mantiene en su formato original. si seleccionas otro formato obtendrás un archivo ligeramente comprimido",
"SettingsAudioFormatDescription": "cuando el \"mejor\" formato está seleccionado, obtendrás el audio en la manera que está en el lado del servicio. no estará recodificado. todo lo demás lo estará.",
"Keyphrase": "guarda lo que amas",
"SettingsRemoveWatermark": "desactivar marca de agua",
"ErrorPopupCloseButton": "vale",
"ErrorLengthAudioConvert": "el límite de duración actual para la conversión de audio es de {s} minutos. escoge el formato \"mejor\" !",
"SettingsAudioFullTikTok": "descargar el audio completo",
"SettingsAudioFullTikTokDescription": "se descarga el audio original o el sonido usado en el vídeo sin ningún cambio adicional por el autor del vídeo",
"ErrorCantGetID": "No pude obtener la info completa del enlace acortado. asegúrate de que funciona o prueba un enlace completo.",
"ErrorNoVideosInTweet": "este tweet no tiene videos o gifs. ¡inténtalo con otro!",
"ErrorLengthAudioConvert": "no puedo convertir audio que dure más de {s} minutos. ¡elige el \"mejor\" formato si quieres evitar limitaciones!",
"SettingsAudioFullTikTok": "audio completo",
"SettingsAudioFullTikTokDescription": "descarga el sonido original usado en el video sin ningún cambio adicional hecho por el autor de la publicación.",
"ErrorCantGetID": "no pude obtener la información completa del enlace acortado. ¡comprueba que funcione o intenta con uno directo! si el problema persiste, {ContactLink}.",
"ErrorNoVideosInTweet": "no hay videos ni gifs en este tweet, ¡intenta con otro!",
"ImagePickerTitle": "elige imágenes para descargar",
"ImagePickerDownloadAudio": "descargar audio",
"ImagePickerExplanationPC": "haz clic derecho en una imagen para guardarla.",
"ImagePickerExplanationPhone": "mantén presionada una imagen para guardarla.",
"ErrorNoUrlReturned": "el servidor no devolvió un enlace de descarga. Esto nunca debería suceder. recarga la página y vuelve a intentarlo, pero si eso no ayuda, {ContactLink}.",
"ErrorUnknownStatus": "he recibido una respuesta que no puedo procesar. lo más probable es que algo con el status esté mal. esto nunca debería suceder. recarga la página y vuelve a intentarlo, pero si eso no ayuda, {ContactLink}.",
"PasteFromClipboard": "pegar desde el portapapeles",
"FollowTwitter": "sigue la cuenta de {appName} en twitter para encuestas, actualizaciones y más: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ErrorNoUrlReturned": "no he recibido un enlace de descarga del servidor. esto no debería pasar nunca. intenta de nuevo, pero si todavía no funciona, {ContactLink}.",
"ErrorUnknownStatus": "he recibido una respuesta que no puedo procesar. esto no debería pasar nunca. intenta de nuevo, pero si todavía no funciona, {ContactLink}.",
"PasteFromClipboard": "pegar y descargar",
"ChangelogOlder": "versiones anteriores",
"ChangelogPressToExpand": "presiona para expandir",
"ChangelogPressToExpand": "mostrar más",
"Miscellaneous": "otros",
"ModeToggleAuto": "modo automático ",
"ModeToggleAudio": "modo audio",
"SettingsDisableNotifications": "ocultar burbujas de notificación",
"SettingsDisableNotifications": "ocultar notificaciones",
"MediaPickerTitle": "elige qué guardar",
"MediaPickerExplanationPC": "haz clic o clic derecho para descargar lo que quieras.",
"MediaPickerExplanationPhone": "presiona o presiona y mantén pulsado para descargar lo que quieras.",
"MediaPickerExplanationPhoneIOS": "mantén presionado, oculta la vista previa, y luego selecciona \"descargar archivo enlazado\" para guardar.",
"TwitterSpaceWasntRecorded": "este espacio de twitter no fue grabado, así que no hay nada que descargar. ¡prueba con otro!",
"ErrorCantProcess": "no he podido procesar tu solicitud :(\npuedes intentarlo de nuevo, pero si el problema persiste, por favor {ContactLink}.",
"ChangelogPressToHide": "presiona para ocultar",
"ChangelogPressToHide": "ocultar",
"Donate": "donar",
"DonateSub": "ayúdame a mantenerlo",
"DonateExplanation": "{appName} no muestra anuncios (y nunca lo hará) o vende tus datos, por lo tanto es <span class=\"text-backdrop\">completamente gratis de usar</span>. pero resulta ser que mantener un servicio web usado por miles de personas es más o menos costoso\n\nsi alguna vez has encontrado que {appName} te es útil y quieres mantenerlo en línea, o simplemente quieres darle las gracias al desarrollador, ¡concidera aportar algo! cada centavo ayuda y es MUY apreciado\n",
"DonateExplanation": "{appName} no muestra anuncios ni vende tu información (y nunca lo hara), por lo que es <span class=\"text-backdrop\">de uso completamente gratuito</span>. pero resulta que mantener un servicio web utilizado por mas de 40 mil personas es algo costoso.\n\nsi alguna vez {appName} te ha resultado util y quieres que siga en línea, o simplemente quieres apoyar al desarrollador, ¡considera donar! cada céntimo ayuda y es MUY apreciado :D",
"DonateVia": "donar vía",
"DonateHireMe": "o, como alternativa, puedes <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">contratarme</a>.",
"DonateHireMe": "o puedes <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">contratarme</a>",
"SettingsVideoMute": "silenciar audio",
"SettingsVideoMuteExplanation": "deshabilita el audio en el vídeo descargado cuando sea posible. obtendrás el archivo de vídeo fuente si los canales de vídeo y audio se sirven en dos archivos por el servicio de origen. se ignora cuando el modo de audio está encendido o si el servicio solo soporta audio.",
"SettingsVideoGeneral": "general",
"ErrorSoundCloudNoClientId": "no se pudo encontrar el client_id necesario para obtener datos de audio de soundcloud. Inténtalo de nuevo, y si el problema persiste, {ContactLink}."
"SettingsVideoMuteExplanation": "elimina el audio de las descargas de vídeo cuando sea posible.",
"ErrorSoundCloudNoClientId": "no pude obtener el token temporal necesario para descargar canciones desde soundcloud. intenta nuevamente, pero si el error sigue, {ContactLink}.",
"CollapseServices": "servicios compatibles",
"CollapseSupport": "soporte y código fuente",
"CollapsePrivacy": "política de privacidad",
"ServicesNote": "esta lista no es definitiva y sigue ampliándose con el tiempo, ¡asegúrate de revisarla de vez en cuando!",
"FollowSupport": "sigue a {appName} en mastodon o twitter para obtener asistencia, encuestas, noticias y más:",
"SupportNote": "tenga en cuenta que las preguntas y los problemas pueden tardar un tiempo en responderse, solo hay una persona que se encarga de todo.",
"SourceCode": "informar problemas, explorar el código fuente, destacar o bifurcar el repositorio:",
"PrivacyPolicy": "la política de privacidad de {appName} es simple: no se recopila ni almacena ningún dato de tu persona. cero, nada, nada de nada.\nlo que tu descargues es asunto tuyo, no mío.\n\nalgunos datos que no se pueden rastrear se almacenan temporalmente cuando la descarga solicitada requiere procesamiento en vivo. es necesario para que esa función actué con normalidad.\n\nen ese caso, <span class=\"text-backdrop\">un hash de tu dirección ip en sha256 con salt</span> e información sobre la solicitud del recurso se guardan temporalmente en la memoria RAM del servidor por <span class=\"text-backdrop\">2 minutos</span>. después de 2 minutos toda información guardada con anterioridad se elimina permanentemente. el hash de tu dirección ip <span class=\"text-backdrop\">se utiliza para limitar el acceso al recurso solo a ti</span>.\nnadie (ni siquiera yo) tiene acceso a esta información, porque el código base oficial de {appName} no proporciona una manera de leerla fuera de las funciones para procesarla en primer lugar.\n\npuedes comprobar el <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">repositorio de github</a> de {appName} tu mismo y ver que todo es como se indica.",
"ErrorYTUnavailable": "este video de youtube no está disponible o tiene restricción de edad. no puedo descargar videos con contenido sensible. ¡prueba con otro!",
"ErrorYTTryOtherCodec": "no he podido encontrar nada para descargar con tu configuración. ¡Prueba otro código o calidad!\n\nNota: la api de youtube a veces actúa inesperadamente. Culpa a google por esto, no a mí.",
"SettingsCodecSubtitle": "codec de youtube",
"SettingsCodecDescription": "h264: generalmente mejor soporte con la mayoría de reproductores, pero la calidad máxima es 1080p.\nav1: sin soporte por parte de la mayoría de reproductores, pero soporta 8k y HDR.\nvp9: normalmente la tasa de bits más alta, conserva más detalles. soporta 4k y HDR.\n\nelige h264 si quieres mejor compatibilidad para editores/reproductores/redes sociales.",
"SettingsAudioDub": "pista de audio de youtube",
"SettingsAudioDubDescription": "Define qué pista de audio se utilizará. Si la pista doblada no está disponible, en su lugar se usará el idioma de vídeo original.\n\nOriginal: se utiliza el idioma original del vídeo.\nAuto: se utiliza el idioma del navegador predeterminado (y {appName}).",
"SettingsDubDefault": "original",
"SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "tipo de descargas de vimeo",
"SettingsVimeoPreferDescription": "progresivo: enlace directo al archivo en el cdn de vimeo. la calidad máxima es de 1080p.\ndash: el video y el audio es fusionado por {appName} en un solo archivo. la calidad máxima es 4k.\n\nelige \"progresivo\" si quieres la mejor compatibilidad para editores/reproductores/redes sociales. si \"progresivo\" no está disponible, se utilizará \"dash\" en su lugar."
}
}

View file

@ -0,0 +1,120 @@
{
"name": "englanti",
"substrings": {
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">lähetä ongelma githubissa</a>"
},
"strings": {
"LinkInput": "liitä linkki tähän",
"AboutSummary": "{appName} on sosiaalisen median lataussivusto. nolla mainoksia, seurantalaitteita tai muuta pelottavaa paskaa. liitä vain jakolinkki ja olet valmis rokkaamaan!",
"EmbedBriefDescription": "tallenna sisältöä sosiaalisesta mediasta ilman häiriöitä",
"MadeWithLove": "tehty wukko kanssa <3",
"AccessibilityInputArea": "linkin syöttöalue",
"AccessibilityOpenAbout": "avaa \"tietoa\"-ponnahdusikkuna",
"AccessibilityDownloadButton": "latauspainike",
"AccessibilityOpenSettings": "avaa \"asetukset\" -ponnahdusikkuna",
"AccessibilityClosePopup": "sulje ponnahdusikkuna",
"AccessibilityOpenDonate": "avaa \"lahjoitus\"-ponnahdusikkuna",
"TitlePopupAbout": "mikä on {appName}?",
"TitlePopupSettings": "asetukset",
"TitlePopupError": "voi ei...",
"TitlePopupChangelog": "mikä on uutta?",
"TitlePopupDonate": "tuki {appName}",
"TitlePopupDownload": "lataa",
"ErrorSomethingWentWrong": "jotain meni pieleen enkä saanut sinulle mitään. voit yrittää uudelleen, mutta jos ongelma jatkuu, {ContactLink}.",
"ErrorUnsupported": "näyttää siltä, että tätä palvelua ei vielä tueta tai linkkisi on virheellinen.",
"ErrorBrokenLink": "{s} on tuettu, mutta linkissäsi on jotain vikaa. et ehkä kopioinut sitä kokonaan?",
"ErrorNoLink": "en voi arvata, mitä haluat ladata! anna minulle se linkki",
"ErrorPageRenderFail": "jotain meni pieleen ja sivua ei voitu renderöidä. jos se on toistuva tai kriittinen ongelma, {ContactLink}. olisi hyödyllistä, jos voisit antaa nykyiset vahvistushajautusvaiheet ({s}) ja virheen jälleenrakennusvaiheet. Kiitos jo etukäteen",
"ErrorRateLimit": "teet liian monta pyyntöä. yritä myöhemmin uudelleen!",
"ErrorCouldntFetch": "linkistäsi ei löytynyt tietoa. tarkista onko se oikein ja yritä uudelleen.",
"ErrorLengthLimit": "nykyinen pituusrajoitus on {s} minuuttia. video, jonka yritit ladata, on yli {s} minuuttia. valitse jotain muuta!",
"ErrorBadFetch": "tapahtui virhe, kun yritettiin hakea tietoja linkistäsi. oletko varma, että se toimii? tarkista toimiiko se ja yritä uudelleen.",
"ErrorCorruptedStream": "tämä lataus on valitettavasti vioittunut. yritä uudelleen tai kokeile toista muotoa ja resoluutiota.",
"ErrorNoInternet": "internetiä ei ole tai {appName}-sovellusliittymä on poissa käytöstä. tarkista yhteys ja yritä uudelleen.",
"ErrorCantConnectToServiceAPI": "ei voi muodostaa yhteyttä sovellukseen {s}. näyttää siltä, että {s} ei ole käytössä tai palvelimen {appName} IP-osoite on estetty. yritä myöhemmin uudelleen.",
"ErrorEmptyDownload": "En näe täältä mitään ladattavaa. kokeile toista linkkiä.",
"ErrorLiveVideo": "En voi katsoa tulevaisuuteen ja ladata live-videota tapahtumista. odota striimin loppumista ja yritä uudelleen!",
"SettingsAppearanceSubtitle": "ulkomuoto",
"SettingsThemeSubtitle": "teema",
"SettingsFormatSubtitle": "latausmuoto",
"SettingsQualitySubtitle": "laatu",
"SettingsThemeAuto": "Automaattinen",
"SettingsThemeLight": "valoa",
"SettingsThemeDark": "tumma",
"SettingsQualitySwitchMax": "maksimi",
"SettingsQualitySwitchHigh": "korkea",
"SettingsQualitySwitchMedium": "keskikokoinen",
"SettingsQualitySwitchLow": "matala",
"SettingsQualitySwitchLowest": "alhaisin",
"SettingsKeepDownloadButton": "pitää &gt;&gt; näköpiirissä",
"AccessibilityKeepDownloadButton": "pidä latauspainike aina näkyvissä",
"SettingsEnableDownloadPopup": "kysy keinoa säästää",
"AccessibilityEnableDownloadPopup": "kysy mitä tehdä latauksille",
"SettingsFormatDescription": "valitse webm saadaksesi parhaan mahdollisen laadun. webm-videot ovat yleensä korkeampia bittinopeutta, mutta ios-laitteet eivät voi toistaa niitä alkuperäisesti.",
"SettingsQualityDescription": "jos valittua laatua ei ole saatavilla, valitaan lähin.\njos haluat lähettää youtube-videon sosiaalisessa mediassa, valitse mp4- ja 720p-yhdistelmä.",
"LinkGitHubChanges": "&gt;&gt; katso aiemmat sitoumukset ja osallistu githubissa",
"NoScriptMessage": "{appName} käyttää JavaScriptiä API-pyyntöihin ja interaktiiviseen käyttöliittymään. sinun on sallittava JavaScript käyttää tätä sivustoa. Minulla ei ole mainoksia tai seurantalaitteita, pinky lupaus.",
"DownloadPopupDescriptionIOS": "pidä latauspainiketta painettuna, piilota videon esikatselu ja valitse sitten \"lataa linkitetty tiedosto\".",
"DownloadPopupDescription": "latauspainike avaa uuden välilehden, jossa on pyydetty tiedosto. voit poistaa tämän ponnahdusikkunan käytöstä asetuksista.",
"DownloadPopupWayToSave": "valitse tapa säästää",
"ClickToCopy": "paina kopioidaksesi",
"Download": "lataa",
"CopyURL": "kopioi url",
"AboutTab": "noin",
"ChangelogTab": "muutosloki",
"DonationsTab": "lahjoituksia",
"SettingsVideoTab": "video",
"SettingsAudioTab": "audio",
"SettingsOtherTab": "muu",
"ChangelogLastMajor": "nykyinen versio & sitoudu",
"AccessibilityModeToggle": "vaihda lataustilaa",
"DonateLinksDescription": "lahjoituslinkit avautuvat uuteen välilehteen. tämä on paras tapa lahjoittaa, jos haluat minun vastaanottavan lahjoituksesi suoraan.",
"SettingsAudioFormatBest": "parhaat",
"SettingsAudioFormatDescription": "kun paras muoto valitaan, saat äänen parhaalla mahdollisella laadulla, koska sitä ei ole koodattu uudelleen. kaikki muu koodataan uudelleen.",
"Keyphrase": "säästää mitä rakastat",
"SettingsRemoveWatermark": "poista vesileima käytöstä",
"ErrorPopupCloseButton": "sain sen",
"ErrorLengthAudioConvert": "äänen muuntamisen nykyinen pituusrajoitus on {s} minuuttia. Valitse \"paras\" muoto, jos haluat välttää rajoituksia.",
"SettingsAudioFullTikTok": "lataa täysi ääni",
"SettingsAudioFullTikTokDescription": "lataa alkuperäisen äänen tai videossa käytetyn äänen ilman videon tekijän tekemiä lisämuutoksia.",
"ErrorCantGetID": "en saanut kaikkia tietoja lyhennetystä linkistä. varmista, että se toimii tai kokeile täydellistä.",
"ErrorNoVideosInTweet": "en löytänyt videoita tai gifiä tästä twiitistä. kokeile toista!",
"ImagePickerTitle": "valitse ladattavat kuvat",
"ImagePickerDownloadAudio": "lataa ääni",
"ImagePickerExplanationPC": "tallenna kuva oikealla painikkeella.",
"ImagePickerExplanationPhone": "paina ja pidä kuvaa tallentaaksesi sen.",
"ErrorNoUrlReturned": "palvelin ei palauttanut latauslinkkiä. tämän ei pitäisi koskaan tapahtua. lataa sivu uudelleen ja yritä uudelleen, mutta jos se ei auta, {ContactLink}.",
"ErrorUnknownStatus": "sain vastauksen, jota en voi käsitellä. todennäköisimmin jokin tila on vialla. tämän ei pitäisi koskaan tapahtua. lataa sivu uudelleen ja yritä uudelleen, mutta jos se ei auta, {ContactLink}.",
"PasteFromClipboard": "liitä leikepöydältä",
"ChangelogOlder": "aiemmat versiot",
"ChangelogPressToExpand": "paina laajentaaksesi",
"Miscellaneous": "sekalaista",
"ModeToggleAuto": "automaattinen tila",
"ModeToggleAudio": "äänitila",
"SettingsDisableNotifications": "piilota ilmoituspisteet",
"MediaPickerTitle": "valita mitä säästää",
"MediaPickerExplanationPC": "napsauta tai napsauta hiiren kakkospainikkeella ladataksesi haluamasi.",
"MediaPickerExplanationPhone": "paina tai paina ja pidä painettuna ladataksesi haluamasi.",
"MediaPickerExplanationPhoneIOS": "paina ja pidä painettuna, piilota esikatselu ja valitse sitten \"lataa linkitetty tiedosto\" tallentaaksesi.",
"TwitterSpaceWasntRecorded": "tätä twitter-tilaa ei tallennettu, joten ei ole mitään ladattavaa. kokeile toista!",
"ErrorCantProcess": "en voinut käsitellä pyyntöäsi :(\nvoit yrittää uudelleen, mutta jos ongelma jatkuu, {ContactLink}.",
"ChangelogPressToHide": "paina tiivistääksesi",
"Donate": "lahjoittaa",
"DonateSub": "auta minua jatkamaan sitä",
"DonateExplanation": "{appName} ei näytä (eikä koskaan) näytä mainoksia tai myy tietojasi, joten sen <span class=\"text-backdrop\">käyttö on täysin ilmaista</span>. mutta osoittaa, että tuhansien ihmisten käyttämän verkkopalvelun ylläpitäminen on jonkin verran kallista.\n\nJos olet joskus pitänyt {appName}-sovelluksesta hyödyllisenä ja haluat pitää sen verkossa tai haluat vain kiittää kehittäjää, harkitse osallistumista! jokainen sentti auttaa ja sitä arvostetaan ERITTÄIN.",
"DonateVia": "lahjoita kautta",
"DonateHireMe": "tai vaihtoehtoisesti voit <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">palkata minut</a>.",
"SettingsVideoMute": "mykistää äänen",
"SettingsVideoMuteExplanation": "poistaa äänen ladatusta videosta, jos mahdollista. ohitetaan, kun äänitila on päällä tai palvelu tukee vain ääntä.",
"SettingsVideoGeneral": "yleistä",
"ErrorSoundCloudNoClientId": "ei löytynyt asiakastunnusta, jota tarvitaan äänidatan hakemiseen soundcloudista. yritä uudelleen ja jos ongelma jatkuu, {ContactLink}.",
"CollapseServices": "tuetut palvelut",
"CollapseSupport": "tuki & lähdekoodi",
"CollapsePrivacy": "tietosuojakäytäntö",
"ServicesNote": "tämä luettelo ei ole lopullinen ja laajenee ajan myötä, muista tarkistaa se silloin tällöin!",
"FollowSupport": "seuraa {appName}a mastodonissa tai twitterissä saadaksesi tukea, kyselyitä, uutisia ja paljon muuta:",
"SupportNote": "huomaa, että kysymyksiin ja ongelmiin vastaaminen voi kestää hetken. kaikkea hallitsee vain yksi henkilö.",
"SourceCode": "ilmoita ongelmista, tutki lähdekoodia, merkitse tai haaroittele repo:",
"PrivacyPolicy": "sovelluksen {appName} tietosuojakäytäntö on yksinkertainen: sinusta ei kerätä tai tallenneta tietoja. nolla, zilch, nada, ei mitään.\nlataamasi asia on sinun, ei minun.\n\njotkut ei-takaisinjäljitettävät tiedot tallennetaan väliaikaisesti, kun pyydetty lataus vaatii live-renderöinnin. se on välttämätöntä, jotta tämä ominaisuus toimii.\n\nsiinä tapauksessa <span class=\"text-backdrop\">ip-osoitteesi suolattu sha256-hajautus</span> ja tiedot pyydetystä streamista tallennetaan tilapäisesti palvelimen RAM-muistiin <span class=\"text-backdrop\">2 minuutiksi</span>. 2 minuutin kuluttua kaikki aiemmin tallennetut tiedot poistetaan pysyvästi. IP-osoitteesi tiivistettä <span class=\"text-backdrop\">käytetään streamin käytön rajoittamiseen vain sinulle</span>.\nkenelläkään (edes minulla) ei ole pääsyä näihin tietoihin, koska virallinen kobolttikoodikanta ei alun perin tarjoa tapaa lukea niitä käsittelytoimintojen ulkopuolella.\n\nvoit itse tarkistaa sovelluksen {appName} <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github-repon</a> ja nähdä, että mitään ei todellakaan ole tallennettu pysyvästi."
}
}

View file

@ -1,19 +1,17 @@
{
"name": "français",
"substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">fais-moi signe</a>"
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">créer un ticket sur github</a>"
},
"strings": {
"LinkInput": "collez le lien ici",
"AboutSummary": "{appName} est l'endroit idéal pour télécharger sur les réseaux sociaux. zero pubs, trackers, ou toute autre connerie effrayante attachée. il suffit de coller un lien de partage et vous êtes prêt à vous lancer!",
"AboutSupportedServices": "services actuellement supportés:",
"EmbedBriefDescription": "sauvegarder le contenu des médias sociaux sans être suivi par des personnes mal intentionnées",
"MadeWithLove": "crée avec <3 par wukko et tous les contributeurs sur github (traduction par Greep)",
"AccessibilityInputArea": "zone de saisie du lien",
"AccessibilityOpenAbout": "ouvrir la fenêtre popup à propos",
"AccessibilityDownloadButton": "bouton de téléchargement",
"AccessibilityOpenSettings": "ouvrir la fenêtre popup des paramètres",
"AccessibilityOpenChangelog": "voir la fenêtre popup du journal des modifications",
"AccessibilityClosePopup": "fermer la popup",
"AccessibilityOpenDonate": "ouvrir une popup de donation",
"TitlePopupAbout": "c'est quoi {appName}?",
@ -27,8 +25,8 @@
"ErrorBrokenLink": "{s} est supporté, mais quelque chose ne va pas avec votre lien. peut-être que vous ne l'avez pas copié entièrement ?",
"ErrorNoLink": "je ne peux pas deviner ce que vous voulez télécharger ! s'il vous plaît donnez-moi un lien.",
"ErrorPageRenderFail": "quelque chose s'est mal passé et la page n'a pas pu s'afficher. Si c'est un problème récurrent ou critique, veuillez {ContactLink}. il serait utile de fournir le hash du commit actuel ({s}) et les étapes de recréation d'erreur. merci :D",
"ErrorRateLimit": "vous faites beaucoup trop de demandes. calmez-vous et réessayez dans quelques minutes.",
"ErrorCouldntFetch": "Impossible de récupérer les métadonnées. Vérifiez si votre lien est correct et réessayez.",
"ErrorRateLimit": "vous faites trop de demandes. réessayez dans une minute!",
"ErrorCouldntFetch": "impossible de récupérer les métadonnées de ton lien. vérifie s'il est correct et réessaye.",
"ErrorLengthLimit": "la durée limite actuelle est de {s} minutes. ce que vous avez essayé de télécharger est plus long que {s} minutes. choisissez autre chose à télécharger !",
"ErrorBadFetch": "Quelque chose s'est mal passé avec la récupération des informations. Vous pouvez essayer un autre format et une autre résolution ou réessayer plus tard.",
"ErrorCorruptedStream": "ce téléchargement est malheureusement corrompu. essayez à nouveau ou essayez un format et une résolution différents.",
@ -39,8 +37,6 @@
"SettingsAppearanceSubtitle": "apparence",
"SettingsThemeSubtitle": "thème",
"SettingsFormatSubtitle": "télecharger le format",
"SettingsMiscSubtitle": "plus de paramètres",
"SettingsDownloadsSubtitle": "télechargement",
"SettingsQualitySubtitle": "qualité",
"SettingsThemeAuto": "auto",
"SettingsThemeLight": "clair",
@ -54,15 +50,11 @@
"AccessibilityKeepDownloadButton": "garder le bouton de téléchargement toujours visible",
"SettingsEnableDownloadPopup": "demander un moyen de sauvegarder",
"AccessibilityEnableDownloadPopup": "demander ce qu'il faut faire avec les téléchargements",
"SettingsFormatDescription": "sélectionnez webm si vous avez besoin de la qualité maximale disponible. les vidéos webm sont généralement de meilleure qualité mais les appareils ios ne peuvent pas les lire en natif.",
"SettingsQualityDescription": "si la résolution choisie n'est pas disponible, la résolution la plus proche est choisie à la place. si vous voulez poster une vidéo youtube sur twitter, choisissez une combinaison de mp4 et 720p. twitter aime beaucoup plus les vidéos de ce type.",
"DonateSubtitle": "aidez-moi à payer l'hébergement",
"DonateDescription": "je n'aime pas vraiment la crypto dans son état actuel, mais c'est le seul moyen fiable pour moi de recevoir de l'argent et de payer quoi que ce soit à l'étranger.",
"LinkGitHubIssues": "&gt;&gt; signaler les problèmes et consulter le code source sur github",
"SettingsFormatDescription": "sélectionne webm si tu as besoin de la qualité maximale disponible. les vidéos webm sont généralement de meilleure qualité mais les appareils ios ne peuvent pas les lire nativement.",
"SettingsQualityDescription": "si la qualité sélectionnée n'est pas disponible, la plus proche sera choisie à sa place.\nsi tu veux publier une vidéo youtube sur les réseaux sociaux, sélectionne mp4 et 720p.",
"LinkGitHubChanges": "&gt;&gt; voir les changements précédents et contribuer sur github",
"LinkDonateContact": "&gt;&gt; faites-moi savoir si la monnaie que vous voulez donner n'est pas listée",
"NoScriptMessage": "{appName} utilise javascript pour les demandes d'api et l'interface interactive. vous devez autoriser javascript pour utiliser ce site. nous n'avons pas de publicités ou de traceurs, c'est promis.",
"DownloadPopupDescriptionIOS": "comme vous avez un appareil ios, vous devez appuyer sur le bouton de téléchargement et le maintenir enfoncé, puis sélectionner \"télécharger la vidéo\" dans la fenêtre popup qui apparaît pour enregistrer la vidéo. cela sera nécessaire tant qu'apple imposera safari webview à tous les développeurs de navigateurs sur ios.",
"DownloadPopupDescriptionIOS": "appuyez et maintenez le bouton de téléchargement, masquez l'aperçu de la vidéo, puis sélectionnez \"télécharger le fichier lié\" pour enregistrer.",
"DownloadPopupDescription": "le bouton de téléchargement ouvre un nouvel onglet avec le fichier demandé. vous pouvez désactiver cette popup dans les paramètres.",
"DownloadPopupWayToSave": "choisissez un moyen de sauvegarder",
"ClickToCopy": "appuyer pour copier",
@ -74,18 +66,14 @@
"SettingsVideoTab": "vidéo",
"SettingsAudioTab": "audio",
"SettingsOtherTab": "autre",
"ChangelogLastCommit": "dernier commit",
"ChangelogLastMajor": "dernière mise à jour majeure",
"ChangelogLastMajor": "version actuelle & commit",
"AccessibilityModeToggle": "basculer le mode de téléchargement",
"DonateLinksDescription": "les liens vers les dons s'ouvrent dans un nouvel onglet. c'est la meilleure façon de donner de l'argent, si vous voulez que je le reçoive directement.",
"DonateLinksDescription": "les liens vers les dons s'ouvrent dans un nouvel onglet. c'est la meilleure façon de faire un don si vous voulez que je le reçoive directement.",
"SettingsAudioFormatBest": "meilleure",
"SettingsAudioFormatDescription": "lorsque le meilleur format est sélectionné, vous obtenez l'audio dans la meilleure qualité disponible, car l'audio est conservé dans son format d'origine. si vous sélectionnez autre chose, vous obtiendrez un fichier légèrement compressé.",
"SettingsAudioFormatDescription": "lorsque le meilleur format est sélectionné, vous obtenez l'audio dans la meilleure qualité disponible, car il n'est pas réencodé. tout le reste sera ré-encodé.",
"Keyphrase": "sauvegardez ce que vous aimez",
"SettingsDisableChangelogOnUpdate": "ne pas afficher le journal de modifications après les mises à jour majeures",
"SettingsRemoveWatermark": "désactiver le filigrane",
"ErrorPopupCloseButton": "fermer",
"ModeToggle": "mode",
"ModeToggleSmart": "intélligent",
"ErrorLengthAudioConvert": "la longueur limite actuelle pour la conversion audio est de {s} minutes. choisissez plutôt le format \"meilleure\" !",
"SettingsAudioFullTikTok": "télécharger l'audio complet",
"SettingsAudioFullTikTokDescription": "cet audio est le plus souvent de la musique ou un son original utilisé dans une vidéo. un audio sans voix off, tts, ou découpage sera téléchargé, s'il est disponible, bien sûr.",
@ -96,6 +84,37 @@
"ImagePickerExplanationPC": "faites un clic droit sur une image pour l'enregistrer.",
"ImagePickerExplanationPhone": "appuyez et maintenez une image enfoncée pour l'enregistrer.",
"ErrorNoUrlReturned": "le serveur n'a pas retourné de lien de téléchargement. Cela ne devrait jamais se produire. Rechargez la page et réessayez, mais si cela n'aide pas, {ContactLink}.",
"ErrorUnknownStatus": "J'ai reçu une réponse que je ne peux pas traiter. Il est fort probable que quelque chose avec le statut est erroné. cela ne devrait jamais arriver. Rechargez la page et réessayez, mais si cela n'aide pas, {ContactLink}."
"ErrorUnknownStatus": "J'ai reçu une réponse que je ne peux pas traiter. Il est fort probable que quelque chose avec le statut est erroné. cela ne devrait jamais arriver. Rechargez la page et réessayez, mais si cela n'aide pas, {ContactLink}.",
"PasteFromClipboard": "coller à partir du presse-papiers",
"ChangelogOlder": "versions précédentes",
"ChangelogPressToExpand": "appuyer pour étendre",
"Miscellaneous": "divers",
"ModeToggleAuto": "mode automatique",
"ModeToggleAudio": "mode audio",
"SettingsDisableNotifications": "masquer les points de notification",
"MediaPickerTitle": "choisir que télécharger",
"MediaPickerExplanationPC": "cliquez ou faites un clic droit pour télécharger ce que vous voulez.",
"MediaPickerExplanationPhone": "appuyez ou appuyez et maintenez pour télécharger ce que vous voulez.",
"MediaPickerExplanationPhoneIOS": "appuyez et maintenez enfoncé, masquer l'aperçu, puis sélectionnez \"télécharger le fichier lié\" pour enregistrer.",
"TwitterSpaceWasntRecorded": "cet espace twitter n'a pas été enregistré, il n'y a donc rien à télécharger. essaie un autre!",
"ErrorCantProcess": "je n'ai pas pu traiter votre demande :(\nvous pouvez réessayer, mais si le problème persiste, veuillez {ContactLink}.",
"ChangelogPressToHide": "appuyer pour minimiser",
"Donate": "donner",
"DonateSub": "aidez-moi à le maintenir",
"DonateExplanation": "{appName} ne diffuse pas (et ne diffusera jamais) d'annonces ni ne vendra vos données. Son utilisation est donc <span class=\"text-backdrop\">entièrement gratuite</span>. mais il s'avère que le maintien d'un service Web utilisé par des milliers de personnes est quelque peu coûteux.\n\nsi vous avez déjà trouvé {appName} utile et que vous souhaitez le garder en ligne, ou si vous souhaitez simplement remercier le développeur, pensez à participer ! chaque centime aide et est TRÈS apprécié.",
"DonateVia": "faire un don par",
"DonateHireMe": "ou, comme alternative, vous pouvez <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">m'engager</a>.",
"SettingsVideoMute": "couper le son",
"SettingsVideoMuteExplanation": "désactive l'audio dans la vidéo téléchargée lorsque cela est possible. ignoré lorsque le mode audio est activé ou que le service ne prend en charge que l'audio.",
"SettingsVideoGeneral": "général",
"ErrorSoundCloudNoClientId": "impossible de trouver client_id requis pour récupérer les données audio de soundcloud. réessayez, et si le problème persiste, {ContactLink}.",
"CollapseServices": "services supportés",
"CollapseSupport": "prise en charge et code source",
"CollapsePrivacy": "politique de confidentialité",
"ServicesNote": "cette liste n'est pas définitive et ne cesse de s'allonger avec le temps, assurez-vous de la consulter de temps en temps !",
"FollowSupport": "suivez {appName} sur mastodon ou twitter pour obtenir de l'aide, des sondages, des actualités et bien plus :",
"SupportNote": "veuillez noter que les questions et les problèmes peuvent prendre un certain temps pour répondre, il n'y a qu'une seule personne qui gère tout.",
"SourceCode": "signalez les problèmes, explorez le code source, créez une étoile ou bifurquez le dépôt :",
"PrivacyPolicy": "La politique de confidentialité de {appName} est simple : aucune donnée vous concernant n'est collectée ou stockée. zéro, zilch, nada, rien.\nce que vous téléchargez est votre affaire, pas la mienne.\n\ncertaines données non traçables sont temporairement stockées lorsque le téléchargement exigé nécessite un rendu en direct. il est nécessaire pour que cette fonctionnalité fonctionne.\n\ndans ce cas, le <span class=\"text-backdrop\">hachage sha256 salé de votre adresse IP</span> et les informations sur le flux exigé sont temporairement stockés dans la RAM du serveur pendant <span class=\"text-backdrop\">deux minutes</span>. après deux minutes, toutes les informations précédemment stockées sont définitivement supprimées. le hachage de votre adresse IP est <span class=\"text-backdrop\">utilisé pour limiter l'accès au flux uniquement pour vous</span>.\npersonne (même moi) n'a accès à ces données, car la base de code officielle du cobalt ne permet pas de les lire en dehors des fonctions de traitement.\n\nvous pouvez vérifier le <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">dépôt github</a> de {appName} par vous-même et voir qu'en effet rien n'est stocké de manière permanente."
}
}

View file

@ -1,19 +1,17 @@
{
"name": "nederlands",
"substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">laat het me weten</a>"
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">dien een probleem in op github</a>"
},
"strings": {
"LinkInput": "plak de link hier",
"AboutSummary": "{appName} is je go-to plaats voor social media downloads. zonder advertenties, trackers, of wat voor creepy bullshit dan ook. plak gewoon een link en je bent er klaar voor!",
"AboutSupportedServices": "ondersteunde apps:",
"EmbedBriefDescription": "bewaar content van sociale media zonder creeps die je volgen",
"MadeWithLove": "met <3 gemaakt door wukko",
"AccessibilityInputArea": "link input plek",
"AccessibilityOpenAbout": "open over popup",
"AccessibilityDownloadButton": "download knop",
"AccessibilityOpenSettings": "instellingen openen popup",
"AccessibilityOpenChangelog": "bekijk de changelog popup ",
"AccessibilityClosePopup": "sluit de popup",
"AccessibilityOpenDonate": "open donatie popup",
"TitlePopupAbout": "wat is {appName}?",
@ -27,42 +25,30 @@
"ErrorBrokenLink": "{s} wordt ondersteund, maar er is iets mis met de link, misschien heb je de link niet volledig gekopieerd?",
"ErrorNoLink": "ik kan je gedachten niet lezen! voer hier een link in.",
"ErrorPageRenderFail": "er is iets misgegaan en de pagina kan niet worden weergegeven. als het de hele tijd gebeurt, neem dan contact op: {ContactLink}. het zou handig zijn als je de huidige commit-hash ({s}) en de stappen om de error te laten gebeuren zou opgeven. bedankt :D",
"ErrorRateLimit": "je maakt te veel verzoeken. probeer opnieuw in een paar minuten.",
"ErrorCouldntFetch": "kan metadata niet ophalen. kijk of de link correct is en probeer opnieuw.",
"ErrorRateLimit": "je doet te veel verzoeken. probeer het over een minuut nog eens!",
"ErrorCouldntFetch": "kon geen info krijgen over je link. controleer of het correct is en probeer het opnieuw.",
"ErrorLengthLimit": "huidige lengtelimiet is {s} minuten. wat je geprobeerd hebt om te downloaden is langer dan {s} minuten. kies iets anders om te downloaden!",
"ErrorBadFetch": "er is iets misgegaan met het ophalen van info. je kan een ander formaat en resolutie kiezen of opnieuw proberen.",
"ErrorCorruptedStream": "deze download is helaas beschadigd. probeer het opnieuw of probeer een ander formaat en resolutie",
"ErrorCorruptedStream": "deze download is helaas beschadigd. probeer het nog eens!",
"ErrorNoInternet": "er is geen internet of de api van {appName} is niet beschikbaar. controleer je verbinding en probeer het opnieuw.",
"ErrorCantConnectToServiceAPI": "ik kon geen verbinding maken met {s} api. het lijkt erop dat {s} niet beschikbaar is of dat het server-ip van {appName} is geblokkeerd. probeer later opnieuw",
"ErrorCantConnectToServiceAPI": "ik kon geen verbinding maken met de service-api. het kan down zijn, of {appName} kan geblokkeerd zijn geraakt. probeer het later nog eens!",
"ErrorEmptyDownload": "er is niks te downloaden. probeer iets anders!",
"ErrorLiveVideo": "ik kan geen live video downloaden. wacht tot de stream afgelopen is en probeer opnieuw.",
"ErrorLiveVideo": "dit is een live video, ik moet nog leren hoe ik in de toekomst moet kijken. wacht tot de stream is afgelopen en probeer het opnieuw!",
"SettingsAppearanceSubtitle": "uiterlijk",
"SettingsThemeSubtitle": "thema",
"SettingsFormatSubtitle": "download formaat",
"SettingsMiscSubtitle": "meer instellingen",
"SettingsDownloadsSubtitle": "downloads",
"SettingsFormatSubtitle": "formaat",
"SettingsQualitySubtitle": "kwaliteit",
"SettingsThemeAuto": "automatisch",
"SettingsThemeLight": "licht",
"SettingsThemeDark": "donker",
"SettingsQualitySwitchMax": "max",
"SettingsQualitySwitchHigh": "hoog",
"SettingsQualitySwitchMedium": "medium",
"SettingsQualitySwitchLow": "laag",
"SettingsQualitySwitchLowest": "laagst",
"SettingsKeepDownloadButton": "laat &gt;&gt; zichtbaar blijven",
"AccessibilityKeepDownloadButton": "laat de download know altijd zichtbaar blijven",
"SettingsEnableDownloadPopup": "vraag voor een manier om op te slaan",
"AccessibilityEnableDownloadPopup": "vraag wat te doen met de downloads",
"SettingsFormatDescription": "selecteer webm als je maximale beschikbare kwaliteit nodig hebt. webm video's zijn meestal van hogere kwaliteit, maar ios apparaten kunnen ze niet native afspelen.",
"SettingsQualityDescription": "als de geselecteerde resolutie niet beschikbaar is, wordt in plaats daarvan de dichtstbijzijnde gekozen. als je een youtube video op twitter wil plaatsen, selecteer dan een combinatie van mp4 en 720p. twitter houdt meer van zulke video's.",
"DonateSubtitle": "help me met het betalen van hosting",
"DonateDescription": "ik hou niet echt van crypto in zijn huidige staat, maar het is de enige betrouwbare manier voor mij om geld te ontvangen en te betalen voor iets in het buitenland.",
"LinkGitHubIssues": "&gt;&gt; meld problemen en bekijk de broncode op github",
"SettingsQualityDescription": "als de geselecteerde kwaliteit niet beschikbaar is, wordt in plaats daarvan de dichtstbijzijnde kwaliteit gebruikt.",
"LinkGitHubChanges": "&gt;&gt; zie eerdere wijzigingen en draag bij op github",
"LinkDonateContact": "&gt;&gt; laat het me weten als de geld soort die je wil doneren niet in de lijst staat",
"NoScriptMessage": "{appName} gebruikt javascript voor api-verzoeken en interactieve interface. je moet javascript toestaan om deze site te gebruiken. we hebben geen advertenties of trackers, pinky promise.",
"DownloadPopupDescriptionIOS": "omdat je een ios-apparaat hebt, moet je de downloadknop ingedrukt houden en vervolgens \"download video\" selecteren in de verschenen pop-up om de video op te slaan. dit moet zolang Apple alle browserontwikkelaars op ios een safari webview forceert.",
"DownloadPopupDescriptionIOS": "houd de downloadknop ingedrukt, verberg het videovoorbeeld en selecteer vervolgens \"download gekoppeld bestand\" om op te slaan.",
"DownloadPopupDescription": "download knop opent een nieuw tabblad met het gevraagde bestand. je kunt deze popup uitschakelen in instellingen.",
"DownloadPopupWayToSave": "kies een manier om op te slaan",
"ClickToCopy": "click om te kopiëren",
@ -74,18 +60,14 @@
"SettingsVideoTab": "video",
"SettingsAudioTab": "audio",
"SettingsOtherTab": "ander",
"ChangelogLastCommit": "laatste commit",
"ChangelogLastMajor": "laatste grote update",
"ChangelogLastMajor": "huidige versie & commit",
"AccessibilityModeToggle": "zet download modus aan of uit",
"DonateLinksDescription": "donatie linken openen in een nieuw tabblad. dit is de beste manier om geld te doneren, als je wil dat ik het direct krijg.",
"DonateLinksDescription": "donatielinks openen in een nieuw tabblad. dit is de beste manier om te doneren als je wilt dat ik je donatie rechtstreeks ontvang.",
"SettingsAudioFormatBest": "beste",
"SettingsAudioFormatDescription": "wanneer het formaat geselecteerd wordt, krijg je de audio in de beste kwaliteit beschikbaar, omdat de audio in zijn originele formaat is gebleven. als je iets anders selecteert dan dat, krijg je een lichtelijk gecompresseerd bestand.",
"SettingsAudioFormatDescription": "wanneer het beste formaat is geselecteerd, krijgt u audio in de best beschikbare kwaliteit, het wordt niet opnieuw gecodeerd. al het andere wordt opnieuw gecodeerd.",
"Keyphrase": "sla op wat je houdt",
"SettingsDisableChangelogOnUpdate": "laat changelog niet zien na grote updates",
"SettingsRemoveWatermark": "zet watermark uit",
"ErrorPopupCloseButton": "snappie",
"ModeToggle": "modus",
"ModeToggleSmart": "slim",
"ErrorLengthAudioConvert": "huidige limit voor de lengte van audioconversie is {s} minuten. kies optie \"beste\" om limieten te voorkomen.",
"SettingsAudioFullTikTok": "download volledige audio",
"SettingsAudioFullTikTokDescription": "download de originele audio/geluid dat in de video gebruikt is, zonder extra veranderingen van de video auteur.",
@ -96,6 +78,44 @@
"ImagePickerExplanationPC": "klik met de rechtermuisknop om een afbeelding op te slaan.",
"ImagePickerExplanationPhone": "houd een afbeelding ingedrukt om op te slaan.",
"ErrorNoUrlReturned": "server heeft geen downloadlink teruggegeven, dit zou nooit moeten gebeuren. ververs de pagina en probeer opnieuw, maar wanneer dit niet helpt, {ContactLink}.",
"ErrorUnknownStatus": "ik heb een antwoord ontvangen dat ik niet kan verwerken. hoogstwaarschijnlijk iets dat met de status fout is, dit hoort nooit te gebeuren. ververs de pagina en probeer opnieuw, maar wanneer dit niet helpt, {ContactLink}."
"ErrorUnknownStatus": "ik heb een antwoord ontvangen dat ik niet kan verwerken. hoogstwaarschijnlijk iets dat met de status fout is, dit hoort nooit te gebeuren. ververs de pagina en probeer opnieuw, maar wanneer dit niet helpt, {ContactLink}.",
"PasteFromClipboard": "plakken vanaf klembord",
"ChangelogOlder": "vorige versies",
"ChangelogPressToExpand": "druk op om uit te vouwen",
"Miscellaneous": "overige",
"ModeToggleAuto": "auto modus",
"ModeToggleAudio": "audio modus",
"SettingsDisableNotifications": "meldingsstijl verbergen",
"MediaPickerTitle": "kies wat u wilt opslaan",
"MediaPickerExplanationPC": "klik of klik met de rechtermuisknop om te downloaden wat je wilt.",
"MediaPickerExplanationPhone": "druk of houd ingedrukt om te downloaden wat je wilt.",
"MediaPickerExplanationPhoneIOS": "houd ingedrukt, verberg het voorbeeld en selecteer vervolgens \"download gekoppeld bestand\" om op te slaan.",
"TwitterSpaceWasntRecorded": "deze twitterruimte is niet opgenomen, dus er valt niets te downloaden. probeer een andere!",
"ErrorCantProcess": "ik kon je verzoek niet verwerken :(\nje kunt het opnieuw proberen, maar als het probleem zich blijft voordoen, {ContactLink}.",
"ChangelogPressToHide": "druk om in te klappen",
"Donate": "doneren",
"DonateSub": "help mij onderhouden",
"DonateExplanation": "{appName} geeft (en zal nooit) geen advertenties weer en verkoopt uw gegevens niet, daarom is het <span class=\"text-backdrop\">volledig gratis te gebruiken</span>. maar blijkt dat het onderhouden van een webservice die door duizenden mensen wordt gebruikt, enigszins kostbaar is.\n\nals je {appName} ooit nuttig vond en het online wilt houden, of gewoon de ontwikkelaar wilt bedanken, overweeg dan om mee te doen! elke cent helpt en wordt ZEER gewaardeerd.",
"DonateVia": "doneren via",
"DonateHireMe": "of, als alternatief, kunt u <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">mij inhuren</a>.",
"SettingsVideoMute": "geluid dempen",
"SettingsVideoMuteExplanation": "schakelt audio in gedownloade video indien mogelijk uit.",
"ErrorSoundCloudNoClientId": "kon client_id niet vinden die nodig is om audiogegevens op te halen van soundcloud. probeer het opnieuw en als het probleem zich blijft voordoen, {ContactLink}.",
"CollapseServices": "ondersteunde diensten",
"CollapseSupport": "ondersteuning & broncode",
"CollapsePrivacy": "privacybeleid",
"ServicesNote": "deze lijst is niet definitief en blijft in de loop van de tijd uitbreiden, zorg ervoor dat je hem af en toe bekijkt!",
"FollowSupport": "volg {appName} op mastodon of twitter voor ondersteuning, polls, nieuws en meer:",
"SupportNote": "houd er rekening mee dat het even kan duren voordat vragen en problemen worden beantwoord, er is maar één persoon die alles beheert.",
"SourceCode": "rapporteer problemen, verken de broncode, ster of fork de repo:",
"PrivacyPolicy": "het privacybeleid van {appName} is eenvoudig: er worden geen gegevens over u verzameld of opgeslagen. nul, zilch, nada, niets.\nwat u downloadt is uw zaak, niet de mijne.\n\nsommige niet-herleidbare gegevens worden tijdelijk opgeslagen wanneer de gevraagde download live weergave vereist. het is noodzakelijk om die functie te laten functioneren.\n\nin dat geval worden <span class=\"text-backdrop\">gezouten sha256-hash van uw ip-adres</span> en informatie over de aangevraagde stream tijdelijk <span class=\"text-backdrop\">2 minuten< opgeslagen in het RAM-geheugen van de server</span>. na 2 minuten is alle eerder opgeslagen informatie definitief verwijderd. hash van je ip-adres wordt <span class=\"text-backdrop\">gebruikt om de toegang tot de stream alleen voor jou te beperken</span>.\nniemand (zelfs ik) heeft geen toegang tot deze gegevens, omdat de officiële {appName}-codebase in de eerste plaats geen manier biedt om deze buiten de verwerkingsfuncties om te lezen.\n\nje kunt de <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> van {appName} zelf bekijken en zien dat inderdaad niets permanent wordt opgeslagen.",
"ErrorYTUnavailable": "deze YouTube-video is niet beschikbaar of heeft een leeftijdsbeperking. ik kan momenteel geen video's met gevoelige inhoud downloaden. probeer een andere!",
"ErrorYTTryOtherCodec": "ik kon niets vinden om te downloaden met uw instellingen. probeer een andere codec of kwaliteit!\n\nopmerking: youtube api werkt soms onverwachts. geef Google hiervoor de schuld, niet mij.",
"SettingsCodecSubtitle": "youtube-codec",
"SettingsCodecDescription": "h264: over het algemeen betere ondersteuning voor spelers, maar de kwaliteit komt uit bij 1080p.\nav1: lage spelersondersteuning, maar ondersteunt 8k & HDR.\nvp9: meestal de hoogste bitsnelheid, behoudt de meeste details. ondersteunt 4k & HDR.\n\nals je de beste compatibiliteit met editor/speler/sociale media wilt, kies dan h264.",
"SettingsAudioDub": "youtube-audiotrack",
"SettingsAudioDubDescription": "bepaalt welke audiotrack zal worden gebruikt. als er geen nagesynchroniseerde track beschikbaar is, wordt in plaats daarvan de originele video taal gebruikt.\n\norigineel: originele video taal wordt gebruikt.\nauto: standaard browser (en {appName}) taal wordt gebruikt.",
"SettingsDubDefault": "origineel",
"SettingsDubAuto": "automatisch"
}
}

View file

@ -1,19 +1,17 @@
{
"name": "polski",
"substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">daj mi znać</a>"
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">zgłoś problem na githubie</a>"
},
"strings": {
"LinkInput": "wklej link tutaj",
"AboutSummary": "{appName} to najlepsze miejsce do pobierania z mediów społecznościowych. zero reklam, śledzenia i innych podobnych głupot. po prostu wklejasz link i lecisz!",
"AboutSupportedServices": "aktualnie wspierane strony:",
"EmbedBriefDescription": "pobieraj rzeczy z social mediów bez reklam i śledzenia",
"AboutSummary": "{appName} to twój nowy najlepszy przyjaciel do pobierania treści z mediów społecznościowych. zero reklam, trackerów, czy jakiś innych dziwacznych pierdół. po prostu wklej link i lecisz!",
"EmbedBriefDescription": "zapisuj to co kochasz, bez reklam, trackerów, czy jakiś innych dziwacznych pierdół.",
"MadeWithLove": "zrobione z <3 przez wukko",
"AccessibilityInputArea": "pole wklejania linku",
"AccessibilityOpenAbout": "otwórz okno informacji",
"AccessibilityDownloadButton": "przycisk pobierania",
"AccessibilityOpenSettings": "otwórz okno ustawień",
"AccessibilityOpenChangelog": "otwórz okno aktualizacji",
"AccessibilityClosePopup": "zamknij okno",
"AccessibilityOpenDonate": "otwórz okno darowizn",
"TitlePopupAbout": "czym jest {appName}?",
@ -21,52 +19,40 @@
"TitlePopupError": "ups...",
"TitlePopupChangelog": "co nowego?",
"TitlePopupDonate": "wesprzyj {appName}",
"TitlePopupDownload": "pobierz",
"ErrorSomethingWentWrong": "coś poszło nie tak i nie udało się niczego dla ciebie pobrać. możesz spróbować jeszcze raz, ale jeżeli problem nie ustąpi, {ContactLink}.",
"ErrorUnsupported": "wygląda na to, że ta strona nie jest jeszcze wspierana, albo twój link jest nieprawidłowy.",
"TitlePopupDownload": "jak kontynuować?",
"ErrorSomethingWentWrong": "coś poszło nie tak i nie udało mi się niczego pobrać. spróbuj ponownie, jeśli wciąż pojawia się problem, {ContactLink}",
"ErrorUnsupported": "wygląda na to, że ta usługa nie jest jeszcze obsługiwana albo link jest nieprawidłowy. upewnij się, czy wkleiłeś link poprawnie.",
"ErrorBrokenLink": "{s} jest wspierany, ale coś jest nie tak z twoim linkiem. może nie został skopiowany w całości?",
"ErrorNoLink": "nie potrafię czytać ci w myślach! proszę, daj mi link",
"ErrorPageRenderFail": "coś poszło nie tak i strona nie mogła zostać wyrenderowana. jeżeli problem jest krytyczny lub się powtarza, proszę {ContactLink}. byłoby dobrze gdybym otrzymał hash commita ({s}) i kroki do odtworzenia błędu. dzięki :D",
"ErrorRateLimit": "wysyłasz zbyt dużo żądań. uspokój się i spróbuj ponownie za parę minut.",
"ErrorCouldntFetch": "nie udało się pobrać metadanych. sprawdź, czy twój link jest poprawny i spróbuj ponownie.",
"ErrorLengthLimit": "aktualny limit długości to {s} minut. to, co próbujesz pobrać, jest dłuższe niż {s} minut. pobierz coś innego!",
"ErrorBadFetch": "coś poszło nie tak z pobieraniem informacji. wybierz inny format i rozdzielczość albo po prostu spróbuj ponownie później.",
"ErrorCorruptedStream": "niestety ten plik jest uszkodzony. spróbuj ponownie lub wybierz inny format i rozdzielczość.",
"ErrorNoLink": "nie potrafię zgadnąć, co chcesz pobrać! proszę, daj mi link :(",
"ErrorPageRenderFail": "jeśli to czytasz, to stało się coś bardzo złego z renderem strony. proszę {ContactLink}. upewnij się, że podasz domenę, na której pojawił się ten błąd i przekaż aktualny skrót commitu ({s}). z góry dzięki :D",
"ErrorRateLimit": "robisz za dużo żądań. spróbuj ponownie za minutę!",
"ErrorCouldntFetch": "nic nie znalazłem z tego linku. upewnij się, że działa poprawnie i spróbuj ponownie! niektóre treści mogą być objęte ograniczeniami regionowymi, więc miej to na uwadze.",
"ErrorLengthLimit": "nie jestem w stanie przetworzyć filmików dłuższych niż {s} minut, wybierz coś krótszego!",
"ErrorBadFetch": "coś poszło nie tak podczas uzyskiwania informacji o twoim linku. jesteś pewien, że działa? sprawdź, czy jest poprawny i spróbuj ponownie.",
"ErrorNoInternet": "nie masz dostępu do internetu albo api {appName} nie działa. sprawdź swoje połączenie i spróbuj ponownie.",
"ErrorCantConnectToServiceAPI": "nie mogę połączyć się z api {s}. wygląda na to, że {s} nie działa albo adres ip serwera {appName} został zablokowany. spróbuj ponownie później.",
"ErrorEmptyDownload": "nie ma tu nic do pobrania! spróbuj pobrać coś innego.",
"ErrorLiveVideo": "nie mogę pobierać transmisji na żywo. poczekaj, aż stream się zakończy i spróbuj ponownie.",
"ErrorCantConnectToServiceAPI": "nie mogłem się połączyć z usługą api. być może jest offline, albo {appName} został zablokowany. spróbuj ponownie, jeśli wciąż pojawia się problem, {ContactLink}.",
"ErrorEmptyDownload": "nie widzę tu nic, co mógłbym pobrać. spróbuj podać inny link!",
"ErrorLiveVideo": "to film na żywo, jeszcze się nie nauczyłem, jak spojrzeć w przyszłość. poczekaj na zakończenie transmisji i spróbuj ponownie!",
"SettingsAppearanceSubtitle": "wygląd",
"SettingsThemeSubtitle": "motyw",
"SettingsFormatSubtitle": "format pliku",
"SettingsDownloadsSubtitle": "pobrane pliki",
"SettingsFormatSubtitle": "format",
"SettingsQualitySubtitle": "jakość",
"SettingsThemeAuto": "automatyczny",
"SettingsThemeAuto": "auto",
"SettingsThemeLight": "jasny",
"SettingsThemeDark": "ciemny",
"SettingsQualitySwitchMax": "maksymalna",
"SettingsQualitySwitchHigh": "wysoka",
"SettingsQualitySwitchMedium": "średnia",
"SettingsQualitySwitchLow": "niska",
"SettingsQualitySwitchLowest": "minimalna",
"SettingsKeepDownloadButton": "pozostaw &gt;&gt; widoczny",
"AccessibilityKeepDownloadButton": "pozostaw przycisk pobierania zawsze widoczny",
"SettingsEnableDownloadPopup": "pytaj o sposób zapisu",
"SettingsEnableDownloadPopup": "zapytaj jak zapisać",
"AccessibilityEnableDownloadPopup": "pytaj co zrobić z pobranymi plikami",
"SettingsFormatDescription": "wybierz webm, jeżeli potrzebujesz najwyższej możliwej jakości. filmy webm są zwykle wyższej jakości, ale urządzenia z ios nie odtwarzają ich natywnie.",
"SettingsQualityDescription": "jeżeli wybrana jakość nie będzie dostępna, zostanie wybrana najbliższa pasująca.\njeżeli chcesz wrzucić film z youtube na social media, wybierz połączenie mp4 i 720p. te filmy zazwyczaj nie używają kodeka av1, więc powinny się odtwarzać w zasadzie wszędzie.",
"DonateSubtitle": "ciężko się teraz płaci za hosting",
"DonateDescription": "nie podoba mi się stan w jakim są teraz kryptowaluty, ale na razie jest to dla mnie jedyny sposób żeby płacić za coś za granicą. karty mastercard/visa i usługi takie jak paypal nie są już dostępną opcją.",
"LinkGitHubIssues": "&gt;&gt; zgłoś problem lub zobacz kod źródłowy na githubie",
"LinkGitHubChanges": "&gt;&gt; zobacz poprzednie zmiany lub pomóż nam tworzyć na githubie",
"LinkDonateContact": "&gt;&gt; daj mi znać, jeżeli waluta, którą chcesz wesprzeć nie jest na liście",
"NoScriptMessage": "{appName} używa javascriptu do żądań api i interaktywnego interfejsu. musisz zezwolić na javascript, jeżeli chcesz używać tej strony. nie mamy żadnych reklam ani śledzenia, obiecujemy.",
"DownloadPopupDescriptionIOS": "ponieważ masz urządzenie z systemem ios, musisz nacisnąć i przytrzymać przycisk pobierania i wybrać \"pobierz film\" w wyskakującym oknie aby zapisać film. będzie to wymagane tak długo, jak apple będzie wymuszało safari webview na wszystkich deweloperach przeglądarek na ios.",
"SettingsQualityDescription": "jeśli wybrana jakość nie jest dostępna, zamiast niej używana jest najbliższa.",
"LinkGitHubChanges": "&gt;&gt; zobacz poprzednie zmiany i pomóż na githubie",
"NoScriptMessage": "{appName} używa javascriptu do żądań api i interaktywnego interfejsu. musisz zezwolić na javascript, by korzystać z tej strony. nie ma tu żadnych uciążliwych skryptów, obiecuję.",
"DownloadPopupDescriptionIOS": "naciśnij i przytrzymaj przycisk pobierania, ukryj podgląd wideo, a następnie wybierz \"pobierz wskazywany plik\", aby zapisać.",
"DownloadPopupDescription": "przycisk pobierania otwiera nową kartę z pobieranym plikiem. możesz wyłączyć to okno w ustawieniach.",
"DownloadPopupWayToSave": "wybierz sposób zapisu",
"ClickToCopy": "kliknij, aby skopiować",
"ClickToCopy": "kliknij aby skopiować",
"Download": "pobierz",
"CopyURL": "skopiuj url",
"CopyURL": "kopiuj",
"AboutTab": "o aplikacji",
"ChangelogTab": "lista zmian",
"DonationsTab": "darowizny",
@ -75,37 +61,63 @@
"SettingsOtherTab": "inne",
"ChangelogLastMajor": "bieżąca wersja i commit",
"AccessibilityModeToggle": "przełącz tryb pobierania",
"DonateLinksDescription": "linki do darowizn otwierają się w nowej karcie. to najlepszy sposób podarowania pieniędzy, jeśli chcesz, aby dotarły do mnie bezpośrednio.",
"DonateLinksDescription": "to najlepszy sposób na wsparcie mnie, jeśli chcesz, by darowizna przyszła bezpośrednio.",
"SettingsAudioFormatBest": "najlepszy",
"SettingsAudioFormatDescription": "gdy wybierzesz najlepszy format, dostaniesz audio w najlepszej możliwej jakości, ponieważ jest zachowane w oryginalnym formacie. gdy wybierzesz któryś inny, dostaniesz lekko skompresowany plik.",
"Keyphrase": "zapisz to, co kochasz",
"SettingsAudioFormatDescription": "kiedy format \"najlepszy\" jest wybrany, otrzymasz audio w takiej formie, jakiej jest po stronie serwisu, nie będzie one ponownie przetwarzane. ale wszystko inne będzie ponownie kodowane.",
"Keyphrase": "zapisuj to co kochasz",
"SettingsRemoveWatermark": "wyłącz znak wodny",
"ErrorPopupCloseButton": "rozumiem",
"ErrorLengthAudioConvert": "aktualny limit długości konwersji dźwięku wynosi {s} minut. wybierz \"najlepszy\" format, jeśli chcesz uniknąć ograniczeń.",
"SettingsAudioFullTikTok": "pobierz pełny dźwięk",
"SettingsAudioFullTikTokDescription": "pobiera oryginalny dźwięk lub dźwięk używany w filmie bez żadnych dodatkowych zmian ze strony autora wideo.",
"ErrorCantGetID": "nie można było uzyskać informacji ze skróconego linku. upewnij się, że link działa lub spróbuj pełnego.",
"ErrorNoVideosInTweet": "ten tweet nie zawiera filmów ani gifów. spróbuj innego!",
"ImagePickerTitle": "wybierz obrazy do pobrania",
"ErrorPopupCloseButton": "jasne",
"ErrorLengthAudioConvert": "nie jestem w stanie przekonwertować audio dłuższego niż {s} minut. wybierz format \"najlepszy\", jeśli chcesz uniknąć ograniczeń!",
"SettingsAudioFullTikTok": "pełne audio",
"SettingsAudioFullTikTokDescription": "pobiera oryginalny dźwięk użyty w filmiku bez dodatkowych zmian od strony autora filmu.",
"ErrorCantGetID": "nie udało mi się uzyskać wszystkich informacji ze skróconego linku. upewnij się, że działa, albo spróbuj użyć pełnego! jeśli wciąż pojawia się problem, {ContactLink}",
"ErrorNoVideosInTweet": "w tym tweecie nie ma ani filmów, ani gifów, spróbuj inny!",
"ImagePickerTitle": "wybierz zdjęcia do pobrania",
"ImagePickerDownloadAudio": "pobierz dźwięk",
"ImagePickerExplanationPC": "kliknij prawym przyciskiem myszy na obraz, aby go zapisać.",
"ImagePickerExplanationPhone": "naciśnij i przytrzymaj obraz, aby go zapisać.",
"ErrorNoUrlReturned": "serwer nie zwrócił linku do pobrania. to nie powinno się zdarzyć. odśwież stronę i spróbuj ponownie, ale jeśli to nie pomoże, {ContactLink}.",
"ErrorUnknownStatus": "otrzymano odpowiedź, której nie można przetworzyć. prawdopodobnie coś o statusie jest nieprawidłowe. to nigdy nie powinno się zdarzyć. odśwież stronę i spróbuj ponownie, ale jeśli to nie pomoże, {ContactLink}.",
"PasteFromClipboard": "wklej ze schowka",
"FollowTwitter": "obserwuj {appName} na twitterze po ankiety, aktualizacje i więcej: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ImagePickerExplanationPC": "kliknij prawym przyciskiem myszy na zdjęcie, by je zapisać.",
"ImagePickerExplanationPhone": "naciśnij i przytrzymaj zdjęcie by go zapisać.",
"ErrorNoUrlReturned": "nie otrzymałem linku do pobrania pliku z serwera. to nigdy nie powinno się zdarzyć. spróbuj ponownie, jeśli wciąż pojawia się problem, {ContactLink}",
"ErrorUnknownStatus": "odebrałem odpowiedź, którą nie jestem w stanie przetworzyć. to nigdy nie powinno się zdarzyć. spróbuj ponownie, jeśli wciąż pojawia się problem, {ContactLink}",
"PasteFromClipboard": "wklej i pobierz",
"ChangelogOlder": "poprzednie wersje",
"ChangelogPressToExpand": "pokaż",
"ChangelogPressToExpand": "rozwiń",
"Miscellaneous": "pozostałe",
"ModeToggleAuto": "tryb auto",
"ModeToggleAudio": "tryb audio",
"SettingsDisableNotifications": "ukryj plakietki z powiadomieniami",
"SettingsDisableNotifications": "ukryj powiadomienia",
"MediaPickerTitle": "wybierz co zapisać",
"MediaPickerExplanationPC": "kliknij lub kliknij prawym przyciskiem, aby pobrać to, co chcesz",
"MediaPickerExplanationPhone": "naciśnij lub naciśnij i przytrzymaj, aby zapisać to, co chcesz",
"MediaPickerExplanationPhoneIOS": "naciśnij i przytrzymaj, ukryj podgląd i wybierz \"odnośnik pobierania\", aby zapisać.",
"TwitterSpaceWasntRecorded": "ten pokój na twitterze nie był nagrywany, więc nie mogę nic pobrać. spróbuj inny!",
"ErrorCantProcess": "no i chuj, nie udało się :(\nmożesz spróbować ponownie, ale jeśli problem będzie się powtarzał, {ContactLink}.",
"ChangelogPressToHide": "zwiń"
"TwitterSpaceWasntRecorded": "ten pokój na twitterze nie był nagrywany, więc nie mogę nic pobrać. spróbuj innego!",
"ErrorCantProcess": "nie mogłem przetworzyć twojego żądania :(\nspróbuj ponownie, a jeśli problem będzie się powtarzał, {ContactLink}.",
"ChangelogPressToHide": "zwiń",
"Donate": "darowizny",
"DonateSub": "pomóż mi utrzymać",
"DonateExplanation": "{appName} nigdy, przenigdy nie pokaże ci reklam, ani nie odsprzeda twoich danych, co czyni go <span class=\"text-backdrop\">w pełni darmowym</span>. ale utrzymywanie serwisu dla ponad 40 tysięcy użytkowników okazuje się być nieco kosztowne.\n\njeśli {appName} ułatwił ci kiedyś życie i chcesz pomóc utrzymać stronę online, albo po prostu podziękować twórcy, rozważ wysłanie dotacji developerowi! każdy grosik pomaga i jest MEGA mile widziany :D",
"DonateVia": "przekaż darowiznę poprzez",
"DonateHireMe": "albo po prostu możesz <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">mnie zatrudnić</a>",
"SettingsVideoMute": "wycisz dźwięk",
"SettingsVideoMuteExplanation": "usuwa dźwięk z pobranego filmu, gdy to możliwe",
"ErrorSoundCloudNoClientId": "nie udało mi się pozyskać tymczasowego tokenu, który wymagany jest do pobrania utworów z soundcloud. spróbuj ponownie, jeśli wciąż pojawia się problem, {ContactLink}",
"CollapseServices": "wspierane usługi",
"CollapseSupport": "wsparcie & kod źródłowy",
"CollapsePrivacy": "polityka prywatności",
"ServicesNote": "ta lista nie jest końcowa i rośnie z upływem czasu, więc sprawdzaj ją raz na jakiś czas!",
"FollowSupport": "obserwuj {appName} na mastodonie lub twitterze dla wsparcia, ankiet, wiadomości i więcej:",
"SupportNote": "proszę pamiętaj, że odpowiedzi na pytania i zgłoszenia mogą zająć chwilę, jest tylko jedna osoba zajmująca się tym wszystkim.",
"SourceCode": "zgłoś problemy, zajrzyj w kod źródłowy, zagwiazdkuj lub sforkuj repo:",
"PrivacyPolicy": "polityka prywatności {appName} jest prosta: nie przechowujemy ani nie gromadzimy twoich danych. zero, zilch, nada, nic a nic.\nto co pobierasz to twoja sprawa, nie moja.\n\nniektóre nieidentyfikowalne dane są tymczasowo przechowywane, gdy żądane pobieranie wymaga renderowania na żywo. jest to wymagane by ta funkcja działała prawidłowo.\n\nw tym wypadku <span class=\"text-backdrop\">salted sha256 hash twojego adresu ip</span> i informacje o żądaniu są tymczasowo przechowywane w RAMie serwera przez <span class=\"text-backdrop\">2 minuty</span>. po dwóch minutach, wszystkie tymczasowo przechowane dane zostają permanentnie usunięte. hash twojego adresu ip jest <span class=\"text-backdrop\">wykorzystywany do ograniczenia dostępności streama tylko i wyłącznie dla ciebie</span>.\nnikt (nawet ja) nie ma dostępu do tych danych, gdyż kod {appName} nie pozwala na odczytywanie tych danych poza tymi funkcjami w jakikolwiek sposób.\n\njak chcesz to sam możesz spojrzeć na <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">repo na githubie</a> {appName} i faktycznie się przekonać, że wszystko gra.",
"ErrorYTUnavailable": "ten film z youtube jest niedostępny lub ograniczony wiekowo. obecnie nie mogę pobierać filmów z wrażliwymi treściami. spróbuj inny!",
"ErrorYTTryOtherCodec": "nie mogłem znaleźć niczego do pobrania z twoimi ustawieniami. spróbuj innego kodeka lub jakość!\n\nuwaga: api youtube czasami działa niespodziewanie. obwiniaj za to google, nie mnie.",
"SettingsCodecSubtitle": "kodek youtube",
"SettingsCodecDescription": "h264: generalnie zapewnia lepsze wsparcie dla odtwarzaczy, ale jakość sięga jedynie do 1080p.\nav1: słabe wsparcie dla odtwarzaczy, ale wspiera 8k i HDR.\nvp9: zazwyczaj najwyższy bitrate, zachowuje najwięcej szczegółów. wspiera 4k i HDR.\n\nwybierz h264 jeśli celujesz w najlepszą kompatybilność dla edytorów/odtwarzaczy/mediów społecznościowych.",
"SettingsAudioDub": "ścieżka dźwiękowa youtube",
"SettingsAudioDubDescription": "określa, która ścieżka dźwiękowa będzie użyta. jeśli ścieżka z dubbingiem nie jest dostępna, zamiast tego używany jest oryginalny język wideo.\n\noryginalne: używany jest oryginalny język wideo.\nauto: używany jest domyślny język przeglądarki (i {appName}).",
"SettingsDubDefault": "oryginalne",
"SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "typ pobieranych plików z vimeo",
"SettingsVimeoPreferDescription": "progresywny: bezpośredni link do cdn vimeo. maksymalna jakość to 1080p.\ndash: filmik i audio są scalane przez {appName} w jeden plik. maksymalna jakość to 4k.\n\nwybierz \"progresywny\" jak celujesz w najlepszą kompatybilność z edytorami/odtwarzaczami/mediami społecznościowymi. jeśli progresywne pobieranie nie jest dostępne, użyj \"dash\".",
"ShareURL": "udostępnij"
}
}

View file

@ -0,0 +1,121 @@
{
"name": "engleză",
"substrings": {
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">Depune o problemă pe GitHub</a>"
},
"strings": {
"LinkInput": "inserați link-ul aici",
"AboutSummary": "{appName} este destinația ta pentru descărcări de pe rețele de socializare. fără reclame, trackere, sau orice alte prostii. simplu inserează un link distribuit si ești gata de descărcare!",
"EmbedBriefDescription": "salvează conținut de pe rețele de socializare fără întreruperi",
"MadeWithLove": "făcut de <3 de wukko",
"AccessibilityInputArea": "spațiu pentru link",
"AccessibilityOpenAbout": "deschide fără fereastră nouă",
"AccessibilityDownloadButton": "buton descărcare",
"AccessibilityOpenSettings": "deschide fereastră setări",
"AccessibilityClosePopup": "inchide-ți fereastra",
"AccessibilityOpenDonate": "deschideți fereastră donații",
"TitlePopupAbout": "ce este {appName}?",
"TitlePopupSettings": "setări",
"TitlePopupError": "hopa...",
"TitlePopupChangelog": "ce este nou?",
"TitlePopupDonate": "suportați {appName}",
"TitlePopupDownload": "descărcați",
"ErrorSomethingWentWrong": "ceva a mers greșit si nu am putut sa obțin nimic pentru dumneavoastră. pute-ți sa încercați din nou, dar dacă această problemă persistă, vă rog contactați {ContactLink}",
"ErrorUnsupported": "se pare că această rețea nu este suportată încă sau linkul dumneavoastră nu este valid.",
"ErrorBrokenLink": "{s} este suportat, dar este ceva greșit cu link-ul dumneavoastră, poate nu l-ați copiat bine?",
"ErrorNoLink": "nu pot să ghicesc ce vreți sa descărcați! vă rog dați-mi un link!",
"ErrorPageRenderFail": "ceva nu a mers bine si pagina nu a putut să fie redată. dacă asta e o problemă recurentă sau critică, vă rog contactați {ContactLink}. ar fi foarte folositor dacă a-ți da comiterea hash curentă {s} și pașii de recreare a erorii. vă mulțumim în avans :D",
"ErrorRateLimit": "Faci prea multe cereri. Încearcă din nou într-un minut!",
"ErrorCouldntFetch": "Nu s-au putut obține informații despre link-ul tău. Verifică dacă este corect și încearcă din nou.",
"ErrorLengthLimit": "limita de durată curentă este de {s} minute. videoclipul pe care l-ați ales durează mai mult de {s} minute. alege-ți altceva",
"ErrorBadFetch": "A apărut o eroare atunci când s-a încercat obținerea de informații despre link-ul tău. Ești sigur că funcționează? Verifică dacă funcționează și încearcă din nou.",
"ErrorCorruptedStream": "această descărcare este din păcate coruptă. încearcă din nou!",
"ErrorNoInternet": "nu aveți internet sau API-ul {appName} este oprit. verificați conexiunea și încercați din nou",
"ErrorCantConnectToServiceAPI": "nu m-am putut conecta la serviciul api. se poate sa fie offline, sau {appName} ar fi blocat. încearcă din nou puțin mai târziu!",
"ErrorEmptyDownload": "nu văd nimic care ar putea fi descărcat de aici. încercați un link diferit.",
"ErrorLiveVideo": "acesta este un videoclip live, încă nu am învățat cum să văd viitorul. așteptați ca fluxul să se termine și să încercați din nou!",
"SettingsAppearanceSubtitle": "aparență",
"SettingsThemeSubtitle": "temă",
"SettingsFormatSubtitle": "format",
"SettingsQualitySubtitle": "calitate",
"SettingsThemeAuto": "auto",
"SettingsThemeLight": "luminos",
"SettingsThemeDark": "întunecat",
"SettingsKeepDownloadButton": "păstrază &gt;&gt; vizibil",
"AccessibilityKeepDownloadButton": "păstrează butonul de descărcare mereu vizibil",
"SettingsEnableDownloadPopup": "cere un mod de a salva",
"AccessibilityEnableDownloadPopup": "cere ce să facem cu descărcările",
"SettingsQualityDescription": "în cazul în care calitatea selectată nu este disponibilă, cea mai apropiată este folosită în schimb.",
"LinkGitHubChanges": "&gt;&gt; vezi comitele anterioare si contribuie pe github",
"NoScriptMessage": "{appName} Folosește JavaScript pentru cereri API și interfața interactivă. Trebuie să permiți JavaScript pentru a folosi acest site. Nu am reclame sau trackere, promit pe degetul mic.",
"DownloadPopupDescriptionIOS": "Apăsați și țineți apăsat butonul de descărcare, ascundeți previzualizarea video, apoi selectați \"download linked file\" pentru a salva.",
"DownloadPopupDescription": "Butonul de descărcare deschide o filă nouă cu fișierul solicitat. Puteți dezactiva acest pop-up în setări.",
"DownloadPopupWayToSave": "Alege un mod de a salva",
"ClickToCopy": "Apăsaţi pentru a copia",
"Download": "descărcați",
"CopyURL": "Copiază URL",
"AboutTab": "Despre",
"ChangelogTab": "Istoric modificări",
"DonationsTab": "Donații",
"SettingsVideoTab": "Video",
"SettingsAudioTab": "Audio",
"SettingsOtherTab": "Altele",
"ChangelogLastMajor": "Versiunea curentă & commit-uri",
"AccessibilityModeToggle": "Comută modul de descărcare",
"DonateLinksDescription": "Linkurile de donație se deschid într-o filă nouă. Acesta este cel mai bun mod de a dona dacă vrei să primesc donația ta direct.",
"SettingsAudioFormatBest": "foarte bun",
"SettingsAudioFormatDescription": "când formatul foarte bun este selectat, vei primi audio la cea mai bună calitate disponibilă, dacă nu este recodificat. toate celelalte vor fi recodificate.",
"Keyphrase": "salvează ceea ce îți place",
"SettingsRemoveWatermark": "dezactivează watermark",
"ErrorPopupCloseButton": "am înțeles",
"ErrorLengthAudioConvert": "limita de lungime curentă pentru conversia audio este de {s} minute. alegeți formatul \"foarte bun\" dacă doriți să evitați limitările.",
"SettingsAudioFullTikTok": "descarcă audio complet",
"SettingsAudioFullTikTokDescription": "descarcă sunetul original sau sunetul folosit în video fără modificări suplimentare din partea autorului video.",
"ErrorCantGetID": "nu am putut obține toate informațiile din link-ul scurtat. asigurați-vă că funcționează sau încercați unul complet.",
"ErrorNoVideosInTweet": "nu am putut găsi videoclipuri sau gif-uri pe acest tweet. încearcă altul!",
"ImagePickerTitle": "alege imaginile de descărcat",
"ImagePickerDownloadAudio": "descarcă audio",
"ImagePickerExplanationPC": "Click dreapta pe o imagine pentru a o salva.",
"ImagePickerExplanationPhone": "Apăsați și țineți apăsată o imagine pentru a o salva.",
"ErrorNoUrlReturned": "serverul nu a returnat un link de descărcare. Acest lucru nu ar trebui să se întâmple niciodată. reîncărcaţi pagina şi încercaţi din nou, dar dacă nu ajută, {ContactLink}.",
"ErrorUnknownStatus": "Am primit un răspuns ce nu pot procesa. Cel mai probabil ceva cu statutul este greșit. Acest lucru nu ar trebui să se întâmple niciodată. Reîncărcați pagina și încercați din nou, dar dacă nu ajută, {ContactLink}.",
"PasteFromClipboard": "Inserați din clipboard",
"ChangelogOlder": "Versiuni anterioare",
"ChangelogPressToExpand": "Apăsați pentru a extinde",
"Miscellaneous": "Altele",
"ModeToggleAuto": "Mod auto",
"ModeToggleAudio": "Mod Audio",
"SettingsDisableNotifications": "Ascundeți punctele de notificare",
"MediaPickerTitle": "Alegeți ce să salvați",
"MediaPickerExplanationPC": "Click sau Click dreapta pentru a descărca ce vrei.",
"MediaPickerExplanationPhone": "Apăsați sau țineți apăsat pentru a descărca ceea ce doriți.",
"MediaPickerExplanationPhoneIOS": "Apăsați și țineți apăsat butonul de descărcare, ascundeți previzualizarea video, apoi selectați \"download linked file\" pentru a salva.",
"TwitterSpaceWasntRecorded": "Acest spațiu Twitter nu a fost înregistrat, așa că nu este nimic de descărcat. Încercați altul!",
"ErrorCantProcess": "Nu am putut procesa cererea dvs. :(\nputeți încerca din nou, dar dacă problema persistă, vă rugăm {ContactLink}.",
"ChangelogPressToHide": "Apăsați pentru a restrânge",
"Donate": "Donează",
"DonateSub": "Ajută-mă să îl mențin",
"DonateExplanation": "{appName} nu (si niciodată nu) va servi anunțuri sau va vinde datele tale, de aceea <span class=\"text-backdrop\"> este complet gratuit pentru folosință</span>. dar se pare că menținerea unui serviciu web folosit de mii de oameni este oarecum costisitor.\n\ndacă ai găsit vreodată {appName} folositor și vrei să îl păstrezi online, sau pur și simplu vrei să-i mulțumești dezvoltatorului, consideră să donezi! fiecare cent ajută și este FOARTE apreciat.",
"DonateVia": "donează prin",
"DonateHireMe": "sau, ca alternativă, poți <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">să mă angajezi</a>.",
"SettingsVideoMute": "oprește sunetul",
"SettingsVideoMuteExplanation": "dezactivează audio în videoclipul descărcat atunci când este posibil.",
"ErrorSoundCloudNoClientId": "nu a putut găsi client_id necesar pentru a prelua date audio de la soundcloud. încercați din nou, iar dacă problema persistă, {ContactLink}.",
"CollapseServices": "servicii suportate",
"CollapseSupport": "suport & cod sursă",
"CollapsePrivacy": "politica de confidențialitate",
"ServicesNote": "această listă nu este finală și continuă să se extindă în timp, asigură-te că verifici din când în când!",
"FollowSupport": "urmărește {appName} pe mastodon sau twitter pentru suport, sondaje, știri și multe altele:",
"SupportNote": "vă rugăm să reţineţi că întrebările şi problemele pot dura un timp pentru a răspunde, există o singură persoană care gestionează totul.",
"SourceCode": "raportează probleme, explorează codul sursă, adaugă la favorite sau furcă repo-ul:",
"PrivacyPolicy": "politica de confidențialitate a {appName} este simplă: nu sunt colectate sau stocate date despre dvs. zero, zilch, nada, nimic.\nceea ce descarci este treaba ta, nu a mea.\n\nunele date care nu pot fi urmărite sunt stocate temporar atunci când descărcarea solicitată necesită redare live. este necesar ca această caracteristică să funcționeze.\n\nîn acest caz, <span class=\"text-backdrop\">hash sha256 sărat al adresei dvs. IP</span> și informațiile despre fluxul solicitat sunt stocate temporar în memoria RAM a serverului timp de <span class=\"text-backdrop\">2 minute</span>. după 2 minute toate informațiile stocate anterior sunt șterse definitiv. hash-ul adresei dvs. IP este <span class=\"text-backdrop\">utilizat pentru a limita accesul la flux numai pentru dvs.</span>.\nnimeni (chiar și eu) nu are acces la aceste date, deoarece baza de cod oficială {appName} nu oferă o modalitate de a le citi în afara funcțiilor de procesare, în primul rând.\n\nputeți verifica pentru dvs. <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">repozitoriul github</a> de la {appName} și să vedeți că, într-adevăr, nimic nu este stocat permanent.",
"ErrorYTUnavailable": "acest videoclip youtube este indisponibil sau restricționat de vârstă. în prezent nu pot descărca videoclipuri cu conținut sensibil. Încercați altul!",
"ErrorYTTryOtherCodec": "nu am putut găsi nimic de descărcat cu setările tale. încearcă un alt codec sau calitate!\n\nnotă: api-ul youtube uneori acționează neașteptat. dă vina pe google pentru asta, nu pe mine.",
"SettingsCodecSubtitle": "codec youtube",
"SettingsCodecDescription": "h264: suport general mai bun pentru playere, dar calitatea maximă este la 1080p.\nav1: suport scăzut pentru playere, dar suportă 8k & HDR.\nvp9: de obicei cel mai mare bitrate, păstrează cele mai detaliate detalii. suportă 4k & HDR.\n\ndacă doriți cel mai bun editor/player/social media, alegeți h264.",
"SettingsAudioDub": "pistă audio youtube",
"SettingsAudioDubDescription": "definește ce pistă audio va fi folosită. dacă piesa dublată nu este disponibilă, limba video originală este folosită în schimb.\n\noriginal: este folosită limba video originală.\nauto: se folosește limba browser-ului implicit (și limba {appName}).",
"SettingsDubDefault": "original",
"SettingsDubAuto": "auto"
}
}

View file

@ -7,7 +7,7 @@
"LinkInput": "вставь ссылку сюда",
"AboutSummary": "{appName} - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано wukko, с <3",
"MadeWithLove": "сделано с <3 wukko",
"AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания",
@ -29,7 +29,7 @@
"ErrorCouldntFetch": "у меня не получилось ничего найти по этой ссылке. убедись, что она работает, и попробуй ещё раз. некоторый контент может быть залочен на регион.",
"ErrorLengthLimit": "я не могу обрабатывать видео длиннее чем {s} минут(ы), так что скачай что-нибудь покороче!",
"ErrorBadFetch": "произошла какая-то ошибка при получении данных по твоей ссылке. убедись, что она работает, и попробуй ещё раз.",
"ErrorNoInternet": "не получилось подключиться к серверу. проверь подключение к интернету и попробуй ещё раз!",
"ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.",
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же {appName} заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
"ErrorLiveVideo": "я пока что не умею заглядывать в будущее, поэтому дождись окончания прямого эфира, и потом уже скачивай видео!",
@ -40,15 +40,15 @@
"SettingsThemeAuto": "авто",
"SettingsThemeLight": "светлая",
"SettingsThemeDark": "тёмная",
"SettingsKeepDownloadButton": "всегда показывать &gt;&gt;",
"AccessibilityKeepDownloadButton": "всегда показывать кнопку скачивания на экране",
"SettingsKeepDownloadButton": "оставлять &gt;&gt; на экране",
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
"SettingsEnableDownloadPopup": "выбор метода скачивания",
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на гитхабе",
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
"DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода сохранения файла в настройках.",
"DownloadPopupWayToSave": "выбери, как сохранить",
"ClickToCopy": "нажми, чтобы скопировать",
"Download": "скачать",
@ -65,7 +65,7 @@
"SettingsAudioFormatBest": "лучший",
"SettingsAudioFormatDescription": "когда выбран \"лучший\", ты получишь аудио без каких-либо изменений. такое, какое оно есть на стороне сервиса. если же выбрано что-то другое, то аудио будет немного сжато.",
"Keyphrase": "сохраняй то, что любишь",
"SettingsRemoveWatermark": "убирать ватермарку",
"SettingsRemoveWatermark": "убрать ватермарку",
"ErrorPopupCloseButton": "ясно",
"ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат, чтобы обойти ограничения.",
"SettingsAudioFullTikTok": "полное аудио",
@ -73,9 +73,9 @@
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.",
"ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!",
"ImagePickerTitle": "выбери картинки для скачивания",
"ImagePickerDownloadAudio": "скачать звук",
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.",
"ImagePickerExplanationPhone": "зажми и удерживай картинку, чтобы её сохранить.",
"ImagePickerDownloadAudio": "скачать аудио",
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на изображение, чтобы его сохранить.",
"ImagePickerExplanationPhone": "зажми и удерживай изображение, чтобы его сохранить.",
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
"PasteFromClipboard": "вставить и скачать",
@ -88,8 +88,8 @@
"MediaPickerTitle": "выбери, что сохранить",
"MediaPickerExplanationPC": "кликни, чтобы скачать. также можно скачать через контекстное меню правой кнопки мыши.",
"MediaPickerExplanationPhone": "нажми, или нажми и удерживай, чтобы скачать.",
"MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью и выбери \"загрузить файл по ссылке\", чтобы скачать.",
"TwitterSpaceWasntRecorded": "мне нечего скачать, так как этот twitter space не был записан. попробуй другой!",
"MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью, и наконец выбери \"загрузить файл по ссылке\".",
"TwitterSpaceWasntRecorded": "этот twitter space не был записан, поэтому я не могу его скачать. попробуй другой!",
"ErrorCantProcess": "я не смог обработать твой запрос :(\nты можешь попробовать ещё раз, но если не поможет, то {ContactLink}.",
"ChangelogPressToHide": "скрыть",
"Donate": "задонатить",

View file

@ -5,21 +5,17 @@ import loadJson from "../modules/sub/loadJSON.js";
const locPath = './src/localization/languages'
let loc = {}
let languages = [];
export function loadLoc() {
fs.readdir(locPath, (err, files) => {
if (err) return false;
files.forEach(file => {
loc[file.split('.')[0]] = loadJson(`${locPath}/${file}`);
languages.push(file.split('.')[0])
loc[file.split('.')[0]] = loadJson(`${locPath}/${file}`)
});
})
}
loadLoc();
export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "&bull;");
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/{bS}/g, '<div class=\"bullpadding\">').replace(/{bE}/g, '</div>').replace(/\*;/g, "&bull;");
}
export function replaceAll(lang, str, string, replacement) {
let s = replaceBase(str[string])
@ -45,4 +41,3 @@ export default function(lang, string, replacement) {
return `!!${string}!!`
}
}
export const languageList = languages;

View file

@ -9,39 +9,34 @@ import match from "./processing/match.js";
export async function getJSON(originalURL, lang, obj) {
try {
let patternMatch, url = decodeURIComponent(originalURL),
hostname = new URL(url).hostname.split('.'),
host = hostname[hostname.length - 2];
if (!url.startsWith('https://')) return apiJSON(0, { t: errorUnsupported(lang) });
switch(host) {
case "youtu":
let url = decodeURIComponent(originalURL);
if (!url.includes('http://')) {
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2],
patternMatch;
if (host === "youtu") {
host = "youtube";
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
break;
case "goo":
if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/") {
host = "soundcloud";
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
}
if (host === "goo" && url.substring(0, 30) === "https://soundcloud.app.goo.gl/") {
host = "soundcloud"
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
}
if (host === "tumblr" && !url.includes("blog/view")) {
if (url.slice(-1) == '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], '');
}
if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) {
for (let i in patterns[host]["patterns"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]);
if (patternMatch) break;
}
break;
case "tumblr":
if (!url.includes("blog/view")) {
if (url.slice(-1) === '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], '')
}
break;
}
if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) return apiJSON(0, { t: errorUnsupported(lang) });
for (let i in patterns[host]["patterns"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(`.${patterns[host]['tld'] ? patterns[host]['tld'] : "com"}/`)[1].replace('.', ''));
if (patternMatch) break
}
if (!patternMatch) return apiJSON(0, { t: errorUnsupported(lang) });
return await match(host, patternMatch, url, lang, obj)
if (patternMatch) {
return await match(host, patternMatch, url, lang, obj);
} else return apiJSON(0, { t: errorUnsupported(lang) });
} else return apiJSON(0, { t: errorUnsupported(lang) });
} else return apiJSON(0, { t: errorUnsupported(lang) });
} catch (e) {
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') })
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
}
}

View file

@ -1,45 +1,12 @@
import * as esbuild from "esbuild";
import * as fs from "fs";
import { languageList } from "../localization/manager.js";
import page from "./pageRender/page.js";
function cleanHTML(html) {
let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, '');
return clean
}
export async function buildFront(commitHash, branch) {
export async function buildFront() {
try {
// build html
if (!fs.existsSync('./build/')){
fs.mkdirSync('./build/');
fs.mkdirSync('./build/ios/');
fs.mkdirSync('./build/pc/');
fs.mkdirSync('./build/mob/');
}
for (let i in languageList) {
i = languageList[i];
let params = {
"hash": commitHash,
"lang": i,
"useragent": "pc",
"branch": branch
}
fs.writeFileSync(`./build/pc/${i}.html`, cleanHTML(page(params)));
params["useragent"] = "iphone os";
fs.writeFileSync(`./build/ios/${i}.html`, cleanHTML(page(params)));
params["useragent"] = "android";
fs.writeFileSync(`./build/mob/${i}.html`, cleanHTML(page(params)));
}
// build js & css
await esbuild.build({
entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'],
outdir: 'min/',
outdir: `min/`,
minify: true,
loader: { '.js': 'js', '.css': 'css', },
charset: 'utf8'
loader: { ".js": "js", ".css": "css" }
})
} catch (e) {
return;

View file

@ -1,45 +1,20 @@
{
"current": {
"version": "5.3",
"title": "better looks, better feel",
"banner": "cattired.webp",
"content": "this update isn't as big as previous ones, but it still greatly enhances the cobalt experience.\n\nhere's what's up:\n*; new mode switcher! elegant and 100% clear. should no longer cause any confusion. let me know if you like it better this way :D\n*; wide paste button on mobile is back, but now it's even closer to your finger.\n*; removed the weird grey chin on changelog banners.\n*; removed left-handed layout toggle since it is no longer needed.\n*; fixed input area display in chromium 112+.\n*; centered the main action box.\n*; cleaned up css of main action box to get rid of tricks and ensure correct display on all devices.\n*; fixed a bug that'd cause notifications dots to disappear when an unrelated checkbox was checked.\n\nhopefully from now on i'll focus on adding support for more services.\nthank you for using cobalt. stay cool :)"
},
"history": [{
"version": "5.2",
"title": "fastest one in the game",
"banner": "catspeed.webp",
"content": "hey, notice anything different? well, at very least the page loaded way faster! this update includes many improvements and fixes, but also some new features.\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; twitter retweet links are now supported.\n*; all vimeo videos should now be possible to download.\n*; you now can download audio from vimeo.\n*; it's now possible to pick between preferred vimeo download method in settings.\n*; fixed issues related to tiktok, twitter, twitter spaces, and vimeo downloads.\n*; overall cobalt performance should be MUCH better.\n\nservice improvements:\n*; added support for twitter retweet links. now all kinds of tweet links are supported.\n*; fixed the issue related to periods in tiktok usernames (#96).\n*; fixed twitter spaces downloads.\n*; added support for audio downloads from vimeo.\n*; added ability to choose between \"progressive\" and \"dash\" vimeo downloads. go to settings > video to pick your preference.\n*; fixed the issue related to vimeo quality picking.\n*; fixed the issue when vimeo module wouldn't show appropriate errors and instead would fallback to default ones.\n*; improved audio only downloads for some edge cases.\n*; (hopefully) better youtube reliability.\n*; temporarily disabled douyin support due to api endpoint cut off.\n\ninterface improvements:\n*; merged clipboard and mode switcher rows into one for mobile view.\n*; added left-handed layout toggle for those who prefer to have the clipboard button on left.\n*; new custom-made clipboard icon. now it clearly indicates what it does.\n*; improved english and russian localization. both are way more direct and less bloaty.\n*; frontend page is now rendered once and is cached on disk instead of being rendered every time someone requests a page. this greatly improves page loading speeds and further reduces strain put on the server.\n*; frontend page is now minimized just like js and css files. this should minimize traffic wasted on loading the page, along with minor loading speed improvement.\n*; added proper checkbox icon for better clarity.\n*; checkboxes are now stretched edge-to-edge on phone to be easier to manage for right-handed people.\n*; removed button hover highlights on phones.\n*; fixed button press animations for safari on ios.\n*; fixed text selection on ios. previously you could select text or images anywhere, but now they're selectable in limited places, just like on other platforms.\n*; frontend platform is now marked in settings: p is for pc; m is for mobile; i is for ios. this is done for possible future debugging and issue-solving.\n*; better error messaging.\n\ninternal improvements:\n*; better rate limiting, there should be way less cases of accidental limits.\n*; added support for m3u8 playlists. this will be useful for future additions, and is currently used by vimeo module.\n*; added support for \"chop\" stream format for vimeo downloads.\n*; fixed vk user id extraction. i assumed the - in url was a separator, but it's actually a part of id.\n*; completely reworked the vimeo module. it's much cleaner and better performant now.\n*; minor clean ups across the board.\n\nnot really related to this update, but thank you for 50k monthly users! i really appreciate that you're still here, because that means i'm doing some things right :D"
}, {
"version": "5.1",
"title": "the evil has been defeated",
"banner": "happymeowth.webp",
"content": "hey, ever wanted to download a youtube video without a hassle? cobalt is here to help. this update fixes all issues related to youtube downloads.\nnot only that, but it also introduces features never before seen in a downloader, such as youtube dub downloads! read below to see what's up :)\n\n<span class=\"text-backdrop\">tl;dr:</span>\n*; audio in youtube videos FINALLY no longer gets cut off.\n*; you now can pick any video resolution you want (from 360p to 8k) and any possible youtube video codec (h264/av1/vp9).\n*; you now can download youtube videos with dubs in your native language. just check settings > audio.\n*; youtube processing has been vastly sped up.\n\nok, now onto the nerdy part of changelog. this update is pretty huge and includes improvements across the board.\n\nservice improvements:\n*; all youtube functionality has been reworked. cobalt now relies on innertube apis, not web scraping.\n*; random audio cut off issue has been fixed, let me know if it ever occurs again. (closes #62, #66, #75, #88).\n*; added support for youtube dubs. currently it's using your browser's default language when enabled, but i have plans on making a picker. i'll ask people on twitter and mastodon if this feature is needed, and add a picker in next updates.\n*; instead of adding more quality presets, i added granular quality options. pick whatever you like, from 360p up to 4320p (for all services, not just youtube).\n*; replaced a format picker with codec picker for youtube. you can pick h264, av1, or vp9. all of them should work as expected (closes #88).\n*; youtube audio files are now properly matched to corresponding video files.\n*; it's now always possible to download pristine h264 720p/360p videos from youtube. these videos will work ANYWHERE, so they're default for mobile.\n*; youtube requests are no longer permanently cached, ram usage should drop even further.\n*; youtube video and audio file names now include codec and dub language when applicable.\n*; max video and audio duration limits have been bumped up to 3 hours.\n*; general performance of entire youtube download process has been greatly improved.\n*; vk module has been reworked to be more compact and not make use of outdated technique of quality picking. should also be way more reliable.\n\ninternal improvements:\n*; cleaned up services config, all constants have been moved directly to modules for quicker access.\n*; matching module has been slightly cleaned up.\n\ninterface improvements:\n*; many descriptions and error messages have been slightly tuned to be less wordy.\n*; unnecessary title duplications in settings have been merged into one.\n*; added more clarity to quality and codec descriptions.\n\nif you use cobalt api, please note that you have to update your creation to support new features.\n\nthis is the second batch of 5.x improvements, there's way more to come. thank you for being here, i really appreciate your support.\n\nif you want to thank me (the developer), there's a nice tab under this changelog that has \"donations\" text on it. anything helps me continue developing and hosting the friendliest media downloader :D"
}, {
"version": "5.0",
"title": "it's all about attention to detail!",
"banner": "valentines.webp",
"content": "happy valentine's day! i have an update for you, as a gift :D\n\ntl;dr: added support for <span class=\"text-backdrop\">reddit gifs</span>, fixed douyin downloads, fixed vimeo quality picking, revamped entirety of codebase, and many other fixes.\n\nhere's more info:\n\nthis update is mostly about cleaning up and polishing the codebase, but it also has some new features. here's what's up:\n\nservice-related improvements:\n*; you now can download gifs from reddit!\n*; attempting to download a video from douyin no longer throws an error (bytedance changed the api endpoint, yet again).\n*; fixed quality picking for vimeo downloads.\n*; fixed length limit check in vimeo module.\n*; fixed support for \"user view\" vk clips links.\n*; various twitter errors are now displayed correctly instead of falling back to the default error.\n*; state of all services is now tested on each commit.\n\nui improvements:\n*; cobalt social links no longer disappear if you have an aggressive ad blocking extension installed.\n*; various localization improvements for both english and russian.\n*; changed some service aliases to display full list of supported downloads.\n*; added current branch information to version text (in settings).\n*; fixed typos in older changelogs.\n\ninternal improvements:\n*; <span class=\"text-backdrop\">everything</span> has been sanitized, improved, and refactored. code is now much easier to read and maintain.\n*; rewrote and/or optimized all modules that were messy or inefficient.\n*; all git interaction functions now store info in cache instead of fetching it every time the function is called.\n*; added a test script that checks functionality of all supported services.\n*; updated deepsource config. checks are more accurate now.\n*; requests from internet explorer are now dropped entirely instead of redirecting people stuck in 90s to a proper browser download page. this was done to avoid (my) personal bias towards browsers.\n\ni put a ton of effort into this version, and i hope you like it as much as i do.\n\nthank you for using cobalt. there's so much more to come :)"
}, {
"version": "4.8",
"title": "prettier than ever",
"banner": "catmakeup.webp",
"content": "this version brings many visual improvements and a completely revamped \"about\" tab.\n\nwhat's new in \"about\" tab:\n*; all information is now split into collapsible sections, making it easier to navigate.\n*; added privacy policy to further prove that none of your data is collected.\n*; added emoji to the page title to make it look consistent with other pages.\n*; added mastodon account handle and link.\n*; there are now short notes at the end of each section.\n*; other changes that are too small to describe. just go check it out!\n\nvisual improvements:\n*; less wasted space: paddings and margins have been reduced and optimized for usability, consistency, and overall beauty.\n*; all <a class=\"text-backdrop italic\" href=\"https://youtu.be/dQw4w9WgXcQ\" target=\"_blank\">links</a> are now in italic. it's much easier to tell them apart from <span class=\"text-backdrop\">regular highlights</span>.\n*; error popup no longer looks broken and out of place.\n*; download popup now has a proper close button, not something from 2.x era.\n*; emoji are no longer selectable or draggable.\n*; better scalability: desktop layout for home screen is shown if device viewport is wide enough to fit in three action buttons.\n*; page shouldn't look broken on phones in landscape mode (i still highly recommend using cobalt in portrait mode).\n*; removed bulletpoint padding. it was unnecessary.\n*; updated some service names.\n\nas always, you can suggest features or report bugs on any platform listed in the \"support\" section of about tab.\n\nthank you for using cobalt. i hope you have a good day :)"
}, {
"version": "4.7",
"title": "we're better together! thank you for bug reports.",
"banner": "bettertogether.webp",
"content": "this update includes a bunch of improvements, many of which were made thanks to the community :D\n\nservice-related improvements:\n*; private soundcloud links are now supported (#68);\n*; tiktok usernames with dots in them no longer confuse cobalt (#71);\n*; .ogg files no longer wrongfully include a video channel (#67);\n*; fixed an issue that caused cobalt to freak out when user attempted to download an audio from audio-only service with \"mute video\" option enabled.\n\nui improvements:\n*; popup padding has been evened out. popups are now able to fit in more information on scroll, especially on mobile;\n*; all buttons are now of even size and are displayed without any padding issues across all modern browsers and devices;\n*; checkbox is no longer crippled on ios;\n*; many explanation texts have been simplified to get rid of unnecessary bloat (no bullshit, remember?);\n*; moved tiktok section in video settings higher due to higher priority;\n*; fixed unexpectedly displayed scrollbars on switch rows in firefox.\n\nstability improvements:\n*; ffmpeg process now should end upon finishing the render;\n*; ffmpeg should also quit when download is abruptly cut off;\n*; fixed a memory leak that was caused by misconfigured stream information caching (#63).\n\ninternal improvements:\n*; requested streams are now stored in cache for 2 minutes instead of 1000 hours (yes, 1000 hours, i fucked up);\n*; cached data is now reused if user requests same content within 2 minutes;\n*; page render module is now even cleaner than before;\n*; proper support for bullet-points in loc strings.\n\nyou can suggest features or report bugs on <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">twitter</a>. both work just fine, use whichever you're more comfortable with.\n\nthank you for using cobalt, and thank you for reading this changelog.\n\nyou're amazing, keep it up :)"
}, {
"content": "this update includes a bunch of improvements, many of which were made thanks to the community :D\n\nservice-related improvements:\n*; private soundcloud links are now supported (#68);\n*; tiktok usernames with dots in them no longer confuse cobalt (#71);\n*; .ogg files no longer wrongfully include a video channel (#67);\n*; fixed an issue that caused cobalt to freak out when user attempted to download an audio from audio-only service with \"mute video\" option enabled.\n\nui improvements:\n*; popup padding has been evened out. popups are now able to fit in more information on scroll, especially on mobile;\n*; all buttons are now of even size and are displayed without any padding issues across all modern browsers and devices;\n*; checkbox is no longer crippled on ios;\n*; many explanation texts have been simplified to get rid of unnecessary bloat (no bullshit, remember?);\n*; moved tiktok section in video settings higher due to higher priority;\n*; fixed unexpectedly displayed scrollbars on switch rows in firefox.\n\nstability improvements:\n*; ffmpeg process now should end upon finishing the render;\n*; ffmpeg should also quit when download is abruptly cut off;\n*; fixed a memory leak that was caused by misconfigured stream information caching (#63).\n\ninternal improvements:\n*; requested streams are now stored in cache for 2 minutes instead of 1000 hours (yes, 1000 hours, i fucked up);\n*; cached data is now reused if user requests same content within 2 minutes;\n*; page render module is now even cleaner than before;\n*; proper support for bullet-points in loc strings.\n\nyou can suggest features or report bugs on <a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">twitter</a>. both work just fine, use whichever you're more comfortable with.\n\nthank you for using cobalt, and thank you for reading this changelog.\n\nyou're amazing, keep it up :)"
},
"history": [{
"version": "4.6",
"title": "mute videos and proper soundcloud support",
"banner": "shutup.png",
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n*; you now can download videos with no audio! simply enable the \"mute audio\" option in settings &gt; audio.\n*; soundcloud module has been updated, and downloads should no longer break after some time.\nvisual improvements:\n*; moved some things around in settings popup, and added separators where separation is needed.\n*; updated some texts in english and russian.\n*; version and commit hash have been joined together, now they're a single unit.\ninternal improvements:\n*; updated api documentation to include isAudioMuted.\n*; simplified the startup message.\n*; created render elements for separator and explanation due to high duplication of them in the page.\n*; fully deprecated GET method for API requests.\n*; fixed some code quirks.\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n{bS}*; you now can download videos with no audio! simply enable the \"mute audio\" option in settings &gt; audio.\n*; soundcloud module has been updated, and downloads should no longer break after some time.{bE}\nvisual improvements:\n{bS}*; moved some things around in settings popup, and added separators where separation is needed.\n*; updated some texts in english and russian.\n*; version and commit hash have been joined together, now they're a single unit.{bE}\ninternal improvements:\n{bS}*; updated api documentation to include isAudioMuted.\n*; simplified the startup message.\n*; created render elements for separator and explanation due to high duplication of them in the page.\n*; fully deprecated GET method for API requests.\n*; fixed some code quirks.{bE}\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
}, {
"version": "4.5",
"title": "better, faster, stronger, stable",
"banner": "meowthstrong.webp",
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop italic\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n{bS}*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.{bE}\nuser interface improvements:\n{bS}*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.{bE}\ninternal improvements:\n{bS}*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.{bE}\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
}, {
"version": "4.4",
"title": "over 1 million monthly requests. thank you.",
@ -48,12 +23,12 @@
}, {
"version": "4.3.2",
"title": "twitter improvements & changelog overhaul",
"content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- cobalt version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!"
"content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- {appName} version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!"
}, {
"version": "4.3",
"title": "developers, developers, developers, developers",
"banner": "developersdevelopersdevelopers.webp",
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop italic\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop italic\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on {appName} for getting content from social media. the api has been revamped and <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- {appName} web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using {appName}'s api, make sure to mention <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
}, {
"version": "4.2",
"title": "optimized quality picking and 8k video support",
@ -61,15 +36,15 @@
}, {
"version": "4.1",
"title": "better tiktok image downloads",
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, cobalt will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in cobalt 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, {appName} will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in {appName} 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
}, {
"version": "4.0",
"title": "better and faster than ever",
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop italic\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- {appName} now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when {appName} is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, {appName} will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>."
}, {
"version": "3.7",
"title": "support for multi media tweets is here!",
"content": "cobalt now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and cobalt will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for cobalt, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code."
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- {appName} is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through {appName}'s code."
}, {
"version": "3.6.2 + 3.6.3",
"title": "less disturbance",
@ -77,18 +52,18 @@
}, {
"version": "3.6",
"title": "improvements all around!",
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- {appName} is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
}, {
"version": "3.5.4",
"title": "tiktok support is back :D",
"content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop italic\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works."
"content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works."
}, {
"version": "3.5.2",
"title": "vk clips support, improved changelog system, and less bugs",
"content": "new features: \n- added support for vk clips. cobalt now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, cobalt now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- cobalt should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side."
"content": "new features: \n- added support for vk clips. {appName} now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, {appName} now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- {appName} should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side."
}, {
"version": "3.5",
"title": "ui revamp and usability improvements",
"content": "new features:\n- cobalt now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, cobalt won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in cobalt. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved."
"title": "ui revamp and usability imporvements",
"content": "new features:\n- {appName} now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, {appName} won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in {appName}. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved."
}]
}

View file

@ -10,9 +10,12 @@ export const
version = packageJson.version,
streamLifespan = config.streamLifespan,
maxVideoDuration = config.maxVideoDuration,
maxAudioDuration = config.maxAudioDuration,
genericUserAgent = config.genericUserAgent,
repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo,
quality = config.quality,
internetExplorerRedirect = config.internetExplorerRedirect,
donations = config.donations,
ffmpegArgs = config.ffmpegArgs,
supportedAudio = config.supportedAudio,

View file

@ -3,7 +3,7 @@ const names = {
"🎬": "clapper_board",
"💰": "money_bag",
"🎉": "party_popper",
"❓": "question_mark",
"❓": "red_question_mark",
"✨": "sparkles",
"🪅": "pinata",
"🪄": "magic_wand",
@ -18,23 +18,17 @@ const names = {
"🕯️": "candle",
"😺": "cat",
"🐶": "dog",
"🎂": "cake",
"🐘": "elephant",
"🐦": "bird",
"🐙": "octopus",
"🔮": "crystal_ball",
"💪": "biceps"
"🎂": "cake"
}
let sizing = {
22: 0.4,
30: 0.7,
48: 0.9,
64: 0.9
48: 0.9
}
export default function(emoji, size, disablePadding) {
if (!size) size = 22;
let padding = size !== 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : false;
let padding = size !== 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : ``;
if (disablePadding) padding = 'margin-right:0!important;';
if (!names[emoji]) emoji = "❓";
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}"` : ''}alt="${emoji}" src="emoji/${names[emoji]}.svg">`
return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">`
}

View file

@ -2,24 +2,26 @@ import { celebrations } from "../config.js";
export function switcher(obj) {
let items = ``;
if (obj.name === "download") {
items = obj.items;
} else {
for (let i = 0; i < obj.items.length; i++) {
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []
items += `<button id="${obj.name}-${obj.items[i]["action"]}" class="switch${classes.length > 0 ? ' ' + classes.join(' ') : ''}" onclick="changeSwitcher('${obj.name}', '${obj.items[i]["action"]}')">${obj.items[i]["text"] ? obj.items[i]["text"] : obj.items[i]["action"]}</button>`
}
switch(obj.name) {
case "download":
items = obj.items;
break;
default:
for (let i = 0; i < obj.items.length; i++) {
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []
items += `<button id="${obj.name}-${obj.items[i]["action"]}" class="switch${classes.length > 0 ? ' ' + classes.join(' ') : ''}" onclick="changeSwitcher('${obj.name}', '${obj.items[i]["action"]}')">${obj.items[i]["text"] ? obj.items[i]["text"] : obj.items[i]["action"]}</button>`
}
break;
}
if (obj.noParent) return `<div class="switches">${items}</div>`;
return `<div id="${obj.name}-switcher" class="switch-container">
return `
<div id="${obj.name}-switcher" class="switch-container">
${obj.subtitle ? `<div class="subtitle">${obj.subtitle}</div>` : ``}
<div class="switches">${items}</div>
${obj.explanation ? `<div class="explanation">${obj.explanation}</div>` : ``}
</div>`
}
export function checkbox(action, text, paddingType, aria) {
export function checkbox(action, text, aria, paddingType) {
let paddingClass = ` `
switch (paddingType) {
case 1:
@ -31,8 +33,6 @@ export function checkbox(action, text, paddingType, aria) {
case 3:
paddingClass += "no-margin"
break;
case 4:
paddingClass += "top-margin-only"
}
return `<label id="${action}-chkbx" class="checkbox${paddingClass}">
<input id="${action}" type="checkbox" ${aria ? `aria-label="${aria}"` : `aria-label="${text}"`} onclick="checkbox('${action}')">
@ -65,9 +65,9 @@ export function popup(obj) {
}
return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">` : ''}
${obj.buttonOnly ? obj.emoji : ``}
<div id="popup-header" class="popup-header">
${obj.standalone && !obj.buttonOnly ? `<button id="close-button" class="switch up" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>` : ''}
${obj.buttonOnly ? obj.header.emoji : ``}
${obj.standalone && !obj.buttonOnly ? `<button id="popup-close" class="button mono" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>` : ''}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
@ -88,7 +88,7 @@ export function multiPagePopup(obj) {
tabs += `<button id="tab-button-${obj.name}-${obj.tabs[i]["name"]}" class="switch tab tab-${obj.name}" onclick="changeTab(event, 'tab-${obj.name}-${obj.tabs[i]["name"]}', '${obj.name}')">${obj.tabs[i]["title"]}</button>`
tabContent += `<div id="tab-${obj.name}-${obj.tabs[i]["name"]}" class="popup-tab-content tab-content-${obj.name}">${obj.tabs[i]["content"]}</div>`
}
tabs += `<button id="close-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
@ -98,25 +98,13 @@ export function multiPagePopup(obj) {
<div id="popup-tabs" class="switches popup-tabs">${tabs}</div>
</div>`
}
export function collapsibleList(arr) {
let items = ``
for (let i = 0; i < arr.length; i++) {
items += `<div id="${arr[i]["name"]}-collapse" class="collapse-list">
<div class="collapse-header" onclick="expandCollapsible(event)">
<div class="collapse-title">${arr[i]["title"]}</div>
<div class="collapse-indicator">^</div>
</div>
<div id="${arr[i]["name"]}-body" class="collapse-body">${arr[i]["body"]}</div>
</div>`
}
return items;
}
export function popupWithBottomButtons(obj) {
let tabs = ``
for (let i = 0; i < obj.buttons.length; i++) {
tabs += obj.buttons[i]
}
tabs += `<button id="close-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
@ -128,15 +116,13 @@ export function popupWithBottomButtons(obj) {
</div>`
}
export function backdropLink(link, text) {
return `<a class="text-backdrop italic" href="${link}" target="_blank">${text}</a>`
}
export function socialLink(emoji, name, handle, url) {
return `<div class="cobalt-support-link">${emoji} ${name}: <a class="text-backdrop italic" href="${url}" target="_blank">${handle}</a></div>`
return `<a class="text-backdrop" href="${link}" target="_blank">${text}</a>`
}
export function settingsCategory(obj) {
return `<div id="settings-${obj.name}" class="settings-category">
<div class="category-title">${obj.title ? obj.title : obj.name}</div>
<div class="category-content">${obj.body}</div>
<div class="settings-category-content">${obj.body}</div>
</div>`
}

View file

@ -1,11 +0,0 @@
import { languageList } from "../../localization/manager.js";
export default function(lang, userAgent) {
let language = languageList.includes(lang) ? lang : "en";
let ua = userAgent.toLowerCase();
let platform = (ua.match("android") || ua.match("iphone os")) ? "mob" : "pc";
if (platform === "mob" && ua.match("iphone os")) platform = "ios";
return `/build/${platform}/${language}.html`;
}

View file

@ -1,17 +1,13 @@
import changelogManager from "../changelog/changelogManager.js"
let cache = {}
export function changelogHistory() { // blockId 0
if (cache['0']) return cache['0'];
let history = changelogManager("history");
let render = ``;
let historyLen = history.length;
let historyLen = history.length
for (let i in history) {
let separator = (i !== 0 && i !== historyLen) ? '<div class="separator"></div>' : '';
let separator = (i != 0 && i != historyLen) ? '<div class="separator"></div>' : ''
render += `${separator}${history[i]["banner"] ? `<div class="changelog-banner"><img class="changelog-img" src="${history[i]["banner"]}" onerror="this.style.display='none'"></img></div>` : ''}<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div><div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
}
cache['0'] = render;
return render;
}

View file

@ -1,5 +1,5 @@
import { backdropLink, celebrationsEmoji, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js";
import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
import { backdropLink, celebrationsEmoji, checkbox, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher } from "./elements.js";
import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js";
import emoji from "../emoji.js";
@ -11,7 +11,7 @@ let enabledServices = Object.keys(s).filter((p) => {
if (s[p].enabled) return true;
}).sort().map((p) => {
return `<br>&bull; ${s[p].alias ? s[p].alias : p}`
}).join('').substring(4)
}).join(';').substring(4)
let donate = ``
let donateLinks = ``
@ -33,10 +33,6 @@ export default function(obj) {
let ua = obj.useragent.toLowerCase();
let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os");
let platform = isMobile ? "m" : "p";
if (isMobile && isIOS) platform = "i";
audioFormats[0]["text"] = t('SettingsAudioFormatBest');
try {
@ -51,7 +47,7 @@ export default function(obj) {
<meta property="og:url" content="${process.env.selfURL}" />
<meta property="og:title" content="${appName}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
<meta property="og:image" content="icons/generic.png" />
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" />
<meta name="description" content="${t('AboutSummary')}" />
<meta name="theme-color" content="#000000" />
@ -68,7 +64,7 @@ export default function(obj) {
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
</head>
<body id="cobalt-body" ${platform === "p" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
<body id="cobalt-body" data-nosnippet>
${multiPagePopup({
name: "about",
closeAria: t('AccessibilityClosePopup'),
@ -83,30 +79,21 @@ export default function(obj) {
url: authorInfo.link
},
closeAria: t('AccessibilityClosePopup'),
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
title: t('TitlePopupAbout')
},
body: [{
text: t('AboutSummary')
}, {
text: collapsibleList([{
"name": "services",
"title": t("CollapseServices"),
"body": `${enabledServices}<br/><br/>${t("ServicesNote")}`
}, {
"name": "support",
"title": t("CollapseSupport"),
"body": `${t("FollowSupport")}<br/>
${socialLink(emoji("🐘"), "mastodon", authorInfo.support.mastodon.handle, authorInfo.support.mastodon.url)}
${socialLink(emoji("🐦"), "twitter", authorInfo.support.twitter.handle, authorInfo.support.twitter.url)}<br/>
${t("SourceCode")}<br/>
${socialLink(emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo)}<br/>
${t("SupportNote")}`
}, {
"name": "privacy",
"title": t("CollapsePrivacy"),
"body": t("PrivacyPolicy")
}])
+ `${process.env.DEPLOYMENT_ID && process.env.INTERNAL_IP ? '<a id="hop-attribution" class="explanation" href="https://hop.io/" target="_blank">powered by hop.io</a>' : ''}`
text: `${t('AboutSupportedServices')}`,
nopadding: true
}, {
text: `<div class="bullpadding">${enabledServices}.</div>`
}, {
text: obj.lang !== "ru" ? t('FollowTwitter') : "",
classes: ["desc-padding"]
}, {
text: backdropLink(repo, t('LinkGitHubIssues')),
classes: ["bottom-link"]
}]
})
}, {
@ -191,7 +178,7 @@ export default function(obj) {
closeAria: t('AccessibilityClosePopup'),
header: {
aboveTitle: {
text: `v.${version}-${obj.hash}${platform} (${obj.branch})`,
text: `v.${version}-${obj.hash}`,
url: `${repo}/commit/${obj.hash}`
},
title: `${emoji("⚙️", 30)} ${t('TitlePopupSettings')}`
@ -201,67 +188,43 @@ export default function(obj) {
title: `${emoji("🎬")} ${t('SettingsVideoTab')}`,
content: settingsCategory({
name: "downloads",
title: t('SettingsQualitySubtitle'),
title: t('SettingsVideoGeneral'),
body: switcher({
name: "vQuality",
subtitle: t('SettingsQualitySubtitle'),
explanation: t('SettingsQualityDescription'),
items: [{
"action": "max",
"text": "4320p+"
"text": `${t('SettingsQualitySwitchMax')}<br/>(2160p+)`
}, {
"action": "2160",
"text": "2160p"
"action": "hig",
"text": `${t('SettingsQualitySwitchHigh')}<br/>(${quality.hig}p)`
}, {
"action": "1440",
"text": "1440p"
"action": "mid",
"text": `${t('SettingsQualitySwitchMedium')}<br/>(${quality.mid}p)`
}, {
"action": "1080",
"text": "1080p"
}, {
"action": "720",
"text": "720p"
}, {
"action": "480",
"text": "480p"
}, {
"action": "360",
"text": "360p"
"action": "low",
"text": `${t('SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
}]
})
})
+ settingsCategory({
name: "tiktok",
title: "tiktok & douyin",
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3)
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'))
})
+ settingsCategory({
name: t('SettingsCodecSubtitle'),
name: "youtube",
body: switcher({
name: "vCodec",
explanation: t('SettingsCodecDescription'),
name: "vFormat",
subtitle: t('SettingsFormatSubtitle'),
explanation: t('SettingsFormatDescription'),
items: [{
"action": "h264",
"text": "h264 (mp4)"
"action": "mp4",
"text": "mp4 (av1)"
}, {
"action": "av1",
"text": "av1 (mp4)"
}, {
"action": "vp9",
"text": "vp9 (webm)"
}]
})
})
+ settingsCategory({
name: t('SettingsVimeoPrefer'),
body: switcher({
name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'),
items: [{
"action": "false",
"text": "progressive"
}, {
"action": "true",
"text": "dash"
"action": "webm",
"text": "webm (vp9)"
}]
})
})
@ -270,32 +233,18 @@ export default function(obj) {
title: `${emoji("🎶")} ${t('SettingsAudioTab')}`,
content: settingsCategory({
name: "general",
title: t('SettingsFormatSubtitle'),
body:
switcher({
name: "aFormat",
explanation: t('SettingsAudioFormatDescription'),
items: audioFormats
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
}) + settingsCategory({
name: "dub",
title: t("SettingsAudioDub"),
body: switcher({
name: "dubLang",
explanation: t('SettingsAudioDubDescription'),
items: [{
"action": "original",
"text": t('SettingsDubDefault')
}, {
"action": "auto",
"text": t('SettingsDubAuto')
}]
})
}) + settingsCategory({
name: "tiktok",
title: "tiktok & douyin",
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription'))
})
title: t('SettingsAudioTab'),
body: switcher({
name: "aFormat",
subtitle: t('SettingsFormatSubtitle'),
explanation: t('SettingsAudioFormatDescription'),
items: audioFormats
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
}) + settingsCategory({
name: "tiktok",
title: "tiktok & douyin",
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), t('SettingsAudioFullTikTok'), 3) + `<div class="explanation">${t('SettingsAudioFullTikTokDescription')}</div>`
})
}, {
name: "other",
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
@ -315,11 +264,11 @@ export default function(obj) {
"action": "light",
"text": t('SettingsThemeLight')
}]
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), 4, t('AccessibilityKeepDownloadButton'))
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), t('AccessibilityKeepDownloadButton'), 2)
}) + settingsCategory({
name: "miscellaneous",
title: t('Miscellaneous'),
body: checkbox("disableChangelog", t('SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", t('SettingsEnableDownloadPopup'), 1, t('AccessibilityEnableDownloadPopup')) : ''}`
body: checkbox("disableChangelog", t('SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", t('SettingsEnableDownloadPopup'), t('AccessibilityEnableDownloadPopup'), 1) : ''}`
})
}],
})}
@ -334,8 +283,7 @@ export default function(obj) {
name: "download",
subtitle: t('DownloadPopupWayToSave'),
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
items: `<a id="pd-download" class="switch full" target="_blank" href="/">${t('Download')}</a>
<div id="pd-share" class="switch full">${t('ShareURL')}</div>
items: `<a id="pd-download" class="switch full space-right" target="_blank" href="/">${t('Download')}</a>
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
})
})}
@ -353,37 +301,27 @@ export default function(obj) {
name: "error",
standalone: true,
buttonOnly: true,
emoji: emoji("☹️", 48, 1),
classes: ["small"],
buttonText: t('ErrorPopupCloseButton'),
header: {
closeAria: t('AccessibilityClosePopup'),
title: t('TitlePopupError'),
emoji: emoji("☹️", 64, 1),
title: t('TitlePopupError')
},
body: `<div id="desc-error" class="desc-padding subtext"></div>`
})}
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
<div id="cobalt-main-box" class="center" style="visibility: hidden;">
<div id="logo">${appName}</div>
<div id="download-area">
<div id="logo-area">${appName}</div>
<div id="download-area" class="mobile-center">
<div id="top">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
</div>
<div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
${switcher({
name: "audioMode",
noParent: true,
items: [{
"action": "false",
"text": `${emoji("✨")} ${t("ModeToggleAuto")}`
}, {
"action": "true",
"text": `${emoji("🎶")} ${t("ModeToggleAudio")}`
}]
})}
<button id="pasteFromClipboard" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
<button id="audioMode" class="switch" onclick="toggle('audioMode')" aria-label="${t('AccessibilityModeToggle')}">${emoji("✨", 22, 1)}</button>
</div>
</div>
</div>
@ -413,6 +351,8 @@ export default function(obj) {
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
collapseHistory: ` + "`" + t('ChangelogPressToHide') + "`" + `,
toggleDefault: '${emoji("✨")} ${t("ModeToggleAuto")}',
toggleAudio: '${emoji("🎶")} ${t("ModeToggleAudio")}',
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,

View file

@ -1,61 +1,67 @@
import { apiJSON } from "../sub/utils.js";
import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js";
import { errorUnsupported, genericError } from "../sub/errors.js";
import loc from "../../localization/manager.js";
import { testers } from "./servicesPatternTesters.js";
import matchActionDecider from "./matchActionDecider.js";
import bilibili from "./services/bilibili.js";
import reddit from "./services/reddit.js";
import twitter from "./services/twitter.js";
import youtube from "./services/youtube.js";
import vk from "./services/vk.js";
import tiktok from "./services/tiktok.js";
import tumblr from "./services/tumblr.js";
import vimeo from "./services/vimeo.js";
import soundcloud from "./services/soundcloud.js";
import bilibili from "../services/bilibili.js";
import reddit from "../services/reddit.js";
import twitter from "../services/twitter.js";
import youtube from "../services/youtube.js";
import vk from "../services/vk.js";
import tiktok from "../services/tiktok.js";
import tumblr from "../services/tumblr.js";
import matchActionDecider from "./matchActionDecider.js";
import vimeo from "../services/vimeo.js";
import soundcloud from "../services/soundcloud.js";
export default async function (host, patternMatch, url, lang, obj) {
try {
let r, isAudioOnly = !!obj.isAudioOnly;
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) });
if (!(testers[host](patternMatch))) throw Error();
let r;
switch (host) {
case "twitter":
r = await twitter({
id: patternMatch["id"] ? patternMatch["id"] : false,
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false,
lang: lang
});
if (r.isAudioOnly) obj.isAudioOnly = true
break;
case "vk":
r = await vk({
url: url,
userId: patternMatch["userId"],
videoId: patternMatch["videoId"],
quality: obj.vQuality
lang: lang, quality: obj.vQuality
});
break;
case "bilibili":
r = await bilibili({
id: patternMatch["id"].slice(0, 12)
id: patternMatch["id"].slice(0, 12),
lang: lang
});
break;
case "youtube":
let fetchInfo = {
id: patternMatch["id"].slice(0, 11),
quality: obj.vQuality,
format: obj.vCodec,
isAudioOnly: isAudioOnly,
isAudioMuted: obj.isAudioMuted,
dubLang: obj.dubLang
}
if (url.match('music.youtube.com') || isAudioOnly === true) {
fetchInfo.quality = "max";
fetchInfo.format = "vp9";
fetchInfo.isAudioOnly = true
lang: lang, quality: obj.vQuality,
format: "webm"
};
if (url.match('music.youtube.com') || obj.isAudioOnly == true) obj.vFormat = "audio";
switch (obj.vFormat) {
case "mp4":
fetchInfo["format"] = "mp4";
break;
case "audio":
fetchInfo["format"] = "webm";
fetchInfo["isAudioOnly"] = true;
fetchInfo["quality"] = "max";
obj.isAudioOnly = true;
break;
}
r = await youtube(fetchInfo);
break;
@ -63,7 +69,7 @@ export default async function (host, patternMatch, url, lang, obj) {
r = await reddit({
sub: patternMatch["sub"],
id: patternMatch["id"],
title: patternMatch["title"]
title: patternMatch["title"], lang: lang,
});
break;
case "douyin":
@ -71,47 +77,40 @@ export default async function (host, patternMatch, url, lang, obj) {
r = await tiktok({
host: host,
postId: patternMatch["postId"],
id: patternMatch["id"],
noWatermark: obj.isNoTTWatermark,
fullAudio: obj.isTTFullAudio,
isAudioOnly: isAudioOnly
id: patternMatch["id"], lang: lang,
noWatermark: obj.isNoTTWatermark, fullAudio: obj.isTTFullAudio,
isAudioOnly: obj.isAudioOnly
});
if (r.isAudioOnly) obj.isAudioOnly = true;
break;
case "tumblr":
r = await tumblr({
id: patternMatch["id"],
url: url,
user: patternMatch["user"] ? patternMatch["user"] : false
id: patternMatch["id"], url: url, user: patternMatch["user"] ? patternMatch["user"] : false,
lang: lang
});
break;
case "vimeo":
r = await vimeo({
id: patternMatch["id"].slice(0, 11),
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash
id: patternMatch["id"].slice(0, 11), quality: obj.vQuality,
lang: lang
});
break;
case "soundcloud":
isAudioOnly = true;
obj.isAudioOnly = true;
r = await soundcloud({
author: patternMatch["author"],
song: patternMatch["song"], url: url,
author: patternMatch["author"], song: patternMatch["song"], url: url,
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false,
accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false,
format: obj.aFormat
format: obj.aFormat,
lang: lang
});
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}
if (r.isAudioOnly) isAudioOnly = true;
let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted;
if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) });
return matchActionDecider(r, host, obj.ip, obj.aFormat, isAudioOnly, lang, isAudioMuted);
return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang, obj.isAudioMuted) : apiJSON(0, {
t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error)
});
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View file

@ -1,144 +1,121 @@
import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js";
import { audioIgnore, services, supportedAudio } from "../config.js"
import { apiJSON } from "../sub/utils.js"
import loc from "../../localization/manager.js";
export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMuted) {
let action,
responseType = 2,
defaultParams = {
u: r.urls,
if (!isAudioOnly && !r.picker && !isAudioMuted) {
switch (host) {
case "twitter":
return apiJSON(1, { u: r.urls });
case "vk":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "bilibili":
return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time
});
case "youtube":
return apiJSON(2, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time,
});
case "reddit":
return apiJSON(r.typeId, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tiktok":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "douyin":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tumblr":
return apiJSON(1, { u: r.urls });
case "vimeo":
if (Array.isArray(r.urls)) {
return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename
});
} else {
return apiJSON(1, { u: r.urls });
}
}
} else if (isAudioMuted && !isAudioOnly) {
let isSplit = Array.isArray(r.urls);
return apiJSON(2, {
type: isSplit ? "bridge" : "mute",
u: isSplit ? r.urls[0] : r.urls,
service: host,
ip: ip,
filename: r.filename,
},
params = {}
if (!isAudioOnly && !r.picker && !isAudioMuted) action = "video";
if (r.isM3U8) action = "singleM3U8";
if (isAudioOnly && !r.picker) action = "audio";
if (r.picker) action = "picker";
if (isAudioMuted) action = "muteVideo";
if (action === "picker" || action === "audio") {
defaultParams.filename = r.audioFilename;
defaultParams.isAudioOnly = true;
defaultParams.audioFormat = audioFormat;
}
switch (action) {
case "video":
switch (host) {
case "bilibili":
params = { type: "render", time: r.time };
break;
case "youtube":
params = { type: r.type, time: r.time };
break;
case "reddit":
responseType = r.typeId;
params = { type: r.type };
break;
case "vimeo":
if (Array.isArray(r.urls)) {
params = { type: "render" }
} else {
responseType = 1;
}
break;
case "vk":
case "douyin":
case "tiktok":
params = { type: "bridge" };
break;
case "tumblr":
case "twitter":
responseType = 1;
break;
}
break;
case "singleM3U8":
params = { type: "videoM3U8" }
break;
case "muteVideo":
params = {
type: Array.isArray(r.urls) ? "bridge" : "mute",
u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
mute: true
}
break;
case "picker":
responseType = 5;
switch (host) {
case "twitter":
params = { picker: r.picker };
break;
case "douyin":
case "tiktok":
let pickerType = "render";
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3";
pickerType = "bridge"
}
params = {
type: pickerType,
picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls,
copy: audioFormat === "best" ? true : false
}
}
break;
case "audio":
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
let processType = "render";
let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host === "tiktok" || host === "douyin") && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) {
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3";
processType = "bridge"
}
} else if (audioFormat === "best") {
audioFormat = "m4a";
processType = "bridge"
}
}
if ((audioFormat === "best" && services[host]["bestAudio"])
|| services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"];
processType = "bridge"
} else if (audioFormat === "best") {
audioFormat = "m4a";
copy = true;
if (r.audioFilename.includes("twitterspaces")) {
mute: true,
});
} else if (r.picker) {
switch (host) {
case "douyin":
case "tiktok":
let type = "render";
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
copy = false
type = "bridge"
}
return apiJSON(5, {
type: type,
picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
})
case "twitter":
return apiJSON(5, {
picker: r.picker, service: host
})
}
} else if (isAudioOnly) {
if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
let type = "render";
let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host == "tiktok" || host == "douyin") && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) {
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
type = "bridge"
}
} else if (audioFormat === "best") {
audioFormat = "m4a"
type = "bridge"
}
if (r.isM3U8 || host === "vimeo") {
copy = false;
processType = "render"
}
if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"]
type = "bridge"
} else if (audioFormat === "best") {
audioFormat = "m4a"
copy = true
if (r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
params = {
type: processType,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls,
audioFormat: audioFormat,
copy: copy,
fileMetadata: r.fileMetadata ? r.fileMetadata : false
}
break;
default:
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
}
return apiJSON(2, {
type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true,
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
})
} else {
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
}
return apiJSON(responseType, {...defaultParams, ...params})
}

View file

@ -1,28 +0,0 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js";
// TO-DO: quality picking, bilibili.tv support, and higher quality downloads (currently requires an account)
export default async function(obj) {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
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]);
if (streamData.data.timelength > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
return {
urls: [video[0]["baseUrl"], audio[0]["baseUrl"]],
time: streamData.data.timelength,
audioFilename: `bilibili_${obj.id}_audio`,
filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4`
};
}

View file

@ -1,28 +0,0 @@
import { maxVideoDuration } from "../../config.js";
export default async function(obj) {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false });
if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"];
if (data.url.endsWith('.gif')) return { typeId: 1, urls: data.url };
if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' };
if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
await fetch(audio, { method: "HEAD" }).then((r) => {if (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''});
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3];
if (!audio.length > 0) return { typeId: 1, urls: video };
return {
typeId: 2,
type: "render",
urls: [video, audio],
audioFilename: `reddit_${id}_audio`,
filename: `reddit_${id}.mp4`
};
}

View file

@ -1,74 +0,0 @@
import { maxVideoDuration } from "../../config.js";
let cachedID = {};
async function findClientID() {
try {
let sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false });
let scVersion = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
if (cachedID.version === scVersion) return cachedID.id;
let scripts = sc.matchAll(/<script.+src="(.+)">/g);
let clientid;
for (let script of scripts) {
let url = script[1];
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then((r) => {return r.text()}).catch(() => { return false });
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') {
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
break;
}
}
cachedID.version = scVersion;
cachedID.id = clientid;
return clientid;
} catch (e) {
return false;
}
}
export default async function(obj) {
let html;
if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`).then((r) => { return r.text() }).catch(() => { return false });
}
if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false });
}
if (!html) return { error: 'ErrorCouldntFetch'};
if (!(html.includes('<script>window.__sc_hydration = ')
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
&& html.includes('{"hydratable":"sound","data":'))) {
return { error: ['ErrorBrokenLink', 'soundcloud'] }
}
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
let clientId = await findClientID();
if (!clientId) return { error: 'ErrorSoundCloudNoClientId' };
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive"),
fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
if (json.duration > maxVideoDuration) return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
if (!file) return { error: 'ErrorCouldntFetch' };
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: json.title,
artist: json.user.username,
}
}
}

View file

@ -1,112 +0,0 @@
import { genericUserAgent } from "../../config.js";
const userAgent = genericUserAgent.split(' Chrome/1')[0],
config = {
tiktok: {
short: "https://vt.tiktok.com/",
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US"
},
douyin: {
short: "https://v.douyin.com/",
api: "https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id={postId}"
}
}
function selector(j, h, id) {
if (!j) return false;
let t;
switch (h) {
case "tiktok":
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] === id) return true })[0];
break;
case "douyin":
t = j['aweme_detail'];
break;
}
if (t.length < 3) return false;
return t;
}
export default async function(obj) {
let postId = obj.postId ? obj.postId : false;
if (!postId) {
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual",
headers: { "user-agent": userAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
postId = html.split('/video/')[1].split('?')[0].replace("/", '')
} else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) {
postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
}
}
if (!postId) return { error: 'ErrorCantGetID' };
let detail;
detail = await fetch(config[obj.host]["api"].replace("{postId}", postId), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then((r) => { return r.json() }).catch(() => { return false });
detail = selector(detail, obj.host, postId);
if (!detail) return { error: 'ErrorCouldntFetch' };
let video, videoFilename, audioFilename, isMp3, audio, images, filenameBase = `${obj.host}_${postId}`;
if (obj.host === "tiktok") {
images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false
} else {
images = detail["images"] ? detail["images"] : false
}
if (!obj.isAudioOnly && !images) {
video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][2].replace("/play/", "/playwm/");
videoFilename = `${filenameBase}_video.mp4`;
if (obj.noWatermark) {
video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0];
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
}
} else {
let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
audio = fallback;
audioFilename = `${filenameBase}_audio_fv`; // fv - from video
if (obj.fullAudio || fallback.includes("music")) {
audio = detail["music"]["play_url"]["url_list"][0]
audioFilename = `${filenameBase}_audio`
}
if (audio.slice(-4) === ".mp3") isMp3 = true;
}
if (video) return {
urls: video,
filename: videoFilename
}
if (images && obj.isAudioOnly) return {
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3
}
if (images) {
let imageLinks = [];
for (let i in images) {
let sel = obj.host === "tiktok" ? images[i]["display_image"]["url_list"] : images[i]["url_list"];
sel = sel.filter((p) => { if (p.includes(".jpeg?")) return true; })
imageLinks.push({url: sel[0]})
}
return {
picker: imageLinks,
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3
}
}
if (audio) return {
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3
}
}

View file

@ -1,14 +0,0 @@
import { genericUserAgent } from "../../config.js";
export default async function(obj) {
let html = await fetch(`https://${
obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '')
}.tumblr.com/post/${obj.id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) return { error: 'ErrorEmptyDownload' };
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }
}

View file

@ -1,108 +0,0 @@
import { genericUserAgent } from "../../config.js";
function bestQuality(arr) {
return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
}
const apiURL = "https://api.twitter.com/1.1"
// TO-DO: move from 1.1 api to graphql
export default async function(obj) {
let _headers = {
"user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
// ^ no explicit content, but with multi media support
"host": "api.twitter.com"
};
let req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`;
if (!obj.spaceId) {
let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
if (!req_status) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
// ^ explicit content, but no multi media support
delete _headers["x-guest-token"]
req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
}
if (!req_status) return { error: 'ErrorCouldntFetch' };
let baseStatus;
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
baseStatus = req_status["extended_entities"]
} else if (req_status["retweeted_status"] && req_status["retweeted_status"]["extended_entities"] && req_status["retweeted_status"]["extended_entities"]["media"]) {
baseStatus = req_status["retweeted_status"]["extended_entities"]
}
if (!baseStatus) return { error: 'ErrorNoVideosInTweet' };
let single, multiple = [], media = baseStatus["media"];
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
if (media.length > 1) {
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
} else if (media.length === 1) {
single = bestQuality(media[0]["video_info"]["variants"])
} else {
return { error: 'ErrorNoVideosInTweet' }
}
if (single) {
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) {
return { picker: multiple }
} else {
return { error: 'ErrorNoVideosInTweet' }
}
} else {
_headers["host"] = "twitter.com";
_headers["content-type"] = "application/json";
let query = {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withReplays":true},
features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"responsive_web_twitter_blue_verified_badge_is_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":false,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"longform_notetweets_richtext_consumption_enabled":false,"responsive_web_enhance_cards_enabled":false}
}
query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1);
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
query = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById?variables=${query.variables}&features=${query.features}`;
let AudioSpaceById = await fetch(query, { headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false });
if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' };
if (!AudioSpaceById.data.audioSpace.metadata) return { error: 'ErrorEmptyDownload' };
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay !== true) return { error: 'TwitterSpaceWasntRecorded' };
let streamStatus = await fetch(
`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }
).then((r) =>{ return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers,
listOfParticipants = `Twitter Space speakers: `;
for (let i in participants) { listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` }
listOfParticipants = listOfParticipants.slice(0, -2);
return {
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`,
isAudioOnly: true,
fileMetadata: {
title: AudioSpaceById.data.audioSpace.metadata.title,
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`,
comment: listOfParticipants,
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
}
}
}
}

View file

@ -1,85 +0,0 @@
import { maxVideoDuration } from "../../config.js";
const resolutionMatch = {
"3840": "2160",
"2732": "1440",
"2048": "1080",
"1920": "1080",
"1366": "720",
"1280": "720",
"960": "480",
"640": "360",
"426": "240"
}
// ^ vimeo you're fucked in the head for this ^
const qualityMatch = {
"2160": "4K",
"1440": "2K",
"480": "540",
"4K": "2160",
"2K": "1440",
"540": "480"
}
export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
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 });
if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "dash";
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
if (downloadType !== "dash") {
if (qualityMatch[quality]) quality = qualityMatch[quality];
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0];
let bestQuality = all[0]["quality"].split('p')[0];
bestQuality = qualityMatch[bestQuality] ? qualityMatch[bestQuality] : bestQuality;
if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality);
if (!best) return { error: 'ErrorEmptyDownload' };
return { urls: best["url"], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4` }
}
if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then((r) => { return r.json() }).catch(() => { return false });
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let type = "parcel";
if (masterJSON.base_url === "../") type = "chop";
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)),
bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality);
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
switch (type) {
case "parcel":
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a) => { if (a['mime_type'] === "audio/mp4") return true }),
bestAudio = masterJSON_Audio[0];
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
break;
case "chop":
videoUrl = `${baseUrl}/sep/video/${bestVideo.id}/master.m3u8`;
break;
}
if (videoUrl) {
return {
urls: audioUrl ? [videoUrl, audioUrl] : videoUrl,
isM3U8: audioUrl ? false : true,
audioFilename: `vimeo_${obj.id}_audio`,
filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4`
}
}
return { error: 'ErrorEmptyDownload' }
}

View file

@ -1,49 +0,0 @@
import { xml2json } from "xml-js";
import { genericUserAgent, maxVideoDuration } from "../../config.js";
const representationMatch = {
"2160": 7,
"1440": 6,
"1080": 5,
"720": 4,
"480": 3,
"360": 2,
"240": 1,
"144": 0
}, resolutionMatch = {
"3840": "2160",
"2560": "1440",
"1920": "1080",
"1280": "720",
"852": "480",
"640": "360",
"426": "240",
// "256": "144"
}
export default async function(o) {
let html;
html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
let quality = o.quality === "max" ? 7 : representationMatch[o.quality],
js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' };
if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })),
repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"],
bestQuality = repr[repr.length - 1],
resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height';
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
if (bestQuality) return {
urls: js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`],
filename: `vk_${o.userId}_${o.videoId}_${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4`
};
return { error: 'ErrorEmptyDownload' }
}

View file

@ -1,97 +0,0 @@
import { Innertube } from 'youtubei.js';
import { maxVideoDuration } from '../../config.js';
const yt = await Innertube.create();
const c = {
h264: {
codec: "avc1",
aCodec: "mp4a",
container: "mp4"
},
av1: {
codec: "av01",
aCodec: "mp4a",
container: "mp4"
},
vp9: {
codec: "vp9",
aCodec: "opus",
container: "webm"
}
}
export default async function(o) {
let info, isDubbed, quality = o.quality === "max" ? "9000" : o.quality; //set quality 9000(p) to be interpreted as max
try {
info = await yt.getBasicInfo(o.id, 'ANDROID');
} catch (e) {
return { error: 'ErrorCantConnectToServiceAPI' };
}
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' };
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter((e) => {
if (e["mime_type"].includes(c[o.format].codec) || e["mime_type"].includes(c[o.format].aCodec)) return true
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
bestQuality = adaptive_formats.find(i => i["has_video"]);
hasAudio = adaptive_formats.find(i => i["has_audio"]);
if (bestQuality) bestQuality = bestQuality['quality_label'].split('p')[0];
if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' };
if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let checkBestAudio = (i) => (i["has_audio"] && !i["has_video"]),
audio = adaptive_formats.find(i => checkBestAudio(i) && i["is_original"]);
if (o.dubLang) {
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
if (dubbedAudio) {
audio = dubbedAudio;
isDubbed = true
}
}
if (hasAudio && o.isAudioOnly) {
let r = {
type: "render",
isAudioOnly: true,
urls: audio.url,
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
fileMetadata: {
title: info.basic_info.title,
artist: info.basic_info.author.replace("- Topic", "").trim(),
}
};
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
let descItems = info.basic_info.short_description.split("\n\n")
r.fileMetadata.album = descItems[2]
r.fileMetadata.copyright = descItems[3]
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
};
return r
}
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
checkBestVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === bestQuality),
checkRightVideo = (i) => (i["has_video"] && !i["has_audio"] && i['quality_label'].split('p')[0] === quality);
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
let single = info.streaming_data.formats.find(i => checkSingle(i));
if (single) return {
type: "bridge",
urls: single.url,
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`
}
};
let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i)));
if (video && audio) return {
type: "render",
urls: [video.url, audio.url],
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`
};
return { error: 'ErrorYTTryOtherCodec' }
}

View file

@ -2,29 +2,60 @@
"audioIgnore": ["vk"],
"config": {
"bilibili": {
"alias": "bilibili (.com only)",
"alias": "bilibili.com",
"patterns": ["video/:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"enabled": true
},
"reddit": {
"alias": "reddit videos & gifs",
"patterns": ["r/:sub/comments/:id/:title"],
"enabled": true
},
"twitter": {
"alias": "twitter posts & spaces & voice",
"alias": "twitter posts & spaces",
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
"enabled": true
},
"vk": {
"alias": "vk video & clips",
"patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"],
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
"quality_match": {
"2160": 7,
"1440": 6,
"1080": 5,
"720": 3,
"480": 2,
"360": 1,
"240": 0,
"144": 4
},
"representation_match": {
"2160": 7,
"1440": 6,
"1080": 5,
"720": 4,
"480": 3,
"360": 2,
"240": 1,
"144": 0
},
"quality": {
"1080": "hig",
"720": "mid",
"480": "low"
},
"enabled": true
},
"youtube": {
"alias": "youtube videos & shorts & music",
"patterns": ["watch?v=:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"bestAudio": "opus",
"quality": {
"1080": "hig",
"720": "mid",
"480": "low"
},
"enabled": true
},
"tumblr": {
@ -32,20 +63,25 @@
"enabled": true
},
"tiktok": {
"alias": "tiktok videos & photos & audio",
"alias": "tiktok videos & slideshow & audio",
"patterns": [":user/video/:postId", ":id", "t/:id"],
"audioFormats": ["best", "m4a", "mp3"],
"enabled": true
},
"douyin": {
"alias": "douyin videos & audio",
"alias": "douyin videos & slideshow & audio",
"patterns": ["video/:postId", ":id"],
"enabled": false
"enabled": true
},
"vimeo": {
"patterns": [":id"],
"enabled": true,
"bestAudio": "mp3"
"resolutionMatch": {
"3840": "2160",
"1920": "1080",
"1280": "720",
"960": "480"
},
"enabled": true
},
"soundcloud": {
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],

View file

@ -1,28 +1,27 @@
export const testers = {
"twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20)
|| (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13),
"twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20) || (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13),
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"]
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] &&
patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12),
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11),
"reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"]
&& patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96),
"reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] &&
patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96),
"tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21)
|| (patternMatch["id"] && patternMatch["id"].length <= 13)),
"tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) ||
(patternMatch["id"] && patternMatch["id"].length <= 13)),
"douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21)
|| (patternMatch["id"] && patternMatch["id"].length <= 13)),
"douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) ||
(patternMatch["id"] && patternMatch["id"].length <= 13)),
"tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21)
|| (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
"tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) ||
(patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)),
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"]
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32))
}
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"] && (patternMatch["author"].length + patternMatch["song"].length) <= 96) ||
(patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32))
};

View file

@ -0,0 +1,29 @@
import { genericUserAgent, maxVideoDuration } from "../config.js";
export default async function(obj) {
try {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength <= maxVideoDuration) {
let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
return { urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], time: streamData.data.timelength, audioFilename: `bilibili_${obj.id}_audio`, filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` };
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorEmptyDownload' };
}
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,27 @@
import { maxVideoDuration } from "../config.js";
export default async function(obj) {
try {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => {return r.json()}).catch(() => {return false});
if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"];
if ("reddit_video" in data["secure_media"] && data["secure_media"]["reddit_video"]["duration"] * 1000 < maxVideoDuration) {
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''});
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]
if (audio.length > 0) {
return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` };
} else {
return { typeId: 1, urls: video };
}
} else {
return { error: 'ErrorEmptyDownload' };
}
} catch (err) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,79 @@
import { genericUserAgent, maxAudioDuration } from "../config.js";
let cachedID = {}
async function findClientID() {
try {
let sc = await fetch('https://soundcloud.com/').then((r) => {return r.text()}).catch(() => {return false});
let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
if (cachedID.version == sc_version) {
return cachedID.id
} else {
let scripts = sc.matchAll(/<script.+src="(.+)">/g);
let clientid;
for (let script of scripts) {
let url = script[1];
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then((r) => {return r.text()}).catch(() => {return false});
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') {
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
break;
}
}
cachedID.version = sc_version;
cachedID.id = clientid;
return clientid;
}
} catch (e) {
return false;
}
}
export default async function(obj) {
try {
let html;
if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false});
}
if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`, {
headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false});
}
if (!html) return { error: 'ErrorCouldntFetch'};
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) {
let clientId = await findClientID();
if (clientId) {
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if (json.duration < maxAudioDuration) {
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
if (!file) return { error: 'ErrorCouldntFetch' };
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: json.title,
artist: json.user.username,
}
}
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
}
} else return { error: 'ErrorSoundCloudNoClientId' }
} else return { error: 'ErrorEmptyDownload' }
} else return { error: ['ErrorBrokenLink', 'soundcloud'] }
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,116 @@
import { genericUserAgent } from "../config.js";
let userAgent = genericUserAgent.split(' Chrome/1')[0]
let config = {
tiktok: {
short: "https://vt.tiktok.com/",
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US",
},
douyin: {
short: "https://v.douyin.com/",
api: "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={postId}",
}
}
function selector(j, h, id) {
if (j) {
let t;
switch (h) {
case "tiktok":
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
break;
case "douyin":
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
break;
}
if (t.length > 0) { return t[0] } else return false
} else return false
}
export default async function(obj) {
try {
if (!obj.postId) {
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual",
headers: { "user-agent": userAgent }
}).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
obj.postId = html.split('/video/')[1].split('?')[0].replace("/", '')
} else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) {
obj.postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
}
}
if (!obj.postId) return { error: 'ErrorCantGetID' };
let detail;
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then((r) => {return r.json()}).catch(() => {return false});
detail = selector(detail, obj.host, obj.postId);
if (!detail) return { error: 'ErrorCouldntFetch' }
let video, videoFilename, audioFilename, isMp3, audio, images,
filenameBase = `${obj.host}_${obj.postId}`;
if (obj.host == "tiktok") {
images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false
} else {
images = detail["images"] ? detail["images"] : false
}
if (!obj.isAudioOnly && !images) {
video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play");
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
if (!obj.noWatermark) {
video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0]
videoFilename = `${filenameBase}_video.mp4`
}
} else {
let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
if (obj.fullAudio || fallback.includes("music")) {
audio = detail["music"]["play_url"]["url_list"][0]
audioFilename = `${filenameBase}_audio`
} else {
audio = fallback
audioFilename = `${filenameBase}_audio_fv` // fv - from video
}
if (audio.slice(-4) === ".mp3") isMp3 = true;
}
if (video) return {
urls: video,
filename: videoFilename
}
if (images && obj.isAudioOnly) {
return {
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3,
}
}
if (images) {
let imageLinks = [];
for (let i in images) {
let sel = obj.host == "tiktok" ? images[i]["display_image"]["url_list"] : images[i]["url_list"];
sel = sel.filter((p) => { if (p.includes(".jpeg?")) return true; })
imageLinks.push({url: sel[0]})
}
return {
picker: imageLinks,
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3,
}
}
if (audio) return {
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3,
}
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,16 @@
import { genericUserAgent } from "../config.js";
export default async function(obj) {
try {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }
} else return { error: 'ErrorEmptyDownload' }
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,102 @@
import { genericUserAgent } from "../config.js";
function bestQuality(arr) {
return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
}
const apiURL = "https://api.twitter.com/1.1"
export default async function(obj) {
try {
let _headers = {
"user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
"host": "api.twitter.com"
};
let req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`
if (!obj.spaceId) {
let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false});
if (!req_status) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
delete _headers["x-guest-token"]
req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
}
if (!req_status) return { error: 'ErrorCouldntFetch' }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
let single, multiple = [], media = req_status["extended_entities"]["media"];
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
if (media.length > 1) {
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
} else if (media.length > 0) {
single = bestQuality(media[0]["video_info"]["variants"])
} else {
return { error: 'ErrorNoVideosInTweet' }
}
if (single) {
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) {
return { picker: multiple }
} else {
return { error: 'ErrorNoVideosInTweet' }
}
} else {
return { error: 'ErrorNoVideosInTweet' }
}
} else {
_headers["host"] = "twitter.com"
_headers["content-type"] = "application/json"
let query = {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true}
}
let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then((r) => {
return r.status == 200 ? r.json() : false;
}).catch((e) => {return false});
if (AudioSpaceById) {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then((r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;});
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers
let listOfParticipants = `Twitter Space speakers: `
for (let i in participants) {
listOfParticipants += `@${participants[i]["twitter_screen_name"]}, `
}
listOfParticipants = listOfParticipants.slice(0, -2);
return {
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`,
isAudioOnly: true,
fileMetadata: {
title: AudioSpaceById.data.audioSpace.metadata.title,
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`,
comment: listOfParticipants,
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
}
}
} else {
return { error: 'TwitterSpaceWasntRecorded' };
}
} else {
return { error: 'ErrorEmptyDownload' }
}
}
} catch (err) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,80 @@
import { quality, services } from "../config.js";
export default async function(obj) {
try {
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false});
if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "";
if (JSON.stringify(api).includes('"progressive":[{')) {
downloadType = "progressive";
} else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash";
switch(downloadType) {
case "progressive":
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0]
try {
if (obj.quality != "max") {
let pref = parseInt(quality[obj.quality], 10)
for (let i in all) {
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)
if (currQuality < pref) {
break;
} else if (currQuality == pref) {
best = all[i]
}
}
}
} catch (e) {
best = all[0]
}
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
case "dash":
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false});
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (masterJSON.video) {
let type = "";
if (masterJSON.base_url.includes("parcel")) {
type = "parcel"
} else if (masterJSON.base_url == "../") {
type = "chop"
}
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;});
let bestVideo = masterJSON_Video[0]
let bestAudio = masterJSON_Audio[0]
switch (type) {
case "parcel":
if (obj.quality != "max") {
let pref = parseInt(quality[obj.quality], 10)
for (let i in masterJSON_Video) {
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
if (currQuality < pref) {
break;
} else if (currQuality == pref) {
bestVideo = masterJSON_Video[i]
}
}
}
let baseUrl = masterJSONURL.split("/sep/")[0]
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`;
let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` }
case "chop": // TO-DO: support chop type of streams
default:
return { error: 'ErrorEmptyDownload' }
}
} else {
return { error: 'ErrorEmptyDownload' }
}
default:
return { error: 'ErrorEmptyDownload' }
}
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,58 @@
import { xml2json } from "xml-js";
import { genericUserAgent, maxVideoDuration, services } from "../config.js";
import selectQuality from "../stream/selectQuality.js";
export default async function(obj) {
try {
let html;
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes(`{"lang":`)) {
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
if (js["mvData"]["is_active_live"] == '0') {
if (js["mvData"]["duration"] <= maxVideoDuration / 1000) {
let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 }));
let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"];
if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) {
repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"];
}
let attr = repr[repr.length - 1]["_attributes"];
let selectedQuality;
let qualities = Object.keys(services.vk.quality_match);
for (let i in qualities) {
if (qualities[i] == attr["height"]) {
selectedQuality = `url${attr["height"]}`;
break;
}
if (qualities[i] == attr["width"]) {
selectedQuality = `url${attr["width"]}`;
break;
}
}
let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1)
let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]);
let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"];
if (selectedQuality in js["player"]["params"][0]) {
return {
urls: js["player"]["params"][0][`url${userQuality}`],
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`
};
} else {
return { error: 'ErrorEmptyDownload' };
}
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorEmptyDownload' };
}
} catch (err) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -0,0 +1,98 @@
import ytdl from "better-ytdl-core";
import { maxVideoDuration, quality as mq } from "../config.js";
import selectQuality from "../stream/selectQuality.js";
export default async function(obj) {
try {
let infoInitial = await ytdl.getInfo(obj.id);
if (infoInitial) {
let info = infoInitial.formats;
if (!info[0]["isLive"]) {
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (!obj.isAudioOnly) {
video = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) {
if (obj.quality != "max") {
if (a["hasAudio"] && mq[obj.quality] == a["height"]) {
fullVideoMatch.push(a)
} else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) {
videoMatch.push(a);
}
}
return true
}
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (obj.quality != "max") {
if (videoMatch.length == 0) {
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim())
videoMatch = video.filter((a) => {
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true;
})
} else if (fullVideoMatch.length > 0) {
videoMatch = [fullVideoMatch[0]]
}
} else videoMatch = [video[0]];
if (obj.quality == "los") videoMatch = [video[video.length - 1]];
}
let generalMeta = {
title: infoInitial.videoDetails.title,
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
}
if (audio[0]["approxDurationMs"] <= maxVideoDuration) {
if (!obj.isAudioOnly && videoMatch.length > 0) {
if (video.length > 0 && audio.length > 0) {
if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) {
return {
type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
} else {
return {
type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
}
} else {
return { error: 'ErrorBadFetch' };
}
} else if (!obj.isAudioOnly) {
return {
type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}`
};
} else if (audio.length > 0) {
let r = {
type: "render",
isAudioOnly: true,
urls: audio[0]["url"],
audioFilename: `youtube_${obj.id}_audio`,
fileMetadata: generalMeta
};
if (infoInitial.videoDetails.description) {
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
if (isAutoGenAudio) {
let descItems = infoInitial.videoDetails.description.split("\n\n")
r.fileMetadata.album = descItems[2]
r.fileMetadata.copyright = descItems[3]
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
}
}
return r
} else {
return { error: 'ErrorBadFetch' };
}
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorCantConnectToServiceAPI' };
}
} catch (e) {
return { error: 'ErrorBadFetch' };
}
}

View file

@ -33,21 +33,22 @@ console.log(
)
rl.question(q, r1 => {
ob['selfURL'] = `http://localhost:9000/`
ob['port'] = 9000
if (r1) ob['selfURL'] = `https://${r1}/`
if (r1) {
ob['selfURL'] = `https://${r1}/`
} else {
ob['selfURL'] = `http://localhost`
}
console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
rl.question(q, r2 => {
if (r2) ob['port'] = r2
if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
console.log(Bright("\nWould you like to enable CORS? It allows other websites and extensions to use your instance's API.\n y/n (n)"))
rl.question(q, r3 => {
if (r3.toLowerCase() !== 'y') ob['cors'] = '0'
final()
})
if (!r1 && !r2) {
ob['selfURL'] = `http://localhost:9000/`
ob['port'] = 9000
} else if (!r1 && r2) {
ob['selfURL'] = `http://localhost:${r2}/`
ob['port'] = r2
} else {
ob['port'] = r2
}
final()
});
})

View file

@ -1,5 +1,4 @@
import NodeCache from "node-cache";
import { nanoid } from 'nanoid';
import { sha256 } from "../sub/crypto.js";
import { streamLifespan } from "../config.js";
@ -12,9 +11,9 @@ streamCache.on("expired", (key) => {
});
export function createStream(obj) {
let streamID = nanoid(),
let streamID = sha256(`${obj.ip},${obj.service},${obj.filename},${obj.audioFormat},${obj.mute}`, salt),
exp = Math.floor(new Date().getTime()) + streamLifespan,
ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, salt);
ghmac = sha256(`${streamID},${obj.service},${obj.ip},${exp}`, salt);
if (!streamCache.has(streamID)) {
streamCache.set(streamID, {
@ -43,17 +42,17 @@ export function createStream(obj) {
export function verifyStream(ip, id, hmac, exp) {
try {
if (id.length === 21) {
let streamInfo = streamCache.get(id);
if (!streamInfo) return { error: 'this stream token does not exist', status: 400 };
let ghmac = sha256(`${id},${ip},${streamInfo.service},${exp}`, salt);
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
&& String(ip) === streamInfo.ip && Number(exp) > Math.floor(new Date().getTime())) {
let streamInfo = streamCache.get(id);
if (streamInfo) {
let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt);
if (hmac == ghmac && ip == streamInfo.ip && ghmac == streamInfo.hmac && exp > Math.floor(new Date().getTime()) && exp == streamInfo.exp) {
return streamInfo;
} else {
return { error: 'Unauthorized', status: 401 };
}
} else {
return { error: 'this stream token does not exist', status: 400 };
}
return { error: 'Unauthorized', status: 401 };
} catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
}

View file

@ -0,0 +1,29 @@
import { services, quality as mq } from "../config.js";
function closest(goal, array) {
return array.sort().reduce(function (prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
});
}
export default function(service, quality, maxQuality) {
if (quality == "max") return maxQuality;
quality = parseInt(mq[quality], 10)
maxQuality = parseInt(maxQuality, 10)
if (quality >= maxQuality || quality == maxQuality) return maxQuality;
if (quality < maxQuality) {
if (services[service]["quality"][quality]) {
return quality
} else {
let s = Object.keys(services[service]["quality_match"]).filter((q) => {
if (q <= quality) {
return true
}
})
return closest(quality, s)
}
}
}

View file

@ -5,25 +5,24 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } fro
export default function(res, ip, id, hmac, exp) {
try {
let streamInfo = verifyStream(ip, id, hmac, exp);
if (streamInfo.error) {
if (!streamInfo.error) {
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res);
} else {
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
break;
case "mute":
streamVideoOnly(streamInfo, res);
break;
default:
streamDefault(streamInfo, res);
break;
}
}
} else {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
}
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res);
return;
}
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
break;
case "videoM3U8":
case "mute":
streamVideoOnly(streamInfo, res);
break;
default:
streamDefault(streamInfo, res);
break;
}
} catch (e) {
res.status(500).json({ status: "error", text: "Internal Server Error" });

View file

@ -6,8 +6,8 @@ import { metadataManager, msToTime } from "../sub/utils.js";
export function streamDefault(streamInfo, res) {
try {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`;
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
const stream = got.get(streamInfo.urls, {
headers: {
@ -15,80 +15,53 @@ export function streamDefault(streamInfo, res) {
},
isStream: true
});
stream.pipe(res).on('error', () => {
res.destroy();
stream.pipe(res).on('error', (err) => {
res.end();
});
stream.on('error', () => {
res.destroy();
});
stream.on('aborted', () => {
res.destroy();
stream.on('error', (err) => {
res.end();
});
} catch (e) {
res.destroy();
res.end();
}
}
export function streamLiveRender(streamInfo, res) {
try {
if (streamInfo.urls.length !== 2) {
res.destroy();
return;
if (streamInfo.urls.length === 2) {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls[0],
'-i', streamInfo.urls[1],
'-map', '0:v',
'-map', '1:a',
];
args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
} else {
res.end();
}
let audio = got.get(streamInfo.urls[1], { isStream: true });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls[0],
'-i', 'pipe:3',
'-map', '0:v',
'-map', '1:a',
];
args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:4');
let ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe', 'pipe'
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
res.on('error', () => {
ffmpegProcess.kill();
res.destroy();
});
ffmpegProcess.stdio[4].pipe(res).on('error', () => {
ffmpegProcess.kill();
res.destroy();
});
audio.pipe(ffmpegProcess.stdio[3]).on('error', () => {
ffmpegProcess.kill();
res.destroy();
});
audio.on('error', () => {
ffmpegProcess.kill();
res.destroy();
});
audio.on('aborted', () => {
ffmpegProcess.kill();
res.destroy();
});
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => {
ffmpegProcess.kill();
res.destroy();
});
} catch (e) {
res.destroy();
res.end();
}
}
export function streamAudioOnly(streamInfo, res) {
@ -98,21 +71,18 @@ export function streamAudioOnly(streamInfo, res) {
'-i', streamInfo.urls
]
if (streamInfo.metadata) {
if (streamInfo.metadata.cover) { // currently corrupts the audio
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0')
if (streamInfo.metadata.cover) { // doesn't work on the server but works locally, no idea why
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0', '-filter:v', 'scale=w=400:h=400,format=yuvj420p')
} else {
args.push('-vn')
}
args = args.concat(metadataManager(streamInfo.metadata))
} else {
args.push('-vn')
}
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"];
args = args.concat(arg);
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]
args = args.concat(arg)
if (streamInfo.metadata.cover) args.push("-c:v", "mjpeg")
if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]);
args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
@ -123,18 +93,17 @@ export function streamAudioOnly(streamInfo, res) {
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => {
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
res.destroy();
res.end();
});
} catch (e) {
res.destroy();
res.end();
}
}
export function streamVideoOnly(streamInfo, res) {
@ -142,11 +111,9 @@ export function streamVideoOnly(streamInfo, res) {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls,
'-c', 'copy'
'-c', 'copy', '-an'
]
if (streamInfo.mute) args.push('-an');
if (streamInfo.service === "vimeo") args.push('-bsf:a', 'aac_adtstoasc');
if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov');
if (format == "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov')
args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
@ -156,19 +123,18 @@ export function streamVideoOnly(streamInfo, res) {
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}${streamInfo.mute ? '_mute' : ''}.${format}"`);
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => {
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
res.destroy();
res.end();
});
} catch (e) {
res.destroy();
res.end();
}
}

View file

@ -1,23 +1,10 @@
import { execSync } from "child_process";
let commit, commitInfo, branch;
export function shortCommit() {
if (commit) return commit;
let c = execSync('git rev-parse --short HEAD').toString().trim();
commit = c;
return c
return execSync('git rev-parse --short HEAD').toString().trim()
}
export function getCommitInfo() {
if (commitInfo) return commitInfo;
let d = execSync(`git show -s --format='%s;;;%B'`).toString().trim().replace(/[\r\n]/gm, '\n').split(';;;');
d[1] = d[1].replace(d[0], '').trim().toString().replace(/[\r\n]/gm, '<br>');
commitInfo = d;
let d = execSync(`git show -s --format='%s;;;%B'`).toString().trim().replace(/[\r\n]/gm, '\n').split(';;;')
d[1] = d[1].replace(d[0], '').trim().toString().replace(/[\r\n]/gm, '<br>')
return d
}
export function getCurrentBranch() {
if (branch) return branch;
let b = execSync('git branch --show-current').toString().trim();
branch = b;
return b
}

View file

@ -3,9 +3,6 @@ import loc from "../../localization/manager.js";
export function errorUnsupported(lang) {
return loc(lang, 'ErrorUnsupported');
}
export function brokenLink(lang, host) {
export function genericError(lang, host) {
return loc(lang, 'ErrorBrokenLink', host);
}
export function genericError(lang, host) {
return loc(lang, 'ErrorBadFetch', host);
}

View file

@ -2,11 +2,11 @@ import { createStream } from "../stream/manage.js";
let apiVar = {
allowed: {
vCodec: ["h264", "av1", "vp9"],
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
vFormat: ["mp4", "webm"],
vQuality: ["max", "hig", "mid", "low", "los"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
},
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash"]
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted"]
}
export function apiJSON(type, obj) {
@ -64,7 +64,6 @@ export function msToTime(d) {
export function cleanURL(url, host) {
let forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"]
switch(host) {
case "vk":
case "youtube":
url = url.split('&')[0];
break;
@ -84,11 +83,8 @@ export function cleanURL(url, host) {
}
return url.slice(0, 128)
}
export function verifyLanguageCode(code) {
return RegExp(/[a-z]{2}/).test(String(code.slice(0, 2).toLowerCase())) ? String(code.slice(0, 2).toLowerCase()) : "en"
}
export function languageCode(req) {
return req.header('Accept-Language') ? verifyLanguageCode(req.header('Accept-Language')) : "en"
return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en"
}
export function unicodeDecode(str) {
return str.replace(/\\u[\dA-F]{4}/gi, (unicode) => {
@ -97,43 +93,36 @@ export function unicodeDecode(str) {
}
export function checkJSONPost(obj) {
let def = {
vCodec: "h264",
vQuality: "720",
vFormat: "mp4",
vQuality: "hig",
aFormat: "mp3",
isAudioOnly: false,
isNoTTWatermark: false,
isTTFullAudio: false,
isAudioMuted: false,
dubLang: false,
vimeoDash: false
}
try {
let objKeys = Object.keys(obj);
if (!(objKeys.length <= 9 && obj.url)) return false;
let defKeys = Object.keys(def);
for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
if (objKeys.length < 8 && obj.url) {
let defKeys = Object.keys(def);
for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
}
}
}
obj["url"] = decodeURIComponent(String(obj["url"]))
let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2]
def["url"] = encodeURIComponent(cleanURL(obj["url"], host))
return def
} else {
return false
}
if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang);
obj["url"] = decodeURIComponent(String(obj["url"]));
let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2];
def["url"] = encodeURIComponent(cleanURL(obj["url"], host));
return def
} catch (e) {
return false
return false;
}
}
export function getIP(req) {
return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip;
}

View file

@ -1,71 +0,0 @@
import "dotenv/config";
import { getJSON } from "../modules/api.js";
import { services } from "../modules/config.js";
import loadJSON from "../modules/sub/loadJSON.js";
import { checkJSONPost } from "../modules/sub/utils.js";
let tests = loadJSON('./src/test/tests.json');
let noTest = [];
let failed = [];
let success = 0;
function addToFail(service, testName, url, status, response) {
failed.push({
service: service,
name: testName,
url: url,
status: status,
response: response
})
}
for (let i in services) {
if (tests[i]) {
console.log(`\nRunning tests for ${i}...\n`)
for (let k = 0; k < tests[i].length; k++) {
let test = tests[i][k];
console.log(`Running test ${k+1}: ${test.name}`);
console.log('params:');
let params = {...{url: test.url}, ...test.params};
console.log(params);
let chck = checkJSONPost(params);
if (chck) {
chck["ip"] = "d21ec524bc2ade41bef569c0361ac57728c69e2764b5cb3cb310fe36568ca53f"; // random sha256
let j = await getJSON(chck["url"], "en", chck);
console.log('\nReceived:');
console.log(j)
if (j.status === test.expected.code && j.body.status === test.expected.status) {
console.log("\n✅ Success.\n");
success++
} else {
console.log(`\n❌ Fail. Expected: ${test.expected.code} & ${test.expected.status}, received: ${j.status} & ${j.body.status}\n`);
addToFail(i, test.name, test.url, j.body.status, j)
}
} else {
console.log("\n❌ couldn't validate the request JSON.\n");
addToFail(i, test.name, test.url, "unknown", {})
}
}
console.log("\n\n")
} else {
console.warn(`No tests found for ${i}.`);
noTest.push(i)
}
}
console.log(`${success} tests succeeded.`);
console.log(`${failed.length} tests failed.`);
console.log(`${noTest.length} services weren't tested.`);
if (failed.length > 0) {
console.log(`\nFailed tests:`);
console.log(failed)
}
if (noTest.length > 0) {
console.log(`\nMissing tests:`);
console.log(noTest)
}

View file

@ -1,759 +0,0 @@
{
"twitter": [{
"name": "regular video",
"url": "https://twitter.com/TwitterSpaces/status/1526955853743546372?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "embedded twitter video",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "mixed media (image + gif)",
"url": "https://twitter.com/Twitter/status/1580661436132757506?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "picker: mixed media (3 gifs + image)",
"url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "picker"
}
}, {
"name": "audio from embedded twitter video (mp3, isAudioOnly)",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": true,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "audio from embedded twitter video (best, isAudioOnly)",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
"params": {
"aFormat": "best",
"isAudioOnly": true,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "audio from embedded twitter video (ogg, isAudioOnly, isAudioMuted)",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
"params": {
"aFormat": "best",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "muted embedded twitter video",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "retweeted video",
"url": "https://twitter.com/winload_exe/status/1633091769482063874",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "inexistent post",
"url": "https://twitter.com/test/status/9487653",
"params": {
"aFormat": "best",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "post with no media content",
"url": "https://twitter.com/elonmusk/status/1604617643973124097?s=20",
"params": {
"aFormat": "best",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "recorded space by nyc (best)",
"url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB",
"params": {
"aFormat": "best",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "recorded space by nyc (mp3)",
"url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "recorded space by nyc (wav, isAudioMuted)",
"url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB",
"params": {
"aFormat": "wav",
"isAudioOnly": false,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "recorded space by service95 & dualipa (mp3, isAudioMuted, isAudioOnly)",
"url": "https://twitter.com/i/spaces/1nAJErvvVXgxL",
"params": {
"aFormat": "mp3",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "unavailable space",
"url": "https://twitter.com/i/spaces/1OwGWwjRjVVGQ?s=20",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "inexistent space",
"url": "https://twitter.com/i/spaces/10Wkie2j29iiI",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}],
"soundcloud": [{
"name": "public song (best)",
"url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing",
"params": {
"aFormat": "best",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "public song (mp3, isAudioMuted)",
"url": "https://soundcloud.com/l2share77/loona-butterfly?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "private song",
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "private song (wav, isAudioMuted)",
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
"params": {
"aFormat": "wav",
"isAudioOnly": false,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "private song (ogg, isAudioMuted, isAudioOnly)",
"url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG",
"params": {
"aFormat": "ogg",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}],
"youtube": [{
"name": "4k video (h264, 1440)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "h264",
"vQuality": "1440"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (vp9, 720)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "vp9",
"vQuality": "720"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (av1, max)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "av1",
"vQuality": "max"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (h264, 720)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "h264",
"vQuality": "720"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (vp9, max, isAudioMuted)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "vp9",
"vQuality": "max",
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (h264, max, isAudioMuted)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "h264",
"vQuality": "max",
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (av1, max, isAudioMuted, isAudioOnly, mp3)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "av1",
"vQuality": "max",
"aFormat": "mp3",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "4k video (av1, max, isAudioMuted, isAudioOnly, best)",
"url": "https://www.youtube.com/watch?v=vPwaXytZcgI",
"params": {
"vCodec": "av1",
"vQuality": "max",
"aFormat": "best",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "music (mp3, isAudioOnly, isAudioMuted)",
"url": "https://music.youtube.com/watch?v=5rGTsvZCEdk&feature=share",
"params": {
"aFormat": "mp3",
"isAudioOnly": true,
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "music (mp3)",
"url": "https://music.youtube.com/watch?v=5rGTsvZCEdk&feature=share",
"params": {
"aFormat": "mp3",
"isAudioOnly": false,
"isAudioMuted": false
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "audio bitrate higher than video, no vp9 video in response (mp3, isAudioOnly)",
"url": "https://www.youtube.com/watch?v=t5nC_ucYBrc",
"params": {
"aFormat": "mp3",
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "audio bitrate higher than video, no vp9 video in response (vp9)",
"url": "https://www.youtube.com/watch?v=t5nC_ucYBrc",
"params": {
"vCodec": "vp9"
},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "short, defaults",
"url": "https://www.youtube.com/shorts/r5FpeOJItbw",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "inexistent video",
"url": "https://youtube.com/watch?v=gnjuHYWGEW",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}],
"vk": [{
"name": "clip, defaults",
"url": "https://vk.com/clip-57274055_456239788",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "clip, 360",
"url": "https://vk.com/clip-57274055_456239788",
"params": {
"vQuality": "360"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "clip different link, max",
"url": "https://vk.com/clips-57274055?z=clip-57274055_456239788",
"params": {
"vQuality": "max"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "video, defaults",
"url": "https://vk.com/video-57274055_456239399",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "inexistent video",
"url": "https://vk.com/video-53333333_456233333",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}],
"douyin": [{
"name": "short link video, with watermark",
"url": "https://v.douyin.com/2p4Aya7/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link video (isNoTTWatermark)",
"url": "https://v.douyin.com/2p4Aya7/",
"params": {
"isNoTTWatermark": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link video (isAudioOnly)",
"url": "https://v.douyin.com/2p4Aya7/",
"params": {
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link video (isAudioOnly, isTTFullAudio)",
"url": "https://v.douyin.com/2p4Aya7/",
"params": {
"isAudioOnly": true,
"isTTFullAudio": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "long link video (isNoTTWatermark)",
"url": "https://www.douyin.com/video/7120601033314716968",
"params": {
"isNoTTWatermark": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "images",
"url": "https://v.douyin.com/MdVwo31/",
"params": {},
"expected": {
"code": 200,
"status": "picker"
}
}, {
"name": "long link inexistent",
"url": "https://www.douyin.com/video/7120851458451417478",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "short link inexistent",
"url": "https://v.douyin.com/2p4ewa7/",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}],
"tiktok": [{
"name": "short link (vt) video, with watermark",
"url": "https://vt.tiktok.com/ZS85U86aa/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link (vt) video (isNoTTWatermark)",
"url": "https://vt.tiktok.com/ZS85U86aa/",
"params": {
"isNoTTWatermark": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link (vm) video (isAudioOnly)",
"url": "https://vm.tiktok.com/ZMYrYAf34/",
"params": {
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "short link (vm) video (isAudioOnly, isTTFullAudio)",
"url": "https://vm.tiktok.com/ZMYrYAf34/",
"params": {
"isAudioOnly": true,
"isTTFullAudio": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "long link video (isNoTTWatermark)",
"url": "https://www.tiktok.com/@fatfatmillycat/video/7195741644585454894",
"params": {
"isNoTTWatermark": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "images",
"url": "https://vt.tiktok.com/ZS8JP89eB/",
"params": {},
"expected": {
"code": 200,
"status": "picker"
}
}, {
"name": "long link inexistent",
"url": "https://www.tiktok.com/@blablabla/video/7120851458451417478",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "short link inexistent",
"url": "https://vt.tiktok.com/2p4ewa7/",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}],
"bilibili": [{
"name": "1080p video",
"url": "https://www.bilibili.com/video/BV18i4y1m7xV/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "1080p video muted",
"url": "https://www.bilibili.com/video/BV18i4y1m7xV/",
"params": {
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "1080p vertical video",
"url": "https://www.bilibili.com/video/BV1uu411z7VV/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "1080p vertical video muted",
"url": "https://www.bilibili.com/video/BV1uu411z7VV/",
"params": {
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}],
"tumblr": [{
"name": "at.tumblr link",
"url": "https://at.tumblr.com/music/704177038274281472/n7x7pr7x4w2b",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "user subdomain link",
"url": "https://garfield-69.tumblr.com/post/696499862852780032",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "web app link",
"url": "https://www.tumblr.com/rongzhi/707729381162958848/english-added-by-me?source=share",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}],
"vimeo": [{
"name": "4k progressive",
"url": "https://vimeo.com/288386543",
"params": {
"vQuality": "2160"
},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "720p progressive",
"url": "https://vimeo.com/288386543",
"params": {
"vQuality": "720"
},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "1080p dash parcel",
"url": "https://vimeo.com/774694040",
"params": {
"vQuality": "1440"
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "720p dash parcel",
"url": "https://vimeo.com/774694040",
"params": {
"vQuality": "360"
},
"expected": {
"code": 200,
"status": "stream"
}
}],
"reddit": [{
"name": "video with audio",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "video with audio (isAudioOnly)",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3",
"params": {
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "video with audio (isAudioMuted)",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3",
"params": {
"isAudioMuted": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "video without audio",
"url": "https://www.reddit.com/r/catvideos/comments/ftoeo7/luna_doesnt_want_to_be_bothered_while_shes_napping/?utm_source=share&utm_medium=web2x&context=3",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "actual gif, not looping video",
"url": "https://www.reddit.com/r/whenthe/comments/109wqy1/god_really_did_some_trolling/?utm_source=share&utm_medium=web2x&context=3",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}]
}