diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..d70b182 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,55 @@ +name: Build Docker image + +on: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get version from package.json + id: package-version + uses: martinbeentjes/npm-get-version-action@v1.3.1 + - name: Get short commit hash + id: commit-hash + run: echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + tags: | + type=raw,value=latest + type=raw,value=${{ steps.package-version.outputs.current-version }} + type=raw,value=${{ steps.package-version.outputs.current-version }}-${{ steps.commit-hash.outputs.commit_short }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/README.md b/README.md index e37752e..be0429d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # cobalt Best way to save what you love. -Live web app: [co.wukko.me](https://co.wukko.me/) +Live web app: [cobalt.tools](https://cobalt.tools/) ![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") @@ -20,10 +20,12 @@ Paste the link, get the video, move on. It's that simple. Just how it should be. | Instagram Reels | ✅ | ✅ | ✅ | | | Pinterest | ✅ | ✅ | ✅ | Support for videos and stories. | | Reddit | ✅ | ✅ | ✅ | Support for GIFs and videos. | +| Rutube | ✅ | ✅ | ✅ | | | SoundCloud | ➖ | ✅ | ➖ | Audio metadata, downloads from private links. | | Streamable | ✅ | ✅ | ✅ | | | TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. | | Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. | +| Twitch Clips | ✅ | ✅ | ✅ | | | Twitter/X * | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. | | Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. | | Vine Archive | ✅ | ✅ | ✅ | | diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 5900b32..a74a89a 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -2,10 +2,12 @@ version: '3.5' services: cobalt-api: - build: . + image: ghcr.io/wukko/cobalt:latest restart: unless-stopped container_name: cobalt-api + init: true + # if container doesn't run detached on your machine, uncomment the next line: #tty: true @@ -21,14 +23,22 @@ services: # replace apiName with your instance's distinctive name - apiName=eu-nl # if you want to use cookies when fetching data from services, uncomment the next line - #- cookiePath=cookies.json + #- cookiePath=/cookies.json # see src/modules/processing/cookie/cookies_example.json for example file. + labels: + - com.centurylinklabs.watchtower.scope=cobalt + + # if you want to use cookies when fetching data from services, uncomment volumes and next line + #volumes: + #- ./cookies.json:/cookies.json cobalt-web: - build: . + image: ghcr.io/wukko/cobalt:latest restart: unless-stopped container_name: cobalt-web + init: true + # if container doesn't run detached on your machine, uncomment the next line: #tty: true @@ -40,6 +50,17 @@ services: environment: - webPort=9001 # replace webURL with your instance's target url in same format - - webURL=https://co.wukko.me/ + - webURL=https://cobalt.tools/ # replace apiURL with preferred api instance url - apiURL=https://co.wuk.sh/ + + labels: + - com.centurylinklabs.watchtower.scope=cobalt + + # update the cobalt image automatically with watchtower + watchtower: + image: ghcr.io/containrrr/watchtower + restart: unless-stopped + command: --cleanup --scope cobalt --interval 900 + volumes: + - /var/run/docker.sock:/var/run/docker.sock \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 7c2a5b3..9957d51 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4,8 +4,7 @@ This document provides info about methods and acceptable variables for all cobal ``` ⚠️ Main API instance has moved to https://co.wuk.sh/ -Previous API domain will stop redirecting users to correct API instance after July 25th. -Make sure to update your projects in time. +Make sure your projects use the correct API domain. ``` ## POST: ``/api/json`` @@ -15,17 +14,18 @@ Request Body Type: ``application/json``
Response Body Type: ``application/json`` ### Request Body Variables -| key | type | variables | default | description | -|:----------------|:--------|:----------------------------------|:----------|:-------------------------------------------------------------------------------| -| url | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | -| vCodec | string | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. | -| vQuality | string | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. | -| aFormat | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | -| isAudioOnly | boolean | ``true / false`` | ``false`` | | -| isNoTTWatermark | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok & Douyin videos have watermarks. | -| isTTFullAudio | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | -| isAudioMuted | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. | -| dubLang | boolean | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. | +| key | type | variables | default | description | +|:--------------------|:--------|:----------------------------------|:----------|:-------------------------------------------------------------------------------| +| ``url`` | string | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. | +| ``vCodec`` | string | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. | +| ``vQuality`` | string | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. | +| ``aFormat`` | string | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | | +| ``isAudioOnly`` | boolean | ``true / false`` | ``false`` | | +| ``isNoTTWatermark`` | boolean | ``true / false`` | ``false`` | Changes whether downloaded TikTok videos have watermarks. | +| ``isTTFullAudio`` | boolean | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. | +| ``isAudioMuted`` | boolean | ``true / false`` | ``false`` | Disables audio track in video downloads. | +| ``dubLang`` | boolean | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. | +| ``disableMetadata`` | boolean | ``true / false`` | ``false`` | Disables file metadata when set to ``true``. | ### Response Body Variables | key | type | variables | diff --git a/package.json b/package.json index 4c075ad..fb1abc1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.1.3", + "version": "7.5", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", @@ -30,6 +30,7 @@ "express": "^4.18.1", "express-rate-limit": "^6.3.0", "ffmpeg-static": "^5.1.0", + "hls-parser": "^0.10.7", "nanoid": "^4.0.2", "node-cache": "^5.1.2", "set-cookie-parser": "2.6.0", diff --git a/src/cobalt.js b/src/cobalt.js index d03bca2..949cccb 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -9,9 +9,6 @@ import { loadLoc } from "./localization/manager.js"; import path from 'path'; import { fileURLToPath } from 'url'; -import { runWeb } from "./core/web.js"; -import { runAPI } from "./core/api.js"; - const app = express(); const gitCommit = shortCommit(); @@ -28,8 +25,10 @@ const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webU const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port)); if (apiMode) { + const { runAPI } = await import('./core/api.js'); runAPI(express, app, gitCommit, gitBranch, __dirname) } else if (webMode) { + const { runWeb } = await import('./core/web.js'); await runWeb(express, app, gitCommit, gitBranch, __dirname) } else { console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`)) diff --git a/src/config.json b/src/config.json index 6337654..10e3286 100644 --- a/src/config.json +++ b/src/config.json @@ -1,23 +1,28 @@ { "streamLifespan": 20000, - "maxVideoDuration": 10800000, + "maxVideoDuration": 18000000, "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "authorInfo": { "name": "wukko", "link": "https://wukko.me/", "contact": "https://wukko.me/contacts", "support": { - "twitter": { - "url": "https://twitter.com/justusecobalt", - "handle": "@justusecobalt" - }, - "mastodon": { - "url": "https://wetdry.world/@cobalt", - "handle": "@cobalt@wetdry.world" - }, - "discord": { - "url": "https://discord.gg/pQPt8HBUPu", - "handle": "cobalt community server" + "default": { + "twitter": { + "emoji": "🐦", + "url": "https://twitter.com/justusecobalt", + "handle": "@justusecobalt" + }, + "mastodon": { + "emoji": "🐘", + "url": "https://wetdry.world/@cobalt", + "handle": "@cobalt@wetdry.world" + }, + "discord": { + "emoji": "👾", + "url": "https://discord.gg/pQPt8HBUPu", + "handle": "cobalt community server" + } } } }, @@ -40,6 +45,7 @@ "02-17": "😺", "02-22": "😺", "03-01": "😺", + "03-08": "💪", "05-26": "🎂", "08-08": "😺", "08-26": "🐶", @@ -59,8 +65,7 @@ "12-28": "🎄", "12-29": "🎄", "12-30": "🎄", - "12-31": "🎄", - "03-08": "💪" + "12-31": "🎄" }, "supportedAudio": ["mp3", "ogg", "wav", "opus"], "ffmpegArgs": { diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 8e666dd..e0e5322 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -175,6 +175,24 @@ input[type="text"], backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px); } +.glass-bkg.alone { + z-index: -1; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; +} +.glass-bkg.small { + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: -1; + position: absolute; + border: var(--accent-highlight) solid 0.15rem; + border-radius: 8px/9px; +} .desktop button:hover, .desktop .switch:hover, .desktop .checkbox:hover, @@ -198,7 +216,7 @@ button:active, .popup.small .switch { background: var(--accent-button-elevated); } -.popup.small .switch:hover { +.desktop .popup.small .switch:hover { background: var(--accent-hover-elevated); } .switch.text-backdrop, @@ -267,7 +285,6 @@ button:active, } .box { background: var(--background); - border: var(--glass) solid .2rem; color: var(--accent); } #url-input-area { @@ -375,7 +392,8 @@ button:active, max-height: 95%; opacity: 0; transform: translate(-50%,-48%)scale(.95); - box-shadow: 0 0 20px 0 var(--accent-hover-transparent); + box-shadow: 0 0 0 0.2rem var(--glass) inset, + 0 0 20px 0 var(--accent-hover-transparent); } .popup.visible { visibility: visible; @@ -404,7 +422,6 @@ button:active, .popup.small { width: 20%; box-shadow: 0px 0px 60px 0px var(--accent-hover); - border: var(--accent-highlight) solid 0.15rem; padding: 1.7rem; transform: translate(-50%,-50%)scale(.95); pointer-events: all; @@ -462,6 +479,7 @@ button:active, align-items: center; gap: 0.7rem; padding-bottom: 0.7rem; + flex-wrap: wrap; } .changelog-tag-version { font-size: 1rem; @@ -478,14 +496,14 @@ button:active, padding-top: 0!important; } .desc-padding { - padding-bottom: 1.5rem; + padding-bottom: 0.7rem; } #popup-subtitle { font-size: 1.1rem; padding-bottom: var(--padding-1); } #popup-desc, -#desc-error, +.desc-error, #popup-info-desc { width: 100%; text-align: left; @@ -494,6 +512,9 @@ button:active, user-select: text; -webkit-user-select: text; } +.desc-error { + padding-bottom: 1.5rem; +} #popup-title { font-size: 1.5rem; line-height: 1.2em; @@ -515,12 +536,12 @@ button:active, .popup-content-inner, .tab-content-settings, #picker-holder { - padding-top: calc(env(safe-area-inset-top)/2 + 4.9rem); + padding-top: calc(env(safe-area-inset-top)/2 + 4.7rem); padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); } .tab-content-settings, #tab-about-about .popup-content-inner { - padding-top: calc(env(safe-area-inset-top)/2 + 6.2rem);; + padding-top: calc(env(safe-area-inset-top)/2 + 6rem);; } .bullpadding { padding-left: 0.58rem; @@ -530,10 +551,9 @@ button:active, z-index: 999; padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem); width: 100%; - border-bottom: var(--accent-highlight) solid 0.1rem; } .settings-category { - padding-bottom: 1rem; + padding-bottom: 0.7rem; } .separator { float: left; @@ -584,6 +604,10 @@ button:active, line-height: 1.3rem!important; color: var(--accent-subtext); } +.explanation.embedded { + margin-top: 0.825rem; + margin-bottom: 0.825rem; +} .subtext { color: var(--accent-subtext); } @@ -629,7 +653,6 @@ button:active, width: auto; flex-direction: row; flex-wrap: nowrap; - overflow-x: scroll; scrollbar-width: none; } .switches .switch { @@ -672,7 +695,6 @@ button:active, width: 100%; padding-top: 0.2rem; padding-bottom: 1.7rem; - border-top: var(--accent-highlight) solid 0.1rem; } .popup-tabs-child { width: 100%; @@ -797,12 +819,16 @@ button:active, width: 100%; text-align: center; position: absolute; - cursor: pointer; display: flex; justify-content: center; align-items: center; padding-top: calc(env(safe-area-inset-top) + 1rem); } +.urgent-text { + display: flex; + align-items: center; + cursor: pointer; +} .no-transparency .glass-bkg, .no-transparency #popup-backdrop { backdrop-filter: none; @@ -815,23 +841,6 @@ button:active, .no-animation #popup-backdrop { transition: none; } -#floating-notification-area { - visibility: visible; - z-index: 999999; - position: absolute; - display: flex; - justify-content: center; - width: 100%; - padding-top: 2rem; -} -.floating-notification { - text-align: center; - padding: 0.6rem 1.2rem; - background: var(--accent-hover-elevated); - display: flex; - box-shadow: 0 0 20px 10px var(--accent-hover); - font-size: 0.85rem; -} .popup-from-bottom { position: fixed; width: 100%; @@ -903,13 +912,17 @@ button:active, .scrollable #popup-content { border-radius: 8px / 9px; } -#popup-header { - border-top-left-radius: 5px; - border-top-right-radius: 5px; +#popup-header .glass-bkg { + border-top-left-radius: 8px 9px; + border-top-right-radius: 8px 9px; + border-bottom: var(--accent-highlight) solid 0.1rem; + top: -1px; } -#popup-tabs { - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; +#popup-tabs .glass-bkg { + border-bottom-left-radius: 8px 9px; + border-bottom-right-radius: 8px 9px; + border-top: var(--accent-highlight) solid 0.1rem; + bottom: -1px; } .switches .first { border-top-left-radius: 5px 6px; @@ -1005,87 +1018,6 @@ button:active, width: calc(100% - 1.3rem); } } -@media screen and (max-width: 320px) { - :root { - --gap: 0.38rem; - --gap-no-icon: 0.38rem; - --line-height: 1.2rem; - } - #popup-title { - font-size: 1.07rem; - line-height: 1.5rem; - } - .checkbox { - width: calc(100% - 1rem); - } - .footer-button, - #audioMode-false, - #audioMode-true, - #paste { - font-size: 0!important; - } - .footer-button .emoji, - #audioMode-false .emoji, - #audioMode-true .emoji, - #paste .emoji { - margin-right: 0; - } - .switch, - .checkbox, - .category-title, - .subtitle, - #popup-desc, - .collapse-title { - font-size: 0.7rem; - } - .collapse-header { - padding: 0.5rem; - } - #popup-above-title, - #url-input-area { - font-size: 0.6rem; - } - .explanation { - font-size: 0.6rem; - margin-top: 0.5rem; - line-height: 1rem!important; - } - #popup-desc { - line-height: 1.2rem; - font-size: 0.64rem; - } - .changelog-subtitle, #popup-subtitle { - 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 { width: calc(100% - (0.7rem * 2)); @@ -1124,10 +1056,20 @@ button:active, padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem); } .popup, - #popup-header, - #popup-tabs { + #popup-header .glass-bkg, + #popup-tabs .glass-bkg, + .glass-bkg.small { border-radius: 0; } + #popup-tabs .glass-bkg { + bottom: 0; + } + .switches { + overflow-x: scroll; + } + .checkbox { + margin-right: 0; + } .popup.center { top: unset; left: unset; @@ -1141,11 +1083,13 @@ button:active, left: 0; transform: none; position: absolute; - border: none; - border-top: var(--accent-highlight) solid 0.15rem; padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.7rem); transform: translateY(30rem); } + .glass-bkg.small { + border: none; + border-top: var(--accent-highlight) solid 0.15rem; + } .popup.small.visible { transform: none; transition: transform 200ms cubic-bezier(0.075, 0.82, 0.165, 1), opacity 130ms ease-in-out; @@ -1173,6 +1117,7 @@ button:active, width: 100%; height: 100%; max-height: 100%; + box-shadow: none; } #popup-tabs { padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); diff --git a/src/front/cobalt.js b/src/front/cobalt.js index ca30951..b4df9a4 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,3 +1,5 @@ +const version = 37; + const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.match("iphone os"); const isMobile = ua.match("android") || ua.match("iphone os"); @@ -5,7 +7,6 @@ const isSafari = ua.match("safari/"); const isFirefox = ua.match("firefox/"); const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103; -const version = 34; 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 = `
`; @@ -18,12 +19,24 @@ const switchers = { "vimeoDash": ["false", "true"], "audioMode": ["false", "true"] }; -const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio", "reduceTransparency", "disableAnimations"]; +const checkboxes = [ + "alwaysVisibleButton", + "disableChangelog", + "downloadPopup", + "disableTikTokWatermark", + "fullTikTokAudio", + "muteAudio", + "reduceTransparency", + "disableAnimations", + "disableMetadata", +]; const exceptions = { // used for mobile devices "vQuality": "720" }; const bottomPopups = ["error", "download"] +const pageQuery = new URLSearchParams(window.location.search); + let store = {}; function changeAPI(url) { @@ -126,6 +139,7 @@ function changeTab(evnt, tabId, tabClass) { evnt.currentTarget.dataset.enabled = "true"; eid(tabId).dataset.enabled = "true"; + eid(tabId).parentElement.scrollTop = 0; if (tabId === "tab-about-changelog" && sGet("changelogStatus") !== `${version}`) notificationCheck("changelog"); if (tabId === "tab-about-about" && !sGet("seenAbout")) notificationCheck("about"); @@ -199,8 +213,8 @@ function popup(type, action, text) { case "picker": switch (text.type) { case "images": - eid("picker-title").innerHTML = loc.pickerImages; - eid("picker-subtitle").innerHTML = loc.pickerImagesExpl; + eid("picker-title").innerHTML = loc.ImagePickerTitle; + eid("picker-subtitle").innerHTML = isMobile ? loc.ImagePickerExplanationPhone : loc.ImagePickerExplanationPC; eid("picker-holder").classList.remove("various"); @@ -217,8 +231,8 @@ function popup(type, action, text) { } break; default: - eid("picker-title").innerHTML = loc.pickerDefault; - eid("picker-subtitle").innerHTML = loc.pickerDefaultExpl; + eid("picker-title").innerHTML = loc.MediaPickerTitle; + eid("picker-subtitle").innerHTML = isMobile ? loc.MediaPickerExplanationPhone : loc.MediaPickerExplanationPC; eid("picker-holder").classList.add("various"); @@ -288,10 +302,10 @@ function loadSettings() { eid("downloadPopup").checked = true; } if (sGet("reduceTransparency") === "true" || isOldFirefox) { - eid("cobalt-body").classList.toggle('no-transparency'); + eid("cobalt-body").classList.add('no-transparency'); } if (sGet("disableAnimations") === "true") { - eid("cobalt-body").classList.toggle('no-animation'); + eid("cobalt-body").classList.add('no-animation'); } for (let i = 0; i < checkboxes.length; i++) { if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true; @@ -326,7 +340,7 @@ function internetError() { eid("url-input-area").disabled = false changeDownloadButton(2, '!!'); setTimeout(() => { changeButton(1); }, 2500); - popup("error", 1, loc.noInternet); + popup("error", 1, loc.ErrorNoInternet); } function resetSettings() { localStorage.clear(); @@ -340,13 +354,13 @@ async function pasteClipboard() { download(eid("url-input-area").value); } } catch (e) { - let errorMessage = loc.featureErrorGeneric; + let errorMessage = loc.FeatureErrorGeneric; let doError = true; let error = String(e).toLowerCase(); - if (error.includes("denied")) errorMessage = loc.clipboardErrorNoPermission; + if (error.includes("denied")) errorMessage = loc.ClipboardErrorNoPermission; if (error.includes("dismissed") || isIOS) doError = false; - if (error.includes("function") && isFirefox) errorMessage = loc.clipboardErrorFirefox; + if (error.includes("function") && isFirefox) errorMessage = loc.ClipboardErrorFirefox; if (doError) popup("error", 1, errorMessage); } @@ -377,6 +391,8 @@ async function download(url) { if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") req.isNoTTWatermark = true; } + if (sGet("disableMetadata") === "true") req.disableMetadata = true; + let j = await fetch(`${apiURL}/api/json`, { method: "POST", body: JSON.stringify(req), @@ -391,7 +407,7 @@ async function download(url) { if (j.text && (!j.url || !j.picker)) { if (j.status === "success") { changeButton(2, j.text) - } else changeButton(0, loc.noURLReturned); + } else changeButton(0, loc.ErrorNoUrlReturned); } switch (j.status) { case "redirect": @@ -409,7 +425,7 @@ async function download(url) { popup('picker', 1, { arr: j.picker, type: j.pickerType }); setTimeout(() => { changeButton(1) }, 2500); } else { - changeButton(0, loc.noURLReturned); + changeButton(0, loc.ErrorNoUrlReturned); } break; case "stream": @@ -431,7 +447,7 @@ async function download(url) { changeButton(2, j.text); break; default: - changeButton(0, loc.unknownStatus); + changeButton(0, loc.ErrorUnknownStatus); break; } } else if (j && j.text) { @@ -466,7 +482,7 @@ async function loadOnDemand(elementId, blockId) { }).catch(() => { throw new Error() }); } if (j.text) { - eid(elementId).innerHTML = `${j.text}`; + eid(elementId).innerHTML = `${j.text}`; } else throw new Error() } catch (e) { eid(elementId).innerHTML = store.historyButton; @@ -476,26 +492,68 @@ async function loadOnDemand(elementId, blockId) { function restoreUpdateHistory() { eid("changelog-history").innerHTML = store.historyButton; } +function unpackSettings(b64) { + let changed = null; + try { + let settingsToImport = JSON.parse(atob(b64)); + let currentSettings = JSON.parse(JSON.stringify(localStorage)); + for (let s in settingsToImport) { + if (checkboxes.includes(s) && (settingsToImport[s] === "true" || settingsToImport[s] === "false") + && currentSettings[s] !== settingsToImport[s]) { + sSet(s, settingsToImport[s]); + changed = true + } + if (switchers[s] && switchers[s].includes(settingsToImport[s]) + && currentSettings[s] !== settingsToImport[s]) { + sSet(s, settingsToImport[s]); + changed = true + } + } + } catch (e) { + changed = false; + } + return changed +} window.onload = () => { + loadCelebrationsEmoji(); + loadSettings(); detectColorScheme(); + changeDownloadButton(0, '>>'); - notificationCheck(); - loadCelebrationsEmoji(); + eid("url-input-area").value = ""; + if (isIOS) { sSet("downloadPopup", "true"); eid("downloadPopup-chkbx").style.display = "none"; } - eid("url-input-area").value = ""; eid("home").style.visibility = 'visible'; eid("home").classList.toggle("visible"); - let urlQuery = new URLSearchParams(window.location.search).get("u"); - if (urlQuery !== null && regex.test(urlQuery)) { - eid("url-input-area").value = urlQuery; - button(); + if (pageQuery.has("u") && regex.test(pageQuery.get("u"))) { + eid("url-input-area").value = pageQuery.get("u"); + button() } + if (pageQuery.has("migration")) { + if (pageQuery.has("settingsData") && !sGet("migrated")) { + let setUn = unpackSettings(pageQuery.get("settingsData")); + if (setUn !== null) { + if (setUn) { + sSet("migrated", "true") + eid("desc-migration").innerHTML += `

${loc.DataTransferSuccess}` + } else { + eid("desc-migration").innerHTML += `

${loc.DataTransferError}` + } + } + } + loadSettings(); + detectColorScheme(); + popup("migration", 1); + } + window.history.replaceState(null, '', window.location.pathname); + + notificationCheck(); } eid("url-input-area").addEventListener("keydown", (e) => { button(); diff --git a/src/front/emoji/3d/cat_grin.svg b/src/front/emoji/3d/cat_grin.svg new file mode 100644 index 0000000..be6e29d --- /dev/null +++ b/src/front/emoji/3d/cat_grin.svg @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/emoji/cat_grin.svg b/src/front/emoji/cat_grin.svg new file mode 100644 index 0000000..4b7cbb0 --- /dev/null +++ b/src/front/emoji/cat_grin.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/emoji/newspaper.svg b/src/front/emoji/newspaper.svg new file mode 100644 index 0000000..ebe0b5f --- /dev/null +++ b/src/front/emoji/newspaper.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/front/updateBanners/meowthsnap.webp b/src/front/updateBanners/meowthsnap.webp new file mode 100644 index 0000000..a1b94d5 Binary files /dev/null and b/src/front/updateBanners/meowthsnap.webp differ diff --git a/src/front/updateBanners/newdomain.webp b/src/front/updateBanners/newdomain.webp new file mode 100644 index 0000000..256784a Binary files /dev/null and b/src/front/updateBanners/newdomain.webp differ diff --git a/src/front/updateBanners/twitchupdate.webp b/src/front/updateBanners/twitchupdate.webp new file mode 100644 index 0000000..45bd622 Binary files /dev/null and b/src/front/updateBanners/twitchupdate.webp differ diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index b087f99..a0dc8f6 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -136,6 +136,14 @@ "KeyboardShortcutClosePopup": "close all popups", "CollapseLegal": "legal stuff", "FairUse": "cobalt is a tool for easing content downloads from internet and takes zero liability. you are responsible for what you download, how you use and distribute that content.\n\ncobalt does not log any info about you, it's impossible for me to snitch on you, but please be mindful when using content of others and always credit original creators!\n\nwhen used in education purposes (lecture, homework, etc) please attach the source link.\n\nfair use and credits benefit everyone.", - "UrgentFeatureUpdate71": "more supported services!" + "UrgentFeatureUpdate71": "more supported services!", + "UrgentThanks": "thank you for support!", + "SettingsDisableMetadata": "don't add metadata", + "UrgentNewDomain": "new domain, same cobalt", + "NewDomainWelcomeTitle": "hey there!", + "NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\ncobalt.tools is the new main domain, aka where you are now. make sure to update your bookmarks and reinstall the web app!", + "DataTransferSuccess": "btw, your settings have been transferred automatically :)", + "DataTransferError": "something went wrong when transferring your preferences. you'll have to open settings and configure cobalt by hand.", + "SupportNotAffiliated": "cobalt is not affiliated with any services listed above." } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index d65353b..ef9ddc4 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -38,8 +38,8 @@ "SettingsFormatSubtitle": "формат", "SettingsQualitySubtitle": "качество", "SettingsThemeAuto": "авто", - "SettingsThemeLight": "светлая", - "SettingsThemeDark": "тёмная", + "SettingsThemeLight": "светлый", + "SettingsThemeDark": "тёмный", "SettingsKeepDownloadButton": "всегда показывать >>", "AccessibilityKeepDownloadButton": "всегда показывать кнопку скачивания на экране", "SettingsEnableDownloadPopup": "выбор метода скачивания", @@ -52,7 +52,7 @@ "ClickToCopy": "нажми, чтобы скопировать", "Download": "скачать", "CopyURL": "скопировать", - "AboutTab": "о кобальте", + "AboutTab": "о сайте", "ChangelogTab": "изменения", "DonationsTab": "донаты", "SettingsVideoTab": "видео", @@ -64,11 +64,11 @@ "SettingsAudioFormatBest": "лучший", "SettingsAudioFormatDescription": "когда выбран \"лучший\", ты получишь аудио без каких-либо изменений. такое, какое оно есть на стороне сервиса. если же выбрано что-то другое, то аудио будет немного сжато.", "Keyphrase": "сохраняй то, что любишь", - "SettingsRemoveWatermark": "убирать ватермарку", + "SettingsRemoveWatermark": "убрать ватермарку", "ErrorPopupCloseButton": "ясно", "ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат, чтобы обойти ограничения.", "SettingsAudioFullTikTok": "полное аудио", - "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео. без каких-либо изменений от автора поста.", + "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, использованный в видео, без каких-либо изменений от автора поста.", "ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, а если так и не получится, то {ContactLink}.", "ErrorNoVideosInTweet": "я не смог найти никакого медиа контента в этом твите. попробуй другой!", "ImagePickerTitle": "выбери картинки для скачивания", @@ -91,7 +91,7 @@ "TwitterSpaceWasntRecorded": "мне нечего скачать, так как этот twitter space не был записан. попробуй другой!", "ErrorCantProcess": "я не смог обработать твой запрос :(\nты можешь попробовать ещё раз, но если не поможет, то {ContactLink}.", "ChangelogPressToHide": "скрыть", - "Donate": "задонатить", + "Donate": "донаты", "DonateSub": "ты можешь помочь!", "DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно для всех. но разработка и поддержка медиа сервиса, которым пользуются более 350 тысяч людей, обходится довольно затратно. мне, как студенту, оплачивать такое в одиночку довольно трудно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал работать и развиваться, то это можно сделать через донаты!\n\nделая донат ты помогаешь всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nза последние несколько месяцев благодаря донатам я смог:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api кобальта для свободного публичного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; перейти к надёжному поставщику облачной инфры.\n*; разделить фронтенд и api для обеспечения отказоустойчивости и децентрализации в будущем.\n\nкаждый донат невероятно ценится и помогает кобальту развиваться!", "DonateVia": "открыть", @@ -102,11 +102,11 @@ "CollapseServices": "что поддерживается?", "CollapseSupport": "поддержка и исходный код", "CollapsePrivacy": "политика конфиденциальности", - "ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!", - "FollowSupport": "оставайтесь на связи с кобальтом для новостей, поддержки, участия в опросах, и многого другого:", - "SupportNote": "так как я один занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", + "ServicesNote": "этот список далеко не финальный и постоянно пополняется, заглядывай сюда почаще!", + "FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:", + "SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", - "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется живой рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 20 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", + "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение 20 секунд. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть исходный код кобальта и убедиться, что всё так, как заявлено.", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "SettingsCodecSubtitle": "кодек для видео с youtube", @@ -124,7 +124,7 @@ "PopupCloseDone": "готово", "Accessibility": "общедоступность", "SettingsReduceTransparency": "уменьшить прозрачность", - "SettingsDisableAnimations": "выключить анимации", + "SettingsDisableAnimations": "убрать анимации", "FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!", "ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным здесь\n\n...или же ты можешь просто вставить ссылку вручную.", "ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.", @@ -137,6 +137,15 @@ "KeyboardShortcutClosePopup": "закрыть все окна", "CollapseLegal": "правовые штучки", "FairUse": "кобальт - это инструмент для облегчения скачивания контента из интернета, и он не несёт никакой ответственности. ты несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент.\n\nкобальт не собирает никакой информации о тебе, и не может донести на тебя, но, пожалуйста, будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nпри использовании в образовательных целях (лекции, домашние задания и т.д.), пожалуйста, прикладывай ссылку на источник.\n\nчестное использование и указание авторства выгодно всем.", - "UrgentFeatureUpdate71": "расширение поддержки сервисов!" + "UrgentFeatureUpdate71": "расширение поддержки сервисов!", + "UrgentThanks": "спасибо за поддержку!", + "SettingsDisableMetadata": "не добавлять метаданные", + "UrgentNewDomain": "новый домен, тот же кобальт", + "NewDomainWelcomeTitle": "привет!", + "NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\ncobalt.tools - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!", + "DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)", + "DataTransferError": "при переносе настроек что-то пошло не так. придётся зайти в настройки и настроить кобальт вручную.", + "SupportNotAffiliated": "кобальт не аффилирован ни с одним из перечисленных выше сервисов.", + "SupportMetaNoticeRU": "деятельность meta platforms inc. (владелец instagram) запрещена на территории россии." } } diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index 5500f65..db2a0f8 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -1,5 +1,36 @@ { "current": { + "version": "7.5", + "date": "September 16, 2023", + "title": "support for twitch clips and rutube!", + "banner": { + "file": "twitchupdate.webp", + "width": 851, + "height": 640 + }, + "content": "hey! this update (finally) adds support for twitch clips and rutube, among other smaller changes.\n\nservice improvements:\n*; added support for twitch clips. no vods, they're unnecessary. just clip whatever you want to download!\n*; added support for rutube in case you ever wanted to download something russian.\n\ninterface improvements:\n*; added a note about cobalt not being affiliated with any supported services.\n*; added a note about meta (the company) in russian.\n*; better russian localization. will keep improving it to make it sound not so robotic over time.\n\nother improvements:\n*; all official servers are now using the docker package. and so should you!\n*; moved the load balancer to poland. requests should be slightly faster now.\n*; minor codebase clean up.\n\nif you're confused about the new domain, read the older changelog! just scroll lower and press \"expand\".\n\ni hope you find this update useful and have a wonderful day :)\n\nbtw, cobalt has a pretty active community server on discord. go to about > support & source code to join!" + }, + "history": [{ + "version": "7.4", + "date": "September 9, 2023", + "title": "new domain, what's coming in future, bug fixes, and more!", + "banner": { + "file": "newdomain.webp", + "width": 960, + "height": 540 + }, + "content": "cobalt is finally moving to its own domain! many of you have been anticipating this, and many kept forgetting the link due to how cryptic it was.\n\nwell, worry no more - cobalt.tools is here.\n\nif you haven't yet, open co.wukko.me to transfer your settings here! no additional action from you is required. just open the old link and cobalt will do everything for you :)\n\nmake sure to update your bookmarks and reinstall the web app!\n\nhere's what domain change means:\n*; still no ads, same owner, same features, same reliability. just a way more rememberable link (it's literally two words).\n*; cobalt.tools makes it clear that cobalt is a tool and that it's \"cobalt\", not \"wukko\".\n*; i can host various versions of cobalt on subdomains without links looking awkward.\n*; i can host cobalt-related websites without polluting my personal domain's dns (such as crowdin).\n*; i stand by same privacy policies (and in fact am using the same exact server as before).\n\nthe domain change is required for the future of cobalt.\n\nhere's what's coming soon:\n*; support for many top-requested sites, such as (but not limited to) twitch and niconico.\n*; education version of cobalt, as often requested by students and educators.\n*; major localization system upgrade, allowing for simpler community contributions.\n*; region-specific versions with 100% translations and tweaks.\n*; native clients for desktop and mobile (not sure about this one, i'm no superman).\n*; ...and more!\n\nnow, here's what's new in 7.4:\n*; tabs in popups now scroll to top on tab bar tap.\n*; padding across web app was tuned.\n*; (obviously) a migration agent. soon will be used for importing and exporting settings.\n*; some minor clean ups in codebase.\n\nif you want to help cobalt achieve goals listed above, consider donating! donations are the only way i can keep cobalt ad-less, powerful, (basically) limitless, and also 100% free.\n\nin fact, donations have helped me grow cobalt more than i've ever anticipated. just imagine how much better it will be in a year.\n\ngo to donations down below to find ways to donate!\n\nthank you for reading through all of this. i hope you enjoy this update and have a great day :D" + }, { + "version": "7.2 & 7.3", + "date": "September 6, 2023", + "title": "extended video length limit, metadata toggle, ui improvements, and more!", + "banner": { + "file": "meowthsnap.webp", + "width": 500, + "height": 280 + }, + "content": "this update gives cobalt a sharp look in chromium browsers and makes it even more useful than before. check out the full changelog below!\n\nservice improvements:\n*; increased video length limit from 3 hours to 5 hours. feel free to download lectures you need :)\n*; you can now disable file metadata in settings.\n*; fixed a bug which previously caused some downloads to end up being 0 bytes.\n\nui improvements:\n*; fixed clickable area for urgent notice (text on top).\n*; fixed blurry header in chrome.\n*; fixed blurry tab bar in chrome.\n*; fixed blurry switches in chrome.\n*; fixed weirdly rounded corners in popups.\n*; fixed 1px gap on edges of various elements in popup in chrome.\n*; fixed overscrolling in other settings tab on ios.\n*; fixed unexpected button highlight effect on phones.\n*; removed outdated fixes for tiny screens.\n\nother improvements:\n*; cobalt web & api start faster than before, additional preparation functions aren't unexpectedly run anymore.\n*; cobalt is now available as a docker package. check it out on github.\n\nthank you for being here. i hope you have a great day :D" + }, { "version": "7.1", "date": "August 20, 2023", "title": "instagram, streamable, video metadata, and more!", @@ -9,8 +40,7 @@ "height": 358 }, "content": "service improvements:\n*; extended instagram support: high quality photos, videos, reels. everything should work without any issues, enjoy! :)\n*; added support for streamable.com (thanks to #179)\n*; added video metadata to youtube videos.\n*; fixed vk video downloads.\n*; vxtwitter links are now supported.\n*; fixed support for youtube audio dubs.\n\nui improvements:\n*; fixed picker popup: it's now scrollable in all cases and clickable areas don't overlap each other.\n\nbackend improvements:\n*; cobalt will now let you know if something goes wrong during video download instead of nuking the stream.\n*; added support for cookies (thanks to #177)\n*; replaced got with undici (thanks to #182). downloads should be slightly faster and clean of garbage in headers.\n\ninternal improvements:\n*; moved host overrides into its own module.\n*; minor clean ups.\n\neven more cool stuff is coming in future updates! thank you for using cobalt :D" - }, - "history": [{ + }, { "version": "7.0", "date": "August 15, 2023", "title": "biggest ui refresh yet!", diff --git a/src/modules/emoji.js b/src/modules/emoji.js index c019037..04e053f 100644 --- a/src/modules/emoji.js +++ b/src/modules/emoji.js @@ -33,7 +33,9 @@ const names = { "🔗": "link", "⌨": "keyboard", "📑": "boring_document", - "🧮": "abacus" + "🧮": "abacus", + "😸": "cat_grin", + "📰": "newspaper" } let sizing = { 18: 0.8, diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index 66e6de1..65f9669 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -1,4 +1,4 @@ -import { celebrations } from "../config.js"; +import { authorInfo, celebrations } from "../config.js"; import emoji from "../emoji.js"; export const backButtonSVG = ` @@ -68,17 +68,19 @@ export function popup(obj) { } return ` ${obj.standalone ? `