diff --git a/README.md b/README.md index 234ff4b5..baa7da2a 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,61 @@ # cobalt -Best way to save what you love. +Best way to save what you love. +Main instance: [co.wukko.me](https://co.wukko.me/) -Live: [co.wukko.me](https://co.wukko.me/) - -![cobalt logo with repeated logo pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo pattern background") +![cobalt logo with repeated logo pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo pattern background") [![Crowdin](https://badges.crowdin.net/cobalt/localized.svg)](https://crowdin.com/project/cobalt) [![DeepSource](https://deepsource.io/gh/wukko/cobalt.svg/?label=active+issues&token=MsmsJ9zUOKwcQor0yaiFot84)](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge) [![DeepSource](https://deepsource.io/gh/wukko/cobalt.svg/?label=resolved+issues&token=MsmsJ9zUOKwcQor0yaiFot84)](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge) ## What's cobalt? cobalt is a social and media platform downloader that doesn't piss you off. -It's fast, friendly, and doesn't have any bullshit that modern web is filled with: no ads, trackers, or analytics. Paste the link, get the video, move on. It's that simple. Just how it should be. +It's fast, friendly, and doesn't have any bullshit that modern web is filled with: no ads, trackers, or analytics. +Paste the link, get the video, move on. It's that simple. Just how it should be. ## Supported services -| Service | Video + Audio | Only audio | Additional features | -| -------- | :---: | :---: | :----- | -| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. | -| Twitter Spaces | ❌️ | ✅ | Audio metadata. | -| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. | -| YouTube Music | ❌ | ✅ | Audio metadata. | -| Reddit | ✅ | ✅ | GIFs and videos. | -| TikTok | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermark. Full audio downloads. | -| Twitch | ✅ | ✅ | | -| SoundCloud | ❌ | ✅ | Audio metadata, downloads from private links. | -| bilibili.com | ✅ | ✅ | | -| Tumblr | ✅ | ✅ | | -| Vimeo | ✅ | ❌️ | | -| VK Videos & Clips | ✅ | ❌️ | | +| Service | Video + Audio | Only audio | Only video | Additional notes or features | +| -------- | :---: | :---: | :---: | :----- | +| bilibili.com | ✅ | ✅ | ✅ | | +| Instagram | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media posts. | +| Instagram Reels | ✅ | ✅ | ✅ | | +| Reddit | ✅ | ✅ | ✅ | Support for GIFs and videos. | +| SoundCloud | ➖ | ✅ | ➖ | Audio metadata, downloads from private links. | +| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. | +| Tumblr | ✅ | ✅ | ✅ | | +| Twitter | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. | +| Twitter Spaces | ➖ | ✅ | ➖ | Audio metadata with all participants and other info. | +| Twitch | ✅ | ✅ | ✅ | | +| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. | +| Vine Archive | ✅ | ✅ | ✅ | | +| VK Videos | ✅ | ❌ | ❌ | | +| VK Clips | ✅ | ❌ | ❌ | | +| YouTube Videos & Shorts | ✅ | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. Audio metadata & dubs. h264/av1/vp9 codecs. | +| YouTube Music | ➖ | ✅ | ➖ | Audio metadata. | + +This list is not final and keeps expanding over time, make sure to check it once in a while! ## cobalt API -cobalt has an open API that you can use for free. It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself. +cobalt has an open API that you can use in your projects for **free**. +It's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself. ## How to contribute translations You can translate cobalt to any language you want on [cobalt's Crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to Crowdin. ### Translation guidelines: -- All text is **ALWAYS** stylized as **lowercase** unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}` or `{appName}`. +- Text is **ALWAYS** stylized as **lowercase** unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}` or `{appName}`. - Example: "`this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!`". - Notice how **everything is lowercase**, no matter the punctuation marks? Yes, that's cobalt's style and you have to follow it. -- Avoid formal language. Leave it for big and classy tech companies. Use informal language wherever possible. -- Keep translations lively, friendly, and fun. Translate strings as if the user was your buddy. + *Notice how **everything is lowercase**, no matter the punctuation marks? Yes, that's cobalt's style and you have to follow it.* +- Avoid extremely formal language, leave it for big and classy tech companies. Use informal language wherever possible. - You can (and should) rephrase sentences as long as they keep the same sense and send the same message as original. -- You can add wordplays or puns if it feels natural to do so. - Do **NOT** use offensive or explicit vocabulary. -- Check if there are issues in UI with your localization, and optimize it accordingly. If impossible, open an issue. +- Check if there are issues in UI with your localization and optimize it accordingly. If impossible, open an issue. - Be nice. ## Host an instance yourself -You might find cobalt's source code a bit messy, but I do my best to improve it with every commit. - ### Requirements -- Node.js 17.5 or above +- Node.js 18 or above - git -### npm modules -- cors -- dotenv -- esbuild -- express -- express-rate-limit -- ffmpeg-static -- got -- node-cache -- url-pattern -- xml-js -- youtubei.js - Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself. 1. Clone the repo: `git clone https://github.com/wukko/cobalt` @@ -73,20 +63,27 @@ Setup script installs all needed `npm` dependencies, but you have to install `No 3. Run cobalt via `npm start` 4. Done. +### Ubuntu 22.04+ workaround +`nscd` needs to be installed and running so that the `ffmpeg-static` binary can resolve DNS ([#101](https://github.com/wukko/cobalt/issues/101#issuecomment-1494822258)): + +```bash +sudo apt install nscd +sudo service nscd start +``` + ### Docker -It's also possible to host cobalt via a Docker image, but in that case you'd need to set all environment variables by yourself. -That includes: -| Variable | Example | -| -------- | :--- | -| `selfURL` | `https://co.wukko.me/` | -| `port` | `9000` | -| `streamSalt` | `randomly generated sha512 hash` | -| `cors` | `0` | +It's also possible to run cobalt via Docker, but you **need** to set all environment variables yourself: + +| Variable | Description | Example | +| -------- | :--- | :--- | +| `selfURL` | Instance URL | `http://localhost:9000/` or `https://co.wukko.me/` or etc | +| `port` | Instance port | `9000` | +| `cors` | CORS toggle | `0` | ## Disclaimer -cobalt is my passion project, so update release schedule depends solely on my motivation, free time, and mood. Don't expect any consistency in that. +cobalt is my passion project, so update schedule depends solely on my free time, motivation, and mood. +Don't expect any consistency in that. ## License -cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE) license. - +cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE) license. [Fluent Emoji](https://github.com/microsoft/fluentui-emoji) used in the project is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE) license. diff --git a/package.json b/package.json index fd8d37d7..1382b1f5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "5.5", + "version": "5.7", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", @@ -34,6 +34,6 @@ "node-cache": "^5.1.2", "url-pattern": "1.0.3", "xml-js": "^1.6.11", - "youtubei.js": "^5.0.0" + "youtubei.js": "^5.1.0" } } diff --git a/src/cobalt.js b/src/cobalt.js index 118ddfb1..990f5d31 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -23,6 +23,7 @@ import { buildFront } from "./modules/build.js"; import { changelogHistory } from "./modules/pageRender/onDemand.js"; import { sha256 } from "./modules/sub/crypto.js"; import findRendered from "./modules/pageRender/findRendered.js"; +import { celebrationsEmoji } from "./modules/pageRender/elements.js"; if (process.env.selfURL && process.env.port) { const commitHash = shortCommit(); @@ -138,21 +139,29 @@ if (process.env.selfURL && process.env.port) { break; case 'onDemand': if (req.query.blockId) { - let blockId = req.query.blockId.slice(0, 3) + let blockId = req.query.blockId.slice(0, 3); let r, j; switch(blockId) { - case "0": + case "0": // changelog history r = changelogHistory(); j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" }) break; + case "1": // celebrations emoji + r = celebrationsEmoji(); + j = r ? apiJSON(3, { t: r }) : false + break; default: j = apiJSON(0, { t: "couldn't find a block with this id" }) break; } - res.status(j.status).json(j.body); + if (j.body) { + res.status(j.status).json(j.body) + } else { + res.status(204).end() + } } else { - let j = apiJSON(0, { t: "no block id" }) - res.status(j.status).json(j.body); + let j = apiJSON(0, { t: "no block id" }); + res.status(j.status).json(j.body) } break; default: diff --git a/src/config.json b/src/config.json index 5635c179..2871c3ea 100644 --- a/src/config.json +++ b/src/config.json @@ -27,6 +27,9 @@ "boosty": "https://boosty.to/wukko" } }, + "links": { + "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/6d4fe6e5bade4150b8759ce20720c7a3" + }, "celebrations": { "01-01": "🎄", "02-17": "😺", diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 11d41aa0..9f9c8a9c 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -7,8 +7,9 @@ --padding-1: 0.75rem; --line-height: 1.65rem; --red: rgb(255, 0, 61); - --color: rgb(107, 67, 139); - --gap: 0.6rem; + --gap: 0.5rem; + --gap-no-icon: 0.6rem; + --rainbow-gradient: linear-gradient(161deg,#ffe454,#ff6964,#fe85e5,#bd26fe,#587ae9,#8ded95); } @media (prefers-color-scheme: dark) { :root { @@ -19,7 +20,7 @@ --accent-unhover: rgb(100, 100, 100); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(0, 0, 0); - --checkmark: url(vectorIcons/checkmark_b.svg); + --glow-transparency: 0.45; } } @media (prefers-color-scheme: light) { @@ -31,7 +32,7 @@ --accent-unhover: rgb(190, 190, 190); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(255, 255, 255); - --checkmark: url(vectorIcons/checkmark.svg); + --glow-transparency: 0.6; } } [data-theme="dark"] { @@ -42,7 +43,7 @@ --accent-unhover: rgb(100, 100, 100); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(0, 0, 0); - --checkmark: url(vectorIcons/checkmark_b.svg); + --glow-transparency: 0.45; } [data-theme="light"] { --accent: rgb(25, 25, 25); @@ -52,7 +53,7 @@ --accent-unhover: rgb(190, 190, 190); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(255, 255, 255); - --checkmark: url(vectorIcons/checkmark.svg); + --glow-transparency: 0.6; } html, body { @@ -87,7 +88,7 @@ a { align-items: center; flex-direction: row; flex-wrap: nowrap; - padding: 0.55rem 1rem 0.55rem 0.7rem; + padding: calc(var(--gap) - 0.1rem) calc(var(--gap)*2 - 0.2rem) calc(var(--gap) - 0.1rem) var(--gap); width: auto; margin-right: var(--padding-1); margin-bottom: var(--padding-1); @@ -227,7 +228,7 @@ button:active, color: var(--accent); } #url-input-area { - background: var(--background); + background: none; padding: 0 1rem; width: 100%; color: var(--accent); @@ -349,7 +350,7 @@ button:active, } .changelog-subtitle { font-size: 1.1rem; - padding-bottom: 0.7rem; + padding-bottom: var(--gap-no-icon); } .changelog-banner { width: 100%; @@ -441,7 +442,7 @@ button:active, color: var(--accent-unhover-2); border-bottom: 0.05rem solid var(--accent-unhover-2); padding-bottom: 0.25rem; - margin-bottom: 1rem; + margin-bottom: calc(var(--gap-no-icon)*1.5); } .category-title { text-align: left; @@ -474,7 +475,7 @@ button:active, margin-top: 0.5rem; } .explanation { - margin-top: 1rem; + margin-top: 0.8rem; width: 100%; font-size: 0.8rem; text-align: left; @@ -485,7 +486,7 @@ button:active, color: var(--accent-unhover-2); } .switch { - padding: 0.7rem; + padding: var(--gap-no-icon); width: 100%; text-align: left; color: var(--accent); @@ -515,6 +516,13 @@ button:active, overflow-x: scroll; scrollbar-width: none; } +.switches .switch { + padding-left: calc(var(--gap-no-icon) + 0.1rem); + padding-right: calc(var(--gap-no-icon) + 0.1rem); +} +#popup-settings .switches .switch { + text-align: center; +} .autowidth { width: auto; } @@ -524,12 +532,12 @@ button:active, .text-to-copy { user-select: text; -webkit-user-select: text; - border: var(--border-15); + background: var(--accent-button-bg); padding: var(--padding-1); overflow: auto; } #close-button { - max-width: 2.8rem; + max-width: 2.6rem; margin-left: var(--padding-1); background: var(--background); border: var(--border-15); @@ -540,7 +548,7 @@ button:active, float: right; position: absolute; right: 0; - height: 2.8rem; + height: 2.6rem; } .popup-tab-content { display: none; @@ -552,7 +560,7 @@ button:active, width: 100%; } .popup-tabs { - margin-top: 0.8rem; + margin-top: 0.9rem; } .emoji { margin-right: 0.4rem; @@ -660,6 +668,19 @@ button:active, display: block; text-align: right; } +#about-donate-footer::before { + content: ""; + position: absolute; + height: 110%; + width: 32%; + background: var(--rainbow-gradient); + z-index: -2; + filter: blur(5px); + opacity: var(--glow-transparency); +} +#about-donate-footer:active::before { + opacity: 0; +} /* adapt the page according to screen size */ @media screen and (min-width: 2300px) { html { @@ -754,9 +775,14 @@ button:active, } } @media screen and (max-width: 320px) { + :root { + --gap: 0.38rem; + --gap-no-icon: 0.38rem; + --line-height: 1.2rem; + } #popup-title { - font-size: 1.3rem; - line-height: 2rem; + font-size: 1.07rem; + line-height: 1.5rem; } .footer-button, #audioMode-false, @@ -770,22 +796,61 @@ button:active, #paste .emoji { margin-right: 0; } - .switch, .checkbox, .category-title, .subtitle, #popup-desc { - font-size: .75rem; + .switch, + .checkbox, + .category-title, + .subtitle, + #popup-desc, + .collapse-title { + font-size: .7rem; + } + .collapse-header { + padding: 0.5rem; + } + #popup-above-title, + #url-input-area { + font-size: 0.6rem; } .explanation { - font-size: .77rem; - margin-top: 0.8rem; + font-size: .6rem; + margin-top: 0.5rem; + line-height: 1rem!important; } #popup-desc { - line-height: 1.4rem; + line-height: 1.2rem; + font-size: .64rem; } .changelog-subtitle, #popup-subtitle { - font-size: 0.9rem!important; + font-size: 0.8rem!important; } .category-title { margin-bottom: 0.8rem; } + .emoji { + height: 18px; + width: 18px; + } + .desc-padding { + padding-bottom: 0.8rem; + } + #logo { + font-size: 0.8rem; + } + .popup, + .popup.scrollable, + .popup.small { + height: 98%; + } + [type=checkbox] { + width: 15px; + height: 15px; + border: 0.12rem solid var(--accent); + } + [type=checkbox]:before { + transform: scaleY(.8)scaleX(.7)rotate(45deg); + left: 3.4px; + top: -2px; + } } @media screen and (max-width: 720px) { #cobalt-main-box #bottom { @@ -795,13 +860,17 @@ button:active, width: 100%; } #footer { - bottom: 4%; + bottom: 4.9%; transform: translate(-50%, 0%); } #footer-buttons { flex-direction: column; align-items: stretch; } + #about-donate-footer::before { + height: 50%; + width: 50%; + } .footer-pair .footer-button { width: 100%!important; } diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 8b8d9732..cd77b2d2 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,13 +1,11 @@ -let ua = navigator.userAgent.toLowerCase(); -let isIOS = ua.match("iphone os"); -let isMobile = ua.match("android") || ua.match("iphone os"); -let version = 26; -let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); -let notification = `
` +const ua = navigator.userAgent.toLowerCase(); +const isIOS = ua.match("iphone os"); +const isMobile = ua.match("android") || ua.match("iphone os"); +const version = 26; +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 = `
`; -let store = {} - -let switchers = { +const switchers = { "theme": ["auto", "light", "dark"], "vCodec": ["h264", "av1", "vp9"], "vQuality": ["1080", "max", "2160", "1440", "720", "480", "360"], @@ -15,11 +13,15 @@ let switchers = { "dubLang": ["original", "auto"], "vimeoDash": ["false", "true"], "audioMode": ["false", "true"] -} -let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"]; -let exceptions = { // used for mobile devices +}; +const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"]; +const exceptions = { // used for mobile devices "vQuality": "720" -} +}; + +const apiURL = ''; + +let store = {}; function eid(id) { return document.getElementById(id) @@ -333,88 +335,103 @@ async function download(url) { if (url.includes("youtube.com/") || url.includes("/youtu.be/")) req.vCodec = sGet("vCodec").slice(0, 4); if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req.isNoTTWatermark = true; } - await fetch('/api/json', { method: "POST", body: JSON.stringify(req), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(async (r) => { - let j = await r.json(); - if (j.status !== "error" && j.status !== "rate-limit") { - if (j.url || j.picker) { - 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, '?..') - fetch(`${j.audio}&p=1`).then(async (res) => { - let jp = await res.json(); - if (jp.status === "continue") { - changeDownloadButton(2, '>>>'); - popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType }); - setTimeout(() => { changeButton(1) }, 2500); - } else { - changeButton(0, jp.text); - } - }).catch((error) => internetError()); - } else if (j.picker) { + + 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.noURLReturned); + } + 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, '?..') + fetch(`${j.audio}&p=1`).then(async (res) => { + let jp = await res.json(); + if (jp.status === "continue") { changeDownloadButton(2, '>>>'); - popup('picker', 1, { arr: j.picker, type: j.pickerType }); + popup('picker', 1, { audio: j.audio, arr: j.picker, type: j.pickerType }); setTimeout(() => { changeButton(1) }, 2500); } else { - changeButton(0, loc.noURLReturned); + changeButton(0, jp.text); } - break; - case "stream": - changeDownloadButton(2, '?..') - fetch(`${j.url}&p=1`).then(async (res) => { - let jp = await res.json(); - if (jp.status === "continue") { - changeDownloadButton(2, '>>>'); window.location.href = j.url; - setTimeout(() => { changeButton(1) }, 2500); - } else { - changeButton(0, jp.text); - } - }).catch((error) => internetError()); - break; - case "success": - changeButton(2, j.text); - break; - default: - changeButton(0, loc.unknownStatus); - break; + }).catch((error) => internetError()); + } else if (j.picker) { + changeDownloadButton(2, '>>>'); + popup('picker', 1, { arr: j.picker, type: j.pickerType }); + setTimeout(() => { changeButton(1) }, 2500); + } else { + changeButton(0, loc.noURLReturned); } - } else { - if (j.status === "success") { - changeButton(2, j.text) - } else changeButton(0, loc.noURLReturned); - } - } else { - changeButton(0, j.text); + break; + case "stream": + changeDownloadButton(2, '?..') + fetch(`${j.url}&p=1`).then(async (res) => { + let jp = await res.json(); + if (jp.status === "continue") { + changeDownloadButton(2, '>>>'); window.location.href = j.url; + setTimeout(() => { changeButton(1) }, 2500); + } else { + changeButton(0, jp.text); + } + }).catch((error) => internetError()); + break; + case "success": + changeButton(2, j.text); + break; + default: + changeButton(0, loc.unknownStatus); + break; } - }).catch((error) => internetError()); + } else if (j && j.text) { + changeButton(0, j.text); + } +} +async function loadCelebrationsEmoji() { + let bac = eid("about-footer").innerHTML; + try { + let j = await fetch(`${apiURL}/api/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false }); + if (j && j.status === "success" && j.text) { + eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('🐲', j.text); + } + } catch (e) { + eid("about-footer").innerHTML = bac; + } } async function loadOnDemand(elementId, blockId) { + let j = {}; store.historyButton = eid(elementId).innerHTML; - let j = {} - eid(elementId).innerHTML = "..." + eid(elementId).innerHTML = "..."; + try { if (store.historyContent) { j = store.historyContent; } else { - await fetch(`/api/onDemand?blockId=${blockId}`).then(async (r) => { + await fetch(`${apiURL}/api/onDemand?blockId=${blockId}`).then(async(r) => { j = await r.json(); - if (j.status === "success") store.historyContent = j; - }) - } - if (j.status === "success" && j.status !== "rate-limit") { - if (j.text) { - eid(elementId).innerHTML = `${j.text}`; - } else { - throw new Error() - } - } else { - throw new Error() + if (j && j.status === "success") { + store.historyContent = j; + } else throw new Error(); + }).catch(() => { throw new Error() }); } + if (j.text) { + eid(elementId).innerHTML = `${j.text}`; + } else throw new Error() } catch (e) { eid(elementId).innerHTML = store.historyButton; internetError() @@ -431,6 +448,7 @@ window.onload = () => { eid("footer").style.visibility = 'visible'; eid("url-input-area").value = ""; notificationCheck(); + loadCelebrationsEmoji(); if (isIOS) sSet("downloadPopup", "true"); let urlQuery = new URLSearchParams(window.location.search).get("u"); if (urlQuery !== null && regex.test(urlQuery)) { diff --git a/src/front/emoji/money_bag.svg b/src/front/emoji/money_bag.svg deleted file mode 100644 index 561ee98a..00000000 --- a/src/front/emoji/money_bag.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/front/emoji/sparkling_heart.svg b/src/front/emoji/sparkling_heart.svg new file mode 100644 index 00000000..b5dd6eb2 --- /dev/null +++ b/src/front/emoji/sparkling_heart.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index a0c3febb..4f052578 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -47,7 +47,7 @@ "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", "LinkGitHubChanges": ">> see previous commits and contribute on github", "NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", - "DownloadPopupDescriptionIOS": "press and hold the download button, hide the video preview, and then select \"download linked file\" to save.", + "DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add this siri shortcut.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request popup on top, and press \"always allow\".\n\nalternative method: press and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", "DownloadPopupWayToSave": "pick a way to save", "ClickToCopy": "press to copy", @@ -94,7 +94,7 @@ "ChangelogPressToHide": "collapse", "Donate": "donate", "DonateSub": "help me keep it up", - "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's completely free to use. but turns out developing and keeping up a web service used by over 80 thousand people is not that easy.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! every cent helps and is VERY appreciated :D", + "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's completely free to use. but turns out developing and keeping up a web service used by over 150,000 people is not that easy.\n\nif you ever found {appName} useful and want to help continue its development and support, or simply want to thank the developer, consider chipping in! every cent helps and is VERY appreciated :D\n\ncurrently, i have big (scaling) plans, and i need your help. {appName}'s usage is growing daily, so i need to make up for it. donations are more appreciated than ever.\n\ni am yet to earn anything from {appName}, everything goes back to users, so you're essentially helping everyone.", "DonateVia": "donate via", "DonateHireMe": "...or you can hire me :)", "SettingsVideoMute": "mute audio", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index d971c261..7f59f356 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -47,7 +47,7 @@ "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "LinkGitHubChanges": ">> смотри предыдущие изменения на github", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", - "DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.", + "DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь этот сценарий siri.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод: зажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", "DownloadPopupWayToSave": "выбери, как сохранить", "ClickToCopy": "нажми, чтобы скопировать", @@ -94,7 +94,7 @@ "ChangelogPressToHide": "скрыть", "Donate": "задонатить", "DonateSub": "ты можешь помочь!", - "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что разработка и поддержка сервиса, которым пользуются более 80 тысяч людей, обходится довольно трудно.\n\nесли {appName} тебе помог и ты хочешь поблагодарить разработчика, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)", + "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что разработка и поддержка сервиса, которым пользуются более 150 тысяч людей, обходится довольно затратно.\n\nесли {appName} тебе помог и ты хочешь поблагодарить разработчика, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)", "DonateVia": "открыть", "DonateHireMe": "...или же ты можешь пригласить меня на работу :)", "SettingsVideoMute": "убрать аудио", diff --git a/src/localization/manager.js b/src/localization/manager.js index 65c048c8..2f5a334a 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -1,8 +1,8 @@ import * as fs from "fs"; -import { appName, repo } from "../modules/config.js"; +import { appName, links, repo } from "../modules/config.js"; import loadJson from "../modules/sub/loadJSON.js"; -const locPath = './src/localization/languages' +const locPath = './src/localization/languages'; let loc = {} let languages = []; @@ -19,7 +19,7 @@ export function loadLoc() { loadLoc(); export function replaceBase(s) { - return s.replace(/\n/g, '
').replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "•"); + return s.replace(/\n/g, '
').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "•"); } export function replaceAll(lang, str, string, replacement) { let s = replaceBase(str[string]) diff --git a/src/modules/config.js b/src/modules/config.js index 64dfca7d..00b95667 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -16,4 +16,5 @@ export const donations = config.donations, ffmpegArgs = config.ffmpegArgs, supportedAudio = config.supportedAudio, - celebrations = config.celebrations + celebrations = config.celebrations, + links = config.links diff --git a/src/modules/emoji.js b/src/modules/emoji.js index 59214288..284404d4 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -1,7 +1,6 @@ const names = { "🎶": "musical_notes", "🎬": "clapper_board", - "💰": "money_bag", "🎉": "party_popper", "❓": "question_mark", "✨": "sparkles", @@ -23,7 +22,8 @@ const names = { "🐦": "bird", "🐙": "octopus", "🔮": "crystal_ball", - "💪": "biceps" + "💪": "biceps", + "💖": "sparkling_heart" } let sizing = { 22: 0.4, diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index dc016124..cdf155ed 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -1,4 +1,5 @@ import { celebrations } from "../config.js"; +import emoji from "../emoji.js"; export function switcher(obj) { let items = ``; @@ -11,7 +12,7 @@ export function switcher(obj) { } } - if (obj.noParent) return `
${items}
`; + if (obj.noParent) return `
${items}
`; return `
${obj.subtitle ? `
${obj.subtitle}
` : ``}
${items}
@@ -130,8 +131,8 @@ export function popupWithBottomButtons(obj) { export function backdropLink(link, text) { return `${text}` } -export function socialLink(emoji, name, handle, url) { - return `` +export function socialLink(emji, name, handle, url) { + return `` } export function settingsCategory(obj) { return `
@@ -151,12 +152,20 @@ export function footerButtons(obj) { items += ``; break; case "popup": - let context = obj[i]["context"] ? `, '${obj[i]["context"]}'` : '' - let context2 = obj[i+1] && obj[i+1]["context"] ? `, '${obj[i+1]["context"]}'` : '' + let buttonName = obj[i]["context"] ? `${obj[i]["name"]}-${obj[i]["context"]}` : obj[i]["name"], + context = obj[i]["context"] ? `, '${obj[i]["context"]}'` : '', + buttonName2, + context2; + + if (obj[i+1]) { + buttonName2 = obj[i+1]["context"] ? `${obj[i+1]["name"]}-${obj[i+1]["context"]}` : obj[i+1]["name"]; + context2 = obj[i+1]["context"] ? `, '${obj[i+1]["context"]}'` : ''; + } + items += ` `; i++; break; @@ -169,7 +178,12 @@ export function explanation(text) { return `
${text}
` } export function celebrationsEmoji() { - let n = new Date().toISOString().split('T')[0].split('-'); - let dm = `${n[1]}-${n[2]}`; - return Object.keys(celebrations).includes(dm) ? celebrations[dm] : "🐲"; + try { + let n = new Date().toISOString().split('T')[0].split('-'); + let dm = `${n[1]}-${n[2]}`; + let f = Object.keys(celebrations).includes(dm) ? celebrations[dm] : "🐲"; + return f != "🐲" ? emoji(f, 22) : false; + } catch (e) { + return false + } } diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index e2df0af7..d506890a 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -1,4 +1,4 @@ -import { backdropLink, celebrationsEmoji, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js"; +import { backdropLink, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js"; import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js"; import { getCommitInfo } from "../sub/currentCommit.js"; import loc from "../../localization/manager.js"; @@ -149,7 +149,7 @@ export default function(obj) { }) }, { name: "donate", - title: `${emoji("💰")} ${t('DonationsTab')}`, + title: `${emoji("💖")} ${t('DonationsTab')}`, content: popup({ name: "donate", header: { @@ -392,13 +392,13 @@ export default function(obj) { footerButtons([{ name: "about", type: "popup", - text: `${emoji(celebrationsEmoji() , 22)} ${t('AboutTab')}`, + text: `${emoji("🐲" , 22)} ${t('AboutTab')}`, aria: t('AccessibilityOpenAbout') }, { name: "about", type: "popup", context: "donate", - text: `${emoji("💰", 22)} ${t('Donate')}`, + text: `${emoji("💖", 22)} ${t('Donate')}`, aria: t('AccessibilityOpenDonate') }, { name: "settings", diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index 0c9434d9..f285f56c 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -36,8 +36,7 @@ async function findClientID() { export default async function(obj) { let html; if (!obj.author && !obj.song && obj.shortLink) { - html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false }); - if (!html) html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false }) + html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false }); } if (obj.author && obj.song) { html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false }); diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index 4be85342..dc1ad39d 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -44,7 +44,7 @@ export default async function(o) { if (Number(bestQuality._attributes.id) > Number(quality)) bestQuality = repr[quality]; url = js.player.params[0][`url${resolutionMatch[bestQuality._attributes[resolutionPick]]}`]; - filename = `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4` + filename += `${bestQuality._attributes.width}x${bestQuality._attributes.height}.mp4` } else if (js.player.params[0]["url240"]) { // fallback for when video is too old url = js.player.params[0]["url240"]; diff --git a/src/modules/setup.js b/src/modules/setup.js index 4fca6c53..2740a770 100644 --- a/src/modules/setup.js +++ b/src/modules/setup.js @@ -1,6 +1,6 @@ import { existsSync, unlinkSync, appendFileSync } from "fs"; import { createInterface } from "readline"; -import { Cyan, Bright, Green } from "./sub/consoleText.js"; +import { Cyan, Bright } from "./sub/consoleText.js"; import { execSync } from "child_process"; let envPath = './.env'; @@ -42,7 +42,7 @@ rl.question(q, r1 => { if (r2) ob['port'] = r2 if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/` - console.log(Bright("\nWould you like to enable CORS? It allows other websites and extensions to use your instance's API.\n y/n (n)")) + console.log(Bright("\nWould you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)")) rl.question(q, r3 => { if (r3.toLowerCase() !== 'y') ob['cors'] = '0' diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 3aa259f6..e682fcd7 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -2,7 +2,7 @@ import { spawn } from "child_process"; import ffmpeg from "ffmpeg-static"; import got from "got"; import { ffmpegArgs, genericUserAgent } from "../config.js"; -import { metadataManager, msToTime } from "../sub/utils.js"; +import { getThreads, metadataManager, msToTime } from "../sub/utils.js"; export function streamDefault(streamInfo, res) { try { @@ -35,9 +35,9 @@ export function streamLiveRender(streamInfo, res) { return; } let audio = got.get(streamInfo.urls[1], { isStream: true }); - let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ '-loglevel', '-8', + '-threads', `${getThreads()}`, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', @@ -95,6 +95,7 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', + '-threads', `${getThreads()}`, '-i', streamInfo.urls ] if (streamInfo.metadata) { @@ -141,6 +142,7 @@ export function streamVideoOnly(streamInfo, res) { try { let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ '-loglevel', '-8', + '-threads', `${getThreads()}`, '-i', streamInfo.urls, '-c', 'copy' ] diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 4bcfc6d9..345a7540 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -142,3 +142,15 @@ export function checkJSONPost(obj) { export function getIP(req) { return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip; } +export function getThreads() { + try { + if (process.env.ffmpegThreads && process.env.ffmpegThreads.length <= 3 + && (Number(process.env.ffmpegThreads) >= 0 && Number(process.env.ffmpegThreads) <= 256)) { + return process.env.ffmpegThreads + } else { + return '0' + } + } catch (e) { + return '0' + } +} diff --git a/src/test/tests.json b/src/test/tests.json index 57fbefdd..5e11f3b1 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -36,8 +36,8 @@ "status": "redirect" } }, { - "name": "picker: mixed media (3 gifs + image)", - "url": "https://twitter.com/emerald_pedrod/status/1582418163521581063?s=20", + "name": "picker: mixed media (2 videos)", + "url": "https://twitter.com/taehyungsflow/status/1583411488433516544", "params": { "aFormat": "mp3", "isAudioOnly": false, @@ -280,6 +280,22 @@ "code": 200, "status": "stream" } + }, { + "name": "on.soundcloud link", + "url": "https://on.soundcloud.com/wLZre", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "on.soundcloud link, different stream type", + "url": "https://on.soundcloud.com/AG4c", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } }], "youtube": [{ "name": "4k video (h264, 1440)", @@ -617,7 +633,7 @@ } }, { "name": "images", - "url": "https://vt.tiktok.com/ZS8JP89eB/", + "url": "https://www.tiktok.com/@matryoshk4/video/7231234675476532526", "params": {}, "expected": { "code": 200, @@ -804,6 +820,14 @@ "code": 200, "status": "redirect" } + }, { + "name": "regular video", + "url": "https://www.instagram.com/p/CmCVWoIr9OH/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } }, { "name": "reel (isAudioOnly)", "url": "https://www.instagram.com/reel/CoEBV3eM4QR/",