Compare commits

...

24 commits

Author SHA1 Message Date
axui2 71beab06d8
Merge 5941719ab1 into 709d14ee9e 2024-04-30 00:56:20 +01:00
hyperdefined 709d14ee9e
feat: ddinstagram.com support (#402)
Co-authored-by: dumbmoron <log@riseup.net>
2024-04-30 01:11:25 +06:00
wukko 6392f912ad
added an option for tiktok h265 videos, majorly cleaned up frontend (#472)
- cleaned up cobalt.js (by a lot)
- removed notification dot
- removed settings migration
- removed vimeoDash
- turned youtube track language switcher into a toggle
- added clarification as to what youtube dub does
- updated defaults to match with backend
- now matching a url from any string at any place
2024-04-30 00:45:10 +06:00
wukko c1079c544d
localization: update russian description for tiktok h265 2024-04-30 00:38:00 +06:00
wukko 511ad07d2f
front/cobalt.js: actually extract the url from clipboard 2024-04-30 00:34:29 +06:00
wukko 9b0d968cca
cobalt: use test instead of match in pasteClipboard 2024-04-30 00:28:40 +06:00
wukko 0ca393e8ec
docs/api: add tiktokH265 and remove vimeoDash 2024-04-30 00:27:17 +06:00
wukko 9fae8f03ff
front/cobalt.js: fixes based on review 2024-04-30 00:25:43 +06:00
wukko 8f5eec0b5d
added an option for tiktok h265 videos, majorly cleaned up frontend
- cleaned up cobalt.js (by a lot)
- removed notification dot
- removed settings migration
- removed vimeoDash
- turned youtube track language switcher into a toggle
- added clarification as to what youtube dub does
- updated defaults to match with backend
- now matching a url from any string at any place
2024-04-30 00:04:19 +06:00
wukko dc243a1e61
localization: remove double line break in codec desc 2024-04-29 21:42:18 +06:00
wukko f20f87bd1d
rutube: add support for shorts and yappy (#471)
* rutube: add support for shorts and yappy

* tests: add rutube yappy and shorts tests

Closes #465
Closes #466
2024-04-29 17:36:35 +02:00
jj 8f27c86a43
stream/types: set headers for both inputs in streamLiveRender (#470) 2024-04-29 20:29:37 +06:00
jsopn 5fbf35a8d3
refactor: centralize envs and their defaults in modules/config (#464)
* feat(config): centralized env variables and their default values

* fix: fip `corsWildcard` variable check in `corsConfig`

* fix(config): use already declared variables and default some strings to undefined

* fix: check processingPriority against NaN
2024-04-29 13:56:05 +02:00
wukko d780192ada
instagram: add three more ways to get post info (#469)
for total of fucking SIX???
2024-04-29 15:06:30 +06:00
axui2 5941719ab1
Merge branch 'wukko:current' into axui2-patch-1 2024-04-26 23:24:08 +03:00
axui2 4a2c0af728
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-26 23:23:15 +03:00
axui2 048366b46b
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-16 14:42:11 +03:00
axui2 590adf257f
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-16 14:30:52 +03:00
axui2 a205589585
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-08 20:43:52 +03:00
axui2 41e51d20e3
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-06 21:02:39 +03:00
axui2 a55291329a
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-06 20:59:00 +03:00
axui2 6175224f5d
Update ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-06 07:28:25 +03:00
axui2 005d76527f
fixed a spacing error
fixed a spacing error

Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-06 07:02:58 +03:00
axui2 8ed51f2536
Create ar.json
Signed-off-by: axui2 <143213988+axui2@users.noreply.github.com>
2024-04-06 06:50:32 +03:00
25 changed files with 787 additions and 467 deletions

View file

@ -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`. |
| `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 |
| `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
| key | type | variables |

View file

@ -6,6 +6,7 @@ import express from "express";
import { Bright, Green, Red } from "./modules/sub/consoleText.js";
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
import { loadLoc } from "./localization/manager.js";
import { mode } from "./modules/config.js"
import path from 'path';
import { fileURLToPath } from 'url';
@ -22,13 +23,10 @@ app.disable('x-powered-by');
await loadLoc();
const apiMode = process.env.API_URL && !process.env.WEB_URL;
const webMode = process.env.WEB_URL && process.env.API_URL;
if (apiMode) {
if (mode === 'API') {
const { runAPI } = await import('./core/api.js');
runAPI(express, app, gitCommit, gitBranch, __dirname)
} else if (webMode) {
} else if (mode === 'WEB') {
const { runWeb } = await import('./core/web.js');
await runWeb(express, app, gitCommit, gitBranch, __dirname)
} else {

View file

@ -3,9 +3,6 @@
"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",
"authorInfo": {
"name": "wukko",
"link": "https://wukko.me/",
"contact": "https://wukko.me/contacts",
"support": {
"default": {
"email": {

View file

@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
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 { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.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";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.CORS_WILDCARD === '0' ? {
origin: process.env.CORS_URL,
const corsConfig = !env.corsWildcard ? {
origin: env.corsURL,
optionsSuccessStatus: 200
} : {};
@ -163,9 +163,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
version: version,
commit: gitCommit,
branch: gitBranch,
name: process.env.API_NAME || "unknown",
url: process.env.API_URL,
cors: process.env?.CORS_WILDCARD === "0" ? 0 : 1,
name: env.apiName,
url: env.apiURL,
cors: Number(env.corsWildcard),
startTime: `${startTimestamp}`
});
default:
@ -194,12 +194,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
res.redirect('/api/json')
});
app.listen(process.env.API_PORT || 9000, () => {
app.listen(env.apiPort, () => {
console.log(`\n` +
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.API_URL}`)}\n` +
`Port: ${process.env.API_PORT || 9000}\n`
`URL: ${Cyan(`${env.apiURL}`)}\n` +
`Port: ${env.apiPort}\n`
)
});
}

View file

@ -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 { Bright, Cyan } from "../modules/sub/consoleText.js";
@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
return res.redirect('/')
});
app.listen(process.env.WEB_PORT || 9001, () => {
app.listen(env.webPort, () => {
console.log(`\n` +
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.WEB_URL}`)}\n` +
`Port: ${process.env.WEB_PORT || 9001}\n`
`URL: ${Cyan(`${env.webURL}`)}\n` +
`Port: ${env.webPort}\n`
)
})
}

View file

@ -1,5 +1,3 @@
const version = 42;
const ua = navigator.userAgent.toLowerCase();
const isIOS = 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 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 = {
"theme": ["auto", "light", "dark"],
"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"],
"dubLang": ["original", "auto"],
"vimeoDash": ["false", "true"],
"audioMode": ["false", "true"],
"filenamePattern": ["classic", "pretty", "basic", "nerdy"]
};
}
const checkboxes = [
"alwaysVisibleButton",
"downloadPopup",
@ -29,101 +22,127 @@ const checkboxes = [
"disableAnimations",
"disableMetadata",
"twitterGif",
"plausible_ignore"
];
const exceptions = { // used for mobile devices
"vQuality": "720"
};
const bottomPopups = ["error", "download"];
const pageQuery = new URLSearchParams(window.location.search);
"plausible_ignore",
"ytDub",
"tiktokH265"
]
const bottomPopups = ["error", "download"]
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
}
let apiURL = fixApiUrl(defaultApiUrl);
function changeApi(url) {
const changeApi = (url) => {
apiURL = fixApiUrl(url);
return true
}
function eid(id) {
const eid = (id) => {
return document.getElementById(id)
}
function sGet(id) {
const sGet = (id) =>{
return localStorage.getItem(id)
}
function sSet(id, value) {
const sSet = (id, value) => {
localStorage.setItem(id, value)
}
function enable(id) {
const enable = (id) => {
eid(id).dataset.enabled = "true";
}
function disable(id) {
const disable = (id) => {
eid(id).dataset.enabled = "false";
}
function vis(state) {
return (state === 1) ? "visible" : "hidden";
}
function opposite(state) {
const opposite = (state) => {
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) {
case 0:
case "hidden": // hidden, but only visible when alwaysVisibleButton is true
eid("download-button").disabled = true
if (sGet("alwaysVisibleButton") === "true") {
eid("download-button").value = text
eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem'
} else {
eid("download-button").value = ''
eid("download-button").style.padding = '0'
}
break;
case 1:
eid("download-button").disabled = false
eid("download-button").value = text
eid("download-button").style.padding = '0 1rem'
break;
case 2:
case "disabled":
eid("download-button").disabled = true
eid("download-button").value = text
eid("download-button").style.padding = '0 1rem'
break;
default:
eid("download-button").disabled = false
eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem'
break;
}
}
document.addEventListener("keydown", (event) => {
if (event.key === "Tab") {
eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem'
}
})
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) {
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 = '';
button();
}
function copy(id, data) {
let e = document.getElementById(id);
e.classList.add("text-backdrop");
setTimeout(() => { e.classList.remove("text-backdrop") }, 600);
data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText);
const copy = (id, data) => {
let target = document.getElementById(id);
target.classList.add("text-backdrop");
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) {}
}
function detectColorScheme() {
const share = url => navigator?.share({ url }).catch(() => {});
const detectColorScheme = () => {
let theme = "auto";
let localTheme = sGet("theme");
if (localTheme) {
@ -133,7 +152,59 @@ function detectColorScheme() {
}
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();
let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`);
@ -149,46 +220,15 @@ function changeTab(evnt, tabId, tabClass) {
evnt.currentTarget.dataset.enabled = "true";
eid(tabId).dataset.enabled = "true";
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 c = "expanded";
!classlist.contains(c) ? classlist.add(c) : classlist.remove(c);
}
function notificationCheck(type) {
let changed = true;
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() {
const hideAllPopups = () => {
let filter = document.getElementsByClassName('popup');
for (let i = 0; i < filter.length; i++) {
filter[i].classList.remove("visible");
@ -201,13 +241,14 @@ function hideAllPopups() {
eid("picker-download").href = '/';
eid("picker-download").classList.remove("visible");
}
function popup(type, action, text) {
const popup = (type, action, text) => {
if (action === 1) {
hideAllPopups(); // hide the previous popup before showing a new one
store.isPopupOpen = true;
switch (type) {
case "about":
let tabId = sGet("changelogStatus") !== `${version}` ? "changelog" : "about";
let tabId = "about";
if (text) tabId = text;
eid(`tab-button-${type}-${tabId}`).click();
break;
@ -276,7 +317,8 @@ function popup(type, action, text) {
eid(`popup-${type}`).classList.toggle("visible");
eid(`popup-${type}`).focus();
}
function changeSwitcher(li, b) {
const changeSwitcher = (li, b) => {
if (b) {
if (!switchers[li].includes(b)) b = switchers[li][0];
sSet(li, b);
@ -287,14 +329,14 @@ function changeSwitcher(li, b) {
if (li === "filenamePattern") updateFilenamePreview();
} else {
let pref = switchers[li][0];
if (isMobile && exceptions[li]) pref = exceptions[li];
sSet(li, pref);
for (let i in switchers[li]) {
(switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`)
}
}
}
function checkbox(action) {
const checkbox = (action) => {
sSet(action, !!eid(action).checked);
switch(action) {
case "alwaysVisibleButton": button(); break;
@ -302,43 +344,158 @@ function checkbox(action) {
case "disableAnimations": eid("cobalt-body").classList.toggle('no-animation'); break;
}
}
function changeButton(type, text) {
const changeButton = (type, text) => {
switch (type) {
case 0: //error
case "error": //error
eid("url-input-area").disabled = false
eid("url-clear").style.display = "block";
changeDownloadButton(2, '!!');
changeDownloadButton("disabled", '!!');
popup("error", 1, text);
setTimeout(() => { changeButton(1); }, 2500);
setTimeout(() => { changeButton("default") }, 2500);
break;
case 1: //enable back
changeDownloadButton(1, '>>');
case "default": //enable back
changeDownloadButton();
eid("url-clear").style.display = "block";
eid("url-input-area").disabled = false
break;
case 2: //enable back + information popup
case "error-default": //enable back + information popup
popup("error", 1, text);
changeDownloadButton(1, '>>');
changeDownloadButton();
eid("url-clear").style.display = "block";
eid("url-input-area").disabled = false
break;
}
}
function internetError() {
const internetError = () => {
eid("url-input-area").disabled = false
changeDownloadButton(2, '!!');
setTimeout(() => { changeButton(1); }, 2500);
changeDownloadButton("disabled", '!!');
setTimeout(() => { changeButton("default") }, 2500);
popup("error", 1, loc.ErrorNoInternet);
}
function resetSettings() {
const resetSettings = () => {
localStorage.clear();
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 {
let t = await navigator.clipboard.readText();
if (regex.test(t)) {
eid("url-input-area").value = t;
let clipboard = await navigator.clipboard.readText();
let onlyURL = clipboard.match(/https:\/\/[^\s]+/g)
if (onlyURL) {
eid("url-input-area").value = onlyURL;
download(eid("url-input-area").value);
}
} catch (e) {
@ -353,204 +510,58 @@ async function pasteClipboard() {
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;
if (sGet("twitterGif") === "true") req.twitterGif = true;
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;
const loadCelebrationsEmoji = async() => {
let aboutButtonBackup = eid("about-footer").innerHTML;
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) {
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) {
eid("about-footer").innerHTML = bac;
} catch {
eid("about-footer").innerHTML = aboutButtonBackup;
}
}
async function loadOnDemand(elementId, blockId) {
let j = {};
const loadOnDemand = async(elementId, blockId) => {
store.historyButton = eid(elementId).innerHTML;
eid(elementId).innerHTML = `<div class="loader">...</div>`;
try {
if (store.historyContent) {
j = store.historyContent;
} else {
await fetch(`/onDemand?blockId=${blockId}`).then(async(r) => {
j = await r.json();
if (j && j.status === "success") {
store.historyContent = j;
} else throw new Error();
}).catch(() => { throw new Error() });
if (!store.historyContent) {
let j = await fetch(`/onDemand?blockId=${blockId}`).then(r => r.json()).catch(() => {});
if (!j) throw new Error();
if (j.status === "success") {
store.historyContent = j.text
}
}
if (j.text) {
eid(elementId).innerHTML = `<button class="switch bottom-margin" onclick="restoreUpdateHistory()">${loc.ChangelogPressToHide}</button>${j.text}`;
} else throw new Error()
} catch (e) {
eid(elementId).innerHTML =
`<button class="switch bottom-margin" onclick="restoreUpdateHistory()">
${loc.ChangelogPressToHide}
</button>
${store.historyContent}`;
} catch {
eid(elementId).innerHTML = store.historyButton;
internetError()
}
}
function restoreUpdateHistory() {
const restoreUpdateHistory = () => {
eid("changelog-history").innerHTML = store.historyButton;
}
function unpackSettings(b64) {
let changed = null;
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() {
const loadSettings = () => {
if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>'
@ -578,13 +589,14 @@ function loadSettings() {
}
updateFilenamePreview()
}
window.onload = () => {
loadCelebrationsEmoji();
loadSettings();
detectColorScheme();
changeDownloadButton(0, '>>');
changeDownloadButton("hidden");
eid("url-input-area").value = "";
if (isIOS) {
@ -595,37 +607,32 @@ window.onload = () => {
eid("home").style.visibility = '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");
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);
notificationCheck();
// fix for animations not working in Safari
if (isIOS) {
document.addEventListener('touchstart', () => {}, true);
}
}
eid("url-input-area").addEventListener("keydown", (e) => {
button();
})
eid("url-input-area").addEventListener("keyup", (e) => {
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) => {
if (!store.isPopupOpen) {
if (e.metaKey || e.ctrlKey || e.key === "/") eid("url-input-area").focus();

View file

@ -0,0 +1,162 @@
{
"name": "العربية",
"substrings": {
"ContactLink": "تحقق من <a class=\"text-backdrop link\" href=\"{statusPage}\" target=\"_blank\">حالة الموقع</a> أو <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">زور صفحتنا في github</a>"
},
"strings": {
"AppTitleCobalt": "كوبالت",
"LinkInput": "إلصق رابط هنا",
"AboutSummary": "كوبالت ليس مثل بقايا مواقع التحميلات الأخرى، لا إعلانات، ولا تتبعات، ولا أي خرابيط. فقط قم بلصق فيديو وسيتم تنزيله على طول!",
"EmbedBriefDescription": "قم بتنزيل ما تحب بدون إعلانات وتتبعات وأي خرابيط غريب ومخادع.",
"MadeWithLove": "برمجة <a class=\"text-backdrop link\" href=\"https://wukko.me\" target=\"_blank\">wukko</a>، ترجمة <a class=\"text-backdrop link\" href=\"https://github.com/axui2\" target=\"_blank\">اياد ابراهيم</a>",
"AccessibilityInputArea": "حقل لصق النص",
"AccessibilityOpenAbout": "فتح نافذة حول",
"AccessibilityDownloadButton": "زر التنزيل",
"AccessibilityOpenSettings": "فتح نافذة الإعدادات",
"AccessibilityOpenDonate": "فتح نافذة التبرع",
"TitlePopupAbout": "ما هو كوبالت؟",
"TitlePopupSettings": "الإعدادات",
"TitlePopupChangelog": "ما الجديد؟",
"TitlePopupDonate": "إدعم كوبالت",
"TitlePopupDownload": "كيفية التنزيل",
"ErrorSomethingWentWrong": "حدث شيء ما خطأ ولا يمكن الحصول على أي شيء لك. حاول مجددا، لكن إذا استمر الخطأ، {ContactLink}.",
"ErrorUnsupported": "يبدو أن الموقع غير معتمد بعد أو الرابط غير متاح. هل أدخلت الرابط الصحيح؟",
"ErrorBrokenLink": "{s} مدعم، لكن هناك شيء ما خطأ في الرابط. هل لصقته كاملاً؟",
"ErrorNoLink": "لا أفهم ماذا تريد تنزيله! رجاءا أعطني رابط :(",
"ErrorPageRenderFail": "اذا تقرأ هذا فهناك خطأ ما في عارض الصفحات. رجاءا {ContactLink}.",
"ErrorRateLimit": "أنت تقوم بطلبات كثيرة جدا. إنتظر لمدة دقيقة!",
"ErrorCouldntFetch": "لم يمكن العثور على أي شيء ما حول هذا الرابط. تأكد من أنه يعمل وحاول مرة أخرى. قد تكون بعض المحتويات محظورة بمنطقتك.",
"ErrorLengthLimit": "لا يمكن تنزيل فيديوهات أكثر من {s} دقائق. إلصق رابط لفيديو مدته أصغر!",
"ErrorBadFetch": "حدث خطأ ما في محاولة جمع معلومات حول الرابط. هل أنت متأكد بأنه يعمل؟ إذا الرابط كذلك فحاول مرة أخرى.",
"ErrorNoInternet": "أنت غير متصل بالإنترنت أو واجهة برمجة التطبيقات لكوبالت غير متاحة مؤقتًا. تحقق من اتصالك وحاول مرة أخرى.",
"ErrorCantConnectToServiceAPI": "لم يمكن الاتصال بواجهة برمجة تطبيقات الخدمة. ربما كان معطلاً، أو تم منع كوبالت عنها.",
"ErrorEmptyDownload": "لا يوجد أي شيء يمكن تنزيله عن طريق هذا الرابط. جرب رابط مختلف!",
"ErrorLiveVideo": "هذا بث! انتظر إلى أن ينتهي البث ثم حاول مجددا.",
"SettingsAppearanceSubtitle": "المظهر",
"SettingsThemeSubtitle": "السمة",
"SettingsFormatSubtitle": "تهيئة",
"SettingsQualitySubtitle": "الجودة",
"SettingsThemeAuto": "تلقائي",
"SettingsThemeLight": "فاتح",
"SettingsThemeDark": "داكن",
"SettingsKeepDownloadButton": "إظهار &gt;&gt; دائمًا",
"AccessibilityKeepDownloadButton": "إظهار زر التنزيل دائما",
"SettingsEnableDownloadPopup": "إسأل كيفية التنزيل",
"AccessibilityEnableDownloadPopup": "اسأل ماذا أفعل مع التنزيلات",
"SettingsQualityDescription": "إذا الجودة المختارة لم يكن متاح ستيم اختيار أقرب جودة.",
"NoScriptMessage": "يستخدم كوبالت جافا سكريبت لطلبات APIوالواجهة التفاعلية. يجب عليك السماح لجافا سكريبت باستخدام هذا الموقع. لا توجد نصوص خبيثة، أوعدك.",
"DownloadPopupDescriptionIOS": "كيف تنزل صور:\n1. أضف <a class=\"text-backdrop link\" href=\"https://www.icloud.com/shortcuts/f030d6062e634c0cbb7abd388891a115\" target=\"_blank\">إختصار حفظ الصور</a>\n2. إضغط زر (مشاركة) فوق هذا النص\n3. إضغط زر (الحفظ للصور) في حقل المشاركة\n\nكيف تنزل ملفات:\n1. أضف <a class=\"text-backdrop link\" href=\"https://www.icloud.com/shortcuts/1f1cf698795b4d22beb05a5b526e869e\" target=\"_blank\">إختصار حفظ الملفات</a>\n2. إضغط زر (مشاركة) فوق هذا النص\n3. إضغط زر (الحفظ للملفات) في حقل المشاركة\n4. حدد مجلد لحفظ الملف فيه ثم إضغط (فتح)\n\nهذان الإختصارتان يُستخدمان فقط في موقع كوبالت.",
"DownloadPopupDescription": "زر التنزيل يفتح علامة تبويب جديدة تحتوي على الملف المطلوب. يمكنك تعطيل هذه النافذة في الإعدادات.",
"ClickToCopy": "إضغط للنسخ",
"Download": "تنزيل",
"CopyURL": "نسخ",
"AboutTab": "حول",
"ChangelogTab": "سجل التغييرات",
"DonationsTab": "التبرعات",
"SettingsVideoTab": "الفيديو",
"SettingsAudioTab": "الصوت",
"SettingsOtherTab": "أخرى",
"ChangelogLastMajor": "الإصدار الحالي والإلتزام",
"AccessibilityModeToggle": "تبديل وضع التنزيل",
"DonateLinksDescription": "هذه هي أفضل طريقة للتبرع إذا كنت تريد مني أن أتلقى تبرعك مباشرةً.",
"SettingsAudioFormatBest": "أفضل",
"SettingsAudioFormatDescription": "عندما نوع (أفضل) محدد، ستحصل على الصوت بنفس الطريقة كما هي من جانب الخدمة. لم يتم إعادة ترميزه. سيتم إعادة ترميز الأنواع الباقية.",
"Keyphrase": "قم بتنزيل ما تحب",
"ErrorPopupCloseButton": "فهمت",
"ErrorLengthAudioConvert": "لا يمكن تحويل صوت أطول من {s} دقائق. إختار تنسيق (أفضل) بالإعدادات إذا لم ترغب بالقيود!",
"SettingsAudioFullTikTok": "الصوت كاملاً",
"SettingsAudioFullTikTokDescription": "يقوم بتنزيل الصوت الأصلي المستخدم في الفيديو دون أي تغييرات إضافية بواسطة مؤلف المنشور.",
"ErrorCantGetID": "لم يمكن الحصول على المعلومات الكامة من الرابط المختصر. تأكد من أنه يعمل أو جرب رابط آخر! اذا استمر المشكلة {ContactLink}"
"ErrorNoVideosInTweet": "لم يمكن العثور على أي محتوى في هذه التغريدة. جرب واحدة أخرى!",
"ImagePickerTitle": "إختر الصور لتنزيلها",
"ImagePickerDownloadAudio": "تنزيل الصوت",
"ImagePickerExplanationPC": "إضغط على صورة بالزر الأيمن بالماوس لتنزيلها",
"ImagePickerExplanationPhone": "إلمس باستمرار على صورة لتنزيلها.",
"ErrorNoUrlReturned": "لم نتلقى على رابط تحميل من الخدمة. هذا لا ينبغي أن يحدث أبدا.",
"ErrorUnknownStatus": "تم تلقي رد لا يمكن معالجته. هذا لا ينبغي أن يحدث أبدا.",
"PasteFromClipboard": "لصق",
"ChangelogOlder": "الإصدارات القديمة",
"ChangelogPressToExpand": "إظهار المزيد",
"Miscellaneous": "أخرى",
"ModeToggleAuto": "تلقائي",
"ModeToggleAudio": "الصوت",
"MediaPickerTitle": "إختر ما تريد حفظه.",
"MediaPickerExplanationPC": "انقر بزر الماوس الأيمن لتنزيل ما تريد.",
"MediaPickerExplanationPhone": "اضغط مع الاستمرار لتنزيل ما تريد.",
"TwitterSpaceWasntRecorded": "لم يتم تسجيل السبيس هذا، لذا لا يوجد شيء لتنزيله. جرب واحدة أخرى!",
"ErrorCantProcess": "لا يمكن معالجة طلبك :(\nيمكنك المحاولة مرة أخرى!",
"ChangelogPressToHide": "إخفاء",
"Donate": "تبرع",
"DonateSub": "ساعد هذا الموقع في البقاء على الإنترنت",
"DonateExplanation": "كوبالت لا يرمي إعلانات في وجهك وبالتالي فهو <span class=\"text-backdrop\">مجاني للجميع</span>. لكن تطوير وصيانة خدمة إعلامية كثيفة يستخدمها أكثر من 350 ألف شخص أمر مكلف للغاية، سواء من حيث الوقت أو المال.\nإذا كان كوبالت قد ساعدك في الماضي وترغب في مساعدته في النمو والتطور، فيمكنك القيام بذلك عن طريق التبرع! من خلال التبرع فإنك تساعد كل من يستخدم كوبالت: المعلمين والطلاب ومنشئي المحتوى والفنانين والمحاضرين وغيرهم الكثير!",
"DonateVia": "التبرع في",
"DonateHireMe": "فيه صيني تزوج صينية جابوا كاسات\nهذا المترجم نكاته سامجة",
"SettingsVideoMute": "إزالة الصوت",
"SettingsVideoMuteExplanation": "يزيل الصوت من الفيديو إن أمكن.",
"ErrorSoundCloudNoClientId": "لم يمكن الحصول على الرمز المؤقت المطلوب لتنزيل الصوت من ساوند كلاود.",
"CollapseServices": "لم يمكن الحصول على الرمز المؤقت المطلوب لتنزيل الصوت من ساوند كلاود.",
"CollapseSupport": "الدعم وكود المصدر",
"CollapsePrivacy": "سياسة الخصوصية",
"ServicesNote": "هذه القائمة لم تنتهي وتستمر بالتحديث بمرور الوقت. تأكد من التحقق منها مرة في كل فترة!",
"FollowSupport": "صفحاتنا في مواقع التواصل الاجتماعي:",
"SourceCode": "الموقع في GitHub:",
"PrivacyPolicy": "سياسة خصوصية كوبالت بسيطة: لن يتم جمع أو تخزين أي بيانات عنك ابدًا وقطعًا.\nما تقوم بتنزيله هو ما يخصك فقط، ولا يخصني أو أي أحد.\n\nإذا كان تنزيلك يتطلب عرض مباشر، فسيتم تشفير و تخزين بعض البيانات التي لا يمكن تتبعها مؤقتًا في ذاكرة الوصول العشوائي (RAM) للخادم. من الضروري أن تعمل هذه الميزة.\n\nيتم تخزين المعلومات المتعلقة بالمحتوى المطلوب لمدة <span class=\"text-backdrop\">90 ثانية</span> ثم تتم إزالتها نهائيًا.\n\nلا يمكن فك تشفير البيانات المخزنة إلا باستخدام مفاتيح تشفير فريدة من رابط تنزيلك، بالإضافة إلى أن لا يتوفر قاعدة بيانات كوبالت الرسمية طريقة لقراءتها خارج وظائف المعالجة.",
"ErrorYTUnavailable": "هذا الفيديو غير متاح. ربما يكون مقيدا لسبب أو لفئة عمرية. جرب واحدة أخرى!",
"ErrorYTTryOtherCodec": "لم يتم العثور على أي شيء لتنزيله باستخدام إعداداتك. جرب برنامج ترميز أو جودة أخرى! في بعض الأحيان تتعلق واجهة برمجة تطبيقات اليوتيوب بشكل غير متوقع. حاول مرة أخرى أو حاول إعدادات أخرى.",
"SettingsCodecSubtitle": "ترميز يوتيوب",
"SettingsCodecDescription": "h264: دعم أفضل للمشغل، لكن الجودة تصل إلى 1080.\nnav1: دعم ضعيف للمشغل، لكنه يدعم 8k و HDR.\nnvp9: عادةً ما يكون أعلى معدل بت، ويحتفظ بمعظم التفاصيل. يدعم 4k و HDR.\nاختر h264 إذا كنت تريد أفضل توافق مع المحرر/المشغل/الوسائط الاجتماعية.",
"SettingsAudioDub": "المقطع الصوتي لليوتيوب",
"SettingsAudioDubDescription": "يحدد المقطع الصوتي الذي سيتم استخدامه. إذا لم يكن المقطع المدبلج متاحًا، فسيتم استخدام لغة الفيديو الأصلية بدلاً من ذلك. \n\nأصلي: يتم استخدام لغة الفيديو الأصلية\n.تلقائي: يتم استخدام لغة المتصفح الافتراضية (ولغة كوبالت كذلك).",
"SettingsDubDefault": "الأصلي",
"SettingsDubAuto": "تلقائي",
"SettingsVimeoPrefer": "vimeo نوع تنزيلات",
"SettingsVimeoPreferDescription": "متقدم: رابط ملف مباشر إلى CDN لـ Vimeo. الجودة القصوى هي 1080p.\nداش: يتم دمج الفيديو والصوت بواسطة الكوبالت في ملف واحد. الجودة القصوى هي 4K.\nاختر (متقدم) إذا كنت تريد أفضل توافق مع المحرر/المشغل/الوسائط الاجتماعية. إذا لم يكن التنزيل التدريجي متاحًا، فسيتم استخدام داش بدلاً من ذلك",
"ShareURL": "مشاركة",
"ErrorTweetUnavailable": " لم يتم العثور على أي شيء عن هذه التغريدة. قد يكون هذا بسبب أن رؤيتها محدودة. جرب واحدة أخرى!",
"ErrorTwitterRIP": "يقوم تويتر بتقييد الوصول إلى أي محتوى للمستخدمين غير المصادق عليهم. مع أن هناك طريقة للحصول على تغريدات منتظمة، إلا أنه للأسف من المستحيل الحصول على السبيسات في هذا الوقت. أنا أبحث في حلول ممكنة.",
"PopupCloseDone": "تم",
"Accessibility": "تسهيلات الوصول",
"SettingsReduceTransparency": "إزالة الشفافية",
"SettingsDisableAnimations": "إزالة الرسوم المتحركة",
"FeatureErrorGeneric": "متصفحك لا يسمح أو يدعم هذه الميزة. تحقق عندما يكون هناك أي تحديثات متاحة وحاول مرة أخرى!",
"ClipboardErrorFirefox": "أنت تستخدم فيرفوكس حيث تم تعطيل جميع وظائف قراءة الحافظة. يمكنك اصلاح ذلك من خلال الدخول إلى <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/troubleshooting.md#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">هذا الرابط!</a>",
"ClipboardErrorNoPermission": "لا يستطيع كوبالت الوصول إلى أحدث عنصر في حافظتك دون إذنك. إذا كنت لا تريد منح حق الوصول، فما عليك سوى لصق الرابط يدويًا بدلاً من ذلك. إذا قمت بذلك، فانتقل إلى إعدادات الموقع وقم بتمكين إذن الحافظة.",
"SupportSelfTroubleshooting": "تواجه مشكلة؟ جرب هذه الحلول أولاً:",
"AccessibilityGoBack": "ارجع للخلف وأغلق النافذة",
"CollapseKeyboard": "اختصارات لوحة المفاتيح",
"KeyboardShortcutsIntro": " استخدم الكوبالت بشكل أسرع باستخدام هذه الاختصارات:",
"KeyboardShortcutQuickPaste": "لصق الرابط",
"KeyboardShortcutClear": "مسح منطقة إدخال الرابط",
"KeyboardShortcutClosePopup": "إغلاق كل النوافذ",
"CollapseLegal": "الأحكام والشروط",
"FairUse": "كوبالت أداة تسهل تنزيل محتوى في الإنترنت وتستخدم وكيل محدد فلا يُخزن أي محتوى. \n\nأنت مسؤول عما تقوم بتنزيله، وكيفية استخدامك لهذا المحتوى وتوزيعه. يرجى الانتباه عند استخدام محتوى من الآخرين ونسب الفضل دائمًا إلى منشئين المحتوى الأصليين. \n\nعند استخدامها لأغراض تعليمية (محاضرة، واجبات منزلية، الخ) يرجى إرفاق رابط المصدر.",
"SettingsDisableMetadata": "عدم إضافة بيانات وصفية",
"NewDomainWelcomeTitle": "أهلاً بك!",
"DataTransferSuccess": "لعلمك، تم نقل إعداداتك تلقائيًا :)",
"DataTransferError": "حدث خطأ ما أثناء نقل إعداداتك. يجب عليك فتح الإعدادات ثم تغييرها.",
"SupportNotAffiliated": "كوبالت لا ينتسب لهذه المواقع أعلاه",
"SponsoredBy": "برعاية",
"FilenameTitle": "مظهر عنوان الملف",
"FilenamePatternClassic": "كلاسيكي",
"FilenamePatternPretty": "جميل",
"FilenamePatternBasic": "أساسي",
"FilenamePatternNerdy": "مربط",
"FilenameDescription": "كلاسيكي: نمط إسم الملف الافتراضي \nأساسي: المعلومات الأساسية بين قوسين\nجميل: معلومات بين قوسين\nمربط: جميع المعلومات بين قوسين\n\nبعض الخدمات لا تدعم إعداداتك لإسم الملفات ودائمًا تستخدم الخيار الأصلي.",
"Preview": "معاينة",
"FilenamePreviewVideoTitle": "عنوان الفيديو",
"FilenamePreviewAudioTitle": "عنوان الصوت",
"FilenamePreviewAudioAuthor": "مؤدي الصوت",
"StatusPage": "صفحة حالة الخدمة",
"TroubleshootingGuide": "دليل إستكشاف الأخطاء وإصلاحها",
"DonateImageDescription": "مدري والله لا تسألني",
"SettingsTwitterGif": "تحويل صور gif إلى .gif",
"SettingsTwitterGifDescription": "يؤدي تحويل فيديو متكرر لـ GIF بتقليل الجودة وزيادة حجم الملف. إذا تريد كفاءة ممتازة فلا تفَعِّل هذا.",
"ErrorTweetNSFW": "عيب يا ولد ترا الله يشوفك",
"ErrorTweetProtected": "هذه التغريدة من حساب خاص، لذا لا يمكن تحميله. جرب واحدة أخرى!",
"UpdateEncryption": "التشفير والخدمات الجديدة",
"PrivateAnalytics": "التحليلات الخاصة",
"SettingsDisableAnalytics": "إلغاء الاشتراك من التحليلات الخاصة",
"SettingsAnalyticsExplanation": "فَعِّل هذا الخيار إذا كنت لا تريد أن تتضمن في إحصائيات حركة المرور المجهولة عبر الانترنت. هذا الموقع لن يتم تتبعك أو تخزين معلوماتك، ولا يستخدم ملفات تعريف الارتباط.",
"AnalyticsDescription": "يستخدم كوبالت موقع plausible المستضاف ذاتيًا للحصول على رقم تقريبي لعدد مستخدمو هذا الموقع. \n\nيتوافق plausible تمامًا مع الهيئة الوطنية للأمن السيبراني ولا يستخدم ملفات تعريف الارتباط ولا يخزن أي معلومات يمكن التعرف عليها، ولا حتى عنوانك الـ IP. \n\nلا يتم حفظ أي شيء عما تقوم بتنزيله في أي مكان. يتم استخدامه فقط لإحصائيات حركة المرور المجهولة عبر الإنترنت، لا أكثر. \n\nموقع plausible مفتوح المصدر بالكامل، تمامًا مثل كوبالت، وإذا كنت ترغب في معرفة المزيد عنه، يمكنك القيام بذلك <a class=\"text-backdrop link\" href=\"https://plausible.io\" target=\"_blank\">هنا</a>. إذا كنت ترغب في إلغاء الاشتراك في إحصائيات حركة المرور عبر الإنترنت، يمكنك القيام بذلك في الإعدادات > أخرى."
}
}

View file

@ -8,7 +8,7 @@
"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!",
"EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.",
"MadeWithLove": "made with &lt;3 by wukko",
"MadeWithLove": "made with &lt;3 by imput",
"AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button",
@ -90,7 +90,6 @@
"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.",
"DonateVia": "donate via",
"DonateHireMe": "...or you can <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
"SettingsVideoMute": "mute audio",
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
"ErrorSoundCloudNoClientId": "i couldn't get the temporary token that's required to download songs from soundcloud. try again, but if issue persists, {ContactLink}.",
@ -104,13 +103,8 @@
"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!",
"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",
"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",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
"PopupCloseDone": "done",
@ -157,6 +151,10 @@
"PrivateAnalytics": "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).",
"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."
}
}

View file

@ -91,7 +91,6 @@
"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поделиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
"DonateVia": "открыть",
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop link\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
"SettingsVideoMute": "убрать аудио",
"SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.",
"ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",
@ -107,11 +106,6 @@
"SettingsCodecSubtitle": "кодек для youtube видео",
"SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.",
"SettingsAudioDub": "звуковая дорожка для youtube видео",
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.",
"SettingsDubDefault": "оригинал",
"SettingsDubAuto": "авто",
"SettingsVimeoPrefer": "тип загрузок с vimeo",
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".",
"ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!",
"PopupCloseDone": "готово",
@ -159,6 +153,10 @@
"PrivateAnalytics": "приватная аналитика",
"SettingsDisableAnalytics": "отключить приватную аналитику",
"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. работает даже если кобальт не переведён в твой язык."
}
}

View file

@ -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
services = servicesConfigJson.config,
audioIgnore = servicesConfigJson.audioIgnore,
@ -26,4 +51,7 @@ export const
supportedAudio = config.supportedAudio,
celebrations = config.celebrations,
links = config.links,
sponsors = config.sponsors
sponsors = config.sponsors,
mode = (apiURL && !webEnvs.webURL) ? 'API' :
(webEnvs.webURL && apiURL) ? 'WEB' : undefined,
env = mode === 'API' ? apiEnvs : webEnvs

View file

@ -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 { loadFile } from "../sub/loadFromFs.js";
@ -266,5 +266,5 @@ export function sponsoredList() {
}
export function betaTag() {
return process.env.IS_BETA ? '<span class="logo-sub">β</span>' : ''
return env.isBeta ? '<span class="logo-sub">β</span>' : ''
}

View file

@ -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, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js";
import { services as s, version, repo, donations, supportedAudio, links, env } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js";
import emoji from "../emoji.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 enabledServices = Object.keys(s).filter(p => s[p].enabled).sort().map((p) => {
@ -48,10 +68,10 @@ export default function(obj) {
<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: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="description" content="${t('AboutSummary')}">
<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/question.png" as="image">
${process.env.PLAUSIBLE_HOSTNAME ?
${env.plausibleHostname ?
`<script
defer
data-domain="${new URL(process.env.WEB_URL).hostname}"
src="https://${process.env.PLAUSIBLE_HOSTNAME}/js/script.js"
data-domain="${new URL(env.webURL).hostname}"
src="https://${env.plausibleHostname}/js/script.js"
></script>`
: ''}
</head>
@ -98,7 +118,7 @@ export default function(obj) {
header: {
aboveTitle: {
text: t('MadeWithLove'),
url: authorInfo.link
url: repo
},
closeAria: t('AccessibilityGoBack'),
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
@ -169,7 +189,7 @@ export default function(obj) {
name: "privacy",
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
body: t("PrivacyPolicy") + `${
process.env.PLAUSIBLE_HOSTNAME ? `<br><br>${t("AnalyticsDescription")}` : ''
env.plausibleHostname ? `<br><br>${t("AnalyticsDescription")}` : ''
}`
}, {
name: "legal",
@ -177,7 +197,7 @@ export default function(obj) {
body: t("FairUse")
}])
},
...(process.env.SHOW_SPONSORS ?
...(env.showSponsors ?
[{
text: t("SponsoredBy"),
classes: ["sponsored-by-text"],
@ -285,12 +305,6 @@ export default function(obj) {
}, {
text: donate.replace(/REPLACEME/g, t('ClickToCopy')),
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({
name: "codec",
title: t('SettingsCodecSubtitle'),
@ -367,19 +371,24 @@ export default function(obj) {
})
})
+ settingsCategory({
name: "vimeo",
title: t('SettingsVimeoPrefer'),
body: switcher({
name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'),
items: [{
action: "false",
text: "progressive"
}, {
action: "true",
text: "dash"
}]
})
name: "twitter",
title: "twitter",
body: checkbox([{
action: "twitterGif",
name: t("SettingsTwitterGif"),
padding: "no-margin"
}])
+ explanation(t('SettingsTwitterGifDescription'))
})
+ settingsCategory({
name: "tiktok",
title: "tiktok",
body: checkbox([{
action: "tiktokH265",
name: t("SettingsTikTokH265"),
padding: "no-margin"
}])
+ explanation(t('SettingsTikTokH265Description'))
})
}, {
name: "audio",
@ -401,19 +410,14 @@ export default function(obj) {
+ explanation(t('SettingsVideoMuteExplanation'))
})
+ settingsCategory({
name: "dub",
name: "youtube-dub",
title: t("SettingsAudioDub"),
body: switcher({
name: "dubLang",
explanation: t('SettingsAudioDubDescription'),
items: [{
action: "original",
text: t('SettingsDubDefault')
}, {
action: "auto",
text: t('SettingsDubAuto')
}]
})
body: checkbox([{
action: "ytDub",
name: t("SettingsYoutubeDub"),
padding: "no-margin"
}])
+ explanation(t('SettingsYoutubeDubDescription'))
})
+ settingsCategory({
name: "tiktok-audio",
@ -499,7 +503,7 @@ export default function(obj) {
}])
})
+ (() => {
if (process.env.PLAUSIBLE_HOSTNAME) {
if (env.plausibleHostname) {
return settingsCategory({
name: "privacy",
title: t('PrivateAnalytics'),
@ -629,7 +633,7 @@ export default function(obj) {
</footer>
</div>
<script>
let defaultApiUrl = '${process.env.API_URL || ''}';
let defaultApiUrl = '${env.apiURL}';
const loc = ${webLoc(t,
[
'ErrorNoInternet',

View file

@ -1,9 +1,10 @@
import Cookie from './cookie.js';
import { readFile, writeFile } from 'fs/promises';
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
import { env } from '../../../modules/config.js'
const WRITE_INTERVAL = 60000,
cookiePath = process.env.COOKIE_PATH,
cookiePath = env.cookiePath,
COUNTER = Symbol('counter');
let cookies = {}, dirty = false, intervalId;

View file

@ -88,7 +88,8 @@ export default async function(host, patternMatch, url, lang, obj) {
postId: patternMatch.postId,
id: patternMatch.id,
fullAudio: obj.isTTFullAudio,
isAudioOnly: isAudioOnly
isAudioOnly: isAudioOnly,
h265: obj.tiktokH265
});
break;
case "tumblr":
@ -103,8 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
id: patternMatch.id.slice(0, 11),
password: patternMatch.password,
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash
isAudioOnly: isAudioOnly
});
break;
case "soundcloud":
@ -151,6 +151,7 @@ export default async function(host, patternMatch, url, lang, obj) {
case "rutube":
r = await rutube({
id: patternMatch.id,
yappyId: patternMatch.yappyId,
quality: obj.vQuality,
isAudioOnly: isAudioOnly
});

View file

@ -2,11 +2,38 @@ import { createStream } from "../../stream/manage.js";
import { genericUserAgent } from "../../config.js";
import { getCookie, updateCookie } from "../cookie/manager.js";
const commonInstagramHeaders = {
'user-agent': genericUserAgent,
'sec-gpc': '1',
'sec-fetch-site': 'same-origin',
'x-ig-app-id': '936619743392459'
const commonHeaders = {
"user-agent": genericUserAgent,
"sec-gpc": "1",
"sec-fetch-site": "same-origin",
"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 = {
@ -20,7 +47,7 @@ async function findDtsgId(cookie) {
const data = await fetch('https://www.instagram.com/', {
headers: {
...commonInstagramHeaders,
...commonHeaders,
cookie
}
}).then(r => r.text());
@ -38,7 +65,7 @@ async function findDtsgId(cookie) {
async function request(url, cookie, method = 'GET', requestData) {
let headers = {
...commonInstagramHeaders,
...commonHeaders,
'x-ig-www-claim': cookie?._wwwClaim || '0',
'x-csrftoken': cookie?.values()?.csrftoken,
cookie
@ -60,26 +87,36 @@ async function request(url, cookie, method = 'GET', requestData) {
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) {
const data = await fetch(`https://www.instagram.com/p/${id}/embed/captioned/`, {
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",
"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",
...cookie
...embedHeaders,
cookie
}
}).then(r => r.text());
}).then(r => r.text()).catch(() => {});
let embedData = JSON.parse(data?.match(/"init",\[\],\[(.*?)\]\],/)[1]);
@ -196,25 +233,29 @@ function extractNewPost(data, id) {
}
async function getPost(id) {
let data, result, dataType = 'old';
let data, result;
try {
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) {
dataType = 'new';
data = await requestGQL(id, cookie);
}
// web app graphql api (no cookie, cookie)
if (!data) data = await requestGQL(id);
if (!data && cookie) data = await requestGQL(id, cookie);
} catch {}
if (!data) return { error: 'ErrorCouldntFetch' };
if (dataType === 'new') {
result = extractNewPost(data, id)
} else {
if (data?.gql_data) {
result = extractOldPost(data, id)
} else {
result = extractNewPost(data, id)
}
if (result) return result;

View file

@ -1,18 +1,47 @@
import HLS from 'hls-parser';
import { maxVideoDuration } from "../../config.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) {
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 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 ("hls" in play.live_streams) return { error: 'ErrorLiveVideo' };
if (!play.video_balancer || play.detail) return { error: 'ErrorEmptyDownload' };
if (play.detail || !play.video_balancer) return { error: 'ErrorEmptyDownload' };
if (play.live_streams?.hls) return { error: 'ErrorLiveVideo' };
if (play.duration > maxVideoDuration)
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let m3u8 = await fetch(play.video_balancer.m3u8)
.then(r => r.text())
.catch(() => {});
if (play.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let m3u8 = await fetch(play.video_balancer.m3u8).then((r) => { return r.text() }).catch(() => { return false });
if (!m3u8) return { error: 'ErrorCouldntFetch' };
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) {
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
}
let fileMetadata = {
title: cleanString(play.title.trim()),
artist: cleanString(play.author.name.trim()),
@ -31,7 +61,7 @@ export default async function(obj) {
isM3U8: true,
filenameAttributes: {
service: "rutube",
id: play.id,
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,

View file

@ -1,4 +1,4 @@
import { genericUserAgent } from "../../config.js";
import { genericUserAgent, env } from "../../config.js";
const shortDomain = "https://vt.tiktok.com/";
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) {
let postId = obj.postId ? obj.postId : false;
if (!process.env.TIKTOK_DEVICE_INFO) return { error: 'ErrorCouldntFetch' };
if (!env.tiktokDeviceInfo) return { error: 'ErrorCouldntFetch' };
if (!postId) {
let html = await fetch(`${shortDomain}${obj.id}`, {
@ -27,8 +27,7 @@ export default async function(obj) {
}
if (!postId) return { error: 'ErrorCantGetID' };
let deviceInfo = JSON.parse(process.env.TIKTOK_DEVICE_INFO);
deviceInfo = new URLSearchParams(deviceInfo).toString();
let deviceInfo = new URLSearchParams(env.tiktokDeviceInfo).toString();
let apiURL = new URL(apiPath);
apiURL.searchParams.append("aweme_id", postId);
@ -48,8 +47,8 @@ export default async function(obj) {
images = detail.image_post_info?.images;
let playAddr = detail.video.play_addr_h264;
if (!playAddr) playAddr = detail.video.play_addr;
if ((obj.h265 || !playAddr) && detail.video.play_addr)
playAddr = detail.video.play_addr;
if (!obj.isAudioOnly && !images) {
video = playAddr.url_list[0];

View file

@ -39,7 +39,7 @@ export default async function(obj) {
if (!api) return { error: 'ErrorCouldntFetch' };
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 = {
title: cleanString(api.video.title.trim()),

View file

@ -79,6 +79,7 @@
},
"instagram": {
"alias": "instagram reels, posts & stories",
"altDomains": ["ddinstagram.com"],
"patterns": [
"reels/:postId", ":username/reel/:postId", "reel/:postId", "p/:postId", ":username/p/:postId",
"tv/:postId", "stories/:username/:storyId"
@ -110,7 +111,7 @@
"rutube": {
"alias": "rutube videos",
"tld": "ru",
"patterns": ["video/:id", "play/embed/:id"],
"patterns": ["video/:id", "play/embed/:id", "shorts/:id", "yappy/:yappyId"],
"enabled": true
},
"dailymotion": {

View file

@ -19,7 +19,7 @@ export const testers = {
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
"rutube": (patternMatch) =>
patternMatch.id?.length === 32,
patternMatch.id?.length === 32 || patternMatch.yappyId?.length === 32,
"soundcloud": (patternMatch) =>
(patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255)

View file

@ -64,6 +64,12 @@ export function aliasURL(url) {
if (url.hostname === 'dai.ly' && parts.length === 2) {
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

View file

@ -3,7 +3,7 @@ import { randomBytes } from "crypto";
import { nanoid } from 'nanoid';
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";
const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube'];
@ -54,7 +54,7 @@ export function createStream(obj) {
encryptStream(streamData, iv, secret)
)
let streamLink = new URL('/api/stream', process.env.API_URL);
let streamLink = new URL('/api/stream', env.apiURL);
const params = {
't': streamID,
@ -85,7 +85,7 @@ export function createInternalStream(url, obj = {}) {
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);
return streamLink.toString();
}

View file

@ -5,7 +5,7 @@ import { create as contentDisposition } from "content-disposition-header";
import { metadataManager } from "../sub/utils.js";
import { destroyInternalStream } from "./manage.js";
import { ffmpegArgs } from "../config.js";
import { env, ffmpegArgs } from "../config.js";
import { getHeaders } from "./shared.js";
function toRawHeaders(headers) {
@ -44,8 +44,8 @@ function pipe(from, to, done) {
}
function getCommand(args) {
if (process.env.PROCESSING_PRIORITY && process.platform !== "win32") {
return ['nice', ['-n', process.env.PROCESSING_PRIORITY, ffmpeg, ...args]]
if (!isNaN(env.processingPriority) && process.platform !== "win32") {
return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]]
}
return [ffmpeg, args]
}
@ -103,6 +103,7 @@ export function streamLiveRender(streamInfo, res) {
'-loglevel', '-8',
'-headers', rawHeaders,
'-i', streamInfo.urls[0],
'-headers', rawHeaders,
'-i', streamInfo.urls[1],
'-map', '0:v',
'-map', '1:a',

View file

@ -9,7 +9,15 @@ const apiVar = {
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
},
booleanOnly: ["isAudioOnly", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
booleanOnly: [
"isAudioOnly",
"isTTFullAudio",
"isAudioMuted",
"dubLang",
"disableMetadata",
"twitterGif",
"tiktokH265"
]
}
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
@ -83,8 +91,8 @@ export function checkJSONPost(obj) {
isAudioMuted: false,
disableMetadata: false,
dubLang: false,
vimeoDash: false,
twitterGif: false
twitterGif: false,
tiktokH265: false
}
try {
let objKeys = Object.keys(obj);

View file

@ -878,6 +878,30 @@
"code": 200,
"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": [{
"name": "regular vine link (9+10=21)",
@ -1061,6 +1085,22 @@
"code": 200,
"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)",
"url": "https://rutube.ru/video/18a281399b96f9184c647455a86f6724/",