mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-14 01:08:27 +00:00
Merge branch 'current' into add-language-selector
This commit is contained in:
commit
1c1b4ac37d
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
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" ]
|
95
README.md
95
README.md
@ -1,70 +1,60 @@
|
||||
# cobalt
|
||||
Best way to save what you love.
|
||||
Best way to save what you love.
|
||||
Main instance: [co.wukko.me](https://co.wukko.me/)
|
||||
|
||||
Live: [co.wukko.me](https://co.wukko.me/)
|
||||
|
||||

|
||||

|
||||
|
||||
[](https://crowdin.com/project/cobalt) [](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge) [](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.
|
||||
|
||||
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'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.
|
||||
|
||||
## 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 | Only video | Additional notes or features |
|
||||
| -------- | :---: | :---: | :---: | :----- |
|
||||
| bilibili.com | ✅ | ✅ | ✅ | |
|
||||
| Instagram | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media posts. |
|
||||
| Instagram Reels | ✅ | ✅ | ✅ | |
|
||||
| Reddit | ✅ | ✅ | ✅ | Support for GIFs and videos. |
|
||||
| SoundCloud | ➖ | ✅ | ➖ | Audio metadata, downloads from private links. |
|
||||
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. |
|
||||
| Tumblr | ✅ | ✅ | ✅ | |
|
||||
| Twitter | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. |
|
||||
| Twitter Spaces | ➖ | ✅ | ➖ | Audio metadata with all participants and other info. |
|
||||
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. |
|
||||
| Vine Archive | ✅ | ✅ | ✅ | |
|
||||
| VK Videos | ✅ | ❌ | ❌ | |
|
||||
| VK Clips | ✅ | ❌ | ❌ | |
|
||||
| YouTube Videos & Shorts | ✅ | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. |
|
||||
| YouTube Music | ➖ | ✅ | ➖ | Audio metadata. |
|
||||
|
||||
This list is not final and keeps expanding over time, make sure to check it once in a while!
|
||||
|
||||
## cobalt API
|
||||
cobalt has an open API that you can use 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 in your projects for **free**.
|
||||
It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
|
||||
|
||||
## 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.
|
||||
|
||||
### Translation guidelines:
|
||||
- All text is **ALWAYS** stylized as **lowercase** unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}` or `{appName}`.
|
||||
- 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.
|
||||
*Notice how **everything is lowercase**, no matter the punctuation marks? Yes, that's cobalt's style and you have to follow it.*
|
||||
- Avoid extremely formal language, leave it for big and classy tech companies. Use informal language wherever possible.
|
||||
- You can (and should) rephrase sentences as long as they keep the same sense and send the same message as original.
|
||||
- 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.
|
||||
- Check if there are issues in UI with your localization and optimize it accordingly. If impossible, open an issue.
|
||||
- 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 18 or above
|
||||
- git
|
||||
|
||||
### npm modules
|
||||
- cors
|
||||
- dotenv
|
||||
- esbuild
|
||||
- express
|
||||
- express-rate-limit
|
||||
- ffmpeg-static
|
||||
- got
|
||||
- node-cache
|
||||
- url-pattern
|
||||
- xml-js
|
||||
- youtubei.js
|
||||
|
||||
Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself.
|
||||
|
||||
1. Clone the repo: `git clone https://github.com/wukko/cobalt`
|
||||
@ -72,10 +62,27 @@ Setup script installs all needed `npm` dependencies, but you have to install `No
|
||||
3. Run cobalt via `npm start`
|
||||
4. Done.
|
||||
|
||||
### Ubuntu 22.04+ workaround
|
||||
`nscd` needs to be installed and running so that the `ffmpeg-static` binary can resolve DNS ([#101](https://github.com/wukko/cobalt/issues/101#issuecomment-1494822258)):
|
||||
|
||||
```bash
|
||||
sudo apt install nscd
|
||||
sudo service nscd start
|
||||
```
|
||||
|
||||
### Docker
|
||||
It's also possible to run cobalt via Docker, but you **need** to set all environment variables yourself:
|
||||
|
||||
| Variable | Description | Example |
|
||||
| -------- | :--- | :--- |
|
||||
| `selfURL` | Instance URL | `http://localhost:9000/` or `https://co.wukko.me/` or etc |
|
||||
| `port` | Instance port | `9000` |
|
||||
| `cors` | CORS toggle | `0` |
|
||||
|
||||
## 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 update schedule depends solely on my free time, 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) 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.
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "5.3.2",
|
||||
"version": "5.7",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=17.5"
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/cobalt",
|
||||
@ -34,6 +34,6 @@
|
||||
"node-cache": "^5.1.2",
|
||||
"url-pattern": "1.0.3",
|
||||
"xml-js": "^1.6.11",
|
||||
"youtubei.js": "4.1.1"
|
||||
"youtubei.js": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import "dotenv/config";
|
||||
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import * as fs from "fs";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const ipSalt = randomBytes(64).toString('hex');
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@ -13,7 +15,7 @@ const __dirname = path.dirname(__filename).slice(0, -4); // go up another level
|
||||
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
|
||||
import { appName, genericUserAgent, version } from "./modules/config.js";
|
||||
import { getJSON } from "./modules/api.js";
|
||||
import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js";
|
||||
import { apiJSON, checkJSONPost, getIP, languageCode } from "./modules/sub/utils.js";
|
||||
import { Bright, Cyan, Green, Red } from "./modules/sub/consoleText.js";
|
||||
import stream from "./modules/stream/stream.js";
|
||||
import loc from "./localization/manager.js";
|
||||
@ -21,22 +23,23 @@ import { buildFront } from "./modules/build.js";
|
||||
import { changelogHistory } from "./modules/pageRender/onDemand.js";
|
||||
import { sha256 } from "./modules/sub/crypto.js";
|
||||
import findRendered from "./modules/pageRender/findRendered.js";
|
||||
import { celebrationsEmoji } from "./modules/pageRender/elements.js";
|
||||
|
||||
const commitHash = shortCommit();
|
||||
const branch = getCurrentBranch();
|
||||
const app = express();
|
||||
if (process.env.selfURL && process.env.port) {
|
||||
const commitHash = shortCommit();
|
||||
const branch = getCurrentBranch();
|
||||
const app = express();
|
||||
|
||||
const corsConfig = process.env.cors === '0' ? { origin: process.env.selfURL, optionsSuccessStatus: 200 } : {};
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.disable('x-powered-by');
|
||||
const corsConfig = process.env.cors === '0' ? { origin: process.env.selfURL, optionsSuccessStatus: 200 } : {};
|
||||
|
||||
if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && process.env.port) {
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60000,
|
||||
max: 25,
|
||||
standardHeaders: false,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req, res) => sha256(req.ip.replace('::ffff:', ''), process.env.streamSalt),
|
||||
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
|
||||
return;
|
||||
@ -47,7 +50,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
max: 28,
|
||||
standardHeaders: false,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req, res) => sha256(req.ip.replace('::ffff:', ''), process.env.streamSalt),
|
||||
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
|
||||
return;
|
||||
@ -65,7 +68,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
app.use('/', express.static('./src/front'));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
try { decodeURIComponent(req.path) } catch (e) { return res.redirect(process.env.selfURL) }
|
||||
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
|
||||
next();
|
||||
});
|
||||
app.use((req, res, next) => {
|
||||
@ -94,7 +97,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
|
||||
app.post('/api/json', async (req, res) => {
|
||||
try {
|
||||
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
let ip = sha256(getIP(req), ipSalt);
|
||||
let lang = languageCode(req);
|
||||
let j = apiJSON(0, { t: "Bad request" });
|
||||
try {
|
||||
@ -120,7 +123,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
|
||||
app.get('/api/:type', (req, res) => {
|
||||
try {
|
||||
let ip = sha256(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
let ip = sha256(getIP(req), ipSalt);
|
||||
switch (req.params.type) {
|
||||
case 'stream':
|
||||
if (req.query.p) {
|
||||
@ -136,21 +139,29 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
break;
|
||||
case 'onDemand':
|
||||
if (req.query.blockId) {
|
||||
let blockId = req.query.blockId.slice(0, 3)
|
||||
let blockId = req.query.blockId.slice(0, 3);
|
||||
let r, j;
|
||||
switch(blockId) {
|
||||
case "0":
|
||||
case "0": // changelog history
|
||||
r = changelogHistory();
|
||||
j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" })
|
||||
break;
|
||||
case "1": // celebrations emoji
|
||||
r = celebrationsEmoji();
|
||||
j = r ? apiJSON(3, { t: r }) : false
|
||||
break;
|
||||
default:
|
||||
j = apiJSON(0, { t: "couldn't find a block with this id" })
|
||||
break;
|
||||
}
|
||||
res.status(j.status).json(j.body);
|
||||
if (j.body) {
|
||||
res.status(j.status).json(j.body)
|
||||
} else {
|
||||
res.status(204).end()
|
||||
}
|
||||
} else {
|
||||
let j = apiJSON(0, { t: "no block id" })
|
||||
res.status(j.status).json(j.body);
|
||||
let j = apiJSON(0, { t: "no block id" });
|
||||
res.status(j.status).json(j.body)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -166,6 +177,9 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
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)}`);
|
||||
});
|
||||
@ -179,7 +193,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
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`)
|
||||
});
|
||||
})
|
||||
} 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`));
|
||||
}
|
||||
|
@ -27,6 +27,9 @@
|
||||
"boosty": "https://boosty.to/wukko"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"saveToGalleryShortcut": "https://www.icloud.com/shortcuts/6d4fe6e5bade4150b8759ce20720c7a3"
|
||||
},
|
||||
"celebrations": {
|
||||
"01-01": "🎄",
|
||||
"02-17": "😺",
|
||||
|
@ -7,8 +7,9 @@
|
||||
--padding-1: 0.75rem;
|
||||
--line-height: 1.65rem;
|
||||
--red: rgb(255, 0, 61);
|
||||
--color: rgb(107, 67, 139);
|
||||
--gap: 0.6rem;
|
||||
--gap: 0.5rem;
|
||||
--gap-no-icon: 0.6rem;
|
||||
--rainbow-gradient: linear-gradient(161deg,#ffe454,#ff6964,#fe85e5,#bd26fe,#587ae9,#8ded95);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@ -19,7 +20,7 @@
|
||||
--accent-unhover: rgb(100, 100, 100);
|
||||
--accent-unhover-2: rgb(110, 110, 110);
|
||||
--background: rgb(0, 0, 0);
|
||||
--checkmark: url(vectorIcons/checkmark_b.svg);
|
||||
--glow-transparency: 0.45;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
@ -31,7 +32,7 @@
|
||||
--accent-unhover: rgb(190, 190, 190);
|
||||
--accent-unhover-2: rgb(110, 110, 110);
|
||||
--background: rgb(255, 255, 255);
|
||||
--checkmark: url(vectorIcons/checkmark.svg);
|
||||
--glow-transparency: 0.6;
|
||||
}
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
@ -42,7 +43,7 @@
|
||||
--accent-unhover: rgb(100, 100, 100);
|
||||
--accent-unhover-2: rgb(110, 110, 110);
|
||||
--background: rgb(0, 0, 0);
|
||||
--checkmark: url(vectorIcons/checkmark_b.svg);
|
||||
--glow-transparency: 0.45;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--accent: rgb(25, 25, 25);
|
||||
@ -52,7 +53,7 @@
|
||||
--accent-unhover: rgb(190, 190, 190);
|
||||
--accent-unhover-2: rgb(110, 110, 110);
|
||||
--background: rgb(255, 255, 255);
|
||||
--checkmark: url(vectorIcons/checkmark.svg);
|
||||
--glow-transparency: 0.6;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
@ -82,37 +83,51 @@ a {
|
||||
:focus-visible {
|
||||
outline: var(--border-15);
|
||||
}
|
||||
.checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: calc(var(--gap) - 0.1rem) calc(var(--gap)*2 - 0.2rem) calc(var(--gap) - 0.1rem) var(--gap);
|
||||
width: auto;
|
||||
margin-right: var(--padding-1);
|
||||
margin-bottom: var(--padding-1);
|
||||
background: var(--accent-button-bg);
|
||||
}
|
||||
.checkbox-label {
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-right: var(--padding-1);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
z-index: 0;
|
||||
border: 0;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
margin-right: var(--padding-1);
|
||||
border: 0.15rem solid var(--accent);
|
||||
}
|
||||
[type="checkbox"]::before {
|
||||
content: "";
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 0.15rem solid var(--accent);
|
||||
display: block;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
z-index: 5;
|
||||
transform: scaleX(0.9)rotate(45deg);
|
||||
left: 6px;
|
||||
top: 1px;
|
||||
border-bottom: 0.18rem solid var(--background);
|
||||
border-right: 0.18rem solid var(--background);
|
||||
}
|
||||
[type="checkbox"]:checked::before {
|
||||
background: var(--checkmark);
|
||||
background-size: 90%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
[type="checkbox"]:checked::before {
|
||||
[type="checkbox"]:checked {
|
||||
background-color: var(--accent);
|
||||
border: 0.15rem solid var(--accent);
|
||||
border: 0;
|
||||
}
|
||||
.checkbox span {
|
||||
margin-top: 0.21rem;
|
||||
margin-left: 0.4rem;
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
background: none;
|
||||
@ -160,9 +175,6 @@ button:active,
|
||||
cursor: pointer;
|
||||
transform: scale(0.95)
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.button {
|
||||
background: none;
|
||||
border: var(--border-15);
|
||||
@ -216,7 +228,7 @@ input[type="checkbox"] {
|
||||
color: var(--accent);
|
||||
}
|
||||
#url-input-area {
|
||||
background: var(--background);
|
||||
background: none;
|
||||
padding: 0 1rem;
|
||||
width: 100%;
|
||||
color: var(--accent);
|
||||
@ -338,7 +350,7 @@ input[type="checkbox"] {
|
||||
}
|
||||
.changelog-subtitle {
|
||||
font-size: 1.1rem;
|
||||
padding-bottom: 0.7rem;
|
||||
padding-bottom: var(--gap-no-icon);
|
||||
}
|
||||
.changelog-banner {
|
||||
width: 100%;
|
||||
@ -430,7 +442,7 @@ input[type="checkbox"] {
|
||||
color: var(--accent-unhover-2);
|
||||
border-bottom: 0.05rem solid var(--accent-unhover-2);
|
||||
padding-bottom: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: calc(var(--gap-no-icon)*1.5);
|
||||
}
|
||||
.category-title {
|
||||
text-align: left;
|
||||
@ -449,20 +461,6 @@ input[type="checkbox"] {
|
||||
.no-margin {
|
||||
margin: 0!important;
|
||||
}
|
||||
.checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0.55rem 1rem 0.8rem 0.7rem;
|
||||
width: auto;
|
||||
margin-right: var(--padding-1);
|
||||
margin-bottom: var(--padding-1);
|
||||
background: var(--accent-button-bg);
|
||||
}
|
||||
.checkbox-label {
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
.switch-container {
|
||||
width: 100%;
|
||||
}
|
||||
@ -477,7 +475,7 @@ input[type="checkbox"] {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.explanation {
|
||||
margin-top: 1rem;
|
||||
margin-top: 0.8rem;
|
||||
width: 100%;
|
||||
font-size: 0.8rem;
|
||||
text-align: left;
|
||||
@ -488,7 +486,7 @@ input[type="checkbox"] {
|
||||
color: var(--accent-unhover-2);
|
||||
}
|
||||
.switch {
|
||||
padding: 0.7rem;
|
||||
padding: var(--gap-no-icon);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--accent);
|
||||
@ -518,6 +516,13 @@ input[type="checkbox"] {
|
||||
overflow-x: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.switches .switch {
|
||||
padding-left: calc(var(--gap-no-icon) + 0.1rem);
|
||||
padding-right: calc(var(--gap-no-icon) + 0.1rem);
|
||||
}
|
||||
#popup-settings .switches .switch {
|
||||
text-align: center;
|
||||
}
|
||||
.autowidth {
|
||||
width: auto;
|
||||
}
|
||||
@ -527,12 +532,12 @@ input[type="checkbox"] {
|
||||
.text-to-copy {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
border: var(--border-15);
|
||||
background: var(--accent-button-bg);
|
||||
padding: var(--padding-1);
|
||||
overflow: auto;
|
||||
}
|
||||
#close-button {
|
||||
max-width: 2.8rem;
|
||||
max-width: 2.6rem;
|
||||
margin-left: var(--padding-1);
|
||||
background: var(--background);
|
||||
border: var(--border-15);
|
||||
@ -543,7 +548,7 @@ input[type="checkbox"] {
|
||||
float: right;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 2.8rem;
|
||||
height: 2.6rem;
|
||||
}
|
||||
.popup-tab-content {
|
||||
display: none;
|
||||
@ -555,7 +560,7 @@ input[type="checkbox"] {
|
||||
width: 100%;
|
||||
}
|
||||
.popup-tabs {
|
||||
margin-top: 0.8rem;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
.emoji {
|
||||
margin-right: 0.4rem;
|
||||
@ -579,6 +584,7 @@ input[type="checkbox"] {
|
||||
width: 25rem;
|
||||
margin-bottom: var(--padding-1);
|
||||
background-color: var(--accent-button-bg);
|
||||
border: var(--accent-button-bg) 0.18rem solid;
|
||||
position: relative;
|
||||
}
|
||||
#picker-holder {
|
||||
@ -658,6 +664,23 @@ input[type="checkbox"] {
|
||||
#pd-share {
|
||||
display: none;
|
||||
}
|
||||
#hop-attribution {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
#about-donate-footer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 110%;
|
||||
width: 32%;
|
||||
background: var(--rainbow-gradient);
|
||||
z-index: -2;
|
||||
filter: blur(5px);
|
||||
opacity: var(--glow-transparency);
|
||||
}
|
||||
#about-donate-footer:active::before {
|
||||
opacity: 0;
|
||||
}
|
||||
/* adapt the page according to screen size */
|
||||
@media screen and (min-width: 2300px) {
|
||||
html {
|
||||
@ -752,9 +775,14 @@ input[type="checkbox"] {
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 320px) {
|
||||
:root {
|
||||
--gap: 0.38rem;
|
||||
--gap-no-icon: 0.38rem;
|
||||
--line-height: 1.2rem;
|
||||
}
|
||||
#popup-title {
|
||||
font-size: 1.3rem;
|
||||
line-height: 2rem;
|
||||
font-size: 1.07rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.footer-button,
|
||||
#audioMode-false,
|
||||
@ -768,22 +796,61 @@ input[type="checkbox"] {
|
||||
#paste .emoji {
|
||||
margin-right: 0;
|
||||
}
|
||||
.switch, .checkbox, .category-title, .subtitle, #popup-desc {
|
||||
font-size: .75rem;
|
||||
.switch,
|
||||
.checkbox,
|
||||
.category-title,
|
||||
.subtitle,
|
||||
#popup-desc,
|
||||
.collapse-title {
|
||||
font-size: .7rem;
|
||||
}
|
||||
.collapse-header {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
#popup-above-title,
|
||||
#url-input-area {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.explanation {
|
||||
font-size: .77rem;
|
||||
margin-top: 0.8rem;
|
||||
font-size: .6rem;
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1rem!important;
|
||||
}
|
||||
#popup-desc {
|
||||
line-height: 1.4rem;
|
||||
line-height: 1.2rem;
|
||||
font-size: .64rem;
|
||||
}
|
||||
.changelog-subtitle, #popup-subtitle {
|
||||
font-size: 0.9rem!important;
|
||||
font-size: 0.8rem!important;
|
||||
}
|
||||
.category-title {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.emoji {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
.desc-padding {
|
||||
padding-bottom: 0.8rem;
|
||||
}
|
||||
#logo {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.popup,
|
||||
.popup.scrollable,
|
||||
.popup.small {
|
||||
height: 98%;
|
||||
}
|
||||
[type=checkbox] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 0.12rem solid var(--accent);
|
||||
}
|
||||
[type=checkbox]:before {
|
||||
transform: scaleY(.8)scaleX(.7)rotate(45deg);
|
||||
left: 3.4px;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 720px) {
|
||||
#cobalt-main-box #bottom {
|
||||
@ -793,13 +860,17 @@ input[type="checkbox"] {
|
||||
width: 100%;
|
||||
}
|
||||
#footer {
|
||||
bottom: 4%;
|
||||
bottom: 4.9%;
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
#footer-buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
#about-donate-footer::before {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
.footer-pair .footer-button {
|
||||
width: 100%!important;
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
let ua = navigator.userAgent.toLowerCase();
|
||||
let isIOS = ua.match("iphone os");
|
||||
let isMobile = ua.match("android") || ua.match("iphone os");
|
||||
let version = 25;
|
||||
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>`
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isIOS = ua.match("iphone os");
|
||||
const isMobile = ua.match("android") || ua.match("iphone os");
|
||||
const version = 26;
|
||||
const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
|
||||
const notification = `<div class="notification-dot"></div>`;
|
||||
|
||||
let store = {}
|
||||
|
||||
let switchers = {
|
||||
const switchers = {
|
||||
"theme": ["auto", "light", "dark"],
|
||||
"vCodec": ["h264", "av1", "vp9"],
|
||||
"vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"],
|
||||
@ -15,12 +13,16 @@ let switchers = {
|
||||
"dubLang": ["original", "auto"],
|
||||
"vimeoDash": ["false", "true"],
|
||||
"audioMode": ["false", "true"]
|
||||
}
|
||||
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
|
||||
let exceptions = { // used for mobile devices
|
||||
};
|
||||
const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
|
||||
const exceptions = { // used for mobile devices
|
||||
"vQuality": "720"
|
||||
}
|
||||
let dropdowns = ["language"]
|
||||
};
|
||||
let dropdowns = ["language"];
|
||||
|
||||
const apiURL = '';
|
||||
|
||||
let store = {};
|
||||
|
||||
function eid(id) {
|
||||
return document.getElementById(id)
|
||||
@ -363,88 +365,103 @@ async function download(url) {
|
||||
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;
|
||||
}
|
||||
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();
|
||||
if (j.status !== "error" && j.status !== "rate-limit") {
|
||||
if (j.url || j.picker) {
|
||||
switch (j.status) {
|
||||
case "redirect":
|
||||
changeDownloadButton(2, '>>>');
|
||||
setTimeout(() => { changeButton(1); }, 1500);
|
||||
sGet("downloadPopup") === "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
|
||||
break;
|
||||
case "picker":
|
||||
if (j.audio && j.picker) {
|
||||
changeDownloadButton(2, '?..')
|
||||
fetch(`${j.audio}&p=1`).then(async (res) => {
|
||||
let jp = await res.json();
|
||||
if (jp.status === "continue") {
|
||||
changeDownloadButton(2, '>>>');
|
||||
popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType });
|
||||
setTimeout(() => { changeButton(1) }, 2500);
|
||||
} else {
|
||||
changeButton(0, jp.text);
|
||||
}
|
||||
}).catch((error) => internetError());
|
||||
} else if (j.picker) {
|
||||
|
||||
let j = await fetch(`${apiURL}/api/json`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(req),
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }
|
||||
}).then((r) => { return r.json() }).catch((e) => { return false });
|
||||
if (!j) {
|
||||
internetError();
|
||||
return
|
||||
}
|
||||
|
||||
if (j && j.status !== "error" && j.status !== "rate-limit") {
|
||||
if (j.text && (!j.url || !j.picker)) {
|
||||
if (j.status === "success") {
|
||||
changeButton(2, j.text)
|
||||
} else changeButton(0, loc.noURLReturned);
|
||||
}
|
||||
switch (j.status) {
|
||||
case "redirect":
|
||||
changeDownloadButton(2, '>>>');
|
||||
setTimeout(() => { changeButton(1); }, 1500);
|
||||
sGet("downloadPopup") === "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
|
||||
break;
|
||||
case "picker":
|
||||
if (j.audio && j.picker) {
|
||||
changeDownloadButton(2, '?..')
|
||||
fetch(`${j.audio}&p=1`).then(async (res) => {
|
||||
let jp = await res.json();
|
||||
if (jp.status === "continue") {
|
||||
changeDownloadButton(2, '>>>');
|
||||
popup('picker', 1, { arr: j.picker, type: j.pickerType });
|
||||
popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType });
|
||||
setTimeout(() => { changeButton(1) }, 2500);
|
||||
} else {
|
||||
changeButton(0, loc.noURLReturned);
|
||||
changeButton(0, jp.text);
|
||||
}
|
||||
break;
|
||||
case "stream":
|
||||
changeDownloadButton(2, '?..')
|
||||
fetch(`${j.url}&p=1`).then(async (res) => {
|
||||
let jp = await res.json();
|
||||
if (jp.status === "continue") {
|
||||
changeDownloadButton(2, '>>>'); window.location.href = j.url;
|
||||
setTimeout(() => { changeButton(1) }, 2500);
|
||||
} else {
|
||||
changeButton(0, jp.text);
|
||||
}
|
||||
}).catch((error) => internetError());
|
||||
break;
|
||||
case "success":
|
||||
changeButton(2, j.text);
|
||||
break;
|
||||
default:
|
||||
changeButton(0, loc.unknownStatus);
|
||||
break;
|
||||
}).catch((error) => internetError());
|
||||
} else if (j.picker) {
|
||||
changeDownloadButton(2, '>>>');
|
||||
popup('picker', 1, { arr: j.picker, type: j.pickerType });
|
||||
setTimeout(() => { changeButton(1) }, 2500);
|
||||
} else {
|
||||
changeButton(0, loc.noURLReturned);
|
||||
}
|
||||
} else {
|
||||
if (j.status === "success") {
|
||||
changeButton(2, j.text)
|
||||
} else changeButton(0, loc.noURLReturned);
|
||||
}
|
||||
} else {
|
||||
changeButton(0, j.text);
|
||||
break;
|
||||
case "stream":
|
||||
changeDownloadButton(2, '?..')
|
||||
fetch(`${j.url}&p=1`).then(async (res) => {
|
||||
let jp = await res.json();
|
||||
if (jp.status === "continue") {
|
||||
changeDownloadButton(2, '>>>'); window.location.href = j.url;
|
||||
setTimeout(() => { changeButton(1) }, 2500);
|
||||
} else {
|
||||
changeButton(0, jp.text);
|
||||
}
|
||||
}).catch((error) => internetError());
|
||||
break;
|
||||
case "success":
|
||||
changeButton(2, j.text);
|
||||
break;
|
||||
default:
|
||||
changeButton(0, loc.unknownStatus);
|
||||
break;
|
||||
}
|
||||
}).catch((error) => internetError());
|
||||
} else if (j && j.text) {
|
||||
changeButton(0, j.text);
|
||||
}
|
||||
}
|
||||
async function loadCelebrationsEmoji() {
|
||||
let bac = eid("about-footer").innerHTML;
|
||||
try {
|
||||
let j = await fetch(`${apiURL}/api/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false });
|
||||
if (j && j.status === "success" && j.text) {
|
||||
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('<img class="emoji" draggable="false" height="22" width="22" alt="🐲" src="emoji/dragon_face.svg">', j.text);
|
||||
}
|
||||
} catch (e) {
|
||||
eid("about-footer").innerHTML = bac;
|
||||
}
|
||||
}
|
||||
async function loadOnDemand(elementId, blockId) {
|
||||
let j = {};
|
||||
store.historyButton = eid(elementId).innerHTML;
|
||||
let j = {}
|
||||
eid(elementId).innerHTML = "..."
|
||||
eid(elementId).innerHTML = "...";
|
||||
|
||||
try {
|
||||
if (store.historyContent) {
|
||||
j = store.historyContent;
|
||||
} else {
|
||||
await fetch(`/api/onDemand?blockId=${blockId}`).then(async (r) => {
|
||||
await fetch(`${apiURL}/api/onDemand?blockId=${blockId}`).then(async(r) => {
|
||||
j = await r.json();
|
||||
if (j.status === "success") store.historyContent = j;
|
||||
})
|
||||
}
|
||||
if (j.status === "success" && j.status !== "rate-limit") {
|
||||
if (j.text) {
|
||||
eid(elementId).innerHTML = `<button class="switch bottom-margin" onclick="restoreUpdateHistory()">${loc.collapseHistory}</button>${j.text}`;
|
||||
} else {
|
||||
throw new Error()
|
||||
}
|
||||
} else {
|
||||
throw new Error()
|
||||
if (j && j.status === "success") {
|
||||
store.historyContent = j;
|
||||
} else throw new Error();
|
||||
}).catch(() => { throw new Error() });
|
||||
}
|
||||
if (j.text) {
|
||||
eid(elementId).innerHTML = `<button class="switch bottom-margin" onclick="restoreUpdateHistory()">${loc.collapseHistory}</button>${j.text}`;
|
||||
} else throw new Error()
|
||||
} catch (e) {
|
||||
eid(elementId).innerHTML = store.historyButton;
|
||||
internetError()
|
||||
@ -461,6 +478,7 @@ window.onload = () => {
|
||||
eid("footer").style.visibility = 'visible';
|
||||
eid("url-input-area").value = "";
|
||||
notificationCheck();
|
||||
loadCelebrationsEmoji();
|
||||
if (isIOS) sSet("downloadPopup", "true");
|
||||
let urlQuery = new URLSearchParams(window.location.search).get("u");
|
||||
if (urlQuery !== null && regex.test(urlQuery)) {
|
||||
|
@ -1,4 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.44 3.385C17.77 3.765 18.33 3.835 18.74 3.545C19.54 2.975 20.58 3.835 20.17 4.715L17.75 7.485H13.88L11.44 4.685C11.03 3.795 12.07 2.935 12.87 3.505L12.94 3.555C13.35 3.845 13.91 3.765 14.23 3.385L15.1 2.345C15.48 1.885 16.19 1.885 16.57 2.345L17.44 3.385ZM4 21.2249C4 14.6849 9.3 9.38489 15.84 9.38489C22.38 9.38489 27.68 14.6849 27.67 21.2249C27.67 26.0549 23.75 29.9749 18.92 29.9749H12.75C7.92 29.9749 4 26.0549 4 21.2249Z" fill="#FFB02E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.12 7.46484H17.56C18.17 7.46484 18.67 7.96484 18.67 8.57484C18.67 9.18484 18.17 9.68484 17.56 9.68484H14.12C13.51 9.68484 13.01 9.18484 13.01 8.57484C13.01 7.96484 13.51 7.46484 14.12 7.46484ZM15.8401 19.3449H15.9101C17.4101 19.3849 18.6101 20.6049 18.6101 22.1049C18.6101 23.3849 17.7401 24.4549 16.5601 24.7749V25.8949C16.5601 26.2949 16.2401 26.6149 15.8401 26.6149C15.4401 26.6149 15.1201 26.2949 15.1201 25.8949V24.7749C13.9401 24.4549 13.0701 23.3849 13.0701 22.1049C13.0701 21.7049 13.3901 21.3849 13.7901 21.3849C14.1901 21.3849 14.5101 21.7049 14.5101 22.1049C14.5101 22.8449 15.1001 23.4349 15.8301 23.4349C16.5601 23.4349 17.1601 22.8349 17.1601 22.1049C17.1601 21.3749 16.5601 20.7749 15.8301 20.7749H15.7601C14.2701 20.7349 13.0701 19.5149 13.0701 18.0149C13.0701 16.7349 13.9401 15.6649 15.1201 15.3449V14.2249C15.1201 13.8249 15.4401 13.5049 15.8401 13.5049C16.2401 13.5049 16.5601 13.8249 16.5601 14.2249V15.3449C17.7401 15.6649 18.6101 16.7349 18.6101 18.0149C18.6101 18.4149 18.2901 18.7349 17.8901 18.7349C17.4901 18.7349 17.1701 18.4149 17.1701 18.0149C17.1701 17.2849 16.5701 16.6849 15.8401 16.6849C15.1101 16.6849 14.5101 17.2849 14.5101 18.0149C14.5101 18.7449 15.1101 19.3449 15.8401 19.3449Z" fill="#8C5543"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.8 KiB |
5
src/front/emoji/sparkling_heart.svg
Normal file
5
src/front/emoji/sparkling_heart.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0222 4.15959C18.1822 4.65959 16.0022 7.99959 16.0022 7.99959C16.0022 7.99959 13.8222 4.65959 10.9822 4.15959C3.98216 2.92959 1.08217 9.06959 2.25217 13.6496C4.34217 21.7896 16.0022 28.9096 16.0022 28.9096C16.0022 28.9096 27.6722 21.7896 29.7522 13.6496C30.9322 9.06959 28.0322 2.92959 21.0222 4.15959Z" fill="#F92F60" />
|
||||
<path d="M24.6221 8.98956C24.7221 9.21956 24.9021 9.40956 25.1321 9.50956L26.0921 9.92956C26.3821 10.0696 26.3821 10.4796 26.0921 10.6196L25.1221 11.0496C24.8921 11.1496 24.7121 11.3396 24.6121 11.5696L23.8521 13.3596C23.7121 13.6396 23.3021 13.6396 23.1621 13.3596L22.4021 11.5696C22.3021 11.3396 22.1221 11.1496 21.8921 11.0496L20.9221 10.6196C20.6321 10.4796 20.6321 10.0696 20.9221 9.92956L21.8921 9.49956C22.1221 9.39956 22.3021 9.20956 22.4021 8.97956L23.1721 7.19956C23.3121 6.91956 23.7221 6.91956 23.8621 7.19956L24.6221 8.98956Z" fill="#FCD53F" />
|
||||
<path d="M10.552 18.8796C10.742 19.3096 11.072 19.6596 11.502 19.8496L13.282 20.6496C13.812 20.9096 13.812 21.6596 13.282 21.9196L11.502 22.7196C11.072 22.9196 10.732 23.2596 10.552 23.6896L9.14197 26.9996C8.88197 27.5296 8.13197 27.5296 7.87197 26.9996L6.46197 23.6896C6.27197 23.2596 5.94197 22.9096 5.51197 22.7196L3.73197 21.9196C3.20197 21.6596 3.20197 20.9096 3.73197 20.6496L5.51197 19.8496C5.94197 19.6496 6.28197 19.3096 6.46197 18.8796L7.87197 15.5696C8.13197 15.0396 8.88197 15.0396 9.14197 15.5696L10.552 18.8796Z" fill="#FCD53F" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/front/updateBanners/catphonestand.webp
Normal file
BIN
src/front/updateBanners/catphonestand.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 866 KiB |
@ -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 |
@ -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 |
@ -47,7 +47,7 @@
|
||||
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
|
||||
"LinkGitHubChanges": ">> see previous commits and contribute on github",
|
||||
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.",
|
||||
"DownloadPopupDescriptionIOS": "press and hold the download button, hide the video preview, and then select \"download linked file\" to save.",
|
||||
"DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request popup on top, and press \"always allow\".\n\nalternative method: press and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".",
|
||||
"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",
|
||||
@ -71,7 +71,7 @@
|
||||
"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!",
|
||||
"ErrorNoVideosInTweet": "i couldn't find any media content in this tweet. try another one!",
|
||||
"ImagePickerTitle": "pick images to download",
|
||||
"ImagePickerDownloadAudio": "download audio",
|
||||
"ImagePickerExplanationPC": "right click an image to save it.",
|
||||
@ -94,9 +94,9 @@
|
||||
"ChangelogPressToHide": "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 turns out developing and keeping up a web service used by over 150,000 people is not that easy.\n\nif you ever found {appName} useful and want to help continue its development and support, or simply want to thank the developer, consider chipping in! every cent helps and is VERY appreciated :D\n\ncurrently, i have big (scaling) plans, and i need your help. {appName}'s usage is growing daily, so i need to make up for it. <span class=\"text-backdrop\">donations are more appreciated than ever.</span>\n\ni am yet to earn anything from {appName}, everything goes back to users, so you're essentially helping everyone.",
|
||||
"DonateVia": "donate via",
|
||||
"DonateHireMe": "or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a>",
|
||||
"DonateHireMe": "...or you can <a class=\"text-backdrop italic\" 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}.",
|
||||
@ -120,6 +120,7 @@
|
||||
"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.",
|
||||
"SettingsAccessibilitySubtitle": "accessibility",
|
||||
"SettingsAccessibilityLanguage": "interface language:",
|
||||
"ShareURL": "share"
|
||||
"ShareURL": "share",
|
||||
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!"
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@
|
||||
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
|
||||
"LinkGitHubChanges": ">> смотри предыдущие изменения на github",
|
||||
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
|
||||
"DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.",
|
||||
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод: зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
|
||||
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
|
||||
"DownloadPopupWayToSave": "выбери, как сохранить",
|
||||
"ClickToCopy": "нажми, чтобы скопировать",
|
||||
@ -71,7 +71,7 @@
|
||||
"SettingsAudioFullTikTok": "полное аудио",
|
||||
"SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.",
|
||||
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.",
|
||||
"ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!",
|
||||
"ErrorNoVideosInTweet": "я не смог найти никакого медиа контента в этом твите. попробуй другой!",
|
||||
"ImagePickerTitle": "выбери картинки для скачивания",
|
||||
"ImagePickerDownloadAudio": "скачать звук",
|
||||
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.",
|
||||
@ -94,9 +94,9 @@
|
||||
"ChangelogPressToHide": "скрыть",
|
||||
"Donate": "задонатить",
|
||||
"DonateSub": "ты можешь помочь!",
|
||||
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются более 40 тысяч людей, обходится довольно дорого.\n\nесли {appName} тебе помог и ты хочешь поблагодарить или помочь разработчику, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)",
|
||||
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что разработка и поддержка сервиса, которым пользуются более 150 тысяч людей, обходится довольно затратно.\n\nесли {appName} тебе помог и ты хочешь поблагодарить разработчика, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)",
|
||||
"DonateVia": "открыть",
|
||||
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>",
|
||||
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
|
||||
"SettingsVideoMute": "убрать аудио",
|
||||
"SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, но только когда это возможно.",
|
||||
"ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",
|
||||
@ -118,6 +118,7 @@
|
||||
"SettingsDubAuto": "авто",
|
||||
"SettingsVimeoPrefer": "тип загрузок с vimeo",
|
||||
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".",
|
||||
"ShareURL": "поделиться"
|
||||
"ShareURL": "поделиться",
|
||||
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as fs from "fs";
|
||||
import { appName, repo } from "../modules/config.js";
|
||||
import { appName, links, repo } from "../modules/config.js";
|
||||
import loadJson from "../modules/sub/loadJSON.js";
|
||||
|
||||
const locPath = './src/localization/languages'
|
||||
const locPath = './src/localization/languages';
|
||||
|
||||
let loc = {}
|
||||
let languages = [];
|
||||
@ -21,7 +21,7 @@ export function loadLoc() {
|
||||
loadLoc();
|
||||
|
||||
export function replaceBase(s) {
|
||||
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "•");
|
||||
return s.replace(/\n/g, '<br/>').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "•");
|
||||
}
|
||||
export function replaceAll(lang, str, string, replacement) {
|
||||
let s = replaceBase(str[string])
|
||||
|
@ -1,11 +1,16 @@
|
||||
{
|
||||
"current": {
|
||||
"version": "5.4",
|
||||
"title": "instagram support, hop, docker, and more!",
|
||||
"banner": "catphonestand.webp",
|
||||
"content": "something many of you've been waiting for is finally here! try it out and let me know what you think :)\n\n<span class='text-backdrop'>tl;dr:</span>\n*; added experimental instagram support! download any reels or videos you like, and make sure to report any issues you encounter. yes, you can convert either to audio.\n*; fixed support for on.soundcloud links.\n*; added share button to \"how to save?\" popup.\n*; added docker support.\n*; main instance is now powered by <a class=\"text-backdrop italic\" href=\"https://hop.io/\" target=\"_blank\">hop.io</a>.\n\nservice improvements:\n*; added experimental support for videos from instagram. currently only reels and post videos are downloadable, but i'm looking into ways to save high resolution photos too. if you experience any issues, please report them on either of support platforms.\n*; fixed support for on.soundcloud share links. should work just as well as other versions!\n*; fixed an issue that made some youtube videos impossible to download.\n\ninterface improvements:\n*; new css-only checkmark! yes, i can't stop tinkering with it because slight flashing on svg load annoyed me. now it loads instantly (and also looks slightly better).\n*; fixed copy animation.\n*; minor localization improvements.\n*; fixed the embed logo that i broke somewhere in between 5.3 and 5.4.\n\ninternal improvements:\n*; now using nanoid for live render stream ids.\n*; added support for docker. it's kind of clumsy because of how i get .git folder inside the container, but if you know how to do it better, feel free to make a pr.\n*; cobalt now checks only for existence of environment variables, not exactly the .env file.\n*; changed the way user ip address is retrieved for instances using cloudflare.\n*; added ability to disable cors, both to setup script and environment variables.\n*; moved main instance to <a class=\"text-backdrop italic\" href=\"https://hop.io/\" target=\"_blank\">hop.io</a> infra. there should no longer be random downtimes. huge shout out to the hop team for being so nice and helping me out :D\n\ni can't believe how diverse and widespread cobalt has become. it's used in all fields: music production, education, content creation, and even game development. <span class='text-backdrop'>thank you</span>. this is absolutely nuts.\nif you don't mind sharing, please tell me about your use case. i'd really love to hear how you use cobalt and how i could make it even more useful for you."
|
||||
},
|
||||
"history": [{
|
||||
"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",
|
||||
|
@ -16,4 +16,5 @@ export const
|
||||
donations = config.donations,
|
||||
ffmpegArgs = config.ffmpegArgs,
|
||||
supportedAudio = config.supportedAudio,
|
||||
celebrations = config.celebrations
|
||||
celebrations = config.celebrations,
|
||||
links = config.links
|
||||
|
@ -1,7 +1,6 @@
|
||||
const names = {
|
||||
"🎶": "musical_notes",
|
||||
"🎬": "clapper_board",
|
||||
"💰": "money_bag",
|
||||
"🎉": "party_popper",
|
||||
"❓": "question_mark",
|
||||
"✨": "sparkles",
|
||||
@ -23,7 +22,8 @@ const names = {
|
||||
"🐦": "bird",
|
||||
"🐙": "octopus",
|
||||
"🔮": "crystal_ball",
|
||||
"💪": "biceps"
|
||||
"💪": "biceps",
|
||||
"💖": "sparkling_heart"
|
||||
}
|
||||
let sizing = {
|
||||
22: 0.4,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { celebrations } from "../config.js";
|
||||
import emoji from "../emoji.js";
|
||||
|
||||
export function switcher(obj) {
|
||||
let items = ``;
|
||||
@ -11,7 +12,7 @@ export function switcher(obj) {
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.noParent) return `<div class="switches">${items}</div>`;
|
||||
if (obj.noParent) return `<div id="${obj.name}" class="switches">${items}</div>`;
|
||||
return `<div id="${obj.name}-switcher" class="switch-container">
|
||||
${obj.subtitle ? `<div class="subtitle">${obj.subtitle}</div>` : ``}
|
||||
<div class="switches">${items}</div>
|
||||
@ -130,8 +131,8 @@ export function popupWithBottomButtons(obj) {
|
||||
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>`
|
||||
export function socialLink(emji, name, handle, url) {
|
||||
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop italic" href="${url}" target="_blank">${handle}</a></div>`
|
||||
}
|
||||
export function settingsCategory(obj) {
|
||||
return `<div id="settings-${obj.name}" class="settings-category">
|
||||
@ -151,12 +152,20 @@ export function footerButtons(obj) {
|
||||
items += `<button id="${obj[i]["name"]}-footer" class="switch footer-button" onclick="${obj[i]["action"]}()" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>`;
|
||||
break;
|
||||
case "popup":
|
||||
let context = obj[i]["context"] ? `, '${obj[i]["context"]}'` : ''
|
||||
let context2 = obj[i+1] && obj[i+1]["context"] ? `, '${obj[i+1]["context"]}'` : ''
|
||||
let buttonName = obj[i]["context"] ? `${obj[i]["name"]}-${obj[i]["context"]}` : obj[i]["name"],
|
||||
context = obj[i]["context"] ? `, '${obj[i]["context"]}'` : '',
|
||||
buttonName2,
|
||||
context2;
|
||||
|
||||
if (obj[i+1]) {
|
||||
buttonName2 = obj[i+1]["context"] ? `${obj[i+1]["name"]}-${obj[i+1]["context"]}` : obj[i+1]["name"];
|
||||
context2 = obj[i+1]["context"] ? `, '${obj[i+1]["context"]}'` : '';
|
||||
}
|
||||
|
||||
items += `
|
||||
<div class="footer-pair">
|
||||
<button id="${obj[i]["name"]}-footer" class="switch footer-button" onclick="popup('${obj[i]["name"]}', 1${context})" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>
|
||||
${obj[i+1] ? `<button id="${obj[i+1]["name"]}-footer" class="switch footer-button" onclick="popup('${obj[i+1]["name"]}', 1${context2})" aria-label="${obj[i+1]["aria"]}">${obj[i+1]["text"]}</button>`: ''}
|
||||
<button id="${buttonName}-footer" class="switch footer-button" onclick="popup('${obj[i]["name"]}', 1${context})" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>
|
||||
${obj[i+1] ? `<button id="${buttonName2}-footer" class="switch footer-button" onclick="popup('${obj[i+1]["name"]}', 1${context2})" aria-label="${obj[i+1]["aria"]}">${obj[i+1]["text"]}</button>`: ''}
|
||||
</div>`;
|
||||
i++;
|
||||
break;
|
||||
@ -169,9 +178,14 @@ export function explanation(text) {
|
||||
return `<div class="explanation">${text}</div>`
|
||||
}
|
||||
export function celebrationsEmoji() {
|
||||
let n = new Date().toISOString().split('T')[0].split('-');
|
||||
let dm = `${n[1]}-${n[2]}`;
|
||||
return Object.keys(celebrations).includes(dm) ? celebrations[dm] : "🐲";
|
||||
try {
|
||||
let n = new Date().toISOString().split('T')[0].split('-');
|
||||
let dm = `${n[1]}-${n[2]}`;
|
||||
let f = Object.keys(celebrations).includes(dm) ? celebrations[dm] : "🐲";
|
||||
return f != "🐲" ? emoji(f, 22) : false;
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function dropdownSelect(label, action, options) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { backdropLink, celebrationsEmoji, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, dropdownSelect } from "./elements.js";
|
||||
import { backdropLink, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, dropdownSelect } from "./elements.js";
|
||||
|
||||
import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
|
||||
import { getCommitInfo } from "../sub/currentCommit.js";
|
||||
import loc, { languagePickerNames } from "../../localization/manager.js";
|
||||
@ -106,6 +107,7 @@ export default function(obj) {
|
||||
"title": t("CollapsePrivacy"),
|
||||
"body": t("PrivacyPolicy")
|
||||
}])
|
||||
+ `${process.env.DEPLOYMENT_ID && process.env.INTERNAL_IP ? '<a id="hop-attribution" class="explanation" href="https://hop.io/" target="_blank">powered by hop.io</a>' : ''}`
|
||||
}]
|
||||
})
|
||||
}, {
|
||||
@ -148,7 +150,7 @@ export default function(obj) {
|
||||
})
|
||||
}, {
|
||||
name: "donate",
|
||||
title: `${emoji("💰")} ${t('DonationsTab')}`,
|
||||
title: `${emoji("💖")} ${t('DonationsTab')}`,
|
||||
content: popup({
|
||||
name: "donate",
|
||||
header: {
|
||||
@ -395,13 +397,13 @@ export default function(obj) {
|
||||
footerButtons([{
|
||||
name: "about",
|
||||
type: "popup",
|
||||
text: `${emoji(celebrationsEmoji() , 22)} ${t('AboutTab')}`,
|
||||
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
|
||||
aria: t('AccessibilityOpenAbout')
|
||||
}, {
|
||||
name: "about",
|
||||
type: "popup",
|
||||
context: "donate",
|
||||
text: `${emoji("💰", 22)} ${t('Donate')}`,
|
||||
text: `${emoji("💖", 22)} ${t('Donate')}`,
|
||||
aria: t('AccessibilityOpenDonate')
|
||||
}, {
|
||||
name: "settings",
|
||||
|
@ -15,6 +15,8 @@ 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 instagram from "./services/instagram.js";
|
||||
import vine from "./services/vine.js";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
@ -102,6 +104,12 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
format: obj.aFormat
|
||||
});
|
||||
break;
|
||||
case "instagram":
|
||||
r = await instagram({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "vine":
|
||||
r = await vine({ id: patternMatch["id"] });
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
|
@ -52,6 +52,8 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
params = { type: "bridge" };
|
||||
break;
|
||||
|
||||
case "vine":
|
||||
case "instagram":
|
||||
case "tumblr":
|
||||
case "twitter":
|
||||
responseType = 1;
|
||||
@ -72,6 +74,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
case "picker":
|
||||
responseType = 5;
|
||||
switch (host) {
|
||||
case "instagram":
|
||||
case "twitter":
|
||||
params = { picker: r.picker };
|
||||
break;
|
||||
|
36
src/modules/processing/services/instagram.js
Normal file
36
src/modules/processing/services/instagram.js
Normal file
@ -0,0 +1,36 @@
|
||||
import got from "got";
|
||||
|
||||
export default async function(obj) {
|
||||
// i hate this implementation but fetch doesn't work here for some reason (i personally blame facebook)
|
||||
let html;
|
||||
try {
|
||||
html = await got.get(`https://www.instagram.com/p/${obj.id}/`)
|
||||
html.on('error', () => {
|
||||
html = false;
|
||||
});
|
||||
html = html ? html.body : false;
|
||||
} catch (e) {
|
||||
html = false;
|
||||
}
|
||||
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!html.includes('application/ld+json')) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
let single, multiple = [], postInfo = JSON.parse(html.split('script type="application/ld+json"')[1].split('">')[1].split('</script>')[0]);
|
||||
|
||||
if (postInfo.video.length > 1) {
|
||||
for (let i in postInfo.video) { multiple.push({type: "video", thumb: postInfo.video[i]["thumbnailUrl"], url: postInfo.video[i]["contentUrl"]}) }
|
||||
} else if (postInfo.video.length === 1) {
|
||||
single = postInfo.video[0]["contentUrl"]
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
||||
if (single) {
|
||||
return { urls: single, filename: `instagram_${obj.id}.mp4`, audioFilename: `instagram_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
}
|
@ -36,12 +36,12 @@ async function findClientID() {
|
||||
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 });
|
||||
html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (obj.author && obj.song) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false });
|
||||
}
|
||||
if (!html) return { error: 'ErrorCouldntFetch'};
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (!(html.includes('<script>window.__sc_hydration = ')
|
||||
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
|
||||
&& html.includes('{"hydratable":"sound","data":'))) {
|
||||
|
@ -10,5 +10,5 @@ export default async function(obj) {
|
||||
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` }
|
||||
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, filename: `tumblr_${obj.id}.mp4`, audioFilename: `tumblr_${obj.id}_audio` }
|
||||
}
|
||||
|
@ -3,52 +3,60 @@ 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"
|
||||
const apiURL = "https://api.twitter.com"
|
||||
|
||||
// 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"
|
||||
"host": "api.twitter.com",
|
||||
"x-twitter-client-language": "en",
|
||||
"x-twitter-active-user": "yes",
|
||||
"Accept-Language": "en"
|
||||
};
|
||||
let req_act = await fetch(`${apiURL}/guest/activate.json`, {
|
||||
let conversationURL = `${apiURL}/2/timeline/conversation/${obj.id}.json?cards_platform=Web-12&tweet_mode=extended&include_cards=1&include_ext_media_availability=true&include_ext_sensitive_media_warning=true&simple_quoted_tweet=true&trim_user=1`;
|
||||
let activateURL = `${apiURL}/1.1/guest/activate.json`;
|
||||
|
||||
let req_act = await fetch(activateURL, {
|
||||
method: "POST",
|
||||
headers: _headers
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
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`;
|
||||
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
|
||||
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) {
|
||||
let conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) {
|
||||
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
|
||||
// ^ explicit content, but no multi media support
|
||||
delete _headers["x-guest-token"]
|
||||
delete _headers["x-guest-token"];
|
||||
delete _headers["cookie"];
|
||||
|
||||
req_act = await fetch(`${apiURL}/guest/activate.json`, {
|
||||
req_act = await fetch(activateURL, {
|
||||
method: "POST",
|
||||
headers: _headers
|
||||
}).then((r) => { return r.status === 200 ? r.json() : false}).catch(() => { return false });
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
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' };
|
||||
_headers['cookie'] = `guest_id=v1%3A${req_act["guest_token"]};`;
|
||||
|
||||
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"]
|
||||
conversation = await fetch(conversationURL, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
|
||||
}
|
||||
if (!baseStatus) return { error: 'ErrorNoVideosInTweet' };
|
||||
if (!conversation || !conversation.globalObjects.tweets[obj.id]) return { error: 'ErrorTweetUnavailable' };
|
||||
|
||||
let single, multiple = [], media = baseStatus["media"];
|
||||
let baseMedia, baseTweet = conversation.globalObjects.tweets[obj.id];
|
||||
if (baseTweet.retweeted_status_id_str && conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities) {
|
||||
baseMedia = conversation.globalObjects.tweets[baseTweet.retweeted_status_id_str].extended_entities
|
||||
} else if (baseTweet.extended_entities && baseTweet.extended_entities.media) {
|
||||
baseMedia = baseTweet.extended_entities
|
||||
}
|
||||
if (!baseMedia) return { error: 'ErrorNoVideosInTweet' };
|
||||
|
||||
let single, multiple = [], media = baseMedia["media"];
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
|
||||
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"])}) }
|
||||
|
8
src/modules/processing/services/vine.js
Normal file
8
src/modules/processing/services/vine.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default async function(obj) {
|
||||
let post = await fetch(`https://archive.vine.co/posts/${obj.id}.json`).then((r) => { return r.json() }).catch(() => { return false });
|
||||
if (!post) return { error: 'ErrorEmptyDownload' };
|
||||
|
||||
if (post.videoUrl) return { urls: post.videoUrl.replace("http://", "https://"), filename: `vine_${obj.id}.mp4`, audioFilename: `vine_${obj.id}_audio` };
|
||||
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
@ -22,7 +22,7 @@ const representationMatch = {
|
||||
}
|
||||
|
||||
export default async function(o) {
|
||||
let html;
|
||||
let html, url, filename = `vk_${o.userId}_${o.videoId}_`;
|
||||
html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, {
|
||||
headers: { "user-agent": genericUserAgent }
|
||||
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||
@ -35,15 +35,25 @@ export default async function(o) {
|
||||
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 })),
|
||||
if (js.player.params[0]["manifest"]) {
|
||||
let mpd = JSON.parse(xml2json(js.player.params[0]["manifest"], { compact: true, spaces: 4 })),
|
||||
repr = mpd.MPD.Period.AdaptationSet.Representation ? mpd.MPD.Period.AdaptationSet.Representation : mpd.MPD.Period.AdaptationSet[0]["Representation"],
|
||||
bestQuality = repr[repr.length - 1],
|
||||
resolutionPick = Number(bestQuality._attributes.width) > Number(bestQuality._attributes.height) ? 'width': 'height';
|
||||
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
|
||||
|
||||
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`
|
||||
if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality];
|
||||
|
||||
url = js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`];
|
||||
filename += `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4`
|
||||
|
||||
} else if (js.player.params[0]["url240"]) { // fallback for when video is too old
|
||||
url = js.player.params[0]["url240"];
|
||||
filename += `320x240.mp4`
|
||||
}
|
||||
|
||||
if (url && filename) return {
|
||||
urls: url,
|
||||
filename: filename
|
||||
};
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
|
@ -73,10 +73,9 @@ export default async function(o) {
|
||||
};
|
||||
return r
|
||||
}
|
||||
|
||||
let checkSingle = (i) => ((i['quality_label'].split('p')[0] === quality || i['quality_label'].split('p')[0] === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
|
||||
checkBestVideo = (i) => (i['quality_label'].split('p')[0] === bestQuality && !i["has_audio"] && i["has_video"]),
|
||||
checkRightVideo = (i) => (i['quality_label'].split('p')[0] === quality && !i["has_audio"] && i["has_video"]);
|
||||
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));
|
||||
|
@ -51,6 +51,17 @@
|
||||
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
|
||||
"bestAudio": "none",
|
||||
"enabled": true
|
||||
},
|
||||
"instagram": {
|
||||
"alias": "instagram reels & video posts",
|
||||
"patterns": ["reels/:id", "reel/:id", "p/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"vine": {
|
||||
"alias": "vine archive",
|
||||
"tld": "co",
|
||||
"patterns": ["v/:id"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ export const testers = {
|
||||
|| (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13),
|
||||
|
||||
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"]
|
||||
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
|
||||
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length <= 10),
|
||||
|
||||
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12),
|
||||
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11),
|
||||
"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),
|
||||
@ -24,5 +24,9 @@ export const testers = {
|
||||
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
|
||||
|
||||
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"]
|
||||
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32))
|
||||
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32)),
|
||||
|
||||
"instagram": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12)
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import { existsSync, unlinkSync, appendFileSync } from "fs";
|
||||
import { createInterface } from "readline";
|
||||
import { Cyan, Bright, Green } from "./sub/consoleText.js";
|
||||
import { Cyan, Bright } from "./sub/consoleText.js";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
let envPath = './.env';
|
||||
let q = `${Cyan('?')} \x1b[1m`;
|
||||
let ob = { streamSalt: randomBytes(64).toString('hex') }
|
||||
let ob = {}
|
||||
let rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let final = () => {
|
||||
@ -42,6 +41,12 @@ rl.question(q, r1 => {
|
||||
rl.question(q, r2 => {
|
||||
if (r2) ob['port'] = r2
|
||||
if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
|
||||
final()
|
||||
|
||||
console.log(Bright("\nWould you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)"))
|
||||
|
||||
rl.question(q, r3 => {
|
||||
if (r3.toLowerCase() !== 'y') ob['cors'] = '0'
|
||||
final()
|
||||
})
|
||||
});
|
||||
})
|
||||
|
@ -1,11 +1,12 @@
|
||||
import NodeCache from "node-cache";
|
||||
import { randomBytes } from "crypto";
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { sha256 } from "../sub/crypto.js";
|
||||
import { streamLifespan } from "../config.js";
|
||||
|
||||
const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true });
|
||||
const salt = process.env.streamSalt;
|
||||
const streamSalt = randomBytes(64).toString('hex');
|
||||
|
||||
streamCache.on("expired", (key) => {
|
||||
streamCache.del(key);
|
||||
@ -14,7 +15,7 @@ streamCache.on("expired", (key) => {
|
||||
export function createStream(obj) {
|
||||
let streamID = nanoid(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, salt);
|
||||
ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, streamSalt);
|
||||
|
||||
if (!streamCache.has(streamID)) {
|
||||
streamCache.set(streamID, {
|
||||
@ -47,7 +48,7 @@ export function verifyStream(ip, id, hmac, exp) {
|
||||
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);
|
||||
let ghmac = sha256(`${id},${ip},${streamInfo.service},${exp}`, streamSalt);
|
||||
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
|
||||
&& String(ip) === streamInfo.ip && Number(exp) > Math.floor(new Date().getTime())) {
|
||||
return streamInfo;
|
||||
|
@ -2,7 +2,7 @@ import { spawn } from "child_process";
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
import got from "got";
|
||||
import { ffmpegArgs, genericUserAgent } from "../config.js";
|
||||
import { metadataManager, msToTime } from "../sub/utils.js";
|
||||
import { getThreads, metadataManager, msToTime } from "../sub/utils.js";
|
||||
|
||||
export function streamDefault(streamInfo, res) {
|
||||
try {
|
||||
@ -35,9 +35,9 @@ export function streamLiveRender(streamInfo, res) {
|
||||
return;
|
||||
}
|
||||
let audio = got.get(streamInfo.urls[1], { isStream: true });
|
||||
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
|
||||
'-loglevel', '-8',
|
||||
'-threads', `${getThreads()}`,
|
||||
'-i', streamInfo.urls[0],
|
||||
'-i', 'pipe:3',
|
||||
'-map', '0:v',
|
||||
@ -95,6 +95,7 @@ export function streamAudioOnly(streamInfo, res) {
|
||||
try {
|
||||
let args = [
|
||||
'-loglevel', '-8',
|
||||
'-threads', `${getThreads()}`,
|
||||
'-i', streamInfo.urls
|
||||
]
|
||||
if (streamInfo.metadata) {
|
||||
@ -141,6 +142,7 @@ export function streamVideoOnly(streamInfo, res) {
|
||||
try {
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
|
||||
'-loglevel', '-8',
|
||||
'-threads', `${getThreads()}`,
|
||||
'-i', streamInfo.urls,
|
||||
'-c', 'copy'
|
||||
]
|
||||
|
@ -65,6 +65,8 @@ export function cleanURL(url, host) {
|
||||
let forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@"]
|
||||
switch(host) {
|
||||
case "vk":
|
||||
url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0];
|
||||
break;
|
||||
case "youtube":
|
||||
url = url.split('&')[0];
|
||||
break;
|
||||
@ -147,3 +149,18 @@ export function checkJSONPost(obj) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export function getIP(req) {
|
||||
return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip;
|
||||
}
|
||||
export function getThreads() {
|
||||
try {
|
||||
if (process.env.ffmpegThreads && process.env.ffmpegThreads.length <= 3
|
||||
&& (Number(process.env.ffmpegThreads) >= 0 && Number(process.env.ffmpegThreads) <= 256)) {
|
||||
return process.env.ffmpegThreads
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
} catch (e) {
|
||||
return '0'
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "picker: mixed media (3 gifs + image)",
|
||||
"url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20",
|
||||
"name": "picker: mixed media (2 videos)",
|
||||
"url": "https://twitter.com/taehyungsflow/status/1583411488433516544",
|
||||
"params": {
|
||||
"aFormat": "mp3",
|
||||
"isAudioOnly": false,
|
||||
@ -97,6 +97,30 @@
|
||||
}
|
||||
}, {
|
||||
"name": "retweeted video",
|
||||
"url": "https://twitter.com/winload_exe/status/1639005390854602758",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "retweeted video",
|
||||
"url": "https://twitter.com/winload_exe/status/1639005390854602758",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "age-restricted video",
|
||||
"url": "https://twitter.com/FckyeahCharli/status/1650987582749065220",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "retweeted video, isAudioOnly",
|
||||
"url": "https://twitter.com/winload_exe/status/1633091769482063874",
|
||||
"params": {
|
||||
"aFormat": "mp3",
|
||||
@ -256,6 +280,22 @@
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "on.soundcloud link",
|
||||
"url": "https://on.soundcloud.com/wLZre",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "on.soundcloud link, different stream type",
|
||||
"url": "https://on.soundcloud.com/AG4c",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}],
|
||||
"youtube": [{
|
||||
"name": "4k video (h264, 1440)",
|
||||
@ -451,6 +491,14 @@
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "ancient video (fallback to 240p)",
|
||||
"url": "https://vk.com/video-1959_28496479",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "inexistent video",
|
||||
"url": "https://vk.com/video-53333333_456233333",
|
||||
@ -585,7 +633,7 @@
|
||||
}
|
||||
}, {
|
||||
"name": "images",
|
||||
"url": "https://vt.tiktok.com/ZS8JP89eB/",
|
||||
"url": "https://www.tiktok.com/@matryoshk4/video/7231234675476532526",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
@ -755,5 +803,95 @@
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}],
|
||||
"instagram": [{
|
||||
"name": "several videos in a post (picker)",
|
||||
"url": "https://www.instagram.com/p/CqifaD0qiDt/",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "picker"
|
||||
}
|
||||
}, {
|
||||
"name": "reel",
|
||||
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "regular video",
|
||||
"url": "https://www.instagram.com/p/CmCVWoIr9OH/",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "reel (isAudioOnly)",
|
||||
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
|
||||
"params": {
|
||||
"isAudioOnly": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "reel (isAudioMuted)",
|
||||
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
|
||||
"params": {
|
||||
"isAudioMuted": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "inexistent reel",
|
||||
"url": "https://www.instagram.com/reel/XXXXXXXXXX/",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 400,
|
||||
"status": "error"
|
||||
}
|
||||
}, {
|
||||
"name": "inexistent post",
|
||||
"url": "https://www.instagram.com/p/XXXXXXXXXX/",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 400,
|
||||
"status": "error"
|
||||
}
|
||||
}],
|
||||
"vine": [{
|
||||
"name": "regular vine link (9+10=21)",
|
||||
"url": "https://vine.co/v/huwVJIEJW50",
|
||||
"params": {},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "redirect"
|
||||
}
|
||||
}, {
|
||||
"name": "regular vine link (isAudioOnly)",
|
||||
"url": "https://vine.co/v/huwVJIEJW50",
|
||||
"params": {
|
||||
"isAudioOnly": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}, {
|
||||
"name": "regular vine link (isAudioMuted)",
|
||||
"url": "https://vine.co/v/huwVJIEJW50",
|
||||
"params": {
|
||||
"isAudioMuted": true
|
||||
},
|
||||
"expected": {
|
||||
"code": 200,
|
||||
"status": "stream"
|
||||
}
|
||||
}]
|
||||
}
|
Loading…
Reference in New Issue
Block a user