diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 00000000..d70b1821
--- /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 e37752e2..be0429df 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/)

@@ -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 5900b327..a74a89af 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 7c2a5b34..9957d517 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 4c075ad6..fb1abc15 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 d03bca2d..949cccba 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 6337654f..10e3286d 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 8e666dde..e0e5322e 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 ca309511..b4df9a4a 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 = `