mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-18 11:18:28 +00:00
Merge branch 'wukko:current' into current
This commit is contained in:
commit
1fc6db7714
@ -32,7 +32,7 @@ Content-Type: application/json
|
|||||||
| `dubLang` | `boolean` | `true / false` | `false` | backend uses Accept-Language header for youtube video audio tracks when `true`. |
|
| `dubLang` | `boolean` | `true / false` | `false` | backend uses Accept-Language header for youtube video audio tracks when `true`. |
|
||||||
| `disableMetadata` | `boolean` | `true / false` | `false` | disables file metadata when set to `true`. |
|
| `disableMetadata` | `boolean` | `true / false` | `false` | disables file metadata when set to `true`. |
|
||||||
| `twitterGif` | `boolean` | `true / false` | `false` | changes whether twitter gifs are converted to .gif |
|
| `twitterGif` | `boolean` | `true / false` | `false` | changes whether twitter gifs are converted to .gif |
|
||||||
| `vimeoDash` | `boolean` | `true / false` | `false` | changes whether streamed file type is preferred for vimeo videos. |
|
| `tiktokH265` | `boolean` | `true / false` | `false` | changes whether 1080p h265 videos are preferred or not. |
|
||||||
|
|
||||||
### response body variables
|
### response body variables
|
||||||
| key | type | variables |
|
| key | type | variables |
|
||||||
|
@ -6,6 +6,7 @@ import express from "express";
|
|||||||
import { Bright, Green, Red } from "./modules/sub/consoleText.js";
|
import { Bright, Green, Red } from "./modules/sub/consoleText.js";
|
||||||
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
|
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
|
||||||
import { loadLoc } from "./localization/manager.js";
|
import { loadLoc } from "./localization/manager.js";
|
||||||
|
import { mode } from "./modules/config.js"
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@ -22,13 +23,10 @@ app.disable('x-powered-by');
|
|||||||
|
|
||||||
await loadLoc();
|
await loadLoc();
|
||||||
|
|
||||||
const apiMode = process.env.API_URL && !process.env.WEB_URL;
|
if (mode === 'API') {
|
||||||
const webMode = process.env.WEB_URL && process.env.API_URL;
|
|
||||||
|
|
||||||
if (apiMode) {
|
|
||||||
const { runAPI } = await import('./core/api.js');
|
const { runAPI } = await import('./core/api.js');
|
||||||
runAPI(express, app, gitCommit, gitBranch, __dirname)
|
runAPI(express, app, gitCommit, gitBranch, __dirname)
|
||||||
} else if (webMode) {
|
} else if (mode === 'WEB') {
|
||||||
const { runWeb } = await import('./core/web.js');
|
const { runWeb } = await import('./core/web.js');
|
||||||
await runWeb(express, app, gitCommit, gitBranch, __dirname)
|
await runWeb(express, app, gitCommit, gitBranch, __dirname)
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
"maxVideoDuration": 10800000,
|
"maxVideoDuration": 10800000,
|
||||||
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
|
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
|
||||||
"authorInfo": {
|
"authorInfo": {
|
||||||
"name": "wukko",
|
|
||||||
"link": "https://wukko.me/",
|
|
||||||
"contact": "https://wukko.me/contacts",
|
|
||||||
"support": {
|
"support": {
|
||||||
"default": {
|
"default": {
|
||||||
"email": {
|
"email": {
|
||||||
|
@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
|
|||||||
|
|
||||||
const ipSalt = randomBytes(64).toString('hex');
|
const ipSalt = randomBytes(64).toString('hex');
|
||||||
|
|
||||||
import { version } from "../modules/config.js";
|
import { env, version } from "../modules/config.js";
|
||||||
import { getJSON } from "../modules/api.js";
|
import { getJSON } from "../modules/api.js";
|
||||||
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
|
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
|
||||||
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
||||||
@ -14,8 +14,8 @@ import { generateHmac } from "../modules/sub/crypto.js";
|
|||||||
import { verifyStream, getInternalStream } from "../modules/stream/manage.js";
|
import { verifyStream, getInternalStream } from "../modules/stream/manage.js";
|
||||||
|
|
||||||
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
||||||
const corsConfig = process.env.CORS_WILDCARD === '0' ? {
|
const corsConfig = !env.corsWildcard ? {
|
||||||
origin: process.env.CORS_URL,
|
origin: env.corsURL,
|
||||||
optionsSuccessStatus: 200
|
optionsSuccessStatus: 200
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
@ -163,9 +163,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||||||
version: version,
|
version: version,
|
||||||
commit: gitCommit,
|
commit: gitCommit,
|
||||||
branch: gitBranch,
|
branch: gitBranch,
|
||||||
name: process.env.API_NAME || "unknown",
|
name: env.apiName,
|
||||||
url: process.env.API_URL,
|
url: env.apiURL,
|
||||||
cors: process.env?.CORS_WILDCARD === "0" ? 0 : 1,
|
cors: Number(env.corsWildcard),
|
||||||
startTime: `${startTimestamp}`
|
startTime: `${startTimestamp}`
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
@ -194,12 +194,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||||||
res.redirect('/api/json')
|
res.redirect('/api/json')
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(process.env.API_PORT || 9000, () => {
|
app.listen(env.apiPort, () => {
|
||||||
console.log(`\n` +
|
console.log(`\n` +
|
||||||
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
|
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
|
||||||
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
|
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
|
||||||
`URL: ${Cyan(`${process.env.API_URL}`)}\n` +
|
`URL: ${Cyan(`${env.apiURL}`)}\n` +
|
||||||
`Port: ${process.env.API_PORT || 9000}\n`
|
`Port: ${env.apiPort}\n`
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { genericUserAgent, version } from "../modules/config.js";
|
import { genericUserAgent, version, env } from "../modules/config.js";
|
||||||
import { apiJSON, languageCode } from "../modules/sub/utils.js";
|
import { apiJSON, languageCode } from "../modules/sub/utils.js";
|
||||||
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
||||||
|
|
||||||
@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
|
|||||||
return res.redirect('/')
|
return res.redirect('/')
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(process.env.WEB_PORT || 9001, () => {
|
app.listen(env.webPort, () => {
|
||||||
console.log(`\n` +
|
console.log(`\n` +
|
||||||
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
|
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
|
||||||
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
|
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
|
||||||
`URL: ${Cyan(`${process.env.WEB_URL}`)}\n` +
|
`URL: ${Cyan(`${env.webURL}`)}\n` +
|
||||||
`Port: ${process.env.WEB_PORT || 9001}\n`
|
`Port: ${env.webPort}\n`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
const version = 42;
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
const isIOS = ua.match("iphone os");
|
const isIOS = ua.match("iphone os");
|
||||||
const isMobile = ua.match("android") || ua.match("iphone os");
|
const isMobile = ua.match("android") || ua.match("iphone os");
|
||||||
@ -7,19 +5,14 @@ const isSafari = ua.match("safari/");
|
|||||||
const isFirefox = ua.match("firefox/");
|
const isFirefox = ua.match("firefox/");
|
||||||
const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103;
|
const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103;
|
||||||
|
|
||||||
const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
|
|
||||||
const notification = `<span class="notification-dot"></span>`;
|
|
||||||
|
|
||||||
const switchers = {
|
const switchers = {
|
||||||
"theme": ["auto", "light", "dark"],
|
"theme": ["auto", "light", "dark"],
|
||||||
"vCodec": ["h264", "av1", "vp9"],
|
"vCodec": ["h264", "av1", "vp9"],
|
||||||
"vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"],
|
"vQuality": ["720", "max", "2160", "1440", "1080", "480", "360"],
|
||||||
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
|
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
|
||||||
"dubLang": ["original", "auto"],
|
|
||||||
"vimeoDash": ["false", "true"],
|
|
||||||
"audioMode": ["false", "true"],
|
"audioMode": ["false", "true"],
|
||||||
"filenamePattern": ["classic", "pretty", "basic", "nerdy"]
|
"filenamePattern": ["classic", "pretty", "basic", "nerdy"]
|
||||||
};
|
}
|
||||||
const checkboxes = [
|
const checkboxes = [
|
||||||
"alwaysVisibleButton",
|
"alwaysVisibleButton",
|
||||||
"downloadPopup",
|
"downloadPopup",
|
||||||
@ -29,101 +22,127 @@ const checkboxes = [
|
|||||||
"disableAnimations",
|
"disableAnimations",
|
||||||
"disableMetadata",
|
"disableMetadata",
|
||||||
"twitterGif",
|
"twitterGif",
|
||||||
"plausible_ignore"
|
"plausible_ignore",
|
||||||
];
|
"ytDub",
|
||||||
const exceptions = { // used for mobile devices
|
"tiktokH265"
|
||||||
"vQuality": "720"
|
]
|
||||||
};
|
const bottomPopups = ["error", "download"]
|
||||||
const bottomPopups = ["error", "download"];
|
|
||||||
|
|
||||||
const pageQuery = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
let store = {};
|
let store = {};
|
||||||
|
|
||||||
function fixApiUrl(url) {
|
const validLink = (link) => {
|
||||||
|
try {
|
||||||
|
return /^https:/i.test(new URL(link).protocol);
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixApiUrl = (url) => {
|
||||||
return url.endsWith('/') ? url.slice(0, -1) : url
|
return url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiURL = fixApiUrl(defaultApiUrl);
|
let apiURL = fixApiUrl(defaultApiUrl);
|
||||||
|
|
||||||
function changeApi(url) {
|
const changeApi = (url) => {
|
||||||
apiURL = fixApiUrl(url);
|
apiURL = fixApiUrl(url);
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
function eid(id) {
|
|
||||||
|
const eid = (id) => {
|
||||||
return document.getElementById(id)
|
return document.getElementById(id)
|
||||||
}
|
}
|
||||||
function sGet(id) {
|
const sGet = (id) =>{
|
||||||
return localStorage.getItem(id)
|
return localStorage.getItem(id)
|
||||||
}
|
}
|
||||||
function sSet(id, value) {
|
const sSet = (id, value) => {
|
||||||
localStorage.setItem(id, value)
|
localStorage.setItem(id, value)
|
||||||
}
|
}
|
||||||
function enable(id) {
|
const enable = (id) => {
|
||||||
eid(id).dataset.enabled = "true";
|
eid(id).dataset.enabled = "true";
|
||||||
}
|
}
|
||||||
function disable(id) {
|
const disable = (id) => {
|
||||||
eid(id).dataset.enabled = "false";
|
eid(id).dataset.enabled = "false";
|
||||||
}
|
}
|
||||||
function vis(state) {
|
const opposite = (state) => {
|
||||||
return (state === 1) ? "visible" : "hidden";
|
|
||||||
}
|
|
||||||
function opposite(state) {
|
|
||||||
return state === "true" ? "false" : "true";
|
return state === "true" ? "false" : "true";
|
||||||
}
|
}
|
||||||
function changeDownloadButton(action, text) {
|
|
||||||
|
const lazyGet = (key) => {
|
||||||
|
const value = sGet(key);
|
||||||
|
if (key in switchers) {
|
||||||
|
if (switchers[key][0] !== value)
|
||||||
|
return value;
|
||||||
|
} else if (checkboxes.includes(key)) {
|
||||||
|
if (value === 'true')
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeDownloadButton = (action, text) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 0:
|
case "hidden": // hidden, but only visible when alwaysVisibleButton is true
|
||||||
eid("download-button").disabled = true
|
eid("download-button").disabled = true
|
||||||
if (sGet("alwaysVisibleButton") === "true") {
|
if (sGet("alwaysVisibleButton") === "true") {
|
||||||
eid("download-button").value = text
|
eid("download-button").value = '>>'
|
||||||
eid("download-button").style.padding = '0 1rem'
|
eid("download-button").style.padding = '0 1rem'
|
||||||
} else {
|
} else {
|
||||||
eid("download-button").value = ''
|
eid("download-button").value = ''
|
||||||
eid("download-button").style.padding = '0'
|
eid("download-button").style.padding = '0'
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1:
|
case "disabled":
|
||||||
eid("download-button").disabled = false
|
|
||||||
eid("download-button").value = text
|
|
||||||
eid("download-button").style.padding = '0 1rem'
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
eid("download-button").disabled = true
|
eid("download-button").disabled = true
|
||||||
eid("download-button").value = text
|
eid("download-button").value = text
|
||||||
eid("download-button").style.padding = '0 1rem'
|
eid("download-button").style.padding = '0 1rem'
|
||||||
break;
|
break;
|
||||||
}
|
default:
|
||||||
}
|
eid("download-button").disabled = false
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Tab") {
|
|
||||||
eid("download-button").value = '>>'
|
eid("download-button").value = '>>'
|
||||||
eid("download-button").style.padding = '0 1rem'
|
eid("download-button").style.padding = '0 1rem'
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
function button() {
|
|
||||||
let regexTest = regex.test(eid("url-input-area").value);
|
const button = () => {
|
||||||
|
let regexTest = validLink(eid("url-input-area").value);
|
||||||
|
|
||||||
|
eid("url-clear").style.display = "none";
|
||||||
|
|
||||||
if ((eid("url-input-area").value).length > 0) {
|
if ((eid("url-input-area").value).length > 0) {
|
||||||
eid("url-clear").style.display = "block";
|
eid("url-clear").style.display = "block";
|
||||||
} else {
|
|
||||||
eid("url-clear").style.display = "none";
|
|
||||||
}
|
}
|
||||||
regexTest ? changeDownloadButton(1, '>>') : changeDownloadButton(0, '>>');
|
|
||||||
|
if (regexTest) {
|
||||||
|
changeDownloadButton()
|
||||||
|
} else {
|
||||||
|
changeDownloadButton("hidden")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function clearInput() {
|
|
||||||
|
const clearInput = () => {
|
||||||
eid("url-input-area").value = '';
|
eid("url-input-area").value = '';
|
||||||
button();
|
button();
|
||||||
}
|
}
|
||||||
function copy(id, data) {
|
|
||||||
let e = document.getElementById(id);
|
const copy = (id, data) => {
|
||||||
e.classList.add("text-backdrop");
|
let target = document.getElementById(id);
|
||||||
setTimeout(() => { e.classList.remove("text-backdrop") }, 600);
|
target.classList.add("text-backdrop");
|
||||||
data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText);
|
|
||||||
|
setTimeout(() => {
|
||||||
|
target.classList.remove("text-backdrop")
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
navigator.clipboard.writeText(data)
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(e.innerText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function share(url) {
|
|
||||||
try { await navigator.share({url: url}) } catch (e) {}
|
const share = url => navigator?.share({ url }).catch(() => {});
|
||||||
}
|
|
||||||
function detectColorScheme() {
|
const detectColorScheme = () => {
|
||||||
let theme = "auto";
|
let theme = "auto";
|
||||||
let localTheme = sGet("theme");
|
let localTheme = sGet("theme");
|
||||||
if (localTheme) {
|
if (localTheme) {
|
||||||
@ -133,7 +152,59 @@ function detectColorScheme() {
|
|||||||
}
|
}
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
}
|
}
|
||||||
function changeTab(evnt, tabId, tabClass) {
|
|
||||||
|
const updateFilenamePreview = () => {
|
||||||
|
let videoFilePreview = ``;
|
||||||
|
let audioFilePreview = ``;
|
||||||
|
let resMatch = {
|
||||||
|
"max": "3840x2160",
|
||||||
|
"2160": "3840x2160",
|
||||||
|
"1440": "2560x1440",
|
||||||
|
"1080": "1920x1080",
|
||||||
|
"720": "1280x720",
|
||||||
|
"480": "854x480",
|
||||||
|
"360": "640x360",
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(sGet("filenamePattern")) {
|
||||||
|
case "classic":
|
||||||
|
videoFilePreview = `youtube_dQw4w9WgXcQ_${resMatch[sGet('vQuality')]}_${sGet('vCodec')}`
|
||||||
|
+ `${sGet("muteAudio") === "true" ? "_mute" : ""}`
|
||||||
|
+ `.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
||||||
|
audioFilePreview = `youtube_dQw4w9WgXcQ_audio`
|
||||||
|
+ `.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
||||||
|
break;
|
||||||
|
case "basic":
|
||||||
|
videoFilePreview = `${loc.FilenamePreviewVideoTitle} `
|
||||||
|
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, `
|
||||||
|
+ `${sGet('vCodec')}${sGet("muteAudio") === "true" ? ", mute" : ""})`
|
||||||
|
+ `.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
||||||
|
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor}`
|
||||||
|
+ `.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
||||||
|
break;
|
||||||
|
case "pretty":
|
||||||
|
videoFilePreview = `${loc.FilenamePreviewVideoTitle} `
|
||||||
|
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
|
||||||
|
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube)`
|
||||||
|
+ `.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
||||||
|
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud)`
|
||||||
|
+ `.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
||||||
|
break;
|
||||||
|
case "nerdy":
|
||||||
|
videoFilePreview = `${loc.FilenamePreviewVideoTitle} `
|
||||||
|
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
|
||||||
|
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube, dQw4w9WgXcQ)`
|
||||||
|
+ `.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
||||||
|
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} `
|
||||||
|
+ `(soundcloud, 1242868615)`
|
||||||
|
+ `.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eid("video-filename-text").innerHTML = videoFilePreview
|
||||||
|
eid("audio-filename-text").innerHTML = audioFilePreview
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeTab = (evnt, tabId, tabClass) => {
|
||||||
if (tabId === "tab-settings-other") updateFilenamePreview();
|
if (tabId === "tab-settings-other") updateFilenamePreview();
|
||||||
|
|
||||||
let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`);
|
let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`);
|
||||||
@ -149,46 +220,15 @@ function changeTab(evnt, tabId, tabClass) {
|
|||||||
evnt.currentTarget.dataset.enabled = "true";
|
evnt.currentTarget.dataset.enabled = "true";
|
||||||
eid(tabId).dataset.enabled = "true";
|
eid(tabId).dataset.enabled = "true";
|
||||||
eid(tabId).parentElement.scrollTop = 0;
|
eid(tabId).parentElement.scrollTop = 0;
|
||||||
|
|
||||||
if (tabId === "tab-about-changelog" && sGet("changelogStatus") !== `${version}`) notificationCheck("changelog");
|
|
||||||
if (tabId === "tab-about-about" && !sGet("seenAbout")) notificationCheck("about");
|
|
||||||
}
|
}
|
||||||
function expandCollapsible(evnt) {
|
|
||||||
|
const expandCollapsible = (evnt) => {
|
||||||
let classlist = evnt.currentTarget.parentNode.classList;
|
let classlist = evnt.currentTarget.parentNode.classList;
|
||||||
let c = "expanded";
|
let c = "expanded";
|
||||||
!classlist.contains(c) ? classlist.add(c) : classlist.remove(c);
|
!classlist.contains(c) ? classlist.add(c) : classlist.remove(c);
|
||||||
}
|
}
|
||||||
function notificationCheck(type) {
|
|
||||||
let changed = true;
|
const hideAllPopups = () => {
|
||||||
switch (type) {
|
|
||||||
case "about":
|
|
||||||
sSet("seenAbout", "true");
|
|
||||||
break;
|
|
||||||
case "changelog":
|
|
||||||
sSet("changelogStatus", version)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
changed = false;
|
|
||||||
}
|
|
||||||
if (changed && sGet("changelogStatus") === `${version}`) {
|
|
||||||
setTimeout(() => {
|
|
||||||
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace(notification, '');
|
|
||||||
eid("tab-button-about-changelog").innerHTML = eid("tab-button-about-changelog").innerHTML.replace(notification, '')
|
|
||||||
}, 900)
|
|
||||||
}
|
|
||||||
if (!sGet("seenAbout") && !eid("about-footer").innerHTML.includes(notification)) {
|
|
||||||
eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`;
|
|
||||||
}
|
|
||||||
if (sGet("changelogStatus") !== `${version}`) {
|
|
||||||
if (!eid("about-footer").innerHTML.includes(notification)) {
|
|
||||||
eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`;
|
|
||||||
}
|
|
||||||
if (!eid("tab-button-about-changelog").innerHTML.includes(notification)) {
|
|
||||||
eid("tab-button-about-changelog").innerHTML = `${notification}${eid("tab-button-about-changelog").innerHTML}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function hideAllPopups() {
|
|
||||||
let filter = document.getElementsByClassName('popup');
|
let filter = document.getElementsByClassName('popup');
|
||||||
for (let i = 0; i < filter.length; i++) {
|
for (let i = 0; i < filter.length; i++) {
|
||||||
filter[i].classList.remove("visible");
|
filter[i].classList.remove("visible");
|
||||||
@ -201,13 +241,14 @@ function hideAllPopups() {
|
|||||||
eid("picker-download").href = '/';
|
eid("picker-download").href = '/';
|
||||||
eid("picker-download").classList.remove("visible");
|
eid("picker-download").classList.remove("visible");
|
||||||
}
|
}
|
||||||
function popup(type, action, text) {
|
|
||||||
|
const popup = (type, action, text) => {
|
||||||
if (action === 1) {
|
if (action === 1) {
|
||||||
hideAllPopups(); // hide the previous popup before showing a new one
|
hideAllPopups(); // hide the previous popup before showing a new one
|
||||||
store.isPopupOpen = true;
|
store.isPopupOpen = true;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "about":
|
case "about":
|
||||||
let tabId = sGet("changelogStatus") !== `${version}` ? "changelog" : "about";
|
let tabId = "about";
|
||||||
if (text) tabId = text;
|
if (text) tabId = text;
|
||||||
eid(`tab-button-${type}-${tabId}`).click();
|
eid(`tab-button-${type}-${tabId}`).click();
|
||||||
break;
|
break;
|
||||||
@ -276,7 +317,8 @@ function popup(type, action, text) {
|
|||||||
eid(`popup-${type}`).classList.toggle("visible");
|
eid(`popup-${type}`).classList.toggle("visible");
|
||||||
eid(`popup-${type}`).focus();
|
eid(`popup-${type}`).focus();
|
||||||
}
|
}
|
||||||
function changeSwitcher(li, b) {
|
|
||||||
|
const changeSwitcher = (li, b) => {
|
||||||
if (b) {
|
if (b) {
|
||||||
if (!switchers[li].includes(b)) b = switchers[li][0];
|
if (!switchers[li].includes(b)) b = switchers[li][0];
|
||||||
sSet(li, b);
|
sSet(li, b);
|
||||||
@ -287,14 +329,14 @@ function changeSwitcher(li, b) {
|
|||||||
if (li === "filenamePattern") updateFilenamePreview();
|
if (li === "filenamePattern") updateFilenamePreview();
|
||||||
} else {
|
} else {
|
||||||
let pref = switchers[li][0];
|
let pref = switchers[li][0];
|
||||||
if (isMobile && exceptions[li]) pref = exceptions[li];
|
|
||||||
sSet(li, pref);
|
sSet(li, pref);
|
||||||
for (let i in switchers[li]) {
|
for (let i in switchers[li]) {
|
||||||
(switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`)
|
(switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkbox(action) {
|
|
||||||
|
const checkbox = (action) => {
|
||||||
sSet(action, !!eid(action).checked);
|
sSet(action, !!eid(action).checked);
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case "alwaysVisibleButton": button(); break;
|
case "alwaysVisibleButton": button(); break;
|
||||||
@ -302,43 +344,158 @@ function checkbox(action) {
|
|||||||
case "disableAnimations": eid("cobalt-body").classList.toggle('no-animation'); break;
|
case "disableAnimations": eid("cobalt-body").classList.toggle('no-animation'); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function changeButton(type, text) {
|
|
||||||
|
const changeButton = (type, text) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0: //error
|
case "error": //error
|
||||||
eid("url-input-area").disabled = false
|
eid("url-input-area").disabled = false
|
||||||
eid("url-clear").style.display = "block";
|
eid("url-clear").style.display = "block";
|
||||||
changeDownloadButton(2, '!!');
|
changeDownloadButton("disabled", '!!');
|
||||||
popup("error", 1, text);
|
popup("error", 1, text);
|
||||||
setTimeout(() => { changeButton(1); }, 2500);
|
setTimeout(() => { changeButton("default") }, 2500);
|
||||||
break;
|
break;
|
||||||
case 1: //enable back
|
case "default": //enable back
|
||||||
changeDownloadButton(1, '>>');
|
changeDownloadButton();
|
||||||
eid("url-clear").style.display = "block";
|
eid("url-clear").style.display = "block";
|
||||||
eid("url-input-area").disabled = false
|
eid("url-input-area").disabled = false
|
||||||
break;
|
break;
|
||||||
case 2: //enable back + information popup
|
case "error-default": //enable back + information popup
|
||||||
popup("error", 1, text);
|
popup("error", 1, text);
|
||||||
changeDownloadButton(1, '>>');
|
changeDownloadButton();
|
||||||
eid("url-clear").style.display = "block";
|
eid("url-clear").style.display = "block";
|
||||||
eid("url-input-area").disabled = false
|
eid("url-input-area").disabled = false
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function internetError() {
|
|
||||||
|
const internetError = () => {
|
||||||
eid("url-input-area").disabled = false
|
eid("url-input-area").disabled = false
|
||||||
changeDownloadButton(2, '!!');
|
changeDownloadButton("disabled", '!!');
|
||||||
setTimeout(() => { changeButton(1); }, 2500);
|
setTimeout(() => { changeButton("default") }, 2500);
|
||||||
popup("error", 1, loc.ErrorNoInternet);
|
popup("error", 1, loc.ErrorNoInternet);
|
||||||
}
|
}
|
||||||
function resetSettings() {
|
|
||||||
|
const resetSettings = () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
async function pasteClipboard() {
|
|
||||||
|
const download = async(url) => {
|
||||||
|
changeDownloadButton("disabled", '...');
|
||||||
|
|
||||||
|
eid("url-clear").style.display = "none";
|
||||||
|
eid("url-input-area").disabled = true;
|
||||||
|
|
||||||
|
let req = {
|
||||||
|
url,
|
||||||
|
vCodec: lazyGet("vCodec"),
|
||||||
|
vQuality: lazyGet("vQuality"),
|
||||||
|
aFormat: lazyGet("aFormat"),
|
||||||
|
filenamePattern: lazyGet("filenamePattern"),
|
||||||
|
isAudioOnly: lazyGet("audioMode"),
|
||||||
|
isTTFullAudio: lazyGet("fullTikTokAudio"),
|
||||||
|
isAudioMuted: lazyGet("muteAudio"),
|
||||||
|
disableMetadata: lazyGet("disableMetadata"),
|
||||||
|
dubLang: lazyGet("ytDub"),
|
||||||
|
twitterGif: lazyGet("twitterGif"),
|
||||||
|
tiktokH265: lazyGet("tiktokH265"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let j = await fetch(`${apiURL}/api/json`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(r => r.json()).catch(() => {});
|
||||||
|
|
||||||
|
if (!j) {
|
||||||
|
internetError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((j.status === "error" || j.status === "rate-limit") && j && j.text) {
|
||||||
|
changeButton("error", j.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.text && (!j.url || !j.picker)) {
|
||||||
|
if (j.status === "success") {
|
||||||
|
changeButton("error-default", j.text)
|
||||||
|
} else {
|
||||||
|
changeButton("error", loc.ErrorNoUrlReturned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (j.status) {
|
||||||
|
case "redirect":
|
||||||
|
changeDownloadButton("disabled", '>>>');
|
||||||
|
setTimeout(() => { changeButton("default") }, 1500);
|
||||||
|
|
||||||
|
if (sGet("downloadPopup") === "true") {
|
||||||
|
popup('download', 1, j.url)
|
||||||
|
} else {
|
||||||
|
window.open(j.url, '_blank')
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "stream":
|
||||||
|
changeDownloadButton("disabled", '?..');
|
||||||
|
|
||||||
|
let probeStream = await fetch(`${j.url}&p=1`).then(r => r.json()).catch(() => {});
|
||||||
|
if (!probeStream) return internetError();
|
||||||
|
|
||||||
|
if (probeStream.status !== "continue") {
|
||||||
|
changeButton("error", probeStream.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDownloadButton("disabled", '>>>');
|
||||||
|
if (sGet("downloadPopup") === "true") {
|
||||||
|
popup('download', 1, j.url)
|
||||||
|
} else {
|
||||||
|
if (isMobile || isSafari) {
|
||||||
|
window.location.href = j.url;
|
||||||
|
} else {
|
||||||
|
window.open(j.url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => { changeButton("default") }, 2500);
|
||||||
|
break;
|
||||||
|
case "picker":
|
||||||
|
if (j.audio && j.picker) {
|
||||||
|
changeDownloadButton("disabled", '>>>');
|
||||||
|
popup('picker', 1, {
|
||||||
|
audio: j.audio,
|
||||||
|
arr: j.picker,
|
||||||
|
type: j.pickerType
|
||||||
|
});
|
||||||
|
setTimeout(() => { changeButton("default") }, 2500);
|
||||||
|
} else if (j.picker) {
|
||||||
|
changeDownloadButton("disabled", '>>>');
|
||||||
|
popup('picker', 1, {
|
||||||
|
arr: j.picker,
|
||||||
|
type: j.pickerType
|
||||||
|
});
|
||||||
|
setTimeout(() => { changeButton("default") }, 2500);
|
||||||
|
} else {
|
||||||
|
changeButton("error", loc.ErrorNoUrlReturned);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
changeButton("error-default", j.text);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
changeButton("error", loc.ErrorUnknownStatus);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pasteClipboard = async() => {
|
||||||
try {
|
try {
|
||||||
let t = await navigator.clipboard.readText();
|
let clipboard = await navigator.clipboard.readText();
|
||||||
if (regex.test(t)) {
|
let onlyURL = clipboard.match(/https:\/\/[^\s]+/g)
|
||||||
eid("url-input-area").value = t;
|
if (onlyURL) {
|
||||||
|
eid("url-input-area").value = onlyURL;
|
||||||
download(eid("url-input-area").value);
|
download(eid("url-input-area").value);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -353,204 +510,58 @@ async function pasteClipboard() {
|
|||||||
if (doError) popup("error", 1, errorMessage);
|
if (doError) popup("error", 1, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function download(url) {
|
|
||||||
changeDownloadButton(2, '...');
|
|
||||||
eid("url-clear").style.display = "none";
|
|
||||||
eid("url-input-area").disabled = true;
|
|
||||||
let req = {
|
|
||||||
url,
|
|
||||||
aFormat: sGet("aFormat").slice(0, 4),
|
|
||||||
filenamePattern: sGet("filenamePattern"),
|
|
||||||
dubLang: false
|
|
||||||
}
|
|
||||||
if (sGet("dubLang") === "auto") {
|
|
||||||
req.dubLang = true
|
|
||||||
} else if (sGet("dubLang") === "custom") {
|
|
||||||
req.dubLang = true
|
|
||||||
}
|
|
||||||
if (sGet("vimeoDash") === "true") req.vimeoDash = true;
|
|
||||||
if (sGet("audioMode") === "true") {
|
|
||||||
req.isAudioOnly = true;
|
|
||||||
if (sGet("fullTikTokAudio") === "true") req.isTTFullAudio = true; // audio tiktok full
|
|
||||||
} else {
|
|
||||||
req.vQuality = sGet("vQuality").slice(0, 4);
|
|
||||||
if (sGet("muteAudio") === "true") req.isAudioMuted = true;
|
|
||||||
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req.vCodec = sGet("vCodec").slice(0, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sGet("disableMetadata") === "true") req.disableMetadata = true;
|
const loadCelebrationsEmoji = async() => {
|
||||||
if (sGet("twitterGif") === "true") req.twitterGif = true;
|
let aboutButtonBackup = eid("about-footer").innerHTML;
|
||||||
|
|
||||||
let j = await fetch(`${apiURL}/api/json`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(req),
|
|
||||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }
|
|
||||||
}).then((r) => { return r.json() }).catch((e) => { return false });
|
|
||||||
if (!j) {
|
|
||||||
internetError();
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (j && j.status !== "error" && j.status !== "rate-limit") {
|
|
||||||
if (j.text && (!j.url || !j.picker)) {
|
|
||||||
if (j.status === "success") {
|
|
||||||
changeButton(2, j.text)
|
|
||||||
} else changeButton(0, loc.ErrorNoUrlReturned);
|
|
||||||
}
|
|
||||||
switch (j.status) {
|
|
||||||
case "redirect":
|
|
||||||
changeDownloadButton(2, '>>>');
|
|
||||||
setTimeout(() => { changeButton(1); }, 1500);
|
|
||||||
sGet("downloadPopup") === "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
|
|
||||||
break;
|
|
||||||
case "picker":
|
|
||||||
if (j.audio && j.picker) {
|
|
||||||
changeDownloadButton(2, '>>>');
|
|
||||||
popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType });
|
|
||||||
setTimeout(() => { changeButton(1) }, 2500);
|
|
||||||
} else if (j.picker) {
|
|
||||||
changeDownloadButton(2, '>>>');
|
|
||||||
popup('picker', 1, { arr: j.picker, type: j.pickerType });
|
|
||||||
setTimeout(() => { changeButton(1) }, 2500);
|
|
||||||
} else {
|
|
||||||
changeButton(0, loc.ErrorNoUrlReturned);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "stream":
|
|
||||||
changeDownloadButton(2, '?..')
|
|
||||||
fetch(`${j.url}&p=1`).then(async (res) => {
|
|
||||||
let jp = await res.json();
|
|
||||||
if (jp.status === "continue") {
|
|
||||||
changeDownloadButton(2, '>>>');
|
|
||||||
if (sGet("downloadPopup") === "true") {
|
|
||||||
popup('download', 1, j.url)
|
|
||||||
} else {
|
|
||||||
if (isMobile || isSafari) {
|
|
||||||
window.location.href = j.url;
|
|
||||||
} else window.open(j.url, '_blank');
|
|
||||||
}
|
|
||||||
setTimeout(() => { changeButton(1) }, 2500);
|
|
||||||
} else {
|
|
||||||
changeButton(0, jp.text);
|
|
||||||
}
|
|
||||||
}).catch((error) => internetError());
|
|
||||||
break;
|
|
||||||
case "success":
|
|
||||||
changeButton(2, j.text);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
changeButton(0, loc.ErrorUnknownStatus);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (j && j.text) {
|
|
||||||
changeButton(0, j.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function loadCelebrationsEmoji() {
|
|
||||||
let bac = eid("about-footer").innerHTML;
|
|
||||||
try {
|
try {
|
||||||
let j = await fetch(`/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false });
|
let j = await fetch(`/onDemand?blockId=1`).then(r => r.json()).catch(() => {});
|
||||||
|
|
||||||
if (j && j.status === "success" && j.text) {
|
if (j && j.status === "success" && j.text) {
|
||||||
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('<img class="emoji" draggable="false" height="22" width="22" alt="🐲" src="emoji/dragon_face.svg" loading="lazy">', j.text);
|
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace(
|
||||||
|
`<img class="emoji"
|
||||||
|
draggable="false"
|
||||||
|
height="22"
|
||||||
|
width="22
|
||||||
|
alt="🐲"
|
||||||
|
src="emoji/dragon_face.svg"
|
||||||
|
loading="lazy">`,
|
||||||
|
j.text
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
eid("about-footer").innerHTML = bac;
|
eid("about-footer").innerHTML = aboutButtonBackup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function loadOnDemand(elementId, blockId) {
|
|
||||||
let j = {};
|
const loadOnDemand = async(elementId, blockId) => {
|
||||||
store.historyButton = eid(elementId).innerHTML;
|
store.historyButton = eid(elementId).innerHTML;
|
||||||
eid(elementId).innerHTML = `<div class="loader">...</div>`;
|
eid(elementId).innerHTML = `<div class="loader">...</div>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (store.historyContent) {
|
if (!store.historyContent) {
|
||||||
j = store.historyContent;
|
let j = await fetch(`/onDemand?blockId=${blockId}`).then(r => r.json()).catch(() => {});
|
||||||
} else {
|
if (!j) throw new Error();
|
||||||
await fetch(`/onDemand?blockId=${blockId}`).then(async(r) => {
|
|
||||||
j = await r.json();
|
if (j.status === "success") {
|
||||||
if (j && j.status === "success") {
|
store.historyContent = j.text
|
||||||
store.historyContent = j;
|
|
||||||
} else throw new Error();
|
|
||||||
}).catch(() => { throw new Error() });
|
|
||||||
}
|
}
|
||||||
if (j.text) {
|
}
|
||||||
eid(elementId).innerHTML = `<button class="switch bottom-margin" onclick="restoreUpdateHistory()">${loc.ChangelogPressToHide}</button>${j.text}`;
|
eid(elementId).innerHTML =
|
||||||
} else throw new Error()
|
`<button class="switch bottom-margin" onclick="restoreUpdateHistory()">
|
||||||
} catch (e) {
|
${loc.ChangelogPressToHide}
|
||||||
|
</button>
|
||||||
|
${store.historyContent}`;
|
||||||
|
} catch {
|
||||||
eid(elementId).innerHTML = store.historyButton;
|
eid(elementId).innerHTML = store.historyButton;
|
||||||
internetError()
|
internetError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function restoreUpdateHistory() {
|
|
||||||
|
const restoreUpdateHistory = () => {
|
||||||
eid("changelog-history").innerHTML = store.historyButton;
|
eid("changelog-history").innerHTML = store.historyButton;
|
||||||
}
|
}
|
||||||
function unpackSettings(b64) {
|
|
||||||
let changed = null;
|
const loadSettings = () => {
|
||||||
try {
|
|
||||||
let settingsToImport = JSON.parse(atob(b64));
|
|
||||||
let currentSettings = JSON.parse(JSON.stringify(localStorage));
|
|
||||||
for (let s in settingsToImport) {
|
|
||||||
if (checkboxes.includes(s) && (settingsToImport[s] === "true" || settingsToImport[s] === "false")
|
|
||||||
&& currentSettings[s] !== settingsToImport[s]) {
|
|
||||||
sSet(s, settingsToImport[s]);
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if (switchers[s] && switchers[s].includes(settingsToImport[s])
|
|
||||||
&& currentSettings[s] !== settingsToImport[s]) {
|
|
||||||
sSet(s, settingsToImport[s]);
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
changed = false;
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
function updateFilenamePreview() {
|
|
||||||
let videoFilePreview = ``;
|
|
||||||
let audioFilePreview = ``;
|
|
||||||
let resMatch = {
|
|
||||||
"max": "3840x2160",
|
|
||||||
"2160": "3840x2160",
|
|
||||||
"1440": "2560x1440",
|
|
||||||
"1080": "1920x1080",
|
|
||||||
"720": "1280x720",
|
|
||||||
"480": "854x480",
|
|
||||||
"360": "640x360",
|
|
||||||
}
|
|
||||||
// "dubLang"
|
|
||||||
// sGet("muteAudio") === "true"
|
|
||||||
switch(sGet("filenamePattern")) {
|
|
||||||
case "classic":
|
|
||||||
videoFilePreview = `youtube_yPYZpwSpKmA_${resMatch[sGet('vQuality')]}_${sGet('vCodec')}`
|
|
||||||
+ `${sGet("muteAudio") === "true" ? "_mute" : ""}.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
|
||||||
audioFilePreview = `youtube_yPYZpwSpKmA_audio.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
|
||||||
break;
|
|
||||||
case "pretty":
|
|
||||||
videoFilePreview =
|
|
||||||
`${loc.FilenamePreviewVideoTitle} `
|
|
||||||
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
|
|
||||||
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
|
||||||
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
|
||||||
break;
|
|
||||||
case "basic":
|
|
||||||
videoFilePreview =
|
|
||||||
`${loc.FilenamePreviewVideoTitle} `
|
|
||||||
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}${sGet("muteAudio") === "true" ? " mute" : ""}).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
|
||||||
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor}.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
|
||||||
break;
|
|
||||||
case "nerdy":
|
|
||||||
videoFilePreview =
|
|
||||||
`${loc.FilenamePreviewVideoTitle} `
|
|
||||||
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
|
|
||||||
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube, yPYZpwSpKmA).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
|
|
||||||
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud, 1242868615).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
eid("video-filename-text").innerHTML = videoFilePreview
|
|
||||||
eid("audio-filename-text").innerHTML = audioFilePreview
|
|
||||||
}
|
|
||||||
function loadSettings() {
|
|
||||||
if (sGet("alwaysVisibleButton") === "true") {
|
if (sGet("alwaysVisibleButton") === "true") {
|
||||||
eid("alwaysVisibleButton").checked = true;
|
eid("alwaysVisibleButton").checked = true;
|
||||||
eid("download-button").value = '>>'
|
eid("download-button").value = '>>'
|
||||||
@ -578,13 +589,14 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
updateFilenamePreview()
|
updateFilenamePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
loadCelebrationsEmoji();
|
loadCelebrationsEmoji();
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
detectColorScheme();
|
detectColorScheme();
|
||||||
|
|
||||||
changeDownloadButton(0, '>>');
|
changeDownloadButton("hidden");
|
||||||
eid("url-input-area").value = "";
|
eid("url-input-area").value = "";
|
||||||
|
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
@ -595,37 +607,32 @@ window.onload = () => {
|
|||||||
eid("home").style.visibility = 'visible';
|
eid("home").style.visibility = 'visible';
|
||||||
eid("home").classList.toggle("visible");
|
eid("home").classList.toggle("visible");
|
||||||
|
|
||||||
if (pageQuery.has("u") && regex.test(pageQuery.get("u"))) {
|
const pageQuery = new URLSearchParams(window.location.search);
|
||||||
|
if (pageQuery.has("u") && validLink(pageQuery.get("u"))) {
|
||||||
eid("url-input-area").value = pageQuery.get("u");
|
eid("url-input-area").value = pageQuery.get("u");
|
||||||
button()
|
button()
|
||||||
}
|
}
|
||||||
if (pageQuery.has("migration")) {
|
|
||||||
if (pageQuery.has("settingsData") && !sGet("migrated")) {
|
|
||||||
let setUn = unpackSettings(pageQuery.get("settingsData"));
|
|
||||||
if (setUn !== null) {
|
|
||||||
if (setUn) {
|
|
||||||
sSet("migrated", "true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadSettings();
|
|
||||||
detectColorScheme();
|
|
||||||
}
|
|
||||||
window.history.replaceState(null, '', window.location.pathname);
|
window.history.replaceState(null, '', window.location.pathname);
|
||||||
|
|
||||||
notificationCheck();
|
|
||||||
|
|
||||||
// fix for animations not working in Safari
|
// fix for animations not working in Safari
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
document.addEventListener('touchstart', () => {}, true);
|
document.addEventListener('touchstart', () => {}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eid("url-input-area").addEventListener("keydown", (e) => {
|
eid("url-input-area").addEventListener("keydown", (e) => {
|
||||||
button();
|
button();
|
||||||
})
|
})
|
||||||
eid("url-input-area").addEventListener("keyup", (e) => {
|
eid("url-input-area").addEventListener("keyup", (e) => {
|
||||||
if (e.key === 'Enter') eid("download-button").click();
|
if (e.key === 'Enter') eid("download-button").click();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
eid("download-button").value = '>>'
|
||||||
|
eid("download-button").style.padding = '0 1rem'
|
||||||
|
}
|
||||||
|
})
|
||||||
document.onkeydown = (e) => {
|
document.onkeydown = (e) => {
|
||||||
if (!store.isPopupOpen) {
|
if (!store.isPopupOpen) {
|
||||||
if (e.metaKey || e.ctrlKey || e.key === "/") eid("url-input-area").focus();
|
if (e.metaKey || e.ctrlKey || e.key === "/") eid("url-input-area").focus();
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"LinkInput": "paste the link here",
|
"LinkInput": "paste the link here",
|
||||||
"AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
|
"AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
|
||||||
"EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.",
|
"EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.",
|
||||||
"MadeWithLove": "made with <3 by wukko",
|
"MadeWithLove": "made with <3 by imput",
|
||||||
"AccessibilityInputArea": "link input area",
|
"AccessibilityInputArea": "link input area",
|
||||||
"AccessibilityOpenAbout": "open about popup",
|
"AccessibilityOpenAbout": "open about popup",
|
||||||
"AccessibilityDownloadButton": "download button",
|
"AccessibilityDownloadButton": "download button",
|
||||||
@ -90,7 +90,6 @@
|
|||||||
"DonateSub": "help it stay online",
|
"DonateSub": "help it stay online",
|
||||||
"DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, meaning that it's <span class=\"text-backdrop\">completely free to use</span> for everyone. but development and maintenance of a media-heavy service used by over 750k people is quite costly. both in terms of time and money.\n\nif cobalt helped you in the past and you want to keep it growing and evolving, you can return the favor by making a donation!\n\nyour donation will help all cobalt users: educators, students, content creators, artists, musicians, and many, many more!\n\nin past, donations have let cobalt:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open the api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; add resource-intensive features (such as gif conversion).\n*; continue improving our infrastructure.\n*; keep developers happy.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!\n\nif you can't donate, share cobalt with a friend! we don't get ads anywhere, so cobalt is spread by word of mouth.\nsharing is the easiest way to help achieve the goal of better internet for everyone.",
|
"DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, meaning that it's <span class=\"text-backdrop\">completely free to use</span> for everyone. but development and maintenance of a media-heavy service used by over 750k people is quite costly. both in terms of time and money.\n\nif cobalt helped you in the past and you want to keep it growing and evolving, you can return the favor by making a donation!\n\nyour donation will help all cobalt users: educators, students, content creators, artists, musicians, and many, many more!\n\nin past, donations have let cobalt:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open the api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; add resource-intensive features (such as gif conversion).\n*; continue improving our infrastructure.\n*; keep developers happy.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!\n\nif you can't donate, share cobalt with a friend! we don't get ads anywhere, so cobalt is spread by word of mouth.\nsharing is the easiest way to help achieve the goal of better internet for everyone.",
|
||||||
"DonateVia": "donate via",
|
"DonateVia": "donate via",
|
||||||
"DonateHireMe": "...or you can <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
|
|
||||||
"SettingsVideoMute": "mute audio",
|
"SettingsVideoMute": "mute audio",
|
||||||
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
|
"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}.",
|
"ErrorSoundCloudNoClientId": "i couldn't get the temporary token that's required to download songs from soundcloud. try again, but if issue persists, {ContactLink}.",
|
||||||
@ -104,13 +103,8 @@
|
|||||||
"ErrorYTUnavailable": "this youtube video is unavailable. it could be age or region restricted. try another one!",
|
"ErrorYTUnavailable": "this youtube video is unavailable. it could be age or region restricted. try another one!",
|
||||||
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!",
|
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!",
|
||||||
"SettingsCodecSubtitle": "youtube codec",
|
"SettingsCodecSubtitle": "youtube codec",
|
||||||
"SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\n\npick av1 if you want best quality and efficiency.",
|
"SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\npick av1 if you want best quality and efficiency.",
|
||||||
"SettingsAudioDub": "youtube audio track",
|
"SettingsAudioDub": "youtube audio track",
|
||||||
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.",
|
|
||||||
"SettingsDubDefault": "original",
|
|
||||||
"SettingsDubAuto": "auto",
|
|
||||||
"SettingsVimeoPrefer": "vimeo downloads type",
|
|
||||||
"SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by cobalt into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.",
|
|
||||||
"ShareURL": "share",
|
"ShareURL": "share",
|
||||||
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
|
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
|
||||||
"PopupCloseDone": "done",
|
"PopupCloseDone": "done",
|
||||||
@ -157,6 +151,10 @@
|
|||||||
"PrivateAnalytics": "private analytics",
|
"PrivateAnalytics": "private analytics",
|
||||||
"SettingsDisableAnalytics": "opt out of private analytics",
|
"SettingsDisableAnalytics": "opt out of private analytics",
|
||||||
"SettingsAnalyticsExplanation": "enable if you don't want to be included in anonymous traffic stats. read more about this in about > privacy policy (tl;dr: nothing about you is ever stored or tracked, no cookies are used).",
|
"SettingsAnalyticsExplanation": "enable if you don't want to be included in anonymous traffic stats. read more about this in about > privacy policy (tl;dr: nothing about you is ever stored or tracked, no cookies are used).",
|
||||||
"AnalyticsDescription": "cobalt uses a self-hosted plausible instance to get an approximate number of how many people use it.\n\nplausible is fully compliant with GDPR, CCPA and PECR, doesn't use cookies, and never stores any identifiable info, not even your ip address.\n\nall data is aggregated and never personalized. nothing about what you download is ever saved anywhere. it's used just for anonymous traffic stats, nothing more.\n\nplausible is fully open source, just like cobalt, and if you want to learn more about it, you can do so <a class=\"text-backdrop link\" href=\"https://plausible.io\" target=\"_blank\">here</a>. if you wish to opt out of traffic stats, you can do it in settings > other."
|
"AnalyticsDescription": "cobalt uses a self-hosted plausible instance to get an approximate number of how many people use it.\n\nplausible is fully compliant with GDPR, CCPA and PECR, doesn't use cookies, and never stores any identifiable info, not even your ip address.\n\nall data is aggregated and never personalized. nothing about what you download is ever saved anywhere. it's used just for anonymous traffic stats, nothing more.\n\nplausible is fully open source, just like cobalt, and if you want to learn more about it, you can do so <a class=\"text-backdrop link\" href=\"https://plausible.io\" target=\"_blank\">here</a>. if you wish to opt out of traffic stats, you can do it in settings > other.",
|
||||||
|
"SettingsTikTokH265": "prefer h265",
|
||||||
|
"SettingsTikTokH265Description": "download 1080p videos from tiktok in h265/hevc format when available.",
|
||||||
|
"SettingsYoutubeDub": "use browser language",
|
||||||
|
"SettingsYoutubeDubDescription": "uses your browser's default language for youtube dubbed audio tracks. works even if cobalt ui isn't translated to your language."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -91,7 +91,6 @@
|
|||||||
"DonateSub": "ты можешь помочь!",
|
"DonateSub": "ты можешь помочь!",
|
||||||
"DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span> для всех. но разработка и поддержка медиа сервиса, которым пользуются более 750 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
|
"DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span> для всех. но разработка и поддержка медиа сервиса, которым пользуются более 750 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
|
||||||
"DonateVia": "открыть",
|
"DonateVia": "открыть",
|
||||||
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
|
|
||||||
"SettingsVideoMute": "убрать аудио",
|
"SettingsVideoMute": "убрать аудио",
|
||||||
"SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.",
|
"SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.",
|
||||||
"ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",
|
"ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",
|
||||||
@ -107,11 +106,6 @@
|
|||||||
"SettingsCodecSubtitle": "кодек для youtube видео",
|
"SettingsCodecSubtitle": "кодек для youtube видео",
|
||||||
"SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.",
|
"SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.",
|
||||||
"SettingsAudioDub": "звуковая дорожка для youtube видео",
|
"SettingsAudioDub": "звуковая дорожка для youtube видео",
|
||||||
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.",
|
|
||||||
"SettingsDubDefault": "оригинал",
|
|
||||||
"SettingsDubAuto": "авто",
|
|
||||||
"SettingsVimeoPrefer": "тип загрузок с vimeo",
|
|
||||||
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".",
|
|
||||||
"ShareURL": "поделиться",
|
"ShareURL": "поделиться",
|
||||||
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!",
|
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!",
|
||||||
"PopupCloseDone": "готово",
|
"PopupCloseDone": "готово",
|
||||||
@ -159,6 +153,10 @@
|
|||||||
"PrivateAnalytics": "приватная аналитика",
|
"PrivateAnalytics": "приватная аналитика",
|
||||||
"SettingsDisableAnalytics": "отключить приватную аналитику",
|
"SettingsDisableAnalytics": "отключить приватную аналитику",
|
||||||
"SettingsAnalyticsExplanation": "включи, если не хочешь быть частью анонимной статистики трафика. подробнее об этом можно прочитать в политике конфиденциальности (tl;dr: ничего о тебе или твоих действиях не хранится и не отслеживается, даже куки нет).",
|
"SettingsAnalyticsExplanation": "включи, если не хочешь быть частью анонимной статистики трафика. подробнее об этом можно прочитать в политике конфиденциальности (tl;dr: ничего о тебе или твоих действиях не хранится и не отслеживается, даже куки нет).",
|
||||||
"AnalyticsDescription": "кобальт использует собственный инстанс plausible чтобы иметь приблизительное представление о том, сколько людей им пользуются.\n\nplausible полностью соответствует GDPR, CCPA и PECR, не использует куки и никогда не хранит никакой идентифицируемой информации, даже ip-адрес.\n\nвсе данные агрегируются и никогда не персонализируются. ничего о том, что ты скачиваешь, никогда не сохраняется. это просто анонимная статистика трафика, ничего больше.\n\nplausible также как и кобальт имеет открытый исходный код, и, если ты хочешь узнать о нём больше, то это можно сделать <a class=\"text-backdrop link\" href=\"https://plausible.io\" target=\"_blank\">здесь</a>. а если же ты хочешь исключить себя из статистики, то это можно сделать в настройках > другое."
|
"AnalyticsDescription": "кобальт использует собственный инстанс plausible чтобы иметь приблизительное представление о том, сколько людей им пользуются.\n\nplausible полностью соответствует GDPR, CCPA и PECR, не использует куки и никогда не хранит никакой идентифицируемой информации, даже ip-адрес.\n\nвсе данные агрегируются и никогда не персонализируются. ничего о том, что ты скачиваешь, никогда не сохраняется. это просто анонимная статистика трафика, ничего больше.\n\nplausible также как и кобальт имеет открытый исходный код, и, если ты хочешь узнать о нём больше, то это можно сделать <a class=\"text-backdrop link\" href=\"https://plausible.io\" target=\"_blank\">здесь</a>. а если же ты хочешь исключить себя из статистики, то это можно сделать в настройках > другое.",
|
||||||
|
"SettingsTikTokH265": "предпочитать h265",
|
||||||
|
"SettingsTikTokH265Description": "скачивает видео с tiktok в 1080p и h265/hevc, когда это возможно.",
|
||||||
|
"SettingsYoutubeDub": "использовать язык браузера",
|
||||||
|
"SettingsYoutubeDubDescription": "использует главный язык браузера для аудиодорожек на youtube. работает даже если кобальт не переведён в твой язык."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,31 @@ Object.values(servicesConfigJson.config).forEach(service => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const
|
||||||
|
apiURL = process.env.API_URL || '',
|
||||||
|
|
||||||
|
// WEB mode related environment variables
|
||||||
|
webEnvs = {
|
||||||
|
webPort: process.env.WEB_PORT || 9001,
|
||||||
|
webURL: process.env.WEB_URL || '',
|
||||||
|
showSponsors: !!process.env.SHOW_SPONSORS,
|
||||||
|
isBeta: !!process.env.IS_BETA,
|
||||||
|
plausibleHostname: process.env.PLAUSIBLE_HOSTNAME,
|
||||||
|
apiURL
|
||||||
|
},
|
||||||
|
|
||||||
|
// API mode related environment variables
|
||||||
|
apiEnvs = {
|
||||||
|
apiPort: process.env.API_PORT || 9000,
|
||||||
|
apiName: process.env.API_NAME || 'unknown',
|
||||||
|
corsWildcard: process.env.CORS_WILDCARD !== '0',
|
||||||
|
corsURL: process.env.CORS_URL,
|
||||||
|
cookiePath: process.env.COOKIE_PATH,
|
||||||
|
processingPriority: process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY),
|
||||||
|
tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO),
|
||||||
|
apiURL
|
||||||
|
}
|
||||||
|
|
||||||
export const
|
export const
|
||||||
services = servicesConfigJson.config,
|
services = servicesConfigJson.config,
|
||||||
audioIgnore = servicesConfigJson.audioIgnore,
|
audioIgnore = servicesConfigJson.audioIgnore,
|
||||||
@ -26,4 +51,7 @@ export const
|
|||||||
supportedAudio = config.supportedAudio,
|
supportedAudio = config.supportedAudio,
|
||||||
celebrations = config.celebrations,
|
celebrations = config.celebrations,
|
||||||
links = config.links,
|
links = config.links,
|
||||||
sponsors = config.sponsors
|
sponsors = config.sponsors,
|
||||||
|
mode = (apiURL && !webEnvs.webURL) ? 'API' :
|
||||||
|
(webEnvs.webURL && apiURL) ? 'WEB' : undefined,
|
||||||
|
env = mode === 'API' ? apiEnvs : webEnvs
|
@ -1,4 +1,4 @@
|
|||||||
import { authorInfo, celebrations, sponsors } from "../config.js";
|
import { authorInfo, celebrations, sponsors, env } from "../config.js";
|
||||||
import emoji from "../emoji.js";
|
import emoji from "../emoji.js";
|
||||||
import { loadFile } from "../sub/loadFromFs.js";
|
import { loadFile } from "../sub/loadFromFs.js";
|
||||||
|
|
||||||
@ -266,5 +266,5 @@ export function sponsoredList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function betaTag() {
|
export function betaTag() {
|
||||||
return process.env.IS_BETA ? '<span class="logo-sub">β</span>' : ''
|
return env.isBeta ? '<span class="logo-sub">β</span>' : ''
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,30 @@
|
|||||||
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag, linkSVG } from "./elements.js";
|
import { services as s, version, repo, donations, supportedAudio, links, env } from "../config.js";
|
||||||
import { services as s, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js";
|
|
||||||
import { getCommitInfo } from "../sub/currentCommit.js";
|
import { getCommitInfo } from "../sub/currentCommit.js";
|
||||||
import loc from "../../localization/manager.js";
|
import loc from "../../localization/manager.js";
|
||||||
import emoji from "../emoji.js";
|
import emoji from "../emoji.js";
|
||||||
import changelogManager from "../changelog/changelogManager.js";
|
import changelogManager from "../changelog/changelogManager.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
checkbox,
|
||||||
|
collapsibleList,
|
||||||
|
explanation,
|
||||||
|
footerButtons,
|
||||||
|
multiPagePopup,
|
||||||
|
popup,
|
||||||
|
popupWithBottomButtons,
|
||||||
|
sep,
|
||||||
|
settingsCategory,
|
||||||
|
switcher,
|
||||||
|
socialLink,
|
||||||
|
socialLinks,
|
||||||
|
urgentNotice,
|
||||||
|
keyboardShortcuts,
|
||||||
|
webLoc,
|
||||||
|
sponsoredList,
|
||||||
|
betaTag,
|
||||||
|
linkSVG
|
||||||
|
} from "./elements.js";
|
||||||
|
|
||||||
let com = getCommitInfo();
|
let com = getCommitInfo();
|
||||||
|
|
||||||
let enabledServices = Object.keys(s).filter(p => s[p].enabled).sort().map((p) => {
|
let enabledServices = Object.keys(s).filter(p => s[p].enabled).sort().map((p) => {
|
||||||
@ -48,10 +68,10 @@ export default function(obj) {
|
|||||||
|
|
||||||
<title>${t("AppTitleCobalt")}</title>
|
<title>${t("AppTitleCobalt")}</title>
|
||||||
|
|
||||||
<meta property="og:url" content="${process.env.WEB_URL}">
|
<meta property="og:url" content="${env.webURL}">
|
||||||
<meta property="og:title" content="${t("AppTitleCobalt")}">
|
<meta property="og:title" content="${t("AppTitleCobalt")}">
|
||||||
<meta property="og:description" content="${t('EmbedBriefDescription')}">
|
<meta property="og:description" content="${t('EmbedBriefDescription')}">
|
||||||
<meta property="og:image" content="${process.env.WEB_URL}icons/generic.png">
|
<meta property="og:image" content="${env.webURL}icons/generic.png">
|
||||||
<meta name="title" content="${t("AppTitleCobalt")}">
|
<meta name="title" content="${t("AppTitleCobalt")}">
|
||||||
<meta name="description" content="${t('AboutSummary')}">
|
<meta name="description" content="${t('AboutSummary')}">
|
||||||
<meta name="theme-color" content="#000000">
|
<meta name="theme-color" content="#000000">
|
||||||
@ -75,11 +95,11 @@ export default function(obj) {
|
|||||||
<link rel="preload" href="assets/meowbalt/error.png" as="image">
|
<link rel="preload" href="assets/meowbalt/error.png" as="image">
|
||||||
<link rel="preload" href="assets/meowbalt/question.png" as="image">
|
<link rel="preload" href="assets/meowbalt/question.png" as="image">
|
||||||
|
|
||||||
${process.env.PLAUSIBLE_HOSTNAME ?
|
${env.plausibleHostname ?
|
||||||
`<script
|
`<script
|
||||||
defer
|
defer
|
||||||
data-domain="${new URL(process.env.WEB_URL).hostname}"
|
data-domain="${new URL(env.webURL).hostname}"
|
||||||
src="https://${process.env.PLAUSIBLE_HOSTNAME}/js/script.js"
|
src="https://${env.plausibleHostname}/js/script.js"
|
||||||
></script>`
|
></script>`
|
||||||
: ''}
|
: ''}
|
||||||
</head>
|
</head>
|
||||||
@ -98,7 +118,7 @@ export default function(obj) {
|
|||||||
header: {
|
header: {
|
||||||
aboveTitle: {
|
aboveTitle: {
|
||||||
text: t('MadeWithLove'),
|
text: t('MadeWithLove'),
|
||||||
url: authorInfo.link
|
url: repo
|
||||||
},
|
},
|
||||||
closeAria: t('AccessibilityGoBack'),
|
closeAria: t('AccessibilityGoBack'),
|
||||||
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
|
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
|
||||||
@ -169,7 +189,7 @@ export default function(obj) {
|
|||||||
name: "privacy",
|
name: "privacy",
|
||||||
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
|
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
|
||||||
body: t("PrivacyPolicy") + `${
|
body: t("PrivacyPolicy") + `${
|
||||||
process.env.PLAUSIBLE_HOSTNAME ? `<br><br>${t("AnalyticsDescription")}` : ''
|
env.plausibleHostname ? `<br><br>${t("AnalyticsDescription")}` : ''
|
||||||
}`
|
}`
|
||||||
}, {
|
}, {
|
||||||
name: "legal",
|
name: "legal",
|
||||||
@ -177,7 +197,7 @@ export default function(obj) {
|
|||||||
body: t("FairUse")
|
body: t("FairUse")
|
||||||
}])
|
}])
|
||||||
},
|
},
|
||||||
...(process.env.SHOW_SPONSORS ?
|
...(env.showSponsors ?
|
||||||
[{
|
[{
|
||||||
text: t("SponsoredBy"),
|
text: t("SponsoredBy"),
|
||||||
classes: ["sponsored-by-text"],
|
classes: ["sponsored-by-text"],
|
||||||
@ -285,12 +305,6 @@ export default function(obj) {
|
|||||||
}, {
|
}, {
|
||||||
text: donate.replace(/REPLACEME/g, t('ClickToCopy')),
|
text: donate.replace(/REPLACEME/g, t('ClickToCopy')),
|
||||||
classes: ["desc-padding"]
|
classes: ["desc-padding"]
|
||||||
}, {
|
|
||||||
text: sep(),
|
|
||||||
raw: true
|
|
||||||
}, {
|
|
||||||
text: t('DonateHireMe', authorInfo.link),
|
|
||||||
classes: ["desc-padding"]
|
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
}],
|
}],
|
||||||
@ -338,16 +352,6 @@ export default function(obj) {
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
+ settingsCategory({
|
|
||||||
name: "twitter",
|
|
||||||
title: "twitter",
|
|
||||||
body: checkbox([{
|
|
||||||
action: "twitterGif",
|
|
||||||
name: t("SettingsTwitterGif"),
|
|
||||||
padding: "no-margin"
|
|
||||||
}])
|
|
||||||
+ explanation(t('SettingsTwitterGifDescription'))
|
|
||||||
})
|
|
||||||
+ settingsCategory({
|
+ settingsCategory({
|
||||||
name: "codec",
|
name: "codec",
|
||||||
title: t('SettingsCodecSubtitle'),
|
title: t('SettingsCodecSubtitle'),
|
||||||
@ -367,19 +371,24 @@ export default function(obj) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
+ settingsCategory({
|
+ settingsCategory({
|
||||||
name: "vimeo",
|
name: "twitter",
|
||||||
title: t('SettingsVimeoPrefer'),
|
title: "twitter",
|
||||||
body: switcher({
|
body: checkbox([{
|
||||||
name: "vimeoDash",
|
action: "twitterGif",
|
||||||
explanation: t('SettingsVimeoPreferDescription'),
|
name: t("SettingsTwitterGif"),
|
||||||
items: [{
|
padding: "no-margin"
|
||||||
action: "false",
|
}])
|
||||||
text: "progressive"
|
+ explanation(t('SettingsTwitterGifDescription'))
|
||||||
}, {
|
|
||||||
action: "true",
|
|
||||||
text: "dash"
|
|
||||||
}]
|
|
||||||
})
|
})
|
||||||
|
+ settingsCategory({
|
||||||
|
name: "tiktok",
|
||||||
|
title: "tiktok",
|
||||||
|
body: checkbox([{
|
||||||
|
action: "tiktokH265",
|
||||||
|
name: t("SettingsTikTokH265"),
|
||||||
|
padding: "no-margin"
|
||||||
|
}])
|
||||||
|
+ explanation(t('SettingsTikTokH265Description'))
|
||||||
})
|
})
|
||||||
}, {
|
}, {
|
||||||
name: "audio",
|
name: "audio",
|
||||||
@ -401,19 +410,14 @@ export default function(obj) {
|
|||||||
+ explanation(t('SettingsVideoMuteExplanation'))
|
+ explanation(t('SettingsVideoMuteExplanation'))
|
||||||
})
|
})
|
||||||
+ settingsCategory({
|
+ settingsCategory({
|
||||||
name: "dub",
|
name: "youtube-dub",
|
||||||
title: t("SettingsAudioDub"),
|
title: t("SettingsAudioDub"),
|
||||||
body: switcher({
|
body: checkbox([{
|
||||||
name: "dubLang",
|
action: "ytDub",
|
||||||
explanation: t('SettingsAudioDubDescription'),
|
name: t("SettingsYoutubeDub"),
|
||||||
items: [{
|
padding: "no-margin"
|
||||||
action: "original",
|
}])
|
||||||
text: t('SettingsDubDefault')
|
+ explanation(t('SettingsYoutubeDubDescription'))
|
||||||
}, {
|
|
||||||
action: "auto",
|
|
||||||
text: t('SettingsDubAuto')
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
+ settingsCategory({
|
+ settingsCategory({
|
||||||
name: "tiktok-audio",
|
name: "tiktok-audio",
|
||||||
@ -499,7 +503,7 @@ export default function(obj) {
|
|||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
+ (() => {
|
+ (() => {
|
||||||
if (process.env.PLAUSIBLE_HOSTNAME) {
|
if (env.plausibleHostname) {
|
||||||
return settingsCategory({
|
return settingsCategory({
|
||||||
name: "privacy",
|
name: "privacy",
|
||||||
title: t('PrivateAnalytics'),
|
title: t('PrivateAnalytics'),
|
||||||
@ -629,7 +633,7 @@ export default function(obj) {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
let defaultApiUrl = '${process.env.API_URL || ''}';
|
let defaultApiUrl = '${env.apiURL}';
|
||||||
const loc = ${webLoc(t,
|
const loc = ${webLoc(t,
|
||||||
[
|
[
|
||||||
'ErrorNoInternet',
|
'ErrorNoInternet',
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Cookie from './cookie.js';
|
import Cookie from './cookie.js';
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
|
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
|
||||||
|
import { env } from '../../../modules/config.js'
|
||||||
|
|
||||||
const WRITE_INTERVAL = 60000,
|
const WRITE_INTERVAL = 60000,
|
||||||
cookiePath = process.env.COOKIE_PATH,
|
cookiePath = env.cookiePath,
|
||||||
COUNTER = Symbol('counter');
|
COUNTER = Symbol('counter');
|
||||||
|
|
||||||
let cookies = {}, dirty = false, intervalId;
|
let cookies = {}, dirty = false, intervalId;
|
||||||
|
@ -88,7 +88,8 @@ export default async function(host, patternMatch, url, lang, obj) {
|
|||||||
postId: patternMatch.postId,
|
postId: patternMatch.postId,
|
||||||
id: patternMatch.id,
|
id: patternMatch.id,
|
||||||
fullAudio: obj.isTTFullAudio,
|
fullAudio: obj.isTTFullAudio,
|
||||||
isAudioOnly: isAudioOnly
|
isAudioOnly: isAudioOnly,
|
||||||
|
h265: obj.tiktokH265
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "tumblr":
|
case "tumblr":
|
||||||
@ -103,8 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
|
|||||||
id: patternMatch.id.slice(0, 11),
|
id: patternMatch.id.slice(0, 11),
|
||||||
password: patternMatch.password,
|
password: patternMatch.password,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: isAudioOnly,
|
isAudioOnly: isAudioOnly
|
||||||
forceDash: isAudioOnly ? true : obj.vimeoDash
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "soundcloud":
|
case "soundcloud":
|
||||||
@ -151,6 +151,7 @@ export default async function(host, patternMatch, url, lang, obj) {
|
|||||||
case "rutube":
|
case "rutube":
|
||||||
r = await rutube({
|
r = await rutube({
|
||||||
id: patternMatch.id,
|
id: patternMatch.id,
|
||||||
|
yappyId: patternMatch.yappyId,
|
||||||
quality: obj.vQuality,
|
quality: obj.vQuality,
|
||||||
isAudioOnly: isAudioOnly
|
isAudioOnly: isAudioOnly
|
||||||
});
|
});
|
||||||
|
@ -137,40 +137,22 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||||||
audioFormat = "best"
|
audioFormat = "best"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceBestAudio = r.bestAudio || services[host]["bestAudio"];
|
||||||
const isBestAudio = audioFormat === "best";
|
const isBestAudio = audioFormat === "best";
|
||||||
const isBestOrMp3 = audioFormat === "mp3" || isBestAudio;
|
const isBestOrMp3 = isBestAudio || audioFormat === "mp3";
|
||||||
const isBestAudioDefined = isBestAudio && services[host]["bestAudio"];
|
const isBestAudioDefined = isBestAudio && serviceBestAudio;
|
||||||
const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]);
|
const isBestHostAudio = serviceBestAudio && (audioFormat === serviceBestAudio);
|
||||||
|
|
||||||
const isTikTok = host === "tiktok" || host === "douyin";
|
|
||||||
const isTumblrAudio = host === "tumblr" && !r.filename;
|
const isTumblrAudio = host === "tumblr" && !r.filename;
|
||||||
const isSoundCloud = host === "soundcloud";
|
const isSoundCloud = host === "soundcloud";
|
||||||
|
|
||||||
if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) {
|
|
||||||
if (r.isMp3 && isBestOrMp3) {
|
|
||||||
audioFormat = "mp3";
|
|
||||||
processType = "bridge"
|
|
||||||
} else if (isBestAudio) {
|
|
||||||
audioFormat = "m4a";
|
|
||||||
processType = "bridge"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSoundCloud && services.soundcloud.audioFormats.includes(audioFormat)) {
|
|
||||||
if (r.isMp3 && isBestOrMp3) {
|
|
||||||
audioFormat = "mp3";
|
|
||||||
processType = "render"
|
|
||||||
copy = true
|
|
||||||
} else if (isBestAudio || audioFormat === "opus") {
|
|
||||||
audioFormat = "opus";
|
|
||||||
processType = "render"
|
|
||||||
copy = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBestAudioDefined || isBestHostAudio) {
|
if (isBestAudioDefined || isBestHostAudio) {
|
||||||
audioFormat = services[host]["bestAudio"];
|
audioFormat = serviceBestAudio;
|
||||||
processType = "bridge";
|
processType = "bridge";
|
||||||
|
if (isSoundCloud) {
|
||||||
|
processType = "render"
|
||||||
|
copy = true
|
||||||
|
}
|
||||||
} else if (isBestAudio && !isSoundCloud) {
|
} else if (isBestAudio && !isSoundCloud) {
|
||||||
audioFormat = "m4a";
|
audioFormat = "m4a";
|
||||||
copy = true
|
copy = true
|
||||||
|
@ -2,11 +2,38 @@ import { createStream } from "../../stream/manage.js";
|
|||||||
import { genericUserAgent } from "../../config.js";
|
import { genericUserAgent } from "../../config.js";
|
||||||
import { getCookie, updateCookie } from "../cookie/manager.js";
|
import { getCookie, updateCookie } from "../cookie/manager.js";
|
||||||
|
|
||||||
const commonInstagramHeaders = {
|
const commonHeaders = {
|
||||||
'user-agent': genericUserAgent,
|
"user-agent": genericUserAgent,
|
||||||
'sec-gpc': '1',
|
"sec-gpc": "1",
|
||||||
'sec-fetch-site': 'same-origin',
|
"sec-fetch-site": "same-origin",
|
||||||
'x-ig-app-id': '936619743392459'
|
"x-ig-app-id": "936619743392459"
|
||||||
|
}
|
||||||
|
const mobileHeaders = {
|
||||||
|
"x-ig-app-locale": "en_US",
|
||||||
|
"x-ig-device-locale": "en_US",
|
||||||
|
"x-ig-mapped-locale": "en_US",
|
||||||
|
"user-agent": "Instagram 275.0.0.27.98 Android (33/13; 280dpi; 720x1423; Xiaomi; Redmi 7; onclite; qcom; en_US; 458229237)",
|
||||||
|
"accept-language": "en-US",
|
||||||
|
"x-fb-http-engine": "Liger",
|
||||||
|
"x-fb-client-ip": "True",
|
||||||
|
"x-fb-server-cluster": "True",
|
||||||
|
"content-length": "0",
|
||||||
|
}
|
||||||
|
const embedHeaders = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||||
|
"Accept-Language": "en-GB,en;q=0.9",
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
"Dnt": "1",
|
||||||
|
"Priority": "u=0, i",
|
||||||
|
"Sec-Ch-Ua": 'Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99',
|
||||||
|
"Sec-Ch-Ua-Mobile": "?0",
|
||||||
|
"Sec-Ch-Ua-Platform": "macOS",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedDtsg = {
|
const cachedDtsg = {
|
||||||
@ -20,7 +47,7 @@ async function findDtsgId(cookie) {
|
|||||||
|
|
||||||
const data = await fetch('https://www.instagram.com/', {
|
const data = await fetch('https://www.instagram.com/', {
|
||||||
headers: {
|
headers: {
|
||||||
...commonInstagramHeaders,
|
...commonHeaders,
|
||||||
cookie
|
cookie
|
||||||
}
|
}
|
||||||
}).then(r => r.text());
|
}).then(r => r.text());
|
||||||
@ -38,7 +65,7 @@ async function findDtsgId(cookie) {
|
|||||||
|
|
||||||
async function request(url, cookie, method = 'GET', requestData) {
|
async function request(url, cookie, method = 'GET', requestData) {
|
||||||
let headers = {
|
let headers = {
|
||||||
...commonInstagramHeaders,
|
...commonHeaders,
|
||||||
'x-ig-www-claim': cookie?._wwwClaim || '0',
|
'x-ig-www-claim': cookie?._wwwClaim || '0',
|
||||||
'x-csrftoken': cookie?.values()?.csrftoken,
|
'x-csrftoken': cookie?.values()?.csrftoken,
|
||||||
cookie
|
cookie
|
||||||
@ -60,26 +87,36 @@ async function request(url, cookie, method = 'GET', requestData) {
|
|||||||
return data.json();
|
return data.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestMobileApi(id, cookie) {
|
||||||
|
const oembedURL = new URL('https://i.instagram.com/api/v1/oembed/');
|
||||||
|
oembedURL.searchParams.set('url', `https://www.instagram.com/p/${id}/`);
|
||||||
|
|
||||||
|
const oembed = await fetch(oembedURL, {
|
||||||
|
headers: {
|
||||||
|
...mobileHeaders,
|
||||||
|
cookie
|
||||||
|
}
|
||||||
|
}).then(r => r.json()).catch(() => {});
|
||||||
|
|
||||||
|
const mediaId = oembed?.media_id;
|
||||||
|
if (!mediaId) return false;
|
||||||
|
|
||||||
|
const mediaInfo = await fetch(`https://i.instagram.com/api/v1/media/${mediaId}/info/`, {
|
||||||
|
headers: {
|
||||||
|
...mobileHeaders,
|
||||||
|
cookie
|
||||||
|
}
|
||||||
|
}).then(r => r.json()).catch(() => {});
|
||||||
|
|
||||||
|
return mediaInfo?.items?.[0];
|
||||||
|
}
|
||||||
async function requestHTML(id, cookie) {
|
async function requestHTML(id, cookie) {
|
||||||
const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, {
|
const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
...embedHeaders,
|
||||||
"Accept-Language": "en-GB,en;q=0.9",
|
cookie
|
||||||
"Cache-Control": "max-age=0",
|
|
||||||
"Dnt": "1",
|
|
||||||
"Priority": "u=0, i",
|
|
||||||
"Sec-Ch-Ua": 'Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99',
|
|
||||||
"Sec-Ch-Ua-Mobile": "?0",
|
|
||||||
"Sec-Ch-Ua-Platform": "macOS",
|
|
||||||
"Sec-Fetch-Dest": "document",
|
|
||||||
"Sec-Fetch-Mode": "navigate",
|
|
||||||
"Sec-Fetch-Site": "none",
|
|
||||||
"Sec-Fetch-User": "?1",
|
|
||||||
"Upgrade-Insecure-Requests": "1",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
||||||
...cookie
|
|
||||||
}
|
}
|
||||||
}).then(r => r.text());
|
}).then(r => r.text()).catch(() => {});
|
||||||
|
|
||||||
let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]);
|
let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]);
|
||||||
|
|
||||||
@ -196,25 +233,29 @@ function extractNewPost(data, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getPost(id) {
|
async function getPost(id) {
|
||||||
let data, result, dataType = 'old';
|
let data, result;
|
||||||
try {
|
try {
|
||||||
const cookie = getCookie('instagram');
|
const cookie = getCookie('instagram');
|
||||||
|
|
||||||
data = await requestHTML(id);
|
// mobile api (no cookie, cookie)
|
||||||
|
data = await requestMobileApi(id);
|
||||||
|
if (!data && cookie) data = await requestMobileApi(id, cookie);
|
||||||
|
|
||||||
|
// html embed (no cookie, cookie)
|
||||||
|
if (!data) data = await requestHTML(id);
|
||||||
if (!data && cookie) data = await requestHTML(id, cookie);
|
if (!data && cookie) data = await requestHTML(id, cookie);
|
||||||
|
|
||||||
if (!data) {
|
// web app graphql api (no cookie, cookie)
|
||||||
dataType = 'new';
|
if (!data) data = await requestGQL(id);
|
||||||
data = await requestGQL(id, cookie);
|
if (!data && cookie) data = await requestGQL(id, cookie);
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
if (dataType === 'new') {
|
if (data?.gql_data) {
|
||||||
result = extractNewPost(data, id)
|
|
||||||
} else {
|
|
||||||
result = extractOldPost(data, id)
|
result = extractOldPost(data, id)
|
||||||
|
} else {
|
||||||
|
result = extractNewPost(data, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) return result;
|
if (result) return result;
|
||||||
|
@ -1,18 +1,47 @@
|
|||||||
import HLS from 'hls-parser';
|
import HLS from 'hls-parser';
|
||||||
|
|
||||||
import { maxVideoDuration } from "../../config.js";
|
import { maxVideoDuration } from "../../config.js";
|
||||||
import { cleanString } from '../../sub/utils.js';
|
import { cleanString } from '../../sub/utils.js';
|
||||||
|
|
||||||
|
async function requestJSON(url) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
return await r.json();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function(obj) {
|
export default async function(obj) {
|
||||||
|
if (obj.yappyId) {
|
||||||
|
let yappy = await requestJSON(
|
||||||
|
`https://rutube.ru/pangolin/api/web/yappy/yappypage/?client=wdp&videoId=${obj.yappyId}&page=1&page_size=15`
|
||||||
|
)
|
||||||
|
let yappyURL = yappy?.results?.find(r => r.id === obj.yappyId)?.link;
|
||||||
|
if (!yappyURL) return { error: 'ErrorEmptyDownload' };
|
||||||
|
|
||||||
|
return {
|
||||||
|
urls: yappyURL,
|
||||||
|
filename: `rutube_yappy_${obj.yappyId}.mp4`,
|
||||||
|
audioFilename: `rutube_yappy_${obj.yappyId}_audio`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let quality = obj.quality === "max" ? "9000" : obj.quality;
|
let quality = obj.quality === "max" ? "9000" : obj.quality;
|
||||||
let play = await fetch(`https://rutube.ru/api/play/options/${obj.id}/?no_404=true&referer&pver=v2`).then((r) => { return r.json() }).catch(() => { return false });
|
|
||||||
|
let play = await requestJSON(
|
||||||
|
`https://rutube.ru/api/play/options/${obj.id}/?no_404=true&referer&pver=v2`
|
||||||
|
)
|
||||||
if (!play) return { error: 'ErrorCouldntFetch' };
|
if (!play) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
if ("hls" in play.live_streams) return { error: 'ErrorLiveVideo' };
|
if (play.detail || !play.video_balancer) return { error: 'ErrorEmptyDownload' };
|
||||||
if (!play.video_balancer || play.detail) return { error: 'ErrorEmptyDownload' };
|
if (play.live_streams?.hls) return { error: 'ErrorLiveVideo' };
|
||||||
|
|
||||||
if (play.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
if (play.duration > maxVideoDuration)
|
||||||
|
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||||
|
|
||||||
|
let m3u8 = await fetch(play.video_balancer.m3u8)
|
||||||
|
.then(r => r.text())
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
let m3u8 = await fetch(play.video_balancer.m3u8).then((r) => { return r.text() }).catch(() => { return false });
|
|
||||||
if (!m3u8) return { error: 'ErrorCouldntFetch' };
|
if (!m3u8) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
m3u8 = HLS.parse(m3u8).variants.sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
|
m3u8 = HLS.parse(m3u8).variants.sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
|
||||||
@ -21,6 +50,7 @@ export default async function(obj) {
|
|||||||
if (Number(quality) < bestQuality.resolution.height) {
|
if (Number(quality) < bestQuality.resolution.height) {
|
||||||
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
|
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileMetadata = {
|
let fileMetadata = {
|
||||||
title: cleanString(play.title.trim()),
|
title: cleanString(play.title.trim()),
|
||||||
artist: cleanString(play.author.name.trim()),
|
artist: cleanString(play.author.name.trim()),
|
||||||
@ -31,7 +61,7 @@ export default async function(obj) {
|
|||||||
isM3U8: true,
|
isM3U8: true,
|
||||||
filenameAttributes: {
|
filenameAttributes: {
|
||||||
service: "rutube",
|
service: "rutube",
|
||||||
id: play.id,
|
id: obj.id,
|
||||||
title: fileMetadata.title,
|
title: fileMetadata.title,
|
||||||
author: fileMetadata.artist,
|
author: fileMetadata.artist,
|
||||||
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
|
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
|
||||||
|
@ -4,7 +4,7 @@ import { cleanString } from "../../sub/utils.js";
|
|||||||
const cachedID = {
|
const cachedID = {
|
||||||
version: '',
|
version: '',
|
||||||
id: ''
|
id: ''
|
||||||
};
|
}
|
||||||
|
|
||||||
async function findClientID() {
|
async function findClientID() {
|
||||||
try {
|
try {
|
||||||
@ -32,9 +32,7 @@ async function findClientID() {
|
|||||||
cachedID.id = clientid;
|
cachedID.id = clientid;
|
||||||
|
|
||||||
return clientid;
|
return clientid;
|
||||||
} catch (e) {
|
} catch {}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function(obj) {
|
export default async function(obj) {
|
||||||
@ -58,27 +56,30 @@ export default async function(obj) {
|
|||||||
|
|
||||||
let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => {
|
let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => {
|
||||||
return r.status === 200 ? r.json() : false
|
return r.status === 200 ? r.json() : false
|
||||||
}).catch(() => { return false });
|
}).catch(() => {});
|
||||||
|
|
||||||
if (!json) return { error: 'ErrorCouldntFetch' };
|
if (!json) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
|
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
|
||||||
|
|
||||||
let isMp3,
|
let bestAudio = "opus",
|
||||||
selectedStream = json.media.transcodings.filter(v => v.preset === "opus_0_0")
|
selectedStream = json.media.transcodings.find(v => v.preset === "opus_0_0");
|
||||||
|
|
||||||
// fall back to mp3 if no opus is available
|
// fall back to mp3 if no opus is available
|
||||||
if (selectedStream.length === 0) {
|
if (selectedStream.length === 0) {
|
||||||
selectedStream = json.media.transcodings.filter(v => v.preset === "mp3_0_0")
|
selectedStream = json.media.transcodings.find(v => v.preset === "mp3_0_0");
|
||||||
isMp3 = true
|
bestAudio = "mp3"
|
||||||
}
|
}
|
||||||
let fileUrlBase = selectedStream[0]["url"];
|
|
||||||
|
let fileUrlBase = selectedStream.url;
|
||||||
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
|
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
|
||||||
|
|
||||||
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
|
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
|
||||||
|
|
||||||
if (json.duration > maxVideoDuration) return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };
|
if (json.duration > maxVideoDuration)
|
||||||
|
return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] };
|
||||||
|
|
||||||
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
|
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => {});
|
||||||
if (!file) return { error: 'ErrorCouldntFetch' };
|
if (!file) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
let fileMetadata = {
|
let fileMetadata = {
|
||||||
@ -94,7 +95,7 @@ export default async function(obj) {
|
|||||||
title: fileMetadata.title,
|
title: fileMetadata.title,
|
||||||
author: fileMetadata.artist
|
author: fileMetadata.artist
|
||||||
},
|
},
|
||||||
isMp3,
|
bestAudio,
|
||||||
fileMetadata
|
fileMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { genericUserAgent } from "../../config.js";
|
import { genericUserAgent, env } from "../../config.js";
|
||||||
|
|
||||||
const shortDomain = "https://vt.tiktok.com/";
|
const shortDomain = "https://vt.tiktok.com/";
|
||||||
const apiPath = "https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/?region=US&carrier_region=US";
|
const apiPath = "https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/?region=US&carrier_region=US";
|
||||||
@ -7,7 +7,7 @@ const apiUserAgent = "TikTok/338014 CFNetwork/1410.1 Darwin/22.6.0";
|
|||||||
export default async function(obj) {
|
export default async function(obj) {
|
||||||
let postId = obj.postId ? obj.postId : false;
|
let postId = obj.postId ? obj.postId : false;
|
||||||
|
|
||||||
if (!process.env.TIKTOK_DEVICE_INFO) return { error: 'ErrorCouldntFetch' };
|
if (!env.tiktokDeviceInfo) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
if (!postId) {
|
if (!postId) {
|
||||||
let html = await fetch(`${shortDomain}${obj.id}`, {
|
let html = await fetch(`${shortDomain}${obj.id}`, {
|
||||||
@ -27,8 +27,7 @@ export default async function(obj) {
|
|||||||
}
|
}
|
||||||
if (!postId) return { error: 'ErrorCantGetID' };
|
if (!postId) return { error: 'ErrorCantGetID' };
|
||||||
|
|
||||||
let deviceInfo = JSON.parse(process.env.TIKTOK_DEVICE_INFO);
|
let deviceInfo = new URLSearchParams(env.tiktokDeviceInfo).toString();
|
||||||
deviceInfo = new URLSearchParams(deviceInfo).toString();
|
|
||||||
|
|
||||||
let apiURL = new URL(apiPath);
|
let apiURL = new URL(apiPath);
|
||||||
apiURL.searchParams.append("aweme_id", postId);
|
apiURL.searchParams.append("aweme_id", postId);
|
||||||
@ -42,14 +41,15 @@ export default async function(obj) {
|
|||||||
detail = detail?.aweme_list?.find(v => v.aweme_id === postId);
|
detail = detail?.aweme_list?.find(v => v.aweme_id === postId);
|
||||||
if (!detail) return { error: 'ErrorCouldntFetch' };
|
if (!detail) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
let video, videoFilename, audioFilename, isMp3, audio, images,
|
let video, videoFilename, audioFilename, audio, images,
|
||||||
filenameBase = `tiktok_${detail.author.unique_id}_${postId}`;
|
filenameBase = `tiktok_${detail.author.unique_id}_${postId}`,
|
||||||
|
bestAudio = 'm4a';
|
||||||
|
|
||||||
images = detail.image_post_info?.images;
|
images = detail.image_post_info?.images;
|
||||||
|
|
||||||
let playAddr = detail.video.play_addr_h264;
|
let playAddr = detail.video.play_addr_h264;
|
||||||
|
if ((obj.h265 || !playAddr) && detail.video.play_addr)
|
||||||
if (!playAddr) playAddr = detail.video.play_addr;
|
playAddr = detail.video.play_addr;
|
||||||
|
|
||||||
if (!obj.isAudioOnly && !images) {
|
if (!obj.isAudioOnly && !images) {
|
||||||
video = playAddr.url_list[0];
|
video = playAddr.url_list[0];
|
||||||
@ -57,12 +57,12 @@ export default async function(obj) {
|
|||||||
} else {
|
} else {
|
||||||
let fallback = playAddr.url_list[0];
|
let fallback = playAddr.url_list[0];
|
||||||
audio = fallback;
|
audio = fallback;
|
||||||
audioFilename = `${filenameBase}_audio_fv`; // fv - from video
|
audioFilename = `${filenameBase}_audio`;
|
||||||
if (obj.fullAudio || fallback.includes("music")) {
|
if (obj.fullAudio || fallback.includes("music")) {
|
||||||
audio = detail.music.play_url.url_list[0]
|
audio = detail.music.play_url.url_list[0]
|
||||||
audioFilename = `${filenameBase}_audio`
|
audioFilename = `${filenameBase}_audio_original`
|
||||||
}
|
}
|
||||||
if (audio.slice(-4) === ".mp3") isMp3 = true;
|
if (audio.slice(-4) === ".mp3") bestAudio = 'mp3';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video) return {
|
if (video) return {
|
||||||
@ -73,7 +73,7 @@ export default async function(obj) {
|
|||||||
urls: audio,
|
urls: audio,
|
||||||
audioFilename: audioFilename,
|
audioFilename: audioFilename,
|
||||||
isAudioOnly: true,
|
isAudioOnly: true,
|
||||||
isMp3: isMp3
|
bestAudio
|
||||||
}
|
}
|
||||||
if (images) {
|
if (images) {
|
||||||
let imageLinks = [];
|
let imageLinks = [];
|
||||||
@ -87,13 +87,13 @@ export default async function(obj) {
|
|||||||
urls: audio,
|
urls: audio,
|
||||||
audioFilename: audioFilename,
|
audioFilename: audioFilename,
|
||||||
isAudioOnly: true,
|
isAudioOnly: true,
|
||||||
isMp3: isMp3
|
bestAudio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audio) return {
|
if (audio) return {
|
||||||
urls: audio,
|
urls: audio,
|
||||||
audioFilename: audioFilename,
|
audioFilename: audioFilename,
|
||||||
isAudioOnly: true,
|
isAudioOnly: true,
|
||||||
isMp3: isMp3
|
bestAudio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ export default async function(obj) {
|
|||||||
if (!api) return { error: 'ErrorCouldntFetch' };
|
if (!api) return { error: 'ErrorCouldntFetch' };
|
||||||
|
|
||||||
let downloadType = "dash";
|
let downloadType = "dash";
|
||||||
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
|
if (!obj.isAudioOnly && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
|
||||||
|
|
||||||
let fileMetadata = {
|
let fileMetadata = {
|
||||||
title: cleanString(api.video.title.trim()),
|
title: cleanString(api.video.title.trim()),
|
||||||
|
@ -4,7 +4,7 @@ import { cleanString } from '../../sub/utils.js';
|
|||||||
|
|
||||||
const yt = await Innertube.create();
|
const yt = await Innertube.create();
|
||||||
|
|
||||||
const c = {
|
const codecMatch = {
|
||||||
h264: {
|
h264: {
|
||||||
codec: "avc1",
|
codec: "avc1",
|
||||||
aCodec: "mp4a",
|
aCodec: "mp4a",
|
||||||
@ -23,8 +23,8 @@ const c = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function(o) {
|
export default async function(o) {
|
||||||
let info, isDubbed,
|
let info, isDubbed, format = o.format || "h264";
|
||||||
quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality
|
let quality = o.quality === "max" ? "9000" : o.quality; // 9000(p) - max quality
|
||||||
|
|
||||||
function qual(i) {
|
function qual(i) {
|
||||||
if (!i.quality_label) {
|
if (!i.quality_label) {
|
||||||
@ -56,10 +56,16 @@ export default async function(o) {
|
|||||||
|
|
||||||
let bestQuality, hasAudio;
|
let bestQuality, hasAudio;
|
||||||
|
|
||||||
let adaptive_formats = info.streaming_data.adaptive_formats.filter(e =>
|
const filterByCodec = (formats) => formats.filter(e =>
|
||||||
e.mime_type.includes(c[o.format].codec) || e.mime_type.includes(c[o.format].aCodec)
|
e.mime_type.includes(codecMatch[format].codec) || e.mime_type.includes(codecMatch[format].aCodec)
|
||||||
).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
|
||||||
|
|
||||||
|
let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats);
|
||||||
|
if (adaptive_formats.length === 0 && format === "vp9") {
|
||||||
|
format = "h264"
|
||||||
|
adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats)
|
||||||
|
}
|
||||||
|
|
||||||
bestQuality = adaptive_formats.find(i => i.has_video);
|
bestQuality = adaptive_formats.find(i => i.has_video);
|
||||||
hasAudio = adaptive_formats.find(i => i.has_audio);
|
hasAudio = adaptive_formats.find(i => i.has_audio);
|
||||||
|
|
||||||
@ -105,14 +111,15 @@ export default async function(o) {
|
|||||||
isAudioOnly: true,
|
isAudioOnly: true,
|
||||||
urls: audio.decipher(yt.session.player),
|
urls: audio.decipher(yt.session.player),
|
||||||
filenameAttributes: filenameAttributes,
|
filenameAttributes: filenameAttributes,
|
||||||
fileMetadata: fileMetadata
|
fileMetadata: fileMetadata,
|
||||||
|
bestAudio: format === "h264" ? 'm4a' : 'opus'
|
||||||
}
|
}
|
||||||
const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality,
|
const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality,
|
||||||
checkSingle = i => qual(i) === matchingQuality && i.mime_type.includes(c[o.format].codec),
|
checkSingle = i => qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].codec),
|
||||||
checkRender = i => qual(i) === matchingQuality && i.has_video && !i.has_audio;
|
checkRender = i => qual(i) === matchingQuality && i.has_video && !i.has_audio;
|
||||||
|
|
||||||
let match, type, urls;
|
let match, type, urls;
|
||||||
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
|
if (!o.isAudioOnly && !o.isAudioMuted && format === 'h264') {
|
||||||
match = info.streaming_data.formats.find(checkSingle);
|
match = info.streaming_data.formats.find(checkSingle);
|
||||||
type = "bridge";
|
type = "bridge";
|
||||||
urls = match?.decipher(yt.session.player);
|
urls = match?.decipher(yt.session.player);
|
||||||
@ -128,8 +135,8 @@ export default async function(o) {
|
|||||||
if (match) {
|
if (match) {
|
||||||
filenameAttributes.qualityLabel = match.quality_label;
|
filenameAttributes.qualityLabel = match.quality_label;
|
||||||
filenameAttributes.resolution = `${match.width}x${match.height}`;
|
filenameAttributes.resolution = `${match.width}x${match.height}`;
|
||||||
filenameAttributes.extension = c[o.format].container;
|
filenameAttributes.extension = codecMatch[format].container;
|
||||||
filenameAttributes.youtubeFormat = o.format;
|
filenameAttributes.youtubeFormat = format;
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
urls,
|
urls,
|
||||||
|
@ -57,7 +57,6 @@
|
|||||||
"alias": "tiktok videos, photos & audio",
|
"alias": "tiktok videos, photos & audio",
|
||||||
"patterns": [":user/video/:postId", ":id", "t/:id", ":user/photo/:postId"],
|
"patterns": [":user/video/:postId", ":id", "t/:id", ":user/photo/:postId"],
|
||||||
"subdomains": ["vt", "vm"],
|
"subdomains": ["vt", "vm"],
|
||||||
"audioFormats": ["best", "m4a", "mp3"],
|
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"douyin": {
|
"douyin": {
|
||||||
@ -74,11 +73,11 @@
|
|||||||
"soundcloud": {
|
"soundcloud": {
|
||||||
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
|
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
|
||||||
"subdomains": ["on", "m"],
|
"subdomains": ["on", "m"],
|
||||||
"audioFormats": ["best", "opus", "mp3"],
|
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"instagram": {
|
"instagram": {
|
||||||
"alias": "instagram reels, posts & stories",
|
"alias": "instagram reels, posts & stories",
|
||||||
|
"altDomains": ["ddinstagram.com"],
|
||||||
"patterns": [
|
"patterns": [
|
||||||
"reels/:postId", ":username/reel/:postId", "reel/:postId", "p/:postId", ":username/p/:postId",
|
"reels/:postId", ":username/reel/:postId", "reel/:postId", "p/:postId", ":username/p/:postId",
|
||||||
"tv/:postId", "stories/:username/:storyId"
|
"tv/:postId", "stories/:username/:storyId"
|
||||||
@ -110,7 +109,7 @@
|
|||||||
"rutube": {
|
"rutube": {
|
||||||
"alias": "rutube videos",
|
"alias": "rutube videos",
|
||||||
"tld": "ru",
|
"tld": "ru",
|
||||||
"patterns": ["video/:id", "play/embed/:id"],
|
"patterns": ["video/:id", "play/embed/:id", "shorts/:id", "yappy/:yappyId"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"dailymotion": {
|
"dailymotion": {
|
||||||
|
@ -19,7 +19,7 @@ export const testers = {
|
|||||||
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
|
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
|
||||||
|
|
||||||
"rutube": (patternMatch) =>
|
"rutube": (patternMatch) =>
|
||||||
patternMatch.id?.length === 32,
|
patternMatch.id?.length === 32 || patternMatch.yappyId?.length === 32,
|
||||||
|
|
||||||
"soundcloud": (patternMatch) =>
|
"soundcloud": (patternMatch) =>
|
||||||
(patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255)
|
(patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255)
|
||||||
|
@ -64,6 +64,12 @@ export function aliasURL(url) {
|
|||||||
if (url.hostname === 'dai.ly' && parts.length === 2) {
|
if (url.hostname === 'dai.ly' && parts.length === 2) {
|
||||||
url = new URL(`https://dailymotion.com/video/${parts[1]}`)
|
url = new URL(`https://dailymotion.com/video/${parts[1]}`)
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case "ddinstagram":
|
||||||
|
if (services.instagram.altDomains.includes(host.domain) && [null, 'd', 'g'].includes(host.subdomain)) {
|
||||||
|
url.hostname = 'instagram.com';
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
@ -3,7 +3,7 @@ import { randomBytes } from "crypto";
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
||||||
import { streamLifespan } from "../config.js";
|
import { streamLifespan, env } from "../config.js";
|
||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube'];
|
const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube'];
|
||||||
@ -54,7 +54,7 @@ export function createStream(obj) {
|
|||||||
encryptStream(streamData, iv, secret)
|
encryptStream(streamData, iv, secret)
|
||||||
)
|
)
|
||||||
|
|
||||||
let streamLink = new URL('/api/stream', process.env.API_URL);
|
let streamLink = new URL('/api/stream', env.apiURL);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
't': streamID,
|
't': streamID,
|
||||||
@ -85,7 +85,7 @@ export function createInternalStream(url, obj = {}) {
|
|||||||
controller: new AbortController()
|
controller: new AbortController()
|
||||||
};
|
};
|
||||||
|
|
||||||
let streamLink = new URL('/api/istream', `http://127.0.0.1:${process.env.API_PORT || 9000}`);
|
let streamLink = new URL('/api/istream', `http://127.0.0.1:${env.apiPort}`);
|
||||||
streamLink.searchParams.set('t', streamID);
|
streamLink.searchParams.set('t', streamID);
|
||||||
return streamLink.toString();
|
return streamLink.toString();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { create as contentDisposition } from "content-disposition-header";
|
|||||||
|
|
||||||
import { metadataManager } from "../sub/utils.js";
|
import { metadataManager } from "../sub/utils.js";
|
||||||
import { destroyInternalStream } from "./manage.js";
|
import { destroyInternalStream } from "./manage.js";
|
||||||
import { ffmpegArgs } from "../config.js";
|
import { env, ffmpegArgs } from "../config.js";
|
||||||
import { getHeaders } from "./shared.js";
|
import { getHeaders } from "./shared.js";
|
||||||
|
|
||||||
function toRawHeaders(headers) {
|
function toRawHeaders(headers) {
|
||||||
@ -44,8 +44,8 @@ function pipe(from, to, done) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCommand(args) {
|
function getCommand(args) {
|
||||||
if (process.env.PROCESSING_PRIORITY && process.platform !== "win32") {
|
if (!isNaN(env.processingPriority) && process.platform !== "win32") {
|
||||||
return ['nice', ['-n', process.env.PROCESSING_PRIORITY, ffmpeg, ...args]]
|
return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]]
|
||||||
}
|
}
|
||||||
return [ffmpeg, args]
|
return [ffmpeg, args]
|
||||||
}
|
}
|
||||||
@ -103,6 +103,7 @@ export function streamLiveRender(streamInfo, res) {
|
|||||||
'-loglevel', '-8',
|
'-loglevel', '-8',
|
||||||
'-headers', rawHeaders,
|
'-headers', rawHeaders,
|
||||||
'-i', streamInfo.urls[0],
|
'-i', streamInfo.urls[0],
|
||||||
|
'-headers', rawHeaders,
|
||||||
'-i', streamInfo.urls[1],
|
'-i', streamInfo.urls[1],
|
||||||
'-map', '0:v',
|
'-map', '0:v',
|
||||||
'-map', '1:a',
|
'-map', '1:a',
|
||||||
|
@ -9,7 +9,15 @@ const apiVar = {
|
|||||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
||||||
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
||||||
},
|
},
|
||||||
booleanOnly: ["isAudioOnly", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
|
booleanOnly: [
|
||||||
|
"isAudioOnly",
|
||||||
|
"isTTFullAudio",
|
||||||
|
"isAudioMuted",
|
||||||
|
"dubLang",
|
||||||
|
"disableMetadata",
|
||||||
|
"twitterGif",
|
||||||
|
"tiktokH265"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||||
|
|
||||||
@ -83,8 +91,8 @@ export function checkJSONPost(obj) {
|
|||||||
isAudioMuted: false,
|
isAudioMuted: false,
|
||||||
disableMetadata: false,
|
disableMetadata: false,
|
||||||
dubLang: false,
|
dubLang: false,
|
||||||
vimeoDash: false,
|
twitterGif: false,
|
||||||
twitterGif: false
|
tiktokH265: false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let objKeys = Object.keys(obj);
|
let objKeys = Object.keys(obj);
|
||||||
|
@ -878,6 +878,30 @@
|
|||||||
"code": 200,
|
"code": 200,
|
||||||
"status": "redirect"
|
"status": "redirect"
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"name": "ddinstagram link",
|
||||||
|
"url": "https://ddinstagram.com/p/CmCVWoIr9OH/",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "redirect"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "d.ddinstagram.com link",
|
||||||
|
"url": "https://d.ddinstagram.com/p/CmCVWoIr9OH/",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "redirect"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "g.ddinstagram.com link",
|
||||||
|
"url": "https://g.ddinstagram.com/p/CmCVWoIr9OH/",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "redirect"
|
||||||
|
}
|
||||||
}],
|
}],
|
||||||
"vine": [{
|
"vine": [{
|
||||||
"name": "regular vine link (9+10=21)",
|
"name": "regular vine link (9+10=21)",
|
||||||
@ -1061,6 +1085,22 @@
|
|||||||
"code": 200,
|
"code": 200,
|
||||||
"status": "stream"
|
"status": "stream"
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"name": "yappy",
|
||||||
|
"url": "https://rutube.ru/yappy/f1771e86294748eea8ecb2ac88e55740/",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "stream"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "shorts",
|
||||||
|
"url": "https://rutube.ru/shorts/935c1afafd0e7d52836d671967d53dac/",
|
||||||
|
"params": {},
|
||||||
|
"expected": {
|
||||||
|
"code": 200,
|
||||||
|
"status": "stream"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"name": "vertical video (isAudioOnly)",
|
"name": "vertical video (isAudioOnly)",
|
||||||
"url": "https://rutube.ru/video/18a281399b96f9184c647455a86f6724/",
|
"url": "https://rutube.ru/video/18a281399b96f9184c647455a86f6724/",
|
||||||
|
Loading…
Reference in New Issue
Block a user