diff --git a/docs/api.md b/docs/api.md index e63ee7cd..57509669 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 | diff --git a/package.json b/package.json index 3b0b3443..a2c270fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.12.6", + "version": "7.13", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", @@ -40,6 +40,6 @@ "set-cookie-parser": "2.6.0", "undici": "^6.7.0", "url-pattern": "1.0.3", - "youtubei.js": "^9.2.0" + "youtubei.js": "^9.3.0" } } diff --git a/src/cobalt.js b/src/cobalt.js index 050aec46..473c9b5b 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -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 { diff --git a/src/config.json b/src/config.json index f1aa4a2a..0a32d220 100644 --- a/src/config.json +++ b/src/config.json @@ -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": { diff --git a/src/core/api.js b/src/core/api.js index eda3c014..8ff0b5c1 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -4,18 +4,18 @@ 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"; import stream from "../modules/stream/stream.js"; import loc from "../localization/manager.js"; import { generateHmac } from "../modules/sub/crypto.js"; -import { verifyStream } from "../modules/stream/manage.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 } : {}; @@ -123,42 +123,53 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.get('/api/:type', (req, res) => { try { + let j; switch (req.params.type) { case 'stream': const q = req.query; const checkQueries = q.t && q.e && q.h && q.s && q.i; const checkBaseLength = q.t.length === 21 && q.e.length === 13; const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; - if (checkQueries && checkBaseLength && checkSafeLength) { + if (q.p) { + return res.status(200).json({ + status: "continue" + }) + } let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); if (streamInfo.error) { return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); } - if (q.p) { - return res.status(200).json({ - status: "continue" - }); - } return stream(res, streamInfo); - } else { - let j = apiJSON(0, { - t: "bad request. stream link may be incomplete or corrupted." - }) - return res.status(j.status).json(j.body); - } + } + + j = apiJSON(0, { + t: "bad request. stream link may be incomplete or corrupted." + }) + return res.status(j.status).json(j.body); + case 'istream': + if (!req.ip.endsWith('127.0.0.1')) + return res.sendStatus(403); + if (('' + req.query.t).length !== 21) + return res.sendStatus(400); + + let streamInfo = getInternalStream(req.query.t); + if (!streamInfo) return res.sendStatus(404); + streamInfo.headers = req.headers; + + return stream(res, { type: 'internal', ...streamInfo }); case 'serverInfo': return res.status(200).json({ 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: - let j = apiJSON(0, { + j = apiJSON(0, { t: "unknown response type" }) return res.status(j.status).json(j.body); @@ -183,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` ) }); } diff --git a/src/core/web.js b/src/core/web.js index 7c0cbf33..626574a3 100644 --- a/src/core/web.js +++ b/src/core/web.js @@ -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` ) }) } diff --git a/src/front/cobalt.js b/src/front/cobalt.js index ad2e9e59..0fd638f2 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -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 = ``; - 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('🐲', j.text); + eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace( + ``, + 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 = `
...
`; 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 = `${j.text}`; - } else throw new Error() - } catch (e) { + eid(elementId).innerHTML = + ` + ${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(); diff --git a/src/front/icons/maskable/128.png b/src/front/icons/maskable/128.png new file mode 100644 index 00000000..e8213cfe Binary files /dev/null and b/src/front/icons/maskable/128.png differ diff --git a/src/front/icons/maskable/192.png b/src/front/icons/maskable/192.png new file mode 100644 index 00000000..8268d89a Binary files /dev/null and b/src/front/icons/maskable/192.png differ diff --git a/src/front/icons/maskable/384.png b/src/front/icons/maskable/384.png new file mode 100644 index 00000000..483e42ff Binary files /dev/null and b/src/front/icons/maskable/384.png differ diff --git a/src/front/icons/maskable/48.png b/src/front/icons/maskable/48.png new file mode 100644 index 00000000..02a5bca0 Binary files /dev/null and b/src/front/icons/maskable/48.png differ diff --git a/src/front/icons/maskable/512.png b/src/front/icons/maskable/512.png new file mode 100644 index 00000000..bb4af2f3 Binary files /dev/null and b/src/front/icons/maskable/512.png differ diff --git a/src/front/icons/maskable/72.png b/src/front/icons/maskable/72.png new file mode 100644 index 00000000..903f6bd5 Binary files /dev/null and b/src/front/icons/maskable/72.png differ diff --git a/src/front/icons/maskable/96.png b/src/front/icons/maskable/96.png new file mode 100644 index 00000000..c4b1ae60 Binary files /dev/null and b/src/front/icons/maskable/96.png differ diff --git a/src/front/manifest.webmanifest b/src/front/manifest.webmanifest index 7b4a239e..3777ca6d 100644 --- a/src/front/manifest.webmanifest +++ b/src/front/manifest.webmanifest @@ -18,6 +18,48 @@ "sizes": "512x512", "type": "image/png", "purpose": "any" + }, + { + "src": "/icons/maskable/48.png", + "sizes": "48x48", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/maskable/512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ], "share_target": { diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 546c2841..7558cb20 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -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 <3 by wukko", + "MadeWithLove": "made with <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 completely free to use 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\nevery cent matters and is extremely appreciated, 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 hire me :)", "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}.", @@ -101,19 +100,13 @@ "FollowSupport": "keep in touch with cobalt for news, support, and more:", "SourceCode": "explore source code, report issues, star or fork the repo:", "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for 90 seconds and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data outside of processing functions.\n\nyou can check cobalt's source code yourself and see that everything is as stated.", - "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", - "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api acts unexpectedly. try again or try another settings.", + "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: generally better player support, but quality tops out at 1080p.\nav1: poor player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", + "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!", - "ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.", "PopupCloseDone": "done", "Accessibility": "accessibility", "SettingsReduceTransparency": "reduce transparency", @@ -158,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 here. 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 here. 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." } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 8f66b5b0..4fb99fe5 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -91,7 +91,6 @@ "DonateSub": "ты можешь помочь!", "DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно для всех. но разработка и поддержка медиа сервиса, которым пользуются более 750 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\nкаждый донат невероятно ценится и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.", "DonateVia": "открыть", - "DonateHireMe": "...или же ты можешь пригласить меня на работу :)", "SettingsVideoMute": "убрать аудио", "SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.", "ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.", @@ -102,19 +101,13 @@ "FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:", "SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется рендер, то зашифрованные данные о ней временно хранятся в ОЗУ сервера. это необходимо для работы данной функции.\n\nзашифрованные данные хранятся в течение 90 секунд и затем безвозвратно удаляются.\n\ncохранённые данные можно расшифровать только с помощью уникальных ключей шифрования из твоей ссылки на скачивание. кроме того, официальная кодовая база кобальта не предусматривает возможности чтения эти данные вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", - "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", - "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", - "SettingsCodecSubtitle": "кодек для видео с youtube", - "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.", - "SettingsAudioDub": "звуковая дорожка для видео с youtube", - "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.", - "SettingsDubDefault": "оригинал", - "SettingsDubAuto": "авто", - "SettingsVimeoPrefer": "тип загрузок с vimeo", - "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".", + "ErrorYTUnavailable": "это видео недоступно. возможно оно ограничено по доступу или региону. попробуй другое!", + "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество в настройках!", + "SettingsCodecSubtitle": "кодек для youtube видео", + "SettingsCodecDescription": "h264: лучшая совместимость, средний уровень детализированности. максимальное качество - 1080p.\nav1: лучшее качество, маленький размер файла, наибольшее количество деталей. поддерживает 8k и HDR.\nvp9: такая же детализированность, как и у av1, но файл в 2 раза больше. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость.\nвыбирай av1, если ты хочешь лучшее качество и эффективность.", + "SettingsAudioDub": "звуковая дорожка для youtube видео", "ShareURL": "поделиться", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость ограничена. попробуй другой!", - "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "PopupCloseDone": "готово", "Accessibility": "общедоступность", "SettingsReduceTransparency": "уменьшить прозрачность", @@ -160,6 +153,10 @@ "PrivateAnalytics": "приватная аналитика", "SettingsDisableAnalytics": "отключить приватную аналитику", "SettingsAnalyticsExplanation": "включи, если не хочешь быть частью анонимной статистики трафика. подробнее об этом можно прочитать в политике конфиденциальности (tl;dr: ничего о тебе или твоих действиях не хранится и не отслеживается, даже куки нет).", - "AnalyticsDescription": "кобальт использует собственный инстанс plausible чтобы иметь приблизительное представление о том, сколько людей им пользуются.\n\nplausible полностью соответствует GDPR, CCPA и PECR, не использует куки и никогда не хранит никакой идентифицируемой информации, даже ip-адрес.\n\nвсе данные агрегируются и никогда не персонализируются. ничего о том, что ты скачиваешь, никогда не сохраняется. это просто анонимная статистика трафика, ничего больше.\n\nplausible также как и кобальт имеет открытый исходный код, и, если ты хочешь узнать о нём больше, то это можно сделать здесь. а если же ты хочешь исключить себя из статистики, то это можно сделать в настройках > другое." + "AnalyticsDescription": "кобальт использует собственный инстанс plausible чтобы иметь приблизительное представление о том, сколько людей им пользуются.\n\nplausible полностью соответствует GDPR, CCPA и PECR, не использует куки и никогда не хранит никакой идентифицируемой информации, даже ip-адрес.\n\nвсе данные агрегируются и никогда не персонализируются. ничего о том, что ты скачиваешь, никогда не сохраняется. это просто анонимная статистика трафика, ничего больше.\n\nplausible также как и кобальт имеет открытый исходный код, и, если ты хочешь узнать о нём больше, то это можно сделать здесь. а если же ты хочешь исключить себя из статистики, то это можно сделать в настройках > другое.", + "SettingsTikTokH265": "предпочитать h265", + "SettingsTikTokH265Description": "скачивает видео с tiktok в 1080p и h265/hevc, когда это возможно.", + "SettingsYoutubeDub": "использовать язык браузера", + "SettingsYoutubeDubDescription": "использует главный язык браузера для аудиодорожек на youtube. работает даже если кобальт не переведён в твой язык." } } diff --git a/src/modules/config.js b/src/modules/config.js index 5e079536..b774a8b6 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -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 \ No newline at end of file diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index e59385e1..ae14cd88 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -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 ? 'β' : '' + return env.isBeta ? 'β' : '' } diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 58bb3a97..840bac66 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -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) { ${t("AppTitleCobalt")} - + - + @@ -75,11 +95,11 @@ export default function(obj) { - ${process.env.PLAUSIBLE_HOSTNAME ? + ${env.plausibleHostname ? `` : ''} @@ -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 ? `

${t("AnalyticsDescription")}` : '' + env.plausibleHostname ? `

${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) {