beginning of 2.2
- added download popup to solve the issue with downloads on ios - merged big and small popups into one - made buttons in donation menu act like buttons - began to clean up localisation - added ability to embed repo url into localisation strings - moved ffmpeg args to config for more flexibility (and hopefully future changes) - removed error response in stream that could result in a crash - removed notice for ios users from about cause it's no longer relevant - made error popup look and act like the rest - a tiny bit of clean up - ill do better changelog tomorrow i think
This commit is contained in:
parent
78176be045
commit
137184d469
17 changed files with 201 additions and 200 deletions
13
README.md
13
README.md
|
@ -4,9 +4,9 @@ Sleek and easy to use social media downloader built on JavaScript. Try it out li
|
|||
![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/files/icons/wide.png "cobalt logo")
|
||||
|
||||
## What is cobalt?
|
||||
Everyone is annoyed by the mess video downloaders are on the web, and cobalt aims to be the ultimate social media downloader, that is sleek, easy to use, and doesn't bother you with ads or privacy invasion agreement popups.
|
||||
Everyone is annoyed by the mess video downloaders are on the web, and cobalt aims to be the ultimate social media downloader, that is efficient, pretty, and doesn't bother you with ads or privacy invasion agreement popups.
|
||||
|
||||
cobalt doesn't remux any videos, so videos you get are max quality available (unless you change that in settings).
|
||||
cobalt doesn't remux any videos, so you get videos of max quality available (unless you change that in settings).
|
||||
|
||||
## What's supported?
|
||||
- Twitter
|
||||
|
@ -21,12 +21,10 @@ cobalt doesn't remux any videos, so videos you get are max quality available (un
|
|||
- [ ] Sort contents of .json files
|
||||
- [ ] Rename each entry key to be less linked to specific service (entries like youtubeBroke are awful, I'm sorry)
|
||||
- [ ] Add support for more languages when localisation clean up is done
|
||||
- [ ] Clean up css
|
||||
- [ ] Use esmbuild to minify frontend css and js
|
||||
- [ ] Make switch buttons in settings selectable with keyboard
|
||||
- [ ] Do something about changelog because the way it is right now is not really great
|
||||
- [ ] Remake page rendering module to be more versatile
|
||||
- [ ] Clean up code to be more consistent across modules
|
||||
- [ ] Matching could be redone, I'll see what I can do
|
||||
- [ ] Facebook and Instagram support
|
||||
- [ ] TikTok support (?)
|
||||
|
@ -35,7 +33,7 @@ cobalt doesn't remux any videos, so videos you get are max quality available (un
|
|||
## Disclaimer
|
||||
This is my passion project, so update scheduele depends on my motivation. Don't expect any consistency in that.
|
||||
|
||||
## Make your own homegrown cobalt
|
||||
## Host an instance yourself
|
||||
Code might be a little messy, but I promise to improve it over time.
|
||||
|
||||
### Requirements
|
||||
|
@ -44,6 +42,7 @@ Code might be a little messy, but I promise to improve it over time.
|
|||
|
||||
### npm modules
|
||||
- express
|
||||
- cors
|
||||
- got
|
||||
- url-pattern
|
||||
- xml-js
|
||||
|
@ -57,8 +56,8 @@ Setup script installs all needed **npm** dependencies, but you have to install N
|
|||
|
||||
1. Clone the repo: `git clone https://github.com/wukko/cobalt`
|
||||
2. Run setup script and follow instructions: `npm run setup`
|
||||
3. Run cobalt via `npm start` or `node cobalt`
|
||||
3. Run cobalt via `npm start`
|
||||
4. Done.
|
||||
|
||||
## License
|
||||
cobalt is under [GPL-3.0 license](https://github.com/wukko/cobalt/blob/current/LICENSE), keep that in mind when doing something with it.
|
||||
cobalt is under [GPL-3.0 license](https://github.com/wukko/cobalt/blob/current/LICENSE), please keep that in mind.
|
||||
|
|
|
@ -5,7 +5,7 @@ import cors from "cors";
|
|||
import * as fs from "fs";
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
import currentCommit from "./modules/sub/current-commit.js";
|
||||
import { shortCommit } from "./modules/sub/current-commit.js";
|
||||
import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js";
|
||||
import { getJSON } from "./modules/api.js";
|
||||
import renderPage from "./modules/page-renderer.js";
|
||||
|
@ -14,7 +14,7 @@ import loc from "./modules/sub/loc.js";
|
|||
import { Bright, Cyan } from "./modules/sub/console-text.js";
|
||||
import stream from "./modules/stream/stream.js";
|
||||
|
||||
const commitHash = currentCommit();
|
||||
const commitHash = shortCommit();
|
||||
const app = express();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
|
|
11
config.json
11
config.json
|
@ -1,16 +1,15 @@
|
|||
{
|
||||
"appName": "cobalt",
|
||||
"version": "2.1",
|
||||
"version": "2.2",
|
||||
"streamLifespan": 1800000,
|
||||
"maxVideoDuration": 1920000,
|
||||
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
|
||||
"repo": "https://github.com/wukko/cobalt",
|
||||
"supportedLanguages": ["en"],
|
||||
"authorInfo": {
|
||||
"name": "wukko",
|
||||
"link": "https://wukko.me/",
|
||||
"contact": "https://wukko.me/contacts",
|
||||
"twitter": "uwukko"
|
||||
"contact": "https://wukko.me/contacts"
|
||||
},
|
||||
"internetExplorerRedirect": {
|
||||
"newNT": ["6.1", "6.2", "6.3", "10.0"],
|
||||
|
@ -28,5 +27,9 @@
|
|||
"hig": "1080",
|
||||
"mid": "720",
|
||||
"low": "480"
|
||||
},
|
||||
"ffmpegArgs": {
|
||||
"webm": ["-c:v", "copy", "-c:a", "copy"],
|
||||
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "frag_keyframe+empty_moov"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,13 +98,22 @@ input {
|
|||
}
|
||||
button:hover,
|
||||
.switch:hover,
|
||||
.checkbox:hover {
|
||||
.checkbox:hover,
|
||||
.text-to-copy:hover {
|
||||
background: var(--accent-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch.text-backdrop:hover,
|
||||
.switch.text-backdrop:active,
|
||||
.text-to-copy.text-backdrop:hover,
|
||||
.text-to-copy.text-backdrop:active {
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
}
|
||||
button:active,
|
||||
.switch:active,
|
||||
.checkbox:active {
|
||||
.checkbox:active,
|
||||
.text-to-copy:active {
|
||||
background: var(--accent-press);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -216,13 +225,16 @@ input[type="checkbox"] {
|
|||
.popup {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
width: 55%;
|
||||
height: auto;
|
||||
width: 30%;
|
||||
z-index: 999;
|
||||
padding: 3rem;
|
||||
padding: 3rem 2rem 2rem 2rem;
|
||||
font-size: 0.9rem;
|
||||
max-height: 80%;
|
||||
}
|
||||
.popup-big {
|
||||
width: 55%;
|
||||
}
|
||||
#popup-backdrop {
|
||||
opacity: 0.5;
|
||||
background-color: var(--background);
|
||||
|
@ -233,17 +245,7 @@ input[type="checkbox"] {
|
|||
height: 100%;
|
||||
z-index: 998;
|
||||
}
|
||||
.popup-narrow {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
z-index: 999;
|
||||
padding: 3rem 2rem 2rem 2rem;
|
||||
font-size: 0.9rem;
|
||||
max-height: 80%;
|
||||
}
|
||||
.popup-narrow.scrollable {
|
||||
.popup.scrollable {
|
||||
height: 80%;
|
||||
}
|
||||
.nowrap {
|
||||
|
@ -288,7 +290,7 @@ input[type="checkbox"] {
|
|||
color: var(--accent-unhover-2);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.popup-narrow .popup-content {
|
||||
.popup-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: var(--without-padding);
|
||||
|
@ -305,11 +307,8 @@ input[type="checkbox"] {
|
|||
#close {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
right: 3rem;
|
||||
position: absolute;
|
||||
}
|
||||
.popup-narrow #close {
|
||||
right: 0rem;
|
||||
position: absolute;
|
||||
}
|
||||
.settings-category {
|
||||
padding-bottom: 1.2rem;
|
||||
|
@ -348,6 +347,9 @@ input[type="checkbox"] {
|
|||
color: var(--accent);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.small-padding .subtitle {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.explanation {
|
||||
padding-top: 1rem;
|
||||
width: 100%;
|
||||
|
@ -377,6 +379,9 @@ input[type="checkbox"] {
|
|||
.switch.right {
|
||||
border-right: solid 0.1rem var(--accent);
|
||||
}
|
||||
.switch.space-right {
|
||||
margin-right: 1rem
|
||||
}
|
||||
.switch[data-enabled="true"] {
|
||||
color: var(--background);
|
||||
background: var(--accent);
|
||||
|
@ -411,30 +416,28 @@ input[type="checkbox"] {
|
|||
}
|
||||
}
|
||||
@media screen and (max-width: 1440px) {
|
||||
#cobalt-main-box,
|
||||
.popup {
|
||||
#cobalt-main-box {
|
||||
width: 65%;
|
||||
}
|
||||
.popup-narrow {
|
||||
width: 35%;
|
||||
.popup {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
#cobalt-main-box,
|
||||
.popup {
|
||||
#cobalt-main-box {
|
||||
width: 75%;
|
||||
}
|
||||
.popup-narrow {
|
||||
width: 50%;
|
||||
.popup {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-height: 850px) {
|
||||
.popup-narrow {
|
||||
.popup {
|
||||
height: 80%
|
||||
}
|
||||
}
|
||||
/* mobile page */
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 949px) {
|
||||
#logo-area {
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
|
@ -450,9 +453,7 @@ input[type="checkbox"] {
|
|||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.popup,
|
||||
.popup-narrow,
|
||||
.popup-narrow.scrollable {
|
||||
.popup, .popup.scrollable {
|
||||
border: none;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
|
@ -475,9 +476,7 @@ input[type="checkbox"] {
|
|||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.popup,
|
||||
.popup-narrow,
|
||||
.popup-narrow.scrollable {
|
||||
.popup, .popup.scrollable {
|
||||
border: none;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
|
||||
let switchers = {
|
||||
"theme": ["auto", "light", "dark"],
|
||||
"youtubeFormat": ["mp4", "webm", "audio"],
|
||||
"quality": ["max", "hig", "mid", "low"]
|
||||
}
|
||||
|
||||
function eid(id) {
|
||||
return document.getElementById(id)
|
||||
}
|
||||
|
@ -44,10 +51,10 @@ function button() {
|
|||
let regex = /https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/.test(eid("url-input-area").value);
|
||||
regex ? changeDownloadButton(1, '>>') : changeDownloadButton(0, '>>');
|
||||
}
|
||||
function copy(id) {
|
||||
function copy(id, data) {
|
||||
let e = document.getElementById(id);
|
||||
e.classList.add("text-backdrop");
|
||||
navigator.clipboard.writeText(e.innerText);
|
||||
data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText);
|
||||
setTimeout(() => { e.classList.remove("text-backdrop") }, 600);
|
||||
}
|
||||
function detectColorScheme() {
|
||||
|
@ -65,53 +72,35 @@ function popup(type, action, text) {
|
|||
switch (type) {
|
||||
case "about":
|
||||
eid("popup-about").style.visibility = vis(action);
|
||||
if (!localStorage.getItem("seenAbout")) {
|
||||
localStorage.setItem("seenAbout", "true");
|
||||
}
|
||||
if (!localStorage.getItem("seenAbout")) localStorage.setItem("seenAbout", "true");
|
||||
break;
|
||||
case "error":
|
||||
eid("desc-error").innerHTML = text;
|
||||
eid("popup-error").style.visibility = vis(action);
|
||||
break;
|
||||
case "settings":
|
||||
eid("popup-settings").style.visibility = vis(action);
|
||||
case "download":
|
||||
if (action == 1) {
|
||||
eid("pd-download").href = text;
|
||||
eid("pd-copy").setAttribute("onClick", `copy('pd-copy', '${text}')` );
|
||||
}
|
||||
eid("popup-download").style.visibility = vis(action);
|
||||
break;
|
||||
case "changelog":
|
||||
eid("popup-changelog").style.visibility = vis(action);
|
||||
break;
|
||||
case "donate":
|
||||
eid("popup-donate").style.visibility = vis(action);
|
||||
default:
|
||||
eid(`popup-${type}`).style.visibility = vis(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function changeSwitcher(li, b, u) {
|
||||
if (u) {
|
||||
localStorage.setItem(li, b);
|
||||
}
|
||||
let l = {
|
||||
"theme": ["auto", "light", "dark"],
|
||||
"youtubeFormat": ["mp4", "webm", "audio"],
|
||||
"quality": ["max", "hig", "mid", "low"]
|
||||
}
|
||||
if (u) localStorage.setItem(li, b);
|
||||
if (b) {
|
||||
for (i in l[li]) {
|
||||
if (l[li][i] == b) {
|
||||
enable(`${li}-${b}`)
|
||||
} else {
|
||||
disable(`${li}-${l[li][i]}`)
|
||||
}
|
||||
}
|
||||
if (li == "theme") {
|
||||
detectColorScheme();
|
||||
for (i in switchers[li]) {
|
||||
(switchers[li][i] == b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
|
||||
}
|
||||
if (li == "theme") detectColorScheme();
|
||||
} else {
|
||||
localStorage.setItem(li, l[li][0]);
|
||||
for (i in l[li]) {
|
||||
if (l[li][i] == l[li][0]) {
|
||||
enable(`${li}-${l[li][0]}`)
|
||||
} else {
|
||||
disable(`${li}-${l[li][i]}`)
|
||||
}
|
||||
localStorage.setItem(li, switchers[li][0]);
|
||||
for (i in switchers[li]) {
|
||||
(switchers[li][i] == switchers[li][0]) ? enable(`${li}-${switchers[li][0]}`) : disable(`${li}-${switchers[li][i]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,24 +110,23 @@ function internetError() {
|
|||
popup("error", 1, loc.noInternet);
|
||||
}
|
||||
function checkbox(action) {
|
||||
switch(action) {
|
||||
case 'alwaysVisibleButton':
|
||||
if (eid("always-visible-button").checked) {
|
||||
localStorage.setItem("alwaysVisibleButton", "true");
|
||||
button();
|
||||
} else {
|
||||
localStorage.setItem("alwaysVisibleButton", "false");
|
||||
button();
|
||||
}
|
||||
break;
|
||||
if (eid(action).checked) {
|
||||
localStorage.setItem(action, "true");
|
||||
if (action == "alwaysVisibleButton") button();
|
||||
} else {
|
||||
localStorage.setItem(action, "false");
|
||||
if (action == "alwaysVisibleButton") button();
|
||||
}
|
||||
}
|
||||
function loadSettings() {
|
||||
if (localStorage.getItem("alwaysVisibleButton") == "true") {
|
||||
eid("always-visible-button").checked = true;
|
||||
eid("alwaysVisibleButton").checked = true;
|
||||
eid("download-button").value = '>>'
|
||||
eid("download-button").style.padding = '0 1rem';
|
||||
}
|
||||
if (localStorage.getItem("downloadPopup") == "true" && !isIOS) {
|
||||
eid("downloadPopup").checked = true;
|
||||
}
|
||||
changeSwitcher("theme", localStorage.getItem("theme"))
|
||||
changeSwitcher("youtubeFormat", localStorage.getItem("youtubeFormat"))
|
||||
changeSwitcher("quality", localStorage.getItem("quality"))
|
||||
|
@ -160,8 +148,8 @@ async function download(url) {
|
|||
changeDownloadButton(1, '>>')
|
||||
eid("url-input-area").disabled = false
|
||||
}, 3000)
|
||||
if (navigator.userAgent.toLowerCase().match("iphone os")) {
|
||||
window.location.href = j.url;
|
||||
if (localStorage.getItem("downloadPopup") == "true") {
|
||||
popup('download', 1, j.url)
|
||||
} else {
|
||||
window.open(j.url, '_blank');
|
||||
}
|
||||
|
@ -182,9 +170,7 @@ async function download(url) {
|
|||
changeDownloadButton(2, '!!')
|
||||
popup("error", 1, jp.text);
|
||||
}
|
||||
}).catch((error) => {
|
||||
internetError()
|
||||
});
|
||||
}).catch((error) => internetError());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
@ -192,9 +178,7 @@ async function download(url) {
|
|||
changeDownloadButton(2, '!!')
|
||||
popup("error", 1, j.text);
|
||||
}
|
||||
}).catch((error) => {
|
||||
internetError()
|
||||
});
|
||||
}).catch((error) => internetError());
|
||||
}
|
||||
window.onload = function () {
|
||||
loadSettings();
|
||||
|
@ -203,9 +187,8 @@ window.onload = function () {
|
|||
eid("cobalt-main-box").style.visibility = 'visible';
|
||||
eid("footer").style.visibility = 'visible';
|
||||
eid("url-input-area").value = "";
|
||||
if (!localStorage.getItem("seenAbout")) {
|
||||
popup('about', 1)
|
||||
}
|
||||
if (!localStorage.getItem("seenAbout")) popup('about', 1);
|
||||
if (isIOS) localStorage.setItem("downloadPopup", "true");
|
||||
}
|
||||
eid("url-input-area").addEventListener("keyup", (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
|
|
@ -14,5 +14,6 @@ let supportedLanguages = config.supportedLanguages
|
|||
let quality = config.quality
|
||||
let internetExplorerRedirect = config.internetExplorerRedirect
|
||||
let donations = config.donations
|
||||
let ffmpegArgs = config.ffmpegArgs
|
||||
|
||||
export {appName, version, streamLifespan, maxVideoDuration, genericUserAgent, repo, authorInfo, services, supportedLanguages, quality, internetExplorerRedirect, donations}
|
||||
export {appName, version, streamLifespan, maxVideoDuration, genericUserAgent, repo, authorInfo, services, supportedLanguages, quality, internetExplorerRedirect, donations, ffmpegArgs}
|
|
@ -13,10 +13,12 @@ let enabledServices = Object.keys(s).filter((p) => {
|
|||
return p
|
||||
}
|
||||
}).join(', ')
|
||||
|
||||
let donate = ``
|
||||
for (let i in donations) {
|
||||
donate += `<div class="subtitle">${i} (${loc("en", 'desc', 'clicktocopy').trim()})</div><div id="don-${i}" class="text-to-copy" onClick="copy('don-${i}')">${donations[i]}</div>`
|
||||
donate += `<div class="subtitle">${i} (${loc("en", 'desc', 'clickToCopy').trim()})</div><div id="don-${i}" class="text-to-copy" onClick="copy('don-${i}')">${donations[i]}</div>`
|
||||
}
|
||||
|
||||
export default function(obj) {
|
||||
let isIOS = obj.useragent.toLowerCase().match("iphone os")
|
||||
try {
|
||||
|
@ -49,33 +51,48 @@ export default function(obj) {
|
|||
<noscript><div style="margin: 2rem;">${loc(obj.lang, 'desc', 'noScript')}</div></noscript>
|
||||
</head>
|
||||
<body id="cobalt-body">
|
||||
<div id="popup-about" class="popup-narrow center box" style="visibility: hidden;">
|
||||
<div id="popup-download" class="popup center box" style="visibility: hidden;">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('download', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-subtitle">${loc(obj.lang, 'title', 'download')}</div>
|
||||
</div>
|
||||
<div id="content" class="popup-content">
|
||||
<div id="theme-switcher" class="switch-container small-padding">
|
||||
<div class="subtitle">${loc(obj.lang, 'title', 'pickDownload')}</div>
|
||||
<div class="switches">
|
||||
<a id="pd-download" class="switch full space-right" target="_blank"">${loc(obj.lang, 'desc', 'download')}</a>
|
||||
<div id="pd-copy" class="switch full">${loc(obj.lang, 'desc', 'copy')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="desc" class="explanation about-padding">${isIOS ? loc(obj.lang, 'desc', 'iosDownload') : loc(obj.lang, 'desc', 'normalDownload')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-about" class="popup center box" style="visibility: hidden;">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('about', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-title">${loc(obj.lang, 'title', 'about')}</div>
|
||||
</div>
|
||||
<div id="content" class="popup-content with-footer">
|
||||
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'desc', 'about')}</div>
|
||||
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'desc', 'support_1')} ${enabledServices}.</div>
|
||||
${isIOS ? `<div id="desc" class="popup-subtitle popup-desc"><span class="text-backdrop">${loc(obj.lang, 'desc', 'iosTitle')}</span></div><div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'desc', 'ios')}</div>`: ``}
|
||||
<div id="desc" class="popup-desc"><a class="text-backdrop" href="${repo}">${loc(obj.lang, 'desc', 'sourcecode')}</a></div>
|
||||
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'desc', 'aboutSummary')}</div>
|
||||
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'desc', 'supportedServices')} ${enabledServices}.</div>
|
||||
<div id="desc" class="popup-desc"><a class="text-backdrop" href="${repo}">${loc(obj.lang, 'desc', 'sourceCode')}</a></div>
|
||||
</div>
|
||||
<div id="popup-footer" class="popup-footer">
|
||||
<a id="popup-bottom" class="popup-footer-content" href="${authorInfo.link}">${loc(obj.lang, 'desc', 'popupBottom')}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-changelog" class="popup-narrow center box" style="visibility: hidden;">
|
||||
<div id="popup-changelog" class="popup center box" style="visibility: hidden;">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('changelog', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-title">${loc(obj.lang, 'title', 'changelog')}</div>
|
||||
<div id="desc" class="popup-subtitle">${loc(obj.lang, 'changelog', 'subtitle')}</div>
|
||||
<div id="desc" class="popup-subtitle">${loc(obj.lang, 'changelog', 'subtitle')}(${obj.hash})</div>
|
||||
</div>
|
||||
<div id="content" class="popup-content">
|
||||
<div id="desc" class="popup-desc about-padding">${loc(obj.lang, 'changelog', 'text')}</div>
|
||||
<div id="desc" class="popup-desc"><a class="text-backdrop" href="${repo}">${loc(obj.lang, 'changelog', 'github')}</a></div>
|
||||
<div id="desc" class="popup-desc"><a class="text-backdrop" href="${repo}/commits">${loc(obj.lang, 'changelog', 'github')}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-donate" class="popup-narrow scrollable center box" style="visibility: hidden;">
|
||||
<div id="popup-donate" class="popup scrollable center box" style="visibility: hidden;">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('donate', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-title">${loc(obj.lang, 'title', 'donate')}</div>
|
||||
|
@ -87,7 +104,7 @@ export default function(obj) {
|
|||
<div id="desc" class="popup-desc"><a class="text-backdrop" href="${authorInfo.contact}">${loc(obj.lang, 'desc', 'donateDm')}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-settings" class="popup-narrow scrollable center box" style="visibility: hidden;">
|
||||
<div id="popup-settings" class="popup scrollable center box" style="visibility: hidden;">
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('settings', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="version" class="popup-above-title">v.${version} ~ ${obj.hash}</div>
|
||||
|
@ -95,35 +112,41 @@ export default function(obj) {
|
|||
</div>
|
||||
<div id="content" class="popup-content">
|
||||
<div id="settings-appearance" class="settings-category">
|
||||
<div class="title">${loc(obj.lang, 'settings', 'category-appearance')}</div>
|
||||
<div class="title">${loc(obj.lang, 'settings', 'appearance')}</div>
|
||||
<div class="settings-category-content">
|
||||
<div id="theme-switcher" class="switch-container">
|
||||
<div class="subtitle">${loc(obj.lang, 'settings', 'theme')}</div>
|
||||
<div class="switches">
|
||||
<div id="theme-auto" class="switch full" onclick="changeSwitcher('theme', 'auto', 1)">${loc(obj.lang, 'settings', 'theme-auto')}</div>
|
||||
<div id="theme-dark" class="switch" onclick="changeSwitcher('theme', 'dark', 1)">${loc(obj.lang, 'settings', 'theme-dark')}</div>
|
||||
<div id="theme-light" class="switch full" onclick="changeSwitcher('theme', 'light', 1)">${loc(obj.lang, 'settings', 'theme-light')}</div>
|
||||
<div id="theme-auto" class="switch full" onclick="changeSwitcher('theme', 'auto', 1)">${loc(obj.lang, 'settings', 'themeAuto')}</div>
|
||||
<div id="theme-dark" class="switch" onclick="changeSwitcher('theme', 'dark', 1)">${loc(obj.lang, 'settings', 'themeDark')}</div>
|
||||
<div id="theme-light" class="switch full" onclick="changeSwitcher('theme', 'light', 1)">${loc(obj.lang, 'settings', 'themeLight')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtitle">${loc(obj.lang, 'settings', 'misc')}</div>
|
||||
<label class="checkbox">
|
||||
<input id="always-visible-button" type="checkbox" aria-label="${loc(obj.lang, 'accessibility', 'alwaysVisibleButton')}" onclick="checkbox('alwaysVisibleButton', 'always-visible-button')">${loc(obj.lang, 'settings', 'always-visible')}
|
||||
<input id="alwaysVisibleButton" type="checkbox" aria-label="${loc(obj.lang, 'accessibility', 'alwaysVisibleButton')}" onclick="checkbox('alwaysVisibleButton', 'always-visible-button')">
|
||||
<span>${loc(obj.lang, 'settings', 'alwaysVisibleButton')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings-quality" class="settings-category">
|
||||
<div id="settings-downloads" class="settings-category">
|
||||
<div class="title">${loc(obj.lang, 'settings', 'general')}</div>
|
||||
<div class="settings-category-content">
|
||||
<div id="quality-switcher" class="switch-container">
|
||||
<div class="subtitle">${loc(obj.lang, 'settings', 'quality')}</div>
|
||||
<div class="switches">
|
||||
<div id="quality-max" class="switch full" onclick="changeSwitcher('quality', 'max', 1)">${loc(obj.lang, 'settings', 'q-max')}</div>
|
||||
<div id="quality-hig" class="switch" onclick="changeSwitcher('quality', 'hig', 1)">${loc(obj.lang, 'settings', 'q-hig')}(${quality.hig}p)</div>
|
||||
<div id="quality-mid" class="switch full" onclick="changeSwitcher('quality', 'mid', 1)">${loc(obj.lang, 'settings', 'q-mid')}(${quality.mid}p)</div>
|
||||
<div id="quality-low" class="switch right" onclick="changeSwitcher('quality', 'low', 1)">${loc(obj.lang, 'settings', 'q-low')}(${quality.low}p)</div>
|
||||
<div id="quality-max" class="switch full" onclick="changeSwitcher('quality', 'max', 1)">${loc(obj.lang, 'settings', 'qmax')}</div>
|
||||
<div id="quality-hig" class="switch" onclick="changeSwitcher('quality', 'hig', 1)">${loc(obj.lang, 'settings', 'qhig')}(${quality.hig}p)</div>
|
||||
<div id="quality-mid" class="switch full" onclick="changeSwitcher('quality', 'mid', 1)">${loc(obj.lang, 'settings', 'qmid')}(${quality.mid}p)</div>
|
||||
<div id="quality-low" class="switch right" onclick="changeSwitcher('quality', 'low', 1)">${loc(obj.lang, 'settings', 'qlow')}(${quality.low}p)</div>
|
||||
</div>
|
||||
<div class="explanation">${loc(obj.lang, 'settings', 'q-desc')}</div>
|
||||
<div class="explanation">${loc(obj.lang, 'settings', 'qualityDesc')}</div>
|
||||
</div>
|
||||
${!isIOS ? `<div class="subtitle">${loc(obj.lang, 'settings', 'extra')}</div>
|
||||
<label class="checkbox">
|
||||
<input id="downloadPopup" type="checkbox" aria-label="${loc(obj.lang, 'accessibility', 'downloadPopup')}" onclick="checkbox('downloadPopup', 'always-visible-button')">
|
||||
<span>${loc(obj.lang, 'settings', 'downloadPopupButton')}</span>
|
||||
</label>` : ``}
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings-youtube" class="settings-category">
|
||||
|
@ -136,16 +159,20 @@ export default function(obj) {
|
|||
<div id="youtubeFormat-webm" class="switch" onclick="changeSwitcher('youtubeFormat', 'webm', 1)">webm</div>
|
||||
<div id="youtubeFormat-audio" class="switch full" onclick="changeSwitcher('youtubeFormat', 'audio', 1)">audio only</div>
|
||||
</div>
|
||||
<div class="explanation">${loc(obj.lang, 'settings', 'format-info')}</div>
|
||||
<div class="explanation">${loc(obj.lang, 'settings', 'formatInfo')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-error" class="popup center box" style="visibility: hidden;">
|
||||
<button id="close" class="button mono" onclick="popup('error', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-title">${loc(obj.lang, 'title', 'error')}</div>
|
||||
<div id="desc-error" class="popup-desc"></div>
|
||||
<div id="popup-header" class="popup-header">
|
||||
<button id="close" class="button mono" onclick="popup('error', 0)" aria-label="${loc(obj.lang, 'accessibility', 'close')}">x</button>
|
||||
<div id="title" class="popup-title">${loc(obj.lang, 'title', 'error')}</div>
|
||||
</div>
|
||||
<div id="content" class="popup-content">
|
||||
<div id="desc-error" class="popup-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="popup-backdrop" style="visibility: hidden;"></div>
|
||||
<div id="cobalt-main-box" class="center box" style="visibility: hidden;">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { spawn } from "child_process";
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
import got from "got";
|
||||
import { genericUserAgent } from "../config.js";
|
||||
import { ffmpegArgs, genericUserAgent } from "../config.js";
|
||||
import { msToTime } from "../sub/api-helper.js";
|
||||
import { internalError } from "../sub/errors.js";
|
||||
import loc from "../sub/loc.js";
|
||||
|
@ -16,11 +16,9 @@ export async function streamDefault(streamInfo, res) {
|
|||
isStream: true
|
||||
});
|
||||
stream.pipe(res).on('error', (err) => {
|
||||
internalError(res);
|
||||
throw Error("File stream pipe error.");
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
internalError(res);
|
||||
throw Error("File stream error.")
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -42,18 +40,9 @@ export async function streamLiveRender(streamInfo, res) {
|
|||
'-i', 'pipe:4',
|
||||
'-map', '0:v',
|
||||
'-map', '1:a',
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'copy',
|
||||
];
|
||||
if (format == 'mp4') {
|
||||
args.push('-movflags', 'frag_keyframe+empty_moov');
|
||||
if (streamInfo.service == "youtube") {
|
||||
args.push('-t', msToTime(streamInfo.time));
|
||||
}
|
||||
} else if (format == 'webm') {
|
||||
args.push('-t', msToTime(streamInfo.time));
|
||||
}
|
||||
args.push('-f', format, 'pipe:5');
|
||||
args = args.concat(ffmpegArgs[format])
|
||||
args.push('-t', msToTime(streamInfo.time), '-f', format, 'pipe:5');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
windowsHide: true,
|
||||
stdio: [
|
||||
|
@ -63,25 +52,20 @@ export async function streamLiveRender(streamInfo, res) {
|
|||
});
|
||||
ffmpegProcess.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
audio.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
video.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
|
||||
ffmpegProcess.stdio[5].pipe(res);
|
||||
video.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
audio.pipe(ffmpegProcess.stdio[4]).on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({ status: "error", text: loc('en', 'apiError', 'corruptedVideo') });
|
||||
|
@ -113,17 +97,14 @@ export async function streamAudioOnly(streamInfo, res) {
|
|||
});
|
||||
ffmpegProcess.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
audio.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
|
||||
ffmpegProcess.stdio[4].pipe(res);
|
||||
audio.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
internalError(res);
|
||||
});
|
||||
} catch (e) {
|
||||
internalError(res);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { execSync } from "child_process";
|
||||
|
||||
export default function() {
|
||||
export function shortCommit() {
|
||||
return execSync('git rev-parse --short HEAD').toString().trim()
|
||||
}
|
||||
export function getCommitInfo() {
|
||||
return execSync(`git show -s --format='%s;;;%B'`).toString().trim().slice(1,-1).replace(/[\r\n]/gm, '\n').split(';;;')
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { supportedLanguages, appName } from "../config.js";
|
||||
import { supportedLanguages, appName, repo } from "../config.js";
|
||||
import loadJson from "./load-json.js";
|
||||
|
||||
export default function(lang, cat, string, replacement) {
|
||||
|
@ -8,7 +8,7 @@ export default function(lang, cat, string, replacement) {
|
|||
try {
|
||||
let str = loadJson(`./strings/${lang}/${cat}.json`);
|
||||
if (str && str[string]) {
|
||||
let s = str[string].replace(/\n/g, '<br/>').replace(/{appName}/g, appName)
|
||||
let s = str[string].replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo)
|
||||
if (replacement) {
|
||||
s = s.replace(/{s}/g, replacement)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cobalt",
|
||||
"description": "easy to use social media downloader",
|
||||
"version": "2.1",
|
||||
"version": "2.2",
|
||||
"author": "wukko",
|
||||
"exports": "./cobalt.js",
|
||||
"type": "module",
|
||||
|
@ -14,13 +14,13 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wukko/cobalt-web.git"
|
||||
"url": "git+https://github.com/wukko/cobalt.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wukko/cobalt-web/issues"
|
||||
"url": "https://github.com/wukko/cobalt/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wukko/cobalt-web#readme",
|
||||
"homepage": "https://github.com/wukko/cobalt#readme",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"changelog": "Changelog",
|
||||
"close": "Close button",
|
||||
"alwaysVisibleButton": "Keep the download button always visible",
|
||||
"donate": "Support {appName} by donating"
|
||||
"donate": "Support development of {appName} by donating"
|
||||
}
|
|
@ -2,21 +2,20 @@
|
|||
"generic": "something went wrong and i couldn't get the video. you can try again,",
|
||||
"notSupported": "it seems like this service is not supported yet or your link is invalid.",
|
||||
"brokenLink": "{s} is supported, but something is wrong with your link.",
|
||||
"noURL": "something is wrong with your link. it's probably missing. i can't guess what you want to download!",
|
||||
"badResolution": "your screen resolution is not supported. try a different device.",
|
||||
"noURL": "i can't guess what you want to download! please give me a link.",
|
||||
"tryAgain": "\ncheck the link and try again.",
|
||||
"letMeKnow": "but if issue persists, please <a class=\"text-backdrop nowrap\" href=\"https://wukko.me/contacts\">let me know</a>.",
|
||||
"fatal": "something went wrong and page couldn't be rendered. if you want me to fix this, please <a href=\"https://wukko.me/contacts\">contact me</a>. it'd be useful if you provided the commit hash ({s}) along with recreation steps. thank you :D",
|
||||
"letMeKnow": "but if issue persists, please <a class=\"text-backdrop nowrap\" href=\"{repo}\">let me know</a>.",
|
||||
"fatal": "something went wrong and page couldn't render. if you want me to fix this, please <a href=\"https://wukko.me/contacts\">contact me</a>. it'd be useful if you provided the commit hash ({s}) along with recreation steps. thank you :D",
|
||||
"rateLimit": "you're making way too many requests. calm down and try again in a few minutes.",
|
||||
"youtubeFetch": "couldn't fetch metadata. check if your link is correct and try again.",
|
||||
"youtubeLimit": "current length limit is {s} minutes. what you tried to download was longer than {s} minutes.",
|
||||
"youtubeBroke": "something went wrong with info fetching. you can try a different format or just try again later.",
|
||||
"corruptedVideo": "oddly enough the requested video is corrupted on its origin server. youtube does this sometimes because it's fucking stupid.",
|
||||
"corruptedAudio": "oddly enough the requested audio is corrupted on its origin server. youtube does this sometimes because it's fucking stupid.",
|
||||
"noInternet": "it seems like either {appName} api is down or there's no internet. check your connection and try again.",
|
||||
"youtubeLimit": "current length limit is {s} minutes. what you tried to download was longer than that. pick something else to download!",
|
||||
"youtubeBroke": "something went wrong with info fetching. you can try a different format and resoltuion or just try again later.",
|
||||
"corruptedVideo": "oddly enough the requested video is corrupted on its origin server. youtube does this sometimes because it's a hot pile of mess.",
|
||||
"corruptedAudio": "oddly enough the requested audio is corrupted on its origin server. youtube does this sometimes because it's a hot pile of mess.",
|
||||
"noInternet": "it seems like there's no internet or {appName} api is down. check your connection and try again.",
|
||||
"liveVideo": "i can't download a live video. wait for stream to finish and try again.",
|
||||
"nothingToDownload": "it seems like there's nothing to download. try another link!",
|
||||
"cantConnectToAPI": "i couldn't connect to {s} api. seems like either {s} is down or server ip got blocked. try again later.",
|
||||
"cantConnectToAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.",
|
||||
"noStreamID": "there's no such stream id.",
|
||||
"noType": "there's no such expected response type."
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"subtitle": "everything is new! (v.2.1)",
|
||||
"text": "- added support for: bilibili.com, youtube, youtube music, reddit, vk;\n- remade the way downloads are handled;\n- added proper website branding;\n- added settings, donations, and changelog menu;\n- added manual theme picker;\n- added format picker for youtube;\n- added quality picker for youtube and vk downloads (bilibili and twitter later);\n- improved usability;\n- upgraded the download button to be adaptive depending on current status;\n- popups are now adaptive, too;\n- better scalability;\n- took out trash;\n- moved from commonjs to ems;\n- overall revamp of backend and frontend;\n- fixed various issues that were present in older version.",
|
||||
"subtitle": "beginning of 2.2",
|
||||
"text": "- added download popup to solve the issue with downloads on ios\n- merged big and small popups into one\n- made buttons in donation menu act like buttons\n- began to clean up localisation\n- added ability to embed repo url into localisation strings\n- moved ffmpeg args to config for more flexibility (and hopefully future changes)\n- removed error response in stream that could result in a crash\n- removed notice for ios users from about cause it's no longer relevant\n- made error popup look and act like the rest\n- a tiny bit of clean up",
|
||||
"github": ">> see previous changes and contribute on github"
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
{
|
||||
"input": "paste the link here",
|
||||
"about": "{appName} is your go-to place for social media downloads. zero ads or other creepy bullshit attached. simply paste a share link and you're ready to rock!",
|
||||
"aboutSummary": "{appName} is your go-to place for social media downloads. zero ads or other creepy bullshit attached. simply paste a share link and you're ready to rock!",
|
||||
"embed": "save content from social media without creeps following you around",
|
||||
"support_1": "currently supported services:",
|
||||
"sourcecode": ">> report issues and check out the source code on github",
|
||||
"supportedServices": "currently supported services:",
|
||||
"sourceCode": ">> report issues and check out the source code on github",
|
||||
"popupBottom": "made with <3 by wukko",
|
||||
"noScript": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. we don't have ads or any trackers, pinky promise.",
|
||||
"ios": "it's impossible to download videos directly from browser on ios. you'll have to open share sheet and select \"save to files\" in order to save videos from twitter. if you don't see that option, you have to install the official \"files\" app from app store first.",
|
||||
"iosTitle": "notice for ios users",
|
||||
"donationsSub": "it's quite complicated to pay for hosting right now",
|
||||
"donations": "you can currently donate only in crypto, because i live under dictatorship and can't use debit cards anywhere outside of country. i don't like crypto the way it is right now, but it's the only way for me to pay for anything (including hosting) online. everything else like paypal is no longer available.",
|
||||
"noScript": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. we don't have any ads or trackers, pinky promise.",
|
||||
"donationsSub": "it's a little complicated to pay for hosting right now",
|
||||
"donations": "i don't like crypto how it is right now, but it's the only way for me to pay for anything (including hosting) online. services similar paypal are no longer available too.",
|
||||
"donateDm": ">> let me know if currency you want to donate isn't listed",
|
||||
"clicktocopy": "click to copy"
|
||||
"clickToCopy": "click to copy",
|
||||
"iosDownload": "you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this is required because you have an ios device.",
|
||||
"normalDownload": "download button opens a new tab with requested video. you can disable this popup in settings.",
|
||||
"download": "download",
|
||||
"copy": "copy url",
|
||||
"open": "open"
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
{
|
||||
"category-appearance": "appearance",
|
||||
"always-visible": "keep >> visible",
|
||||
"picker": "always download max quality",
|
||||
"appearance": "appearance",
|
||||
"alwaysVisibleButton": "keep >> visible",
|
||||
"downloadPopupButton": "ask for a way to save",
|
||||
"format": "download format",
|
||||
"format-info": "webm videos are usually higher quality but not all devices can play them. select webm only if you need max quality available. audio only downloads will always be max quality.",
|
||||
"formatInfo": "select webm if you need max quality available. webm videos are usually higher quality but ios devices can't play them natively. all \"audio only\" downloads are max quality.",
|
||||
"theme": "theme",
|
||||
"theme-auto": "auto",
|
||||
"theme-light": "light",
|
||||
"theme-dark": "dark",
|
||||
"themeAuto": "auto",
|
||||
"themeLight": "light",
|
||||
"themeDark": "dark",
|
||||
"misc": "miscellaneous",
|
||||
"general": "downloads",
|
||||
"quality": "quality",
|
||||
"q-max": "max",
|
||||
"q-hig": "high\n",
|
||||
"q-mid": "medium\n",
|
||||
"q-low": "low\n",
|
||||
"q-los": "lowest",
|
||||
"q-desc": "all resolutions listed here are max values. if there's no video of preferred quality closest one gets picked instead."
|
||||
"qmax": "max",
|
||||
"qhig": "high\n",
|
||||
"qmid": "medium\n",
|
||||
"qlow": "low\n",
|
||||
"qlos": "lowest",
|
||||
"qualityDesc": "all resolutions listed here are max values. if there's no video of preferred quality, closest one gets picked instead.",
|
||||
"extra": "extra"
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"about": "owo what's this?",
|
||||
"about": "what's {appName}?",
|
||||
"settings": "settings",
|
||||
"error": "uh-oh...",
|
||||
"changelog": "what's new?",
|
||||
"donate": "support {appName}"
|
||||
"donate": "support {appName}",
|
||||
"download": "download",
|
||||
"pickDownload": "pick a way to save"
|
||||
}
|
Loading…
Reference in a new issue