diff --git a/README.md b/README.md index c58224e9..b29c1162 100644 --- a/README.md +++ b/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. diff --git a/cobalt.js b/cobalt.js index b0809f25..2f6539a8 100644 --- a/cobalt.js +++ b/cobalt.js @@ -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'); diff --git a/config.json b/config.json index c1587b44..91272789 100644 --- a/config.json +++ b/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"] } } diff --git a/files/cobalt.css b/files/cobalt.css index 61f28a9c..1d380283 100644 --- a/files/cobalt.css +++ b/files/cobalt.css @@ -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%; diff --git a/files/cobalt.js b/files/cobalt.js index b8523730..01acf897 100644 --- a/files/cobalt.js +++ b/files/cobalt.js @@ -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') { diff --git a/modules/config.js b/modules/config.js index 792df68b..9e3e8d11 100644 --- a/modules/config.js +++ b/modules/config.js @@ -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} \ No newline at end of file +export {appName, version, streamLifespan, maxVideoDuration, genericUserAgent, repo, authorInfo, services, supportedLanguages, quality, internetExplorerRedirect, donations, ffmpegArgs} \ No newline at end of file diff --git a/modules/page-renderer.js b/modules/page-renderer.js index e918c06f..6e852f61 100644 --- a/modules/page-renderer.js +++ b/modules/page-renderer.js @@ -13,10 +13,12 @@ let enabledServices = Object.keys(s).filter((p) => { return p } }).join(', ') + let donate = `` for (let i in donations) { - donate += `