mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-14 01:08:27 +00:00
Merge branch 'current' into current
This commit is contained in:
commit
ff2eaba727
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" ]
|
10
README.md
10
README.md
@ -72,6 +72,16 @@ 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.
|
||||
|
||||
|
@ -43,8 +43,8 @@ Content live render streaming endpoint.<br>
|
||||
### Request Query Variables
|
||||
| key | variables | description |
|
||||
|:----|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| p | ``1`` | Used for checking the rate limit. |
|
||||
| t | Stream token | Unique stream identificator which is used for retrieving cached stream info data. |
|
||||
| p | ``1`` | Used for probing the rate limit. |
|
||||
| t | Stream token | Unique stream ID. Used for retrieving cached stream info data. |
|
||||
| h | HMAC | Hashed combination of: (hashed) ip address, stream token, expiry timestamp, and service name. Used for verification of stream. |
|
||||
| e | Expiry timestamp | |
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "5.2.2",
|
||||
"version": "5.4.4",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=17.5"
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/cobalt",
|
||||
@ -26,10 +26,11 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"esbuild": "^0.14.51",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.18.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",
|
||||
|
@ -2,7 +2,6 @@ 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';
|
||||
@ -13,7 +12,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";
|
||||
@ -22,20 +21,21 @@ import { changelogHistory } from "./modules/pageRender/onDemand.js";
|
||||
import { sha256 } from "./modules/sub/crypto.js";
|
||||
import findRendered from "./modules/pageRender/findRendered.js";
|
||||
|
||||
const commitHash = shortCommit();
|
||||
const branch = getCurrentBranch();
|
||||
const app = express();
|
||||
if (process.env.selfURL && process.env.streamSalt && process.env.port) {
|
||||
const commitHash = shortCommit();
|
||||
const branch = getCurrentBranch();
|
||||
const app = express();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.use('/api/:type', cors())
|
||||
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), process.env.streamSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
|
||||
return;
|
||||
@ -46,7 +46,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), process.env.streamSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
|
||||
return;
|
||||
@ -55,6 +55,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
|
||||
await buildFront(commitHash, branch);
|
||||
|
||||
app.use('/api/:type', cors(corsConfig));
|
||||
app.use('/api/json', apiLimiter);
|
||||
app.use('/api/stream', apiLimiterStream);
|
||||
app.use('/api/onDemand', apiLimiter);
|
||||
@ -63,7 +64,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) => {
|
||||
@ -92,7 +93,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), process.env.streamSalt);
|
||||
let lang = languageCode(req);
|
||||
let j = apiJSON(0, { t: "Bad request" });
|
||||
try {
|
||||
@ -111,14 +112,14 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
||||
res.status(j.status).json(j.body);
|
||||
return;
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
|
||||
return;
|
||||
res.destroy();
|
||||
return
|
||||
}
|
||||
});
|
||||
|
||||
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), process.env.streamSalt);
|
||||
switch (req.params.type) {
|
||||
case 'stream':
|
||||
if (req.query.p) {
|
||||
@ -164,6 +165,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)}`);
|
||||
});
|
||||
@ -177,7 +181,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`));
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
--line-height: 1.65rem;
|
||||
--red: rgb(255, 0, 61);
|
||||
--color: rgb(107, 67, 139);
|
||||
--gap: 0.6rem;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@ -81,37 +82,51 @@ a {
|
||||
:focus-visible {
|
||||
outline: var(--border-15);
|
||||
}
|
||||
.checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0.55rem 1rem 0.55rem 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;
|
||||
}
|
||||
[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;
|
||||
@ -146,9 +161,11 @@ button:active,
|
||||
background: var(--accent-press);
|
||||
cursor: pointer;
|
||||
}
|
||||
.desktop .switch.text-backdrop:hover,
|
||||
.switch.text-backdrop,
|
||||
.switch.text-backdrop:hover,
|
||||
.switch.text-backdrop:active,
|
||||
.desktop .text-to-copy.text-backdrop:hover,
|
||||
.text-to-copy.text-backdrop,
|
||||
.text-to-copy.text-backdrop:hover,
|
||||
.text-to-copy.text-backdrop:active {
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
@ -157,9 +174,6 @@ button:active,
|
||||
cursor: pointer;
|
||||
transform: scale(0.95)
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.button {
|
||||
background: none;
|
||||
border: var(--border-15);
|
||||
@ -179,14 +193,17 @@ input[type="checkbox"] {
|
||||
position: fixed;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#logo-area {
|
||||
padding-right: 3rem;
|
||||
padding-top: 0.1rem;
|
||||
#logo {
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
width: 7rem;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#download-area {
|
||||
display: flex;
|
||||
@ -195,12 +212,11 @@ input[type="checkbox"] {
|
||||
}
|
||||
#cobalt-main-box #top {
|
||||
display: inline-flex;
|
||||
height: 2rem;
|
||||
margin-top: -0.6rem;
|
||||
height: 2.5rem;
|
||||
flex-direction: row;
|
||||
}
|
||||
#cobalt-main-box #bottom {
|
||||
padding-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@ -212,7 +228,7 @@ input[type="checkbox"] {
|
||||
}
|
||||
#url-input-area {
|
||||
background: var(--background);
|
||||
padding: 1.2rem 1rem;
|
||||
padding: 0 1rem;
|
||||
width: 100%;
|
||||
color: var(--accent);
|
||||
border: 0;
|
||||
@ -222,13 +238,11 @@ input[type="checkbox"] {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
#url-clear {
|
||||
height: 100%;
|
||||
background: none;
|
||||
padding: 0 1.1rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 1rem 0.2rem;
|
||||
transform: none;
|
||||
line-height: 0;
|
||||
height: 1.6rem;
|
||||
margin-top: .4rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
#url-input-area:focus {
|
||||
outline: none;
|
||||
@ -260,7 +274,7 @@ input[type="checkbox"] {
|
||||
#cobalt-main-box #bottom,
|
||||
#footer-buttons,
|
||||
#footer-buttons, .footer-pair {
|
||||
gap: 0.6rem;
|
||||
gap: var(--gap);
|
||||
}
|
||||
#footer-buttons, .footer-pair {
|
||||
display: flex;
|
||||
@ -270,7 +284,7 @@ input[type="checkbox"] {
|
||||
.footer-button {
|
||||
width: auto!important;
|
||||
color: var(--accent-unhover-2);
|
||||
padding: 0.6rem 1.2rem!important;
|
||||
padding: var(--gap) 1.2rem!important;
|
||||
align-content: center;
|
||||
}
|
||||
.notification-dot {
|
||||
@ -339,7 +353,6 @@ input[type="checkbox"] {
|
||||
}
|
||||
.changelog-banner {
|
||||
width: 100%;
|
||||
background-color: var(--accent-button-bg);
|
||||
max-height: 300px;
|
||||
margin-bottom: 1.65rem;
|
||||
float: left;
|
||||
@ -447,20 +460,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%;
|
||||
}
|
||||
@ -488,7 +487,7 @@ input[type="checkbox"] {
|
||||
.switch {
|
||||
padding: 0.7rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
color: var(--accent);
|
||||
background: var(--accent-button-bg);
|
||||
display: flex;
|
||||
@ -577,6 +576,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 {
|
||||
@ -601,7 +601,7 @@ input[type="checkbox"] {
|
||||
position: absolute;
|
||||
background: var(--background);
|
||||
color: var(--accent);
|
||||
padding: 0.3rem 0.6rem;
|
||||
padding: 0.3rem var(--gap);
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
margin: 0.4rem;
|
||||
@ -612,7 +612,7 @@ input[type="checkbox"] {
|
||||
}
|
||||
#cobalt-main-box #bottom button {
|
||||
width: auto;
|
||||
padding: 0.6rem 1.2rem;
|
||||
padding: var(--gap) 1.2rem;
|
||||
}
|
||||
.collapse-list {
|
||||
background: var(--accent-press);
|
||||
@ -648,7 +648,17 @@ input[type="checkbox"] {
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.expanded .collapse-body {
|
||||
display: block
|
||||
display: block;
|
||||
}
|
||||
#download-switcher .switches {
|
||||
gap: var(--gap);
|
||||
}
|
||||
#pd-share {
|
||||
display: none;
|
||||
}
|
||||
#hop-attribution {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
/* adapt the page according to screen size */
|
||||
@media screen and (min-width: 2300px) {
|
||||
@ -723,11 +733,6 @@ input[type="checkbox"] {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 720px) {
|
||||
#leftHandedLayout-chkbx {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* mobile page */
|
||||
@media screen and (max-width: 720px) {
|
||||
#cobalt-main-box, #footer {
|
||||
@ -753,9 +758,18 @@ input[type="checkbox"] {
|
||||
font-size: 1.3rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.footer-button {
|
||||
.footer-button,
|
||||
#audioMode-false,
|
||||
#audioMode-true,
|
||||
#paste {
|
||||
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;
|
||||
}
|
||||
@ -772,27 +786,14 @@ input[type="checkbox"] {
|
||||
.category-title {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.footer-button .emoji {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 720px) {
|
||||
#cobalt-main-box #bottom {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
#cobalt-main-box #bottom button {
|
||||
width: 100%;
|
||||
}
|
||||
#cobalt-main-box #bottom {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
#cobalt-main-box #bottom[data-lefthanded="true"] {
|
||||
flex-direction: row;
|
||||
}
|
||||
#pasteFromClipboard .emoji {
|
||||
margin-right: 0;
|
||||
}
|
||||
#pasteFromClipboard {
|
||||
width: 20%!important;
|
||||
font-size: 0;
|
||||
}
|
||||
#footer {
|
||||
bottom: 4%;
|
||||
transform: translate(-50%, 0%);
|
||||
@ -804,19 +805,17 @@ input[type="checkbox"] {
|
||||
.footer-pair .footer-button {
|
||||
width: 100%!important;
|
||||
}
|
||||
#logo-area {
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
position: fixed;
|
||||
line-height: 0;
|
||||
margin-top: -2rem;
|
||||
#logo {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
#cobalt-main-box {
|
||||
display: flex;
|
||||
border: none;
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 949px) {
|
||||
|
@ -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 = 26;
|
||||
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>`
|
||||
|
||||
@ -13,9 +13,10 @@ let switchers = {
|
||||
"vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"],
|
||||
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
|
||||
"dubLang": ["original", "auto"],
|
||||
"vimeoDash": ["false", "true"]
|
||||
"vimeoDash": ["false", "true"],
|
||||
"audioMode": ["false", "true"]
|
||||
}
|
||||
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio", "leftHandedLayout"];
|
||||
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
|
||||
let exceptions = { // used for mobile devices
|
||||
"vQuality": "720"
|
||||
}
|
||||
@ -90,6 +91,9 @@ function copy(id, data) {
|
||||
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) {}
|
||||
}
|
||||
function detectColorScheme() {
|
||||
let theme = "auto";
|
||||
let localTheme = sGet("theme");
|
||||
@ -173,6 +177,8 @@ 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) {
|
||||
@ -244,37 +250,14 @@ function checkbox(action) {
|
||||
sSet(action, !!eid(action).checked);
|
||||
switch(action) {
|
||||
case "alwaysVisibleButton": button(); break;
|
||||
case "leftHandedLayout":
|
||||
eid("bottom").setAttribute("data-lefthanded", sGet("leftHandedLayout"));
|
||||
break;
|
||||
}
|
||||
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))
|
||||
action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
|
||||
}
|
||||
function loadSettings() {
|
||||
try {
|
||||
if (typeof(navigator.clipboard.readText) == "undefined") throw new Error();
|
||||
} catch (err) {
|
||||
eid("pasteFromClipboard").style.display = "none"
|
||||
eid("paste").style.display = "none";
|
||||
}
|
||||
if (sGet("alwaysVisibleButton") === "true") {
|
||||
eid("alwaysVisibleButton").checked = true;
|
||||
@ -284,13 +267,9 @@ 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))
|
||||
}
|
||||
@ -451,7 +430,6 @@ window.onload = () => {
|
||||
eid("cobalt-main-box").style.visibility = 'visible';
|
||||
eid("footer").style.visibility = 'visible';
|
||||
eid("url-input-area").value = "";
|
||||
eid("bottom").setAttribute("data-lefthanded", sGet("leftHandedLayout"));
|
||||
notificationCheck();
|
||||
if (isIOS) sSet("downloadPopup", "true");
|
||||
let urlQuery = new URLSearchParams(window.location.search).get("u");
|
||||
@ -470,4 +448,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();
|
||||
};
|
||||
}
|
||||
|
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 |
BIN
src/front/updateBanners/cattired.webp
Normal file
BIN
src/front/updateBanners/cattired.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 985 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 |
@ -52,7 +52,7 @@
|
||||
"DownloadPopupWayToSave": "pick a way to save",
|
||||
"ClickToCopy": "press to copy",
|
||||
"Download": "download",
|
||||
"CopyURL": "copy url",
|
||||
"CopyURL": "copy",
|
||||
"AboutTab": "about",
|
||||
"ChangelogTab": "changelog",
|
||||
"DonationsTab": "donations",
|
||||
@ -71,14 +71,14 @@
|
||||
"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.",
|
||||
"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",
|
||||
"PasteFromClipboard": "paste and download",
|
||||
"ChangelogOlder": "previous versions",
|
||||
"ChangelogPressToExpand": "expand",
|
||||
"Miscellaneous": "miscellaneous",
|
||||
@ -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 80 thousand people is not that easy.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! every cent helps and is VERY appreciated :D",
|
||||
"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}.",
|
||||
@ -118,6 +118,7 @@
|
||||
"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.",
|
||||
"LeftHanded": "left-handed layout"
|
||||
"ShareURL": "share",
|
||||
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!"
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
"DownloadPopupWayToSave": "выбери, как сохранить",
|
||||
"ClickToCopy": "нажми, чтобы скопировать",
|
||||
"Download": "скачать",
|
||||
"CopyURL": "скопировать ссылку",
|
||||
"CopyURL": "скопировать",
|
||||
"AboutTab": "о {appName}",
|
||||
"ChangelogTab": "изменения",
|
||||
"DonationsTab": "донаты",
|
||||
@ -71,14 +71,14 @@
|
||||
"SettingsAudioFullTikTok": "полное аудио",
|
||||
"SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.",
|
||||
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.",
|
||||
"ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!",
|
||||
"ErrorNoVideosInTweet": "я не смог найти никакого медиа контента в этом твите. попробуй другой!",
|
||||
"ImagePickerTitle": "выбери картинки для скачивания",
|
||||
"ImagePickerDownloadAudio": "скачать звук",
|
||||
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на картинку, чтобы её сохранить.",
|
||||
"ImagePickerExplanationPhone": "зажми и удерживай картинку, чтобы её сохранить.",
|
||||
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
|
||||
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
|
||||
"PasteFromClipboard": "вставить",
|
||||
"PasteFromClipboard": "вставить и скачать",
|
||||
"ChangelogOlder": "предыдущие версии (на английском)",
|
||||
"ChangelogPressToExpand": "раскрыть",
|
||||
"Miscellaneous": "разное",
|
||||
@ -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>. но оказывается, что разработка и поддержка сервиса, которым пользуются более 80 тысяч людей, обходится довольно трудно.\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\".",
|
||||
"LeftHanded": "режим левши"
|
||||
"ShareURL": "поделиться",
|
||||
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,21 @@
|
||||
{
|
||||
"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 :)"
|
||||
}, {
|
||||
"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"
|
||||
},
|
||||
"history": [{
|
||||
}, {
|
||||
"version": "5.1",
|
||||
"title": "the evil has been defeated",
|
||||
"banner": "happymeowth.webp",
|
||||
|
@ -10,6 +10,8 @@ export function switcher(obj) {
|
||||
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>`
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.noParent) return `<div 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>
|
||||
|
@ -7,9 +7,9 @@ export function changelogHistory() { // blockId 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;
|
||||
|
@ -106,6 +106,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>' : ''}`
|
||||
}]
|
||||
})
|
||||
}, {
|
||||
@ -314,7 +315,7 @@ export default function(obj) {
|
||||
"action": "light",
|
||||
"text": t('SettingsThemeLight')
|
||||
}]
|
||||
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), 4, t('AccessibilityKeepDownloadButton')) + checkbox("leftHandedLayout", t('LeftHanded'), 4)
|
||||
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), 4, t('AccessibilityKeepDownloadButton'))
|
||||
}) + settingsCategory({
|
||||
name: "miscellaneous",
|
||||
title: t('Miscellaneous'),
|
||||
@ -333,7 +334,8 @@ export default function(obj) {
|
||||
name: "download",
|
||||
subtitle: t('DownloadPopupWayToSave'),
|
||||
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
|
||||
items: `<a id="pd-download" class="switch full space-right" target="_blank" href="/">${t('Download')}</a>
|
||||
items: `<a id="pd-download" class="switch full" target="_blank" href="/">${t('Download')}</a>
|
||||
<div id="pd-share" class="switch full">${t('ShareURL')}</div>
|
||||
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
|
||||
})
|
||||
})}
|
||||
@ -362,16 +364,26 @@ export default function(obj) {
|
||||
})}
|
||||
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
|
||||
<div id="cobalt-main-box" class="center" style="visibility: hidden;">
|
||||
<div id="logo-area">${appName}</div>
|
||||
<div id="download-area" class="mobile-center">
|
||||
<div id="logo">${appName}</div>
|
||||
<div id="download-area">
|
||||
<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="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>
|
||||
<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")}`
|
||||
}]
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -401,8 +413,6 @@ 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"}`) + "`" + `,
|
||||
|
@ -15,6 +15,7 @@ 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";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
@ -102,6 +103,9 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
format: obj.aFormat
|
||||
});
|
||||
break;
|
||||
case "instagram":
|
||||
r = await instagram({ id: patternMatch["id"] ? patternMatch["id"] : false });
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
|
||||
params = { type: "bridge" };
|
||||
break;
|
||||
|
||||
case "instagram":
|
||||
case "tumblr":
|
||||
case "twitter":
|
||||
responseType = 1;
|
||||
@ -72,6 +73,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,13 @@ 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://soundcloud.app.goo.gl/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false });
|
||||
if (!html) 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":'))) {
|
||||
|
@ -1,54 +1,73 @@
|
||||
import { genericUserAgent } from "../../config.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
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_ads=v1%3A${req_act["guest_token"]}`,
|
||||
`guest_id_marketing=v1%3A${req_act["guest_token"]}`,
|
||||
`guest_id=v1%3A${req_act["guest_token"]}`,
|
||||
`ct0=${crypto.randomUUID().replace(/-/g, '')};`
|
||||
].join('; ');
|
||||
|
||||
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["x-guest-token"] = req_act["guest_token"]
|
||||
_headers['cookie'] = [
|
||||
`guest_id_ads=v1%3A${req_act["guest_token"]}`,
|
||||
`guest_id_marketing=v1%3A${req_act["guest_token"]}`,
|
||||
`guest_id=v1%3A${req_act["guest_token"]}`,
|
||||
`ct0=${crypto.randomUUID().replace(/-/g, '')};`
|
||||
].join('; ');
|
||||
|
||||
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"])}) }
|
||||
|
@ -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,11 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ export const testers = {
|
||||
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"]
|
||||
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
|
||||
|
||||
"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,7 @@ 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)
|
||||
}
|
||||
|
@ -42,6 +42,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.\n y/n (n)"))
|
||||
|
||||
rl.question(q, r3 => {
|
||||
if (r3.toLowerCase() !== 'y') ob['cors'] = '0'
|
||||
final()
|
||||
})
|
||||
});
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
import NodeCache from "node-cache";
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { sha256 } from "../sub/crypto.js";
|
||||
import { streamLifespan } from "../config.js";
|
||||
@ -11,9 +12,9 @@ streamCache.on("expired", (key) => {
|
||||
});
|
||||
|
||||
export function createStream(obj) {
|
||||
let streamID = sha256(`${obj.ip},${obj.service},${obj.filename},${obj.audioFormat},${obj.mute}`, salt),
|
||||
let streamID = nanoid(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = sha256(`${streamID},${obj.service},${obj.ip},${exp}`, salt);
|
||||
ghmac = sha256(`${streamID},${obj.ip},${obj.service},${exp}`, salt);
|
||||
|
||||
if (!streamCache.has(streamID)) {
|
||||
streamCache.set(streamID, {
|
||||
@ -42,13 +43,15 @@ export function createStream(obj) {
|
||||
|
||||
export function verifyStream(ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = streamCache.get(id);
|
||||
if (!streamInfo) return { error: 'this stream token does not exist', status: 400 };
|
||||
|
||||
let ghmac = sha256(`${id},${streamInfo.service},${ip},${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())) {
|
||||
return streamInfo;
|
||||
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())) {
|
||||
return streamInfo;
|
||||
}
|
||||
}
|
||||
return { error: 'Unauthorized', status: 401 };
|
||||
} catch (e) {
|
||||
|
@ -134,3 +134,6 @@ export function checkJSONPost(obj) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export function getIP(req) {
|
||||
return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip;
|
||||
}
|
||||
|
@ -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",
|
||||
@ -755,5 +779,48 @@
|
||||
"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": "reel (isAudioOnly)",
|
||||
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
|
||||
"params": {
|
||||
"isAudioOnly": 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"
|
||||
}
|
||||
}]
|
||||
}
|
Loading…
Reference in New Issue
Block a user