4.3: open api + post method for main endpoint
This commit is contained in:
parent
4d06554256
commit
316e6423f4
18 changed files with 268 additions and 86 deletions
66
docs/API.md
Normal file
66
docs/API.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# cobalt API Documentation
|
||||
This document provides info about methods and acceptable variables for all cobalt API requests.<br>
|
||||
## POST: ``/api/json``
|
||||
Main processing endpoint.<br>
|
||||
```
|
||||
⚠️ GET method for this endpoint is deprecated and will be removed entirely soon.
|
||||
|
||||
Make sure to update your shortcuts and scripts.
|
||||
Only url query can be used with this method.
|
||||
```
|
||||
Request Body Type: ``application/json``<br>
|
||||
Response Body Type: ``application/json``
|
||||
|
||||
### Request Body Variables
|
||||
| key | type | variables | default | description |
|
||||
|:----------------|:--------|:----------------------------------|:-----------|:----------------------------------------------------------------------|
|
||||
| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
|
||||
| vFormat | string | ``mp4 / webm`` | ``mp4`` | Applies only to YouTube downloads. ``mp4`` is recommended for phones. |
|
||||
| vQuality | string | ``los / low / mid / hig / max`` | ``hig`` | ``mid`` quality is recommended for phones. |
|
||||
| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
|
||||
| isAudioOnly | boolean | ``true / false`` | ``false`` | |
|
||||
| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. |
|
||||
| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
|
||||
|
||||
### Response Body Variables
|
||||
| key | type | variables |
|
||||
|:-----------|:-------|:--------------------------------------------------------------|
|
||||
| status | string | ``error / redirect / stream / success / rate-limit / picker`` |
|
||||
| text | string | Text |
|
||||
| url | string | Direct link to a file / link to cobalt's stream |
|
||||
| pickerType | string | ``various / images`` |
|
||||
| picker | array | Array of picker items |
|
||||
| audio | string | Direct link to a file / link to cobalt's stream |
|
||||
|
||||
### Picker Item Variables
|
||||
Item type: ``object``
|
||||
| key | type | variables | description |
|
||||
|:-----------|:-------|:------------------------------------------------|:--------------------------------------------|
|
||||
| type | string | ``video`` | Used only if ``pickerType`` is ``various``. |
|
||||
| url | string | Direct link to a file / link to cobalt's stream | |
|
||||
| thumb | string | Item thumbnail that's displayed in the picker | Used only for ``video`` type. |
|
||||
|
||||
## GET: ``/api/stream``
|
||||
Content live render streaming endpoint.<br>
|
||||
|
||||
### Request Query Variables
|
||||
| key | variables | description |
|
||||
|:----|:-----------------|:------------------------------------------------------------------------------------------------------------------------------|
|
||||
| p | ``1`` | Used for checking the rate limit. |
|
||||
| t | Stream UUID | Unique stream identificator by which cobalt finds stored stream info data. |
|
||||
| h | HMAC | Hashed combination of: (hashed) ip address, stream uuid, expiry timestamp, and service name. Used for verification of stream. |
|
||||
| e | Expiry timestamp | |
|
||||
|
||||
## GET: ``/api/onDemand``
|
||||
On-demand website element loading. Currently used only for older changelogs.<br>
|
||||
|
||||
### Request Query Variables
|
||||
| key | variables | description |
|
||||
|:--------|:----------|:---------------------------------------|
|
||||
| blockId | ``0`` | Block ID to be rendered on the server. |
|
||||
|
||||
### Response Body Variables
|
||||
| key | type | variables |
|
||||
|:-----------|:-------|:-----------------------------|
|
||||
| status | string | ``error / success`` |
|
||||
| text | string | Error text or rendered block |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "4.2.1",
|
||||
"version": "4.3",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
|
|
|
@ -9,12 +9,13 @@ import { shortCommit } from "./modules/sub/currentCommit.js";
|
|||
import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js";
|
||||
import { getJSON } from "./modules/api.js";
|
||||
import renderPage from "./modules/pageRender/page.js";
|
||||
import { apiJSON, languageCode } from "./modules/sub/utils.js";
|
||||
import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js";
|
||||
import { Bright, Cyan } from "./modules/sub/consoleText.js";
|
||||
import stream from "./modules/stream/stream.js";
|
||||
import loc from "./localization/manager.js";
|
||||
import { buildFront } from "./modules/build.js";
|
||||
import { changelogHistory } from "./modules/pageRender/onDemand.js";
|
||||
import { encrypt } from "./modules/sub/crypto.js";
|
||||
|
||||
const commitHash = shortCommit();
|
||||
const app = express();
|
||||
|
@ -24,7 +25,7 @@ app.disable('x-powered-by');
|
|||
if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && process.env.port) {
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 20 * 60 * 1000,
|
||||
max: 100,
|
||||
max: 800,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res, next, opt) => {
|
||||
|
@ -33,7 +34,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
})
|
||||
const apiLimiterStream = rateLimit({
|
||||
windowMs: 6 * 60 * 1000,
|
||||
max: 24,
|
||||
max: 600,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res, next, opt) => {
|
||||
|
@ -56,36 +57,81 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => {
|
||||
app.use('/api/json', express.json({
|
||||
verify: (req, res, buf) => {
|
||||
try {
|
||||
JSON.parse(buf);
|
||||
if (buf.length > 720) throw new Error();
|
||||
if (req.header('Content-Type') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid content type header' })
|
||||
if (req.header('Accept') != "application/json") res.status(500).json({ 'status': 'error', 'text': 'invalid accept header' })
|
||||
} catch(e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': 'invalid json body.' })
|
||||
}
|
||||
}
|
||||
}));
|
||||
app.post('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => {
|
||||
try {
|
||||
let ip = encrypt(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
switch (req.params.type) {
|
||||
case 'json':
|
||||
if (req.query.url && req.query.url.length < 150) {
|
||||
let j = await getJSON(req.query.url.trim(), languageCode(req), {
|
||||
ip: req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip,
|
||||
format: req.query.format ? req.query.format.slice(0, 5) : "mp4",
|
||||
quality: req.query.quality ? req.query.quality.slice(0, 3) : "mid",
|
||||
audioFormat: req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : "mp3",
|
||||
isAudioOnly: !!req.query.audio,
|
||||
noWatermark: !!req.query.nw,
|
||||
fullAudio: !!req.query.ttfull,
|
||||
})
|
||||
res.status(j.status).json(j.body);
|
||||
} else {
|
||||
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink', process.env.selfURL) })
|
||||
if (!typeof j === "undefined" && j.status && j.body) {
|
||||
try {
|
||||
let request = req.body;
|
||||
let chck = checkJSONPost(request);
|
||||
if (request.url && chck) {
|
||||
chck["ip"] = ip;
|
||||
let j = await getJSON(request.url.trim(), languageCode(req), chck)
|
||||
res.status(j.status).json(j.body);
|
||||
} else {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorUnknownStatus') })
|
||||
try {
|
||||
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink', process.env.selfURL) })
|
||||
res.status(j.status).json(j.body);
|
||||
}
|
||||
catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorUnknownStatus') })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
|
||||
}
|
||||
break;
|
||||
default:
|
||||
let j = apiJSON(0, { t: "unknown response type" })
|
||||
res.status(j.status).json(j.body);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
|
||||
}
|
||||
});
|
||||
app.get('/api/:type', cors({ origin: process.env.selfURL, optionsSuccessStatus: 200 }), async (req, res) => {
|
||||
try {
|
||||
let ip = encrypt(req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip.replace('::ffff:', ''), process.env.streamSalt);
|
||||
switch (req.params.type) {
|
||||
// **
|
||||
// json GET method will be deprecated by 4.5! make sure to move your shortcuts to POST method.
|
||||
// **
|
||||
case 'json':
|
||||
try {
|
||||
if (req.query.url && req.query.url.length < 150) {
|
||||
let chck = checkJSONPost({});
|
||||
chck["ip"] = ip;
|
||||
let j = await getJSON(req.query.url.trim(), languageCode(req), chck)
|
||||
res.status(j.status).json(j.body);
|
||||
} else {
|
||||
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink', process.env.selfURL) })
|
||||
res.status(j.status).json(j.body);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
|
||||
}
|
||||
break;
|
||||
// **
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ will be removed soon ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// **
|
||||
case 'stream':
|
||||
if (req.query.p) {
|
||||
res.status(200).json({ "status": "continue" });
|
||||
} else if (req.query.t) {
|
||||
let ip = req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip
|
||||
} else if (req.query.t && req.query.h && req.query.e) {
|
||||
stream(res, ip, req.query.t, req.query.h, req.query.e);
|
||||
} else {
|
||||
let j = apiJSON(0, { t: "no stream id" })
|
||||
|
@ -117,7 +163,7 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ 'status': 'error', 'text': 'something went wrong.' })
|
||||
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })
|
||||
}
|
||||
});
|
||||
app.get("/api", (req, res) => {
|
||||
|
|
|
@ -305,6 +305,18 @@ input[type="checkbox"] {
|
|||
font-size: 1.1rem;
|
||||
padding-bottom: 0.7rem;
|
||||
}
|
||||
.changelog-banner {
|
||||
width: 100%;
|
||||
background-color: var(--accent-button-bg);
|
||||
max-height: 300px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.changelog-img {
|
||||
object-fit: cover;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -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 = 15;
|
||||
let version = 16;
|
||||
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>`
|
||||
|
||||
|
@ -288,6 +288,7 @@ function loadSettings() {
|
|||
for (let i in switchers) {
|
||||
changeSwitcher(i, sGet(i))
|
||||
}
|
||||
updateMP4Text();
|
||||
}
|
||||
function changeButton(type, text) {
|
||||
switch (type) {
|
||||
|
@ -302,6 +303,12 @@ function changeButton(type, text) {
|
|||
eid("url-clear").style.display = "block";
|
||||
eid("url-input-area").disabled = false
|
||||
break;
|
||||
case 2: //enable back + information popup
|
||||
popup("error", 1, text);
|
||||
changeDownloadButton(1, '>>');
|
||||
eid("url-clear").style.display = "block";
|
||||
eid("url-input-area").disabled = false
|
||||
break;
|
||||
}
|
||||
}
|
||||
function resetSettings() {
|
||||
|
@ -319,23 +326,23 @@ async function download(url) {
|
|||
changeDownloadButton(2, '...');
|
||||
eid("url-clear").style.display = "none";
|
||||
eid("url-input-area").disabled = true;
|
||||
let audioMode = sGet("audioMode");
|
||||
let format = ``;
|
||||
if (audioMode === "false") {
|
||||
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) {
|
||||
format = `&format=${sGet("vFormat")}`
|
||||
} else if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") {
|
||||
format = `&nw=true`
|
||||
}
|
||||
} else {
|
||||
format = `&nw=true`
|
||||
if (sGet("fullTikTokAudio") === "true") format += `&ttfull=true`
|
||||
let req = {
|
||||
url: encodeURIComponent(url.split("&")[0].split('%')[0]),
|
||||
aFormat: sGet("aFormat").slice(0, 4),
|
||||
}
|
||||
let mode = (sGet("audioMode") === "true") ? `audio=true` : `quality=${sGet("vQuality")}`
|
||||
await fetch(`/api/json?audioFormat=${sGet("aFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (r) => {
|
||||
if (sGet("audioMode") === "true") {
|
||||
req["isAudioOnly"] = true;
|
||||
req["isNoTTWatermark"] = true; // video tiktok no watermark
|
||||
if (sGet("fullTikTokAudio") === "true") req["isTTFullAudio"] = true; // audio tiktok full
|
||||
} else {
|
||||
req["vQuality"] = sGet("vQuality").slice(0, 4);
|
||||
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req["vFormat"] = sGet("vFormat").slice(0, 4);
|
||||
if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req["isNoTTWatermark"] = true;
|
||||
}
|
||||
await fetch('/api/json', { method: "POST", body: JSON.stringify(req), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(async (r) => {
|
||||
let j = await r.json();
|
||||
if (j.status !== "error" && j.status !== "rate-limit") {
|
||||
if (j.url) {
|
||||
if (j.url || j.picker) {
|
||||
switch (j.status) {
|
||||
case "redirect":
|
||||
changeDownloadButton(2, '>>>');
|
||||
|
@ -343,21 +350,21 @@ async function download(url) {
|
|||
sGet("downloadPopup") === "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
|
||||
break;
|
||||
case "picker":
|
||||
if (j.audio && j.url) {
|
||||
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.url, type: j.pickerType });
|
||||
popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType });
|
||||
setTimeout(() => { changeButton(1) }, 5000);
|
||||
} else {
|
||||
changeButton(0, jp.text);
|
||||
}
|
||||
}).catch((error) => internetError());
|
||||
} else if (j.url) {
|
||||
} else if (j.picker) {
|
||||
changeDownloadButton(2, '>>>');
|
||||
popup('picker', 1, { arr: j.url, type: j.pickerType });
|
||||
popup('picker', 1, { arr: j.picker, type: j.pickerType });
|
||||
setTimeout(() => { changeButton(1) }, 5000);
|
||||
} else {
|
||||
changeButton(0, loc.noURLReturned);
|
||||
|
@ -375,12 +382,17 @@ async function download(url) {
|
|||
}
|
||||
}).catch((error) => internetError());
|
||||
break;
|
||||
case "success":
|
||||
changeButton(2, j.text);
|
||||
break;
|
||||
default:
|
||||
changeButton(0, loc.unknownStatus);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
changeButton(0, loc.noURLReturned);
|
||||
if (j.status === "success") {
|
||||
changeButton(2, j.text)
|
||||
} else changeButton(0, loc.noURLReturned);
|
||||
}
|
||||
} else {
|
||||
changeButton(0, j.text);
|
||||
|
|
BIN
src/front/updateBanners/developersdevelopersdevelopers.webp
Normal file
BIN
src/front/updateBanners/developersdevelopersdevelopers.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 867 KiB |
|
@ -105,6 +105,7 @@
|
|||
"MediaPickerExplanationPC": "click or right click to download what you want.",
|
||||
"MediaPickerExplanationPhone": "press or press and hold to download what you want.",
|
||||
"MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save.",
|
||||
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!"
|
||||
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!",
|
||||
"ErrorCantProcess": "i couldn't process your request :(\nyou can try again, but if issue persists, please {ContactLink}."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
"MediaPickerExplanationPC": "кликни, чтобы скачать. также можно скачать через контекстное меню правой кнопки мыши.",
|
||||
"MediaPickerExplanationPhone": "нажми, или нажми и удерживай, чтобы скачать.",
|
||||
"MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью, и наконец выбери \"загрузить файл по ссылке\".",
|
||||
"TwitterSpaceWasntRecorded": "этот twitter space не был записан, поэтому я не могу его скачать. попробуй другой!"
|
||||
"TwitterSpaceWasntRecorded": "этот twitter space не был записан, поэтому я не могу его скачать. попробуй другой!",
|
||||
"ErrorCantProcess": "я не смог обработать твой запрос :(\nты можешь попробовать ещё раз, но если не поможет, то {ContactLink}."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import match from "./processing/match.js";
|
|||
|
||||
export async function getJSON(originalURL, lang, obj) {
|
||||
try {
|
||||
let url = decodeURI(originalURL);
|
||||
let url = decodeURIComponent(originalURL);
|
||||
if (!url.includes('http://')) {
|
||||
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
|
||||
host = hostname[hostname.length - 2],
|
||||
|
@ -33,11 +33,9 @@ export async function getJSON(originalURL, lang, obj) {
|
|||
}
|
||||
if (patternMatch) {
|
||||
return await match(host, patternMatch, url, lang, obj);
|
||||
} return apiJSON(0, { t: errorUnsupported(lang) })
|
||||
} return apiJSON(0, { t: errorUnsupported(lang) })
|
||||
} else {
|
||||
return apiJSON(0, { t: errorUnsupported(lang) })
|
||||
}
|
||||
} else return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
} else return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
} else return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
} catch (e) {
|
||||
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
{
|
||||
"current": {
|
||||
"version": "4.3",
|
||||
"title": "developers, developers, developers, developers",
|
||||
"banner": "developersdevelopersdevelopers.webp",
|
||||
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on {appName} for getting content from social media. the api has been revamped and <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- {appName} web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using {appName}'s api, make sure to mention <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
|
||||
},
|
||||
"history": [{
|
||||
"version": "4.2",
|
||||
"title": "optimized quality picking and 8k video support",
|
||||
"content": "- this update fixes quality picking that was accidentally broken in 4.0 update.\n- you now can download videos in 8k from youtube. why would you that? no idea. but i'm more than happy to give you this option.\n- default video quality for downloads from pc is now 1440p, and 720p for phones.\n- default video format is now mp4 for everyone.\n- default audio format is now mp3 for everyone.\n\nyou can always change new defaults back to whatever you prefer in settings.\n\nother changes:\n- added more clarity to quality picker description.\n- youtube video codecs are now right in the picker.\n- setup script is now easier to understand."
|
||||
},
|
||||
"history": [{
|
||||
}, {
|
||||
"version": "4.1",
|
||||
"title": "better tiktok image downloads",
|
||||
"content": "here's what's up:\n- tiktok images are saved as .jpeg instead of .webp (finally, i know).\n- added support for image downloads from douyin.\n- fixed tiktok audio downloads from the image picker.\n- emoji in about button now changes on special occasions. be it halloween or christmas, {appName} will change just a tiny bit to fit in :D\n\nif you're not caught up with new stuff in {appName} 4.x yet, check out the previous changelog down below. there's a ton of stuff to like."
|
||||
|
@ -12,7 +17,8 @@
|
|||
"version": "4.0",
|
||||
"title": "better and faster than ever",
|
||||
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- {appName} now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when {appName} is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, {appName} will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>.\n\nfun fact: average {appName} user is 10 times cooler than a regular person."
|
||||
}, {
|
||||
}],
|
||||
"olderHistory": [{
|
||||
"version": "3.7",
|
||||
"title": "support for multi media tweets is here!",
|
||||
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- {appName} is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through {appName}'s code."
|
||||
|
|
|
@ -7,7 +7,9 @@ export default function(string) {
|
|||
try {
|
||||
switch (string) {
|
||||
case "title":
|
||||
return `${replaceBase(changelog["current"]["title"])} (${changelog["current"]["version"]})` ;
|
||||
return `${replaceBase(changelog["current"]["title"])} (${changelog["current"]["version"]})`;
|
||||
case "banner":
|
||||
return changelog["current"]["banner"] ? `updateBanners/${changelog["current"]["banner"]}` : false;
|
||||
case "content":
|
||||
return replaceBase(changelog["current"]["content"]);
|
||||
case "history":
|
||||
|
|
|
@ -101,6 +101,9 @@ export default function(obj) {
|
|||
body: [{
|
||||
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: changelogManager("banner") ? `<div class="changelog-banner"><img class="changelog-img" src="${changelogManager("banner")}" onerror="this.style.display='none'"></img></div>`: '',
|
||||
raw: true
|
||||
}, {
|
||||
text: changelogManager("title"),
|
||||
classes: ["changelog-subtitle"],
|
||||
|
|
|
@ -34,7 +34,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
url: url,
|
||||
userId: patternMatch["userId"],
|
||||
videoId: patternMatch["videoId"],
|
||||
lang: lang, quality: obj.quality
|
||||
lang: lang, quality: obj.vQuality
|
||||
});
|
||||
break;
|
||||
case "bilibili":
|
||||
|
@ -46,11 +46,11 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
case "youtube":
|
||||
let fetchInfo = {
|
||||
id: patternMatch["id"].slice(0, 11),
|
||||
lang: lang, quality: obj.quality,
|
||||
lang: lang, quality: obj.vQuality,
|
||||
format: "webm"
|
||||
};
|
||||
if (url.match('music.youtube.com') || obj.isAudioOnly == true) obj.format = "audio";
|
||||
switch (obj.format) {
|
||||
if (url.match('music.youtube.com') || obj.isAudioOnly == true) obj.vFormat = "audio";
|
||||
switch (obj.vFormat) {
|
||||
case "mp4":
|
||||
fetchInfo["format"] = "mp4";
|
||||
break;
|
||||
|
@ -76,7 +76,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
host: host,
|
||||
postId: patternMatch["postId"],
|
||||
id: patternMatch["id"], lang: lang,
|
||||
noWatermark: obj.noWatermark, fullAudio: obj.fullAudio,
|
||||
noWatermark: obj.isNoTTWatermark, fullAudio: obj.isTTFullAudio,
|
||||
isAudioOnly: obj.isAudioOnly
|
||||
});
|
||||
if (r.isAudioOnly) obj.isAudioOnly = true
|
||||
|
@ -89,7 +89,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
break;
|
||||
case "vimeo":
|
||||
r = await vimeo({
|
||||
id: patternMatch["id"].slice(0, 11), quality: obj.quality,
|
||||
id: patternMatch["id"].slice(0, 11), quality: obj.vQuality,
|
||||
lang: lang
|
||||
});
|
||||
break;
|
||||
|
@ -98,14 +98,14 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||
r = await soundcloud({
|
||||
author: patternMatch["author"], song: patternMatch["song"], url: url,
|
||||
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false,
|
||||
format: obj.audioFormat,
|
||||
format: obj.aFormat,
|
||||
lang: lang
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
return matchActionDecider(r, host, obj.ip, obj.audioFormat, obj.isAudioOnly)
|
||||
return matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly)
|
||||
} catch (e) {
|
||||
return apiJSON(0, { t: genericError(lang, host) })
|
||||
}
|
||||
|
|
|
@ -10,34 +10,34 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
|||
case "vk":
|
||||
return apiJSON(2, {
|
||||
type: "bridge", u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt
|
||||
filename: r.filename,
|
||||
});
|
||||
case "bilibili":
|
||||
return apiJSON(2, {
|
||||
type: "render", u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt,
|
||||
filename: r.filename,
|
||||
time: r.time
|
||||
});
|
||||
case "youtube":
|
||||
return apiJSON(2, {
|
||||
type: r.type, u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt,
|
||||
filename: r.filename,
|
||||
time: r.time,
|
||||
});
|
||||
case "reddit":
|
||||
return apiJSON(r.typeId, {
|
||||
type: r.type, u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt
|
||||
filename: r.filename,
|
||||
});
|
||||
case "tiktok":
|
||||
return apiJSON(2, {
|
||||
type: "bridge", u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt
|
||||
filename: r.filename,
|
||||
});
|
||||
case "douyin":
|
||||
return apiJSON(2, {
|
||||
type: "bridge", u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename, salt: process.env.streamSalt
|
||||
filename: r.filename,
|
||||
});
|
||||
case "tumblr":
|
||||
return apiJSON(1, { u: r.urls });
|
||||
|
@ -57,7 +57,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
|||
type: type,
|
||||
picker: r.picker,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
|
||||
filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
|
||||
})
|
||||
case "twitter":
|
||||
return apiJSON(5, {
|
||||
|
@ -95,7 +95,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
|||
return apiJSON(2, {
|
||||
type: type,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true,
|
||||
filename: r.audioFilename, isAudioOnly: true,
|
||||
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -71,12 +71,14 @@ export default async function(obj) {
|
|||
audioFilename: `youtube_${obj.id}_audio`,
|
||||
fileMetadata: generalMeta
|
||||
};
|
||||
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
|
||||
if (isAutoGenAudio) {
|
||||
let descItems = infoInitial.videoDetails.description.split("\n\n")
|
||||
r.fileMetadata.album = descItems[2]
|
||||
r.fileMetadata.copyright = descItems[3]
|
||||
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
||||
if (infoInitial.videoDetails.description) {
|
||||
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
|
||||
if (isAutoGenAudio) {
|
||||
let descItems = infoInitial.videoDetails.description.split("\n\n")
|
||||
r.fileMetadata.album = descItems[2]
|
||||
r.fileMetadata.copyright = descItems[3]
|
||||
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
|
||||
}
|
||||
}
|
||||
return r
|
||||
} else {
|
||||
|
|
|
@ -4,12 +4,13 @@ import { UUID, encrypt } from "../sub/crypto.js";
|
|||
import { streamLifespan } from "../config.js";
|
||||
|
||||
const streamCache = new NodeCache({ stdTTL: streamLifespan, checkperiod: 120 });
|
||||
const salt = process.env.streamSalt;
|
||||
|
||||
export function createStream(obj) {
|
||||
let streamUUID = UUID(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = encrypt(`${streamUUID},${obj.url},${obj.ip},${exp}`, obj.salt),
|
||||
iphmac = encrypt(`${obj.ip}`, obj.salt)
|
||||
ghmac = encrypt(`${streamUUID},${obj.service},${obj.ip},${exp}`, salt)
|
||||
|
||||
streamCache.set(streamUUID, {
|
||||
id: streamUUID,
|
||||
service: obj.service,
|
||||
|
@ -17,7 +18,7 @@ export function createStream(obj) {
|
|||
urls: obj.u,
|
||||
filename: obj.filename,
|
||||
hmac: ghmac,
|
||||
ip: iphmac,
|
||||
ip: obj.ip,
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
|
@ -28,12 +29,12 @@ export function createStream(obj) {
|
|||
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
|
||||
}
|
||||
|
||||
export function verifyStream(ip, id, hmac, exp, salt) {
|
||||
export function verifyStream(ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = streamCache.get(id);
|
||||
if (streamInfo) {
|
||||
let ghmac = encrypt(`${id},${streamInfo.url},${ip},${exp}`, salt);
|
||||
if (hmac == ghmac && encrypt(`${ip}`, salt) == streamInfo.ip && ghmac == streamInfo.hmac && exp > Math.floor(new Date().getTime()) && exp == streamInfo.exp) {
|
||||
let ghmac = encrypt(`${id},${streamInfo.service},${ip},${exp}`, salt);
|
||||
if (hmac == ghmac && ip == streamInfo.ip && ghmac == streamInfo.hmac && exp > Math.floor(new Date().getTime()) && exp == streamInfo.exp) {
|
||||
return streamInfo;
|
||||
} else {
|
||||
return { error: 'Unauthorized', status: 401 };
|
||||
|
|
|
@ -4,7 +4,7 @@ import { streamAudioOnly, streamDefault, streamLiveRender } from "./types.js";
|
|||
|
||||
export default function(res, ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = verifyStream(ip, id, hmac, exp, process.env.streamSalt);
|
||||
let streamInfo = verifyStream(ip, id, hmac, exp);
|
||||
if (!streamInfo.error) {
|
||||
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
|
||||
streamAudioOnly(streamInfo, res);
|
||||
|
|
|
@ -22,7 +22,7 @@ export function apiJSON(type, obj) {
|
|||
pickerType = "images"
|
||||
break;
|
||||
}
|
||||
return { status: 200, body: { status: "picker", pickerType: pickerType, url: obj.picker, audio: audio } };
|
||||
return { status: 200, body: { status: "picker", pickerType: pickerType, picker: obj.picker, audio: audio } };
|
||||
default:
|
||||
return { status: 400, body: { status: "error", text: "Bad Request" } };
|
||||
}
|
||||
|
@ -75,3 +75,35 @@ export function unicodeDecode(str) {
|
|||
return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16));
|
||||
});
|
||||
}
|
||||
export function checkJSONPost(obj) {
|
||||
let def = {
|
||||
vFormat: "mp4",
|
||||
vQuality: "hig",
|
||||
aFormat: "mp3",
|
||||
isAudioOnly: false,
|
||||
isNoTTWatermark: false,
|
||||
isTTFullAudio: false
|
||||
}
|
||||
let booleanOnly = ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"]
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
if (objKeys.length < 8) {
|
||||
let objKeys = Object.keys(obj);
|
||||
let defKeys = Object.keys(def);
|
||||
for (let i in objKeys) {
|
||||
if (defKeys.includes(objKeys[i])) {
|
||||
if (booleanOnly.includes(objKeys[i])) {
|
||||
def[objKeys[i]] = obj[objKeys[i]] ? true : false
|
||||
} else {
|
||||
def[objKeys[i]] = obj[objKeys[i]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return def
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue