Merge branch 'current' into fix-hold-various-picker

This commit is contained in:
wukko 2024-01-31 16:12:20 +06:00 committed by GitHub
commit 5a45f5ef64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 2953 additions and 1199 deletions

32
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View file

@ -0,0 +1,32 @@
---
name: bug report
about: report an issue with downloads or something else
title: ''
labels: bug
assignees: ''
---
**bug description**
a clear and concise description of what the bug is.
**reproduction steps**
steps to reproduce the behavior:
1. go to '...'
2. click on '....'
3. download this video: **[link here]**
4. see error
**screenshots**
if applicable, add screenshots or screen recordings to help explain your problem.
**links**
if applicable, add links that cause the issue. more = better.
**platform**
- OS [e.g. iOS, windows]
- browser [e.g. chrome, safari, firefox]
- version [e.g. 115]
**additional context**
add any other context about the problem here.

View file

@ -0,0 +1,17 @@
---
name: feature request
about: suggest a feature for cobalt
title: ''
labels: feature request
assignees: ''
---
**describe the feature you'd like to see**
a clear and concise description of what you want to happen.
**describe alternatives you've considered**
a clear and concise description of any alternative solutions or features you've considered.
**additional context**
add any other context or screenshots about the feature request here.

View file

@ -29,20 +29,22 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version from package.json - name: Get release metadata
id: package-version id: release-meta
uses: martinbeentjes/npm-get-version-action@v1.3.1 run: |
- name: Get short commit hash version=$(cat package.json | jq -r .version)
id: commit-hash echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
run: echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "version=$version" >> $GITHUB_OUTPUT
echo "major_version=$(echo "$version" | cut -d. -f1)" >> $GITHUB_OUTPUT
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
tags: | tags: |
type=raw,value=latest type=raw,value=latest
type=raw,value=${{ steps.package-version.outputs.current-version }} type=raw,value=${{ steps.release-meta.outputs.version }}
type=raw,value=${{ steps.package-version.outputs.current-version }}-${{ steps.commit-hash.outputs.commit_short }} type=raw,value=${{ steps.release-meta.outputs.major_version }}
type=raw,value=${{ steps.release-meta.outputs.version }}-${{ steps.release-meta.outputs.commit_short }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image - name: Build and push Docker image

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# os stuff
.DS_Store
desktop.ini
# npm # npm
node_modules node_modules
package-lock.json package-lock.json

124
README.md
View file

@ -1,78 +1,80 @@
# cobalt # cobalt
Best way to save what you love. best way to save what you love: [cobalt.tools](https://cobalt.tools/)
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") ![cobalt logo with repeated logo (double arrow) pattern background](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/pattern.png "cobalt logo with repeated logo (double arrow) pattern background")
[![DeepSource](https://deepsource.io/gh/wukko/cobalt.svg/?label=active+issues&token=MsmsJ9zUOKwcQor0yaiFot84)](https://deepsource.io/gh/wukko/cobalt/?ref=repository-badge) ## what's cobalt?
cobalt is a media 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***.
## What's cobalt? paste the link, get the file, move on. it's that simple. just how it should be.
cobalt is 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. ## supported services
Paste the link, get the video, move on. It's that simple. Just how it should be. this list is not final and keeps expanding over time. if support for a service you want is missing, create an issue (or a pull request 👀).
## Supported services | service | video + audio | only audio | only video | metadata | rich file names |
| Service | Video + Audio | Only audio | Only video | Additional notes or features | | :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
| -------- | :---: | :---: | :---: | :----- | | bilibili.com | ✅ | ✅ | ✅ | | |
| bilibili.com | ✅ | ✅ | ✅ | | | instagram posts & stories | ✅ | ✅ | ✅ | | |
| Instagram | ✅ | ✅ | ✅ | Supports photos and videos, lets you pick what to save from multi-media posts. | | instagram reels | ✅ | ✅ | ✅ | | |
| Instagram Reels | ✅ | ✅ | ✅ | | | pinterest | ✅ | ✅ | ✅ | | |
| Pinterest | ✅ | ✅ | ✅ | Support for videos and stories. | | reddit | ✅ | ✅ | ✅ | ❌ | ❌ |
| Reddit | ✅ | ✅ | ✅ | Support for GIFs and videos. | | rutube | ✅ | ✅ | ✅ | ✅ | ✅ |
| Rutube | ✅ | ✅ | ✅ | | | soundcloud | | ✅ | | ✅ | ✅ |
| SoundCloud | | ✅ | | Audio metadata, downloads from private links. | | streamable | ✅ | ✅ | ✅ | | |
| Streamable | ✅ | ✅ | ✅ | | | tiktok | ✅ | ✅ | ✅ | ❌ | ❌ |
| TikTok | ✅ | ✅ | ✅ | Supports downloads of: videos with or without watermark, images from slideshow without watermark, full (original) audios. | | tumblr | ✅ | ✅ | ✅ | | |
| Tumblr | ✅ | ✅ | ✅ | Support for audio file downloads. | | twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ |
| Twitch Clips | ✅ | ✅ | ✅ | | | twitter/x | ✅ | ✅ | ✅ | | |
| Twitter/X * | ✅ | ✅ | ✅ | Ability to pick what to save from multi-media tweets. | | vimeo | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vimeo | ✅ | ✅ | ✅ | Audio downloads are only available for dash files. | | vine archive | ✅ | ✅ | ✅ | | |
| Vine Archive | ✅ | ✅ | ✅ | | | vk videos & clips | ✅ | ❌ | ❌ | ✅ | ✅ |
| VK Videos | ✅ | ❌ | ❌ | | | youtube videos, shorts & music | ✅ | ✅ | ✅ | ✅ | ✅ |
| VK Clips | ✅ | ❌ | ❌ | |
| YouTube Videos & Shorts | ✅ | ✅ | ✅ | Support for 8K, 4K, HDR, VR, 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! | emoji | meaning |
| :-----: | :---------------------- |
*Reliability of downloads from Twitter is questionable due to its current management. | ✅ | supported |
| | impossible/unreasonable |
| ❌ | not supported |
## cobalt API ### additional notes or features (per service)
cobalt has an open API that you can use in your projects for **free**. | service | notes or features |
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. | :-------- | :----- |
Feel free to use the main API instance ([co.wuk.sh](https://co.wuk.sh/)) in your projects. | instagram | supports photos, videos, and stories. lets you pick what to save from multi-media posts. |
| pinterest | supports videos and stories. |
| reddit | supports gifs and videos. |
| soundcloud | supports private links. |
| tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. |
| twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. |
| vimeo | audio downloads are only available for dash. |
| youtube | supports videos, music, and shorts. 8K, 4K, HDR, VR, and high FPS videos. rich metadata & dubs. h264/av1/vp9 codecs. |
## Host an instance yourself ## cobalt api
### Requirements cobalt has an open api that you can use in projects *for completely free~*. it's easy and straightforward to use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/api.md) to learn how to use it.
- Node.js 18 or above
- git
Setup script installs all needed `npm` dependencies, but you have to install `Node.js` and `git` yourself. you can use the main api instance ([co.wuk.sh](https://co.wuk.sh/)) in your projects.
1. Clone the repo: `git clone https://github.com/wukko/cobalt` ## how to run your own instance
2. Run setup script and follow instructions: `npm run setup` if you want to run your own instance for whatever purpose, [follow this guide](https://github.com/wukko/cobalt/blob/current/docs/run-an-instance.md).
3. Run cobalt via `npm start` it's *highly* recommended to use a docker compose method unless you run for developing/debugging purposes.
4. Done.
You need to host API and web app separately since v.6.0. Setup script will help you with that! ## sponsors
cobalt is sponsored by [royalehosting.net](https://royalehosting.net/), all main instances are currently hosted on their network :)
### Ubuntu 22.04+ workaround ## ethics and disclaimer
`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)): 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. please be mindful when using content of others and always credit original creators. fair use and credits benefit everyone.
```bash cobalt is ***NOT*** a piracy tool and cannot be used as such. it can only download free, publicly accessible content. such content can be easily downloaded through any browser's dev tools. pressing one button is easier, so i made a convenient, ad-less tool for such repeated actions.
sudo apt install nscd
sudo service nscd start
```
### Docker cobalt is my passion project, update schedule depends solely on my free time, motivation, and mood. don't expect any consistency in update releases.
It's also possible to run cobalt via Docker. I *highly* recommend using Docker compose.
Check out the [example compose file](https://github.com/wukko/cobalt/blob/current/docker-compose.example.yml) and alter it for your needs.
## Disclaimer ## cobalt licenses
cobalt is my passion project, so update schedule depends solely on my free time, motivation, and mood. cobalt code is licensed under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).
Don't expect any consistency in that.
## License update banners and various assets of cobalt branding included within the repo are *not* covered by the AGPL-3.0 license and cannot be used using same terms.
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. ## 3rd party licenses
[Fluent Emoji by Microsoft](https://github.com/microsoft/fluentui-emoji) (used in cobalt) is under [MIT](https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE) license.
[Noto Sans Mono](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/) fonts (used in cobalt) are licensed under the [OFL](https://fonts.google.com/noto/specimen/Noto+Sans+Mono/about) license.
many update banners were taken from [tenor.com](https://tenor.com/).

View file

@ -1,3 +0,0 @@
files:
- source: /src/localization/languages/en.json
translation: /src/localization/languages/%two_letters_code%.json

View file

@ -1,72 +0,0 @@
# cobalt API Documentation
This document provides info about methods and acceptable variables for all cobalt API requests.<br>
```
⚠️ Main API instance has moved to https://co.wuk.sh/
Make sure your projects use the correct API domain.
```
## POST: ``/api/json``
Main processing endpoint.<br>
Request Body Type: ``application/json``<br>
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 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 |
|:---------------|:-----------|:--------------------------------------------------------------|
| ``status`` | ``string`` | ``error / redirect / stream / success / rate-limit / picker`` |
| ``text`` | ``string`` | Text |
| ``url`` | ``string`` | Direct link to a file / link to cobalt's live render |
| ``pickerType`` | ``string`` | ``various / images`` |
| ``picker`` | ``array`` | Array of picker items |
| ``audio`` | ``string`` | Direct link to a file / link to cobalt's live render |
### Picker Item Variables
Item type: ``object``
| key | type | variables | description |
|:---------------|:-----------|:------------------------------------------------|:--------------------------------------------|
| ``type`` | ``string`` | ``video`` | Used only if ``pickerType`` is ``various``. |
| ``url`` | ``string`` | Direct link to a file / link to cobalt's live render | |
| ``thumb`` | ``string`` | Item thumbnail that's displayed in the picker | Used only for ``video`` type. |
## GET: ``/api/stream``
Content live render streaming endpoint.<br>
### Request Query Variables
| key | variables | description |
|:--------|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------|
| ``p`` | ``1`` | Used for probing whether user is rate limited. |
| ``t`` | Stream token | Unique stream ID. Used for retrieving cached stream info data. |
| ``h`` | HMAC | Hashed combination of: (hashed) ip address, stream token, expiry timestamp, and service name. Used for verification of stream. |
| ``e`` | Expiry timestamp | |
## GET: ``/api/serverInfo``
Returns current basic server info.<br>
Response Body Type: ``application/json``
### Response Body Variables
| key | type | variables |
|:--------------|:-----------|:------------------|
| ``version`` | ``string`` | cobalt version |
| ``commit`` | ``string`` | Git commit |
| ``branch`` | ``string`` | Git branch |
| ``name`` | ``string`` | Server name |
| ``url`` | ``string`` | Server url |
| ``cors`` | ``int`` | CORS status |
| ``startTime`` | ``string`` | Server start time |

80
docs/api.md Normal file
View file

@ -0,0 +1,80 @@
# cobalt api documentation
this document provides info about methods and acceptable variables for all cobalt api requests.
```
👍 you can use co.wuk.sh instance in your projects for free, just don't be an asshole.
```
## POST: `/api/json`
cobalt's main processing endpoint.
request body type: `application/json`
response body type: `application/json`
```
⚠️ you must include Accept and Content-Type headers with every POST /api/json request.
Accept: application/json
Content-Type: application/json
```
### request body variables
| key | type | variables | default | description |
|:------------------|:----------|:-----------------------------------|:----------|:--------------------------------------------------------------------------------|
| `url` | `string` | 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` | |
| `filenamePattern` | `string` | `classic / pretty / basic / nerdy` | `classic` | changes the way files are named. previews can be seen in the web app. |
| `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 header for youtube video audio tracks when `true`. |
| `disableMetadata` | `boolean` | `true / false` | `false` | disables file metadata when set to `true`. |
| `twitterGif` | `boolean` | `true / false` | `false` | changes whether twitter gifs are converted to .gif |
### response body variables
| key | type | variables |
|:-------------|:---------|:------------------------------------------------------------|
| `status` | `string` | `error / redirect / stream / success / rate-limit / picker` |
| `text` | `string` | various text, mostly used for errors |
| `url` | `string` | direct link to a file or a link to cobalt's live render |
| `pickerType` | `string` | `various / images` |
| `picker` | `array` | array of picker items |
| `audio` | `string` | direct link to a file or a link to cobalt's live render |
### picker item variables
item type: `object`
| key | type | variables | description |
|:--------|:---------|:--------------------------------------------------------|:---------------------------------------|
| `type` | `string` | `video` | used only if `pickerType`is `various`. |
| `url` | `string` | direct link to a file or a link to cobalt's live render | |
| `thumb` | `string` | item thumbnail that's displayed in the picker | used only for `video` type. |
## GET: `/api/stream`
cobalt's live render (or stream) endpoint. used for sending various media content over to the user.
### request query variables
| key | variables | description |
|:-----|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------|
| `p` | `1` | used for probing whether user is rate limited. |
| `t` | stream token | unique stream id. used for retrieving cached stream info data. |
| `h` | hmac | hashed combination of: (hashed) ip address, stream token, expiry timestamp, and service name. used for verification of stream. |
| `e` | expiry timestamp | |
## GET: `/api/serverInfo`
returns current basic server info.
response body type: `application/json`
### response body variables
| key | type | variables |
|:------------|:---------|:------------------|
| `version` | `string` | cobalt version |
| `commit` | `string` | git commit |
| `branch` | `string` | git branch |
| `name` | `string` | server name |
| `url` | `string` | server url |
| `cors` | `int` | cors status |
| `startTime` | `string` | server start time |

View file

@ -2,7 +2,7 @@ version: '3.5'
services: services:
cobalt-api: cobalt-api:
image: ghcr.io/wukko/cobalt:latest image: ghcr.io/wukko/cobalt:7
restart: unless-stopped restart: unless-stopped
container_name: cobalt-api container_name: cobalt-api
@ -17,14 +17,13 @@ services:
#- 127.0.0.1:9000:9000 #- 127.0.0.1:9000:9000
environment: environment:
- apiPort=9000
# replace apiURL with your instance's target url in same format # replace apiURL with your instance's target url in same format
- apiURL=https://co.wuk.sh/ - apiURL=https://co.wuk.sh/
# replace apiName with your instance's distinctive name # replace apiName with your instance's distinctive name
- apiName=eu-nl - apiName=eu-nl
# if you want to use cookies when fetching data from services, uncomment the next line # 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. # see cookies_example.json for example file.
labels: labels:
- com.centurylinklabs.watchtower.scope=cobalt - com.centurylinklabs.watchtower.scope=cobalt
@ -33,7 +32,7 @@ services:
#- ./cookies.json:/cookies.json #- ./cookies.json:/cookies.json
cobalt-web: cobalt-web:
image: ghcr.io/wukko/cobalt:latest image: ghcr.io/wukko/cobalt:7
restart: unless-stopped restart: unless-stopped
container_name: cobalt-web container_name: cobalt-web
@ -48,7 +47,6 @@ services:
#- 127.0.0.1:9001:9001 #- 127.0.0.1:9001:9001
environment: environment:
- webPort=9001
# replace webURL with your instance's target url in same format # replace webURL with your instance's target url in same format
- webURL=https://cobalt.tools/ - webURL=https://cobalt.tools/
# replace apiURL with preferred api instance url # replace apiURL with preferred api instance url
@ -63,4 +61,4 @@ services:
restart: unless-stopped restart: unless-stopped
command: --cleanup --scope cobalt --interval 900 command: --cleanup --scope cobalt --interval 900
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock

49
docs/run-an-instance.md Normal file
View file

@ -0,0 +1,49 @@
# how to host a cobalt instance yourself
## using docker compose and package from github (recommended)
to run the cobalt docker package, you need to have `docker` and `docker-compose` installed and configured.
if you need help with installing docker, follow *only the first step* of these tutorials by digitalocean:
- [how to install docker](https://www.digitalocean.com/community/tutorial-collections/how-to-install-and-use-docker)
- [how to install docker compose](https://www.digitalocean.com/community/tutorial-collections/how-to-install-docker-compose)
## how to run a cobalt docker package:
1. create a folder for cobalt config file, something like this:
```sh
mkdir cobalt
```
2. go to cobalt folder, and create a docker compose config file:
```sh
cd cobalt && nano docker-compose.yml
```
i'm using `nano` in this example, it may not be available in your distro. you can use any other text editor.
3. copy and paste the [sample config from here](https://github.com/wukko/cobalt/blob/current/docs/examples/docker-compose.example.yml) for either web or api instance (or both, if you wish) and edit it to your needs.
make sure to replace default URLs with your own or cobalt won't work correctly.
4. finally, start the cobalt container (from cobalt directory):
```sh
docker compose up -d
```
if you want your instance to support services that require authentication to view public content, create `cookies.json` file in the same directory as `docker-compose.yml`. example cookies file [can be found here](https://github.com/wukko/cobalt/blob/current/docs/examples/cookies.example.json).
cobalt package will update automatically thanks to watchtower.
it's highly recommended to use a reverse proxy (such as nginx) if you want your instance to face the public internet. look up tutorials online.
## using regular node.js (useful for local development)
setup script installs all needed `npm` dependencies, but you have to install `node.js` *(version 18 or above)* and `git` yourself.
1. clone the repo: `git clone https://github.com/wukko/cobalt`.
2. run setup script and follow instructions: `npm run setup`. you need to host api and web instances separately, so pick whichever applies.
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
```

33
docs/troubleshooting.md Normal file
View file

@ -0,0 +1,33 @@
# self-troubleshooting cobalt
```
🚧 this page is work-in-progress. expect more guides to be added in the future!
```
if any issues occur while using cobalt, you can fix many of them yourself. this document aims to provide guides on how to fix most complicated of them.
use wiki navigation on right to jump between solutions.
## how to fix clipboard pasting in firefox
you can fix this issue by changing a single preference in `about:config`.
### steps to enable clipboard functionality
1. go to `about:config`:
![screenshot showing about:config entered into address bar](https://github.com/wukko/cobalt/assets/71202418/9ad78612-a372-4949-aeac-99dfc41e273c)
2. if asked, read what firefox has to say and press "accept the risk and continue".
⚠ tinkering with other preferences may break your browser. **do not** edit them unless you know what you're doing.
![screenshot showing about:config security warning that reads: "proceed with caution. changing advanced configuration preferences can impact firefox performance or security." lower there's a pre-checked checkbox that says: "warn me when i attempt to access these preferences". lowest element is a blue button that says "accept the risk and continue"](https://github.com/wukko/cobalt/assets/71202418/02328729-dbfe-4ea4-b2ca-7bcf1998c2ca)
3. search for `dom.events.asyncClipboard.readText`
![screenshot showing "dom.events.asyncclipboard.readtext" entered into search on about:config page](https://github.com/wukko/cobalt/assets/71202418/7c7f7e3c-6a6a-40df-8436-277489e72e0b)
4. press the toggle button on very right.
![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page with highlighted toggle button on very right](https://github.com/wukko/cobalt/assets/71202418/b45db18e-f4bf-4f1c-9a8c-f13a63a21335)
5. "false" should change to "true".
![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](https://github.com/wukko/cobalt/assets/71202418/4869b4ff-8385-4cd3-ae59-aa2e03a58b5f)
6. go back to cobalt, reload the page, press `paste and download` button again. this time it works! enjoy simpler downloading experience :)

View file

@ -1,13 +0,0 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "7.5.1", "version": "7.9.5",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",
@ -12,7 +12,8 @@
"start": "node src/cobalt", "start": "node src/cobalt",
"setup": "node src/modules/setup", "setup": "node src/modules/setup",
"test": "node src/test/test", "test": "node src/test/test",
"build": "node src/modules/buildStatic" "build": "node src/modules/buildStatic",
"testFilenames": "node src/test/testFilenamePresets"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -24,6 +25,8 @@
}, },
"homepage": "https://github.com/wukko/cobalt#readme", "homepage": "https://github.com/wukko/cobalt#readme",
"dependencies": { "dependencies": {
"abort-controller": "3.0.0",
"content-disposition-header": "0.6.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"esbuild": "^0.14.51", "esbuild": "^0.14.51",
@ -33,9 +36,10 @@
"hls-parser": "^0.10.7", "hls-parser": "^0.10.7",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"psl": "1.9.0",
"set-cookie-parser": "2.6.0", "set-cookie-parser": "2.6.0",
"undici": "^5.19.1", "undici": "^5.19.1",
"url-pattern": "1.0.3", "url-pattern": "1.0.3",
"youtubei.js": "^5.4.0" "youtubei.js": "^6.4.1"
} }
} }

View file

@ -21,8 +21,8 @@ app.disable('x-powered-by');
await loadLoc(); await loadLoc();
const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port)); const apiMode = process.env.apiURL && !process.env.webURL;
const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port)); const webMode = process.env.webURL && process.env.apiURL;
if (apiMode) { if (apiMode) {
const { runAPI } = await import('./core/api.js'); const { runAPI } = await import('./core/api.js');
@ -31,5 +31,9 @@ if (apiMode) {
const { runWeb } = await import('./core/web.js'); const { runWeb } = await import('./core/web.js');
await runWeb(express, app, gitCommit, gitBranch, __dirname) await runWeb(express, app, gitCommit, gitBranch, __dirname)
} else { } 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`)) 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`)
)
} }

View file

@ -1,6 +1,6 @@
{ {
"streamLifespan": 20000, "streamLifespan": 90000,
"maxVideoDuration": 18000000, "maxVideoDuration": 10800000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "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": { "authorInfo": {
"name": "wukko", "name": "wukko",
@ -8,37 +8,55 @@
"contact": "https://wukko.me/contacts", "contact": "https://wukko.me/contacts",
"support": { "support": {
"default": { "default": {
"email": {
"emoji": "📧",
"url": "mailto:support@cobalt.tools",
"name": "support@cobalt.tools"
},
"twitter": { "twitter": {
"emoji": "🐦", "emoji": "🐦",
"url": "https://twitter.com/justusecobalt", "url": "https://twitter.com/justusecobalt",
"handle": "@justusecobalt" "name": "@justusecobalt"
},
"mastodon": {
"emoji": "🐘",
"url": "https://wetdry.world/@cobalt",
"handle": "@cobalt@wetdry.world"
}, },
"discord": { "discord": {
"emoji": "👾", "emoji": "👾",
"url": "https://discord.gg/pQPt8HBUPu", "url": "https://discord.gg/pQPt8HBUPu",
"handle": "cobalt community server" "name": "cobalt discord server"
}
},
"ru": {
"telegram": {
"emoji": "📬",
"url": "https://t.me/justusecobalt_ru",
"name": "канал в telegram"
},
"email": {
"emoji": "📧",
"url": "mailto:support@cobalt.tools",
"name": "support@cobalt.tools"
} }
} }
} }
}, },
"donations": { "donations": {
"crypto": { "crypto": {
"bitcoin": "bc1q59jyyjvrzj4c22rkk3ljeecq6jmpyscgz9spnd", "monero": "4B1SNB6s8Pq1hxjNeKPEe8Qa8EP3zdL16Sqsa7QDoJcUecKQzEj9BMxWnEnTGu12doKLJBKRDUqnn6V9qfSdXpXi3Nw5Uod",
"ethereum": "0x4B4cF23051c78c7A7E0eA09d39099621c46bc302",
"litecoin": "ltc1qvp0xhrk2m7pa6p6z844qcslfyxv4p3vf95rhna", "litecoin": "ltc1qvp0xhrk2m7pa6p6z844qcslfyxv4p3vf95rhna",
"monero": "4B1SNB6s8Pq1hxjNeKPEe8Qa8EP3zdL16Sqsa7QDoJcUecKQzEj9BMxWnEnTGu12doKLJBKRDUqnn6V9qfSdXpXi3Nw5Uod" "ethereum": "0x4B4cF23051c78c7A7E0eA09d39099621c46bc302",
"usdt-erc20": "0x4B4cF23051c78c7A7E0eA09d39099621c46bc302",
"usdt-trc20": "TVbx7YT3rBfu931Gxko6pRfXtedYqbgnBB",
"bitcoin": "bc1qlvcnlnyzfsgnuxyxsv3k0p0q0yln0azjpadyx4",
"bitcoin-alt": "18PKf6N2cHrmSzz9ZzTSvDd2jAkqGC7SxA",
"ton": "UQA3SO-hHZq1oCCT--u6or6ollB8fd2o52aD8mXiLk9iDZd3"
}, },
"links": { "links": {
"boosty": "https://boosty.to/wukko/donate" "boosty": "https://boosty.to/wukko/donate"
} }
}, },
"links": { "links": {
"saveToGalleryShortcut": "https://www.icloud.com/shortcuts/b401917928fd407daf1db0fd07eb7e78" "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/b401917928fd407daf1db0fd07eb7e78",
"statusPage": "https://status.cobalt.tools/",
"troubleshootingGuide": "https://github.com/wukko/cobalt/blob/current/docs/troubleshooting.md"
}, },
"celebrations": { "celebrations": {
"01-01": "🎄", "01-01": "🎄",
@ -73,6 +91,17 @@
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"], "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"], "copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], "audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
"m4a": ["-movflags", "frag_keyframe+empty_moov"] "m4a": ["-movflags", "frag_keyframe+empty_moov"],
} "gif": ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"]
},
"sponsors": [{
"name": "royale",
"fullName": "RoyaleHosting",
"url": "https://royalehosting.net/",
"logo": {
"width": 605,
"height": 136,
"scale": 5
}
}]
} }

View file

@ -97,7 +97,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
let chck = checkJSONPost(request); let chck = checkJSONPost(request);
if (!chck) throw new Error(); if (!chck) throw new Error();
j = await getJSON(chck["url"], lang, chck); j = await getJSON(chck.url, lang, chck);
} else { } else {
j = apiJSON(0, { j = apiJSON(0, {
t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink') t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink')
@ -139,9 +139,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
version: version, version: version,
commit: gitCommit, commit: gitCommit,
branch: gitBranch, branch: gitBranch,
name: process.env.apiName ? process.env.apiName : "unknown", name: process.env.apiName || "unknown",
url: process.env.apiURL, url: process.env.apiURL,
cors: process.env.cors && process.env.cors === "0" ? 0 : 1, cors: process.env?.cors === "0" ? 0 : 1,
startTime: `${startTimestamp}` startTime: `${startTimestamp}`
}); });
default: default:
@ -167,12 +167,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
res.redirect('/api/json') res.redirect('/api/json')
}); });
app.listen(process.env.apiPort, () => { app.listen(process.env.apiPort || 9000, () => {
console.log(`\n` + console.log(`\n` +
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.apiURL}`)}\n` + `URL: ${Cyan(`${process.env.apiURL}`)}\n` +
`Port: ${process.env.apiPort}\n` `Port: ${process.env.apiPort || 9000}\n`
) )
}); });
} }

View file

@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
return res.redirect('/') return res.redirect('/')
}); });
app.listen(process.env.webPort, () => { app.listen(process.env.webPort || 9001, () => {
console.log(`\n` + console.log(`\n` +
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + `${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.webURL}`)}\n` + `URL: ${Cyan(`${process.env.webURL}`)}\n` +
`Port: ${process.env.webPort}\n` `Port: ${process.env.webPort || 9001}\n`
) )
}) })
} }

View file

@ -9,6 +9,7 @@
--padding-1: 0.75rem; --padding-1: 0.75rem;
--line-height: 1.65rem; --line-height: 1.65rem;
--red: rgb(249, 47, 96); --red: rgb(249, 47, 96);
--blue: rgb(47, 138, 249);
--gap: 0.5rem; --gap: 0.5rem;
--gap-no-icon: 0.6rem; --gap-no-icon: 0.6rem;
} }
@ -34,13 +35,13 @@
--accent: rgb(25, 25, 25); --accent: rgb(25, 25, 25);
--accent-highlight: rgb(25, 25, 25, 4%); --accent-highlight: rgb(25, 25, 25, 4%);
--accent-subtext: rgb(110, 110, 110); --accent-subtext: rgb(110, 110, 110);
--accent-hover: rgb(230, 230, 230); --accent-hover: rgb(225, 225, 225);
--accent-hover-elevated: rgb(215, 215, 215); --accent-hover-elevated: rgb(210, 210, 210);
--accent-hover-transparent: rgba(215, 215, 215, 0.5); --accent-hover-transparent: rgba(215, 215, 215, 0.5);
--accent-button: rgb(225, 225, 225); --accent-button: rgb(232, 232, 232);
--accent-button-elevated: rgb(210, 210, 210); --accent-button-elevated: rgb(215, 215, 215);
--glass: rgba(230, 230, 230, 0.85); --glass: rgba(232, 232, 232, 0.85);
--glass-lite: rgba(230, 230, 230, 0.98); --glass-lite: rgba(232, 232, 232, 0.98);
--subbackground: rgb(240, 240, 240); --subbackground: rgb(240, 240, 240);
--background: rgb(255, 255, 255); --background: rgb(255, 255, 255);
--background-backdrop: rgba(255, 255, 255, 0.5); --background-backdrop: rgba(255, 255, 255, 0.5);
@ -65,13 +66,13 @@
--accent: rgb(25, 25, 25); --accent: rgb(25, 25, 25);
--accent-highlight: rgb(25, 25, 25, 4%); --accent-highlight: rgb(25, 25, 25, 4%);
--accent-subtext: rgb(110, 110, 110); --accent-subtext: rgb(110, 110, 110);
--accent-hover: rgb(230, 230, 230); --accent-hover: rgb(225, 225, 225);
--accent-hover-elevated: rgb(215, 215, 215); --accent-hover-elevated: rgb(210, 210, 210);
--accent-hover-transparent: rgba(219, 219, 219, 0.5); --accent-hover-transparent: rgba(215, 215, 215, 0.5);
--accent-button: rgb(225, 225, 225); --accent-button: rgb(232, 232, 232);
--accent-button-elevated: rgb(210, 210, 210); --accent-button-elevated: rgb(215, 215, 215);
--glass: rgba(230, 230, 230, 0.85); --glass: rgba(232, 232, 232, 0.85);
--glass-lite: rgba(230, 230, 230, 0.98); --glass-lite: rgba(232, 232, 232, 0.98);
--subbackground: rgb(240, 240, 240); --subbackground: rgb(240, 240, 240);
--background: rgb(255, 255, 255); --background: rgb(255, 255, 255);
--background-backdrop: rgba(255, 255, 255, 0.5); --background-backdrop: rgba(255, 255, 255, 0.5);
@ -106,7 +107,7 @@ a {
color: var(--accent-subtext); color: var(--accent-subtext);
} }
.switches::-webkit-scrollbar, .switches::-webkit-scrollbar,
#popup-content::-webkit-scrollbar { .popup-content::-webkit-scrollbar {
display: none; display: none;
} }
:focus-visible { :focus-visible {
@ -253,19 +254,25 @@ button:active,
} }
#cobalt-main-box { #cobalt-main-box {
position: fixed; position: fixed;
width: 60%; width: 40rem;
height: auto; height: auto;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-content: center;
align-items: center;
} }
#logo { #logo {
text-align: left; text-align: left;
font-size: 1rem; font-size: 1rem;
white-space: nowrap; white-space: nowrap;
width: 7rem;
height: 2.5rem; height: 2.5rem;
align-items: center; align-items: center;
display: flex; display: flex;
gap: 0.3rem;
}
.logo-sub {
color: var(--blue);
font-size: 0.8rem;
} }
#download-area { #download-area {
display: flex; display: flex;
@ -289,7 +296,7 @@ button:active,
} }
#url-input-area { #url-input-area {
background: none; background: none;
padding: 0 1rem; padding-left: calc(20px + 1.4rem);
width: 100%; width: 100%;
color: var(--accent); color: var(--accent);
border: 0; border: 0;
@ -310,20 +317,34 @@ button:active,
outline: none; outline: none;
border-bottom: var(--border-10); border-bottom: var(--border-10);
} }
#link-icon {
display: flex;
position: absolute;
width: 20px;
padding-top: 0.2rem;
left: 0.7rem;
flex-wrap: nowrap;
color: var(--accent-subtext);
}
#download-button { #download-button {
height: 2.5rem; height: 2.5rem;
color: var(--accent); color: var(--accent);
background: none; background: none;
border: none; border: none;
font-size: 1.6rem; font-size: 1.8rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
letter-spacing: -0.36rem; letter-spacing: -0.35rem;
font-weight: normal!important;
} }
#download-button:disabled { #download-button:disabled {
color: var(--accent-subtext); color: var(--accent-subtext);
cursor: not-allowed; cursor: not-allowed;
} }
#cobalt-main-box .switch,
#footer .switch {
box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset;
}
#footer { #footer {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
@ -429,31 +450,28 @@ button:active,
.popup.small.visible { .popup.small.visible {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.popup.small #popup-header-contents, .popup.small .popup-header-contents,
.popup.small .popup-content-inner, .popup.small .popup-content-inner,
.popup.small #popup-header { .popup.small .popup-header {
padding: 0; padding: 0;
} }
.popup.small #popup-header { .popup.small .popup-header {
position: relative; position: relative;
border: none; border: none;
} }
.popup.small #popup-title { .popup.small .popup-title {
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
} }
.popup.small .explanation { .popup.small .explanation {
margin-bottom: 0.9rem; margin-bottom: 0.9rem;
} }
#close-error { .popup.small .close-error.switch {
background: var(--accent); background: var(--accent)!important;
color: var(--background); color: var(--background);
} }
.popup.scrollable { .popup.scrollable {
height: 95%; height: 95%;
} }
.scrollable .bottom-link {
padding-bottom: 2rem;
}
.changelog-subtitle { .changelog-subtitle {
font-size: 1.3rem; font-size: 1.3rem;
padding-bottom: var(--gap-no-icon); padding-bottom: var(--gap-no-icon);
@ -502,7 +520,7 @@ button:active,
font-size: 1.1rem; font-size: 1.1rem;
padding-bottom: var(--padding-1); padding-bottom: var(--padding-1);
} }
#popup-desc, .popup-desc,
.desc-error, .desc-error,
#popup-info-desc { #popup-info-desc {
width: 100%; width: 100%;
@ -515,7 +533,7 @@ button:active,
.desc-error { .desc-error {
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
} }
#popup-title { .popup-title {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.2em; line-height: 1.2em;
display: flex; display: flex;
@ -523,11 +541,11 @@ button:active,
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
margin-top: 0.4rem; margin-top: 0.4rem;
} }
#popup-above-title { .popup-above-title {
color: var(--accent-subtext); color: var(--accent-subtext);
font-size: 0.8rem; font-size: 0.8rem;
} }
#popup-content { .popup-content {
overflow-x: scroll; overflow-x: scroll;
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
@ -546,7 +564,7 @@ button:active,
.bullpadding { .bullpadding {
padding-left: 0.58rem; padding-left: 0.58rem;
} }
#popup-header { .popup-header {
position: absolute; position: absolute;
z-index: 999; z-index: 999;
padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem); padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem);
@ -628,16 +646,16 @@ button:active,
.switch:focus { .switch:focus {
box-shadow: var(--inset-focus) inset; box-shadow: var(--inset-focus) inset;
} }
#popup-tabs .switch { .popup-tabs .switch {
background: none; background: none;
} }
.desktop #popup-tabs .switch:hover, .desktop .popup-tabs .switch:hover,
#popup-tabs .switch:active { .popup-tabs .switch:active {
background: var(--accent-hover-transparent); background: var(--accent-hover-transparent);
box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset; box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset;
} }
.switch[data-enabled="true"], .switch[data-enabled="true"],
#popup-tabs .switch[data-enabled="true"] { .popup-tabs .switch[data-enabled="true"] {
color: var(--background); color: var(--background);
background: var(--accent)!important; background: var(--accent)!important;
cursor: default; cursor: default;
@ -675,20 +693,20 @@ button:active,
padding: var(--gap-no-icon); padding: var(--gap-no-icon);
overflow: clip; overflow: clip;
} }
#back-button { .back-button {
padding: 0; padding: 0;
background: none; background: none;
max-width: 4rem; max-width: 4rem;
font-size: 1rem; font-size: 1rem;
} }
#back-button svg path, .back-button svg path,
.collapse-indicator svg path { .collapse-indicator svg path {
fill: var(--accent); fill: var(--accent);
} }
.popup-tab-content[data-enabled="false"] { .popup-tab-content[data-enabled="false"] {
display: none; display: none;
} }
#popup-tabs { .popup-tabs {
z-index: 999; z-index: 999;
bottom: 0; bottom: 0;
position: absolute; position: absolute;
@ -725,11 +743,13 @@ button:active,
} }
#picker-holder { #picker-holder {
display: flex; display: flex;
justify-content: space-between; justify-content: start;
flex-wrap: wrap; flex-wrap: wrap;
align-content: space-around; align-content: space-around;
padding-top: calc(env(safe-area-inset-top)/2 + 7.6rem); padding-top: calc(env(safe-area-inset-top)/2 + 7.6rem);
padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem);
padding-left: 0.2rem;
padding-right: 0.2rem;
} }
.imageBlock { .imageBlock {
width: 100%; width: 100%;
@ -759,9 +779,6 @@ button:active,
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
} }
.collapse-list.last {
margin-bottom: 1rem;
}
.collapse-header { .collapse-header {
padding: 0.5rem var(--padding-1); padding: 0.5rem var(--padding-1);
font-size: 0.95rem; font-size: 0.95rem;
@ -793,6 +810,7 @@ button:active,
.collapse-body { .collapse-body {
display: none; display: none;
padding: var(--padding-1); padding: var(--padding-1);
padding-bottom: 1rem;
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
@ -805,13 +823,9 @@ button:active,
#pd-share { #pd-share {
display: none; display: none;
} }
#about-donate-footer {
box-shadow: 0 0 0 0.1rem var(--red) inset, 0 0 0.6rem 0 var(--red);
z-index: 1;
}
.popup-content-inner, .popup-content-inner,
.tab-content-settings, .tab-content-settings,
#popup-header-contents { .popup-header-contents {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
} }
@ -888,64 +902,111 @@ button:active,
opacity: 1; opacity: 1;
transition: opacity 0.2s ease-out; transition: opacity 0.2s ease-out;
} }
.no-animation #home {
transition: none;
}
.sponsored-by-text {
text-align: center!important;
font-size: .85rem;
color: var(--accent-subtext);
user-select: none;
}
#sponsored-logos {
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.2rem 1rem;
margin-bottom: 1rem;
}
.sponsored-logo svg {
height: inherit;
width: inherit;
}
.sponsored-logo svg path {
fill: var(--accent-subtext);
}
#filename-preview {
background: var(--accent-button);
margin-top: 0.8rem;
}
.filename-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 1rem;
padding: 0.5rem 0.7rem;
}
.filename-item.line {
border-bottom: 0.1rem solid var(--accent-button-elevated);
}
.filename-label {
color: var(--accent-subtext);
font-size: 0.8rem;
}
.filename-container {
overflow-wrap: anywhere;
}
/* rounded corners */ /* rounded corners */
#bottom #paste, #bottom #paste,
#footer .switch, #footer .switch,
#audioMode, #audioMode,
#popup-content .switches, .popup-content .switches,
.checkbox, .checkbox,
.changelog-img, .changelog-img,
.changelog-banner, .changelog-banner,
#close-error, .close-error,
.changelog-tag-version, .changelog-tag-version,
#download-switcher .switch, #download-switcher .switch,
#popup-about .switch, #popup-about .switch,
#popup-tabs .switch, .popup-tabs .switch,
.text-to-copy, .text-to-copy,
.text-to-copy.text-backdrop { .text-to-copy.text-backdrop,
border-radius: 5px / 6px; #filename-preview {
border-radius: 6px / 7px;
} }
[type=checkbox] { [type=checkbox] {
border-radius: 3px / 4px; border-radius: 3px / 4px;
} }
.popup, .popup,
.scrollable #popup-content { .scrollable .popup-content {
border-radius: 8px / 9px; border-radius: 8px;
} }
#popup-header .glass-bkg { .popup-header .glass-bkg {
border-top-left-radius: 8px 9px; border-top-left-radius: 8px 9px;
border-top-right-radius: 8px 9px; border-top-right-radius: 8px 9px;
border-bottom: var(--accent-highlight) solid 0.1rem; border-bottom: var(--accent-highlight) solid 0.1rem;
top: -1px; top: -1px;
} }
#popup-tabs .glass-bkg { .popup-tabs .glass-bkg {
border-bottom-left-radius: 8px 9px; border-bottom-left-radius: 8px 9px;
border-bottom-right-radius: 8px 9px; border-bottom-right-radius: 8px 9px;
border-top: var(--accent-highlight) solid 0.1rem; border-top: var(--accent-highlight) solid 0.1rem;
bottom: -1px; bottom: -1px;
} }
.switches .first { .switches :first-child {
border-top-left-radius: 5px 6px; border-top-left-radius: 6px 7px;
border-bottom-left-radius: 5px 6px; border-bottom-left-radius: 6px 7px;
} }
.switches .last { .switches :last-child {
border-top-right-radius: 5px 6px; border-top-right-radius: 6px 7px;
border-bottom-right-radius: 5px 6px; border-bottom-right-radius: 6px 7px;
} }
.text-backdrop { .text-backdrop {
border-radius: 3px / 4px; border-radius: 3px / 4px;
} }
.collapse-list.first, .collapse-list:first-child,
.collapse-list.first .collapse-header { .collapse-list:first-child .collapse-header {
border-top-left-radius: 6px 7px; border-top-left-radius: 7px 8px;
border-top-right-radius: 6px 7px; border-top-right-radius: 7px 8px;
} }
.collapse-list.last, .collapse-list:last-child,
.collapse-list.last .collapse-header { .collapse-list:last-child .collapse-header {
border-bottom-left-radius: 6px 7px; border-bottom-left-radius: 7px 8px;
border-bottom-right-radius: 6px 7px; border-bottom-right-radius: 7px 8px;
} }
.collapse-list.last.expanded .collapse-header { .collapse-list:last-child.expanded .collapse-header {
border-radius: 0; border-radius: 0;
} }
/* prevent resizing fliecker on ios if web app is installed as standalone */ /* prevent resizing fliecker on ios if web app is installed as standalone */
@ -964,9 +1025,6 @@ button:active,
} }
} }
@media screen and (max-width: 1440px) { @media screen and (max-width: 1440px) {
#cobalt-main-box {
width: 65%;
}
.popup.small { .popup.small {
width: 30% width: 30%
} }
@ -980,9 +1038,6 @@ button:active,
} }
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
#cobalt-main-box {
width: 70%;
}
.popup.small { .popup.small {
width: 35% width: 35%
} }
@ -991,9 +1046,6 @@ button:active,
} }
} }
@media screen and (max-width: 1025px) { @media screen and (max-width: 1025px) {
#cobalt-main-box {
width: 75%;
}
.popup.small { .popup.small {
width: 40% width: 40%
} }
@ -1018,14 +1070,14 @@ button:active,
width: calc(100% - 1.3rem); width: calc(100% - 1.3rem);
} }
} }
@media screen and (max-width: 720px) { @media screen and (max-width: 660px) {
#cobalt-main-box { #cobalt-main-box {
width: calc(100% - (0.7rem * 2)); width: calc(100% - (0.7rem * 2));
} }
#cobalt-main-box #bottom { #cobalt-main-box #bottom {
flex-direction: column-reverse; flex-direction: row-reverse;
} }
#cobalt-main-box #bottom button { #cobalt-main-box #bottom #audioMode button, #audioMode {
width: 100%; width: 100%;
} }
#footer { #footer {
@ -1056,12 +1108,12 @@ button:active,
padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem); padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem);
} }
.popup, .popup,
#popup-header .glass-bkg, .popup-header .glass-bkg,
#popup-tabs .glass-bkg, .popup-tabs .glass-bkg,
.glass-bkg.small { .glass-bkg.small {
border-radius: 0; border-radius: 0;
} }
#popup-tabs .glass-bkg { .popup-tabs .glass-bkg {
bottom: 0; bottom: 0;
} }
.switches { .switches {
@ -1092,17 +1144,21 @@ button:active,
} }
.popup.small.visible { .popup.small.visible {
transform: none; transform: none;
transition: transform 200ms cubic-bezier(0.075, 0.82, 0.165, 1), opacity 130ms ease-in-out; transition: transform 210ms cubic-bezier(0.062, 0.82, 0.165, 1), opacity 130ms ease-in-out;
} }
.popup.small #popup-header { .popup.small .popup-header {
background: none; background: none;
} }
.no-animation .popup.small { .no-animation .popup.small {
transition: none; transition: none;
} }
#close-error { .close-error {
bottom: 3rem; bottom: 3rem;
} }
#picker-holder {
padding-left: 0;
padding-right: 0;
}
#picker-holder::-webkit-scrollbar { #picker-holder::-webkit-scrollbar {
display: none; display: none;
} }
@ -1119,22 +1175,14 @@ button:active,
max-height: 100%; max-height: 100%;
box-shadow: none; box-shadow: none;
} }
#popup-tabs { .popup-tabs {
padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem);
} }
.bottom-link {
padding-bottom: 2rem;
}
.popup-content-inner, .popup-content-inner,
.tab-content-settings, .tab-content-settings,
.popup-tabs-child, .popup-tabs-child,
#popup-header-contents { .popup-header-contents {
padding-left: 0.7rem; padding-left: 0.7rem;
padding-right: 0.7rem; padding-right: 0.7rem;
} }
} }
@media screen and (max-width: 400px) {
.popup-title {
line-height: inherit;
}
}

View file

@ -1,4 +1,4 @@
const version = 37; const version = 41;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isIOS = ua.match("iphone os"); const isIOS = ua.match("iphone os");
@ -17,7 +17,8 @@ const switchers = {
"aFormat": ["mp3", "best", "ogg", "wav", "opus"], "aFormat": ["mp3", "best", "ogg", "wav", "opus"],
"dubLang": ["original", "auto"], "dubLang": ["original", "auto"],
"vimeoDash": ["false", "true"], "vimeoDash": ["false", "true"],
"audioMode": ["false", "true"] "audioMode": ["false", "true"],
"filenamePattern": ["classic", "pretty", "basic", "nerdy"]
}; };
const checkboxes = [ const checkboxes = [
"alwaysVisibleButton", "alwaysVisibleButton",
@ -29,18 +30,25 @@ const checkboxes = [
"reduceTransparency", "reduceTransparency",
"disableAnimations", "disableAnimations",
"disableMetadata", "disableMetadata",
"twitterGif",
]; ];
const exceptions = { // used for mobile devices const exceptions = { // used for mobile devices
"vQuality": "720" "vQuality": "720"
}; };
const bottomPopups = ["error", "download"] const bottomPopups = ["error", "download"];
const pageQuery = new URLSearchParams(window.location.search); const pageQuery = new URLSearchParams(window.location.search);
let store = {}; let store = {};
function changeAPI(url) { function fixApiUrl(url) {
apiURL = url; return url.endsWith('/') ? url.slice(0, -1) : url
}
let apiURL = fixApiUrl(defaultApiUrl);
function changeApi(url) {
apiURL = fixApiUrl(url);
return true return true
} }
function eid(id) { function eid(id) {
@ -127,6 +135,8 @@ function detectColorScheme() {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
} }
function changeTab(evnt, tabId, tabClass) { function changeTab(evnt, tabId, tabClass) {
if (tabId === "tab-settings-other") updateFilenamePreview();
let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`); let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`);
let tablinks = document.getElementsByClassName(`tab-${tabClass}`); let tablinks = document.getElementsByClassName(`tab-${tabClass}`);
@ -194,7 +204,7 @@ function popup(type, action, text) {
store.isPopupOpen = true; store.isPopupOpen = true;
switch (type) { switch (type) {
case "about": case "about":
let tabId = sGet("seenAbout") ? "changelog" : "about"; let tabId = sGet("changelogStatus") !== `${version}` ? "changelog" : "about";
if (text) tabId = text; if (text) tabId = text;
eid(`tab-button-${type}-${tabId}`).click(); eid(`tab-button-${type}-${tabId}`).click();
break; break;
@ -211,11 +221,11 @@ function popup(type, action, text) {
if (navigator.canShare) eid("pd-share").style.display = "flex"; if (navigator.canShare) eid("pd-share").style.display = "flex";
break; break;
case "picker": case "picker":
eid("picker-title").innerHTML = loc.MediaPickerTitle;
eid("picker-subtitle").innerHTML = isMobile ? loc.MediaPickerExplanationPhone : loc.MediaPickerExplanationPC;
switch (text.type) { switch (text.type) {
case "images": case "images":
eid("picker-title").innerHTML = loc.ImagePickerTitle;
eid("picker-subtitle").innerHTML = isMobile ? loc.ImagePickerExplanationPhone : loc.ImagePickerExplanationPC;
eid("picker-holder").classList.remove("various"); eid("picker-holder").classList.remove("various");
eid("picker-download").href = text.audio; eid("picker-download").href = text.audio;
@ -226,14 +236,11 @@ function popup(type, action, text) {
`<a class="picker-image-container" ${ `<a class="picker-image-container" ${
isIOS ? `onClick="share('${text.arr[i]["url"]}')"` : `href="${text.arr[i]["url"]}" target="_blank"` isIOS ? `onClick="share('${text.arr[i]["url"]}')"` : `href="${text.arr[i]["url"]}" target="_blank"`
}>` + }>` +
`<img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'"></img>` + `<img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'">` +
`</a>` `</a>`
} }
break; break;
default: default:
eid("picker-title").innerHTML = loc.MediaPickerTitle;
eid("picker-subtitle").innerHTML = isMobile ? loc.MediaPickerExplanationPhone : loc.MediaPickerExplanationPC;
eid("picker-holder").classList.add("various"); eid("picker-holder").classList.add("various");
for (let i in text.arr) { for (let i in text.arr) {
@ -243,7 +250,7 @@ function popup(type, action, text) {
}>` + }>` +
`<div class="picker-element-name">${text.arr[i].type}</div>` + `<div class="picker-element-name">${text.arr[i].type}</div>` +
(text.arr[i].type === 'photo' ? '' : '<div class="imageBlock"></div>') + (text.arr[i].type === 'photo' ? '' : '<div class="imageBlock"></div>') +
`<img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'"></img>` + `<img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'">` +
`</a>` `</a>`
} }
eid("picker-download").classList.remove("visible"); eid("picker-download").classList.remove("visible");
@ -274,6 +281,7 @@ function changeSwitcher(li, b) {
(switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) (switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
} }
if (li === "theme") detectColorScheme(); if (li === "theme") detectColorScheme();
if (li === "filenamePattern") updateFilenamePreview();
} else { } else {
let pref = switchers[li][0]; let pref = switchers[li][0];
if (isMobile && exceptions[li]) pref = exceptions[li]; if (isMobile && exceptions[li]) pref = exceptions[li];
@ -292,28 +300,6 @@ function checkbox(action) {
} }
action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck(); action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
} }
function loadSettings() {
if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem';
}
if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true;
}
if (sGet("reduceTransparency") === "true" || isOldFirefox) {
eid("cobalt-body").classList.add('no-transparency');
}
if (sGet("disableAnimations") === "true") {
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;
}
for (let i in switchers) {
changeSwitcher(i, sGet(i))
}
}
function changeButton(type, text) { function changeButton(type, text) {
switch (type) { switch (type) {
case 0: //error case 0: //error
@ -370,8 +356,9 @@ async function download(url) {
eid("url-clear").style.display = "none"; eid("url-clear").style.display = "none";
eid("url-input-area").disabled = true; eid("url-input-area").disabled = true;
let req = { let req = {
url: encodeURIComponent(url.split("&")[0].split('%')[0]), url,
aFormat: sGet("aFormat").slice(0, 4), aFormat: sGet("aFormat").slice(0, 4),
filenamePattern: sGet("filenamePattern"),
dubLang: false dubLang: false
} }
if (sGet("dubLang") === "auto") { if (sGet("dubLang") === "auto") {
@ -392,6 +379,7 @@ async function download(url) {
} }
if (sGet("disableMetadata") === "true") req.disableMetadata = true; if (sGet("disableMetadata") === "true") req.disableMetadata = true;
if (sGet("twitterGif") === "true") req.twitterGif = true;
let j = await fetch(`${apiURL}/api/json`, { let j = await fetch(`${apiURL}/api/json`, {
method: "POST", method: "POST",
@ -434,9 +422,13 @@ async function download(url) {
let jp = await res.json(); let jp = await res.json();
if (jp.status === "continue") { if (jp.status === "continue") {
changeDownloadButton(2, '>>>'); changeDownloadButton(2, '>>>');
if (isMobile || isSafari) { if (sGet("downloadPopup") === "true") {
window.location.href = j.url; popup('download', 1, j.url)
} else window.open(j.url, '_blank'); } else {
if (isMobile || isSafari) {
window.location.href = j.url;
} else window.open(j.url, '_blank');
}
setTimeout(() => { changeButton(1) }, 2500); setTimeout(() => { changeButton(1) }, 2500);
} else { } else {
changeButton(0, jp.text); changeButton(0, jp.text);
@ -514,6 +506,73 @@ function unpackSettings(b64) {
} }
return changed return changed
} }
function updateFilenamePreview() {
let videoFilePreview = ``;
let audioFilePreview = ``;
let resMatch = {
"max": "3840x2160",
"2160": "3840x2160",
"1440": "2560x1440",
"1080": "1920x1080",
"720": "1280x720",
"480": "854x480",
"360": "640x360",
}
// "dubLang"
// sGet("muteAudio") === "true"
switch(sGet("filenamePattern")) {
case "classic":
videoFilePreview = `youtube_yPYZpwSpKmA_${resMatch[sGet('vQuality')]}_${sGet('vCodec')}`
+ `${sGet("muteAudio") === "true" ? "_mute" : ""}.${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
audioFilePreview = `youtube_yPYZpwSpKmA_audio.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
break;
case "pretty":
videoFilePreview =
`${loc.FilenamePreviewVideoTitle} `
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
break;
case "basic":
videoFilePreview =
`${loc.FilenamePreviewVideoTitle} `
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}${sGet("muteAudio") === "true" ? " mute" : ""}).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor}.${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
break;
case "nerdy":
videoFilePreview =
`${loc.FilenamePreviewVideoTitle} `
+ `(${sGet('vQuality') === "max" ? "2160p" : `${sGet('vQuality')}p`}, ${sGet('vCodec')}, `
+ `${sGet("muteAudio") === "true" ? "mute, " : ""}youtube, yPYZpwSpKmA).${sGet('vCodec') === "vp9" ? 'webm' : 'mp4'}`;
audioFilePreview = `${loc.FilenamePreviewAudioTitle} - ${loc.FilenamePreviewAudioAuthor} (soundcloud, 1242868615).${sGet('aFormat') !== "best" ? sGet('aFormat') : 'opus'}`;
break;
}
eid("video-filename-text").innerHTML = videoFilePreview
eid("audio-filename-text").innerHTML = audioFilePreview
}
function loadSettings() {
if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem';
}
if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true;
}
if (sGet("reduceTransparency") === "true" || isOldFirefox) {
eid("cobalt-body").classList.add('no-transparency');
}
if (sGet("disableAnimations") === "true") {
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;
}
for (let i in switchers) {
changeSwitcher(i, sGet(i))
}
updateFilenamePreview()
}
window.onload = () => { window.onload = () => {
loadCelebrationsEmoji(); loadCelebrationsEmoji();
@ -541,9 +600,9 @@ window.onload = () => {
if (setUn !== null) { if (setUn !== null) {
if (setUn) { if (setUn) {
sSet("migrated", "true") sSet("migrated", "true")
eid("desc-migration").innerHTML += `<br/><br/>${loc.DataTransferSuccess}` eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferSuccess}`
} else { } else {
eid("desc-migration").innerHTML += `<br/><br/>${loc.DataTransferError}` eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferError}`
} }
} }
} }
@ -554,6 +613,11 @@ window.onload = () => {
window.history.replaceState(null, '', window.location.pathname); window.history.replaceState(null, '', window.location.pathname);
notificationCheck(); notificationCheck();
// fix for animations not working in Safari
if (isIOS) {
document.addEventListener('touchstart', () => {}, true);
}
} }
eid("url-input-area").addEventListener("keydown", (e) => { eid("url-input-area").addEventListener("keydown", (e) => {
button(); button();
@ -563,9 +627,11 @@ eid("url-input-area").addEventListener("keyup", (e) => {
}) })
document.onkeydown = (e) => { document.onkeydown = (e) => {
if (!store.isPopupOpen) { if (!store.isPopupOpen) {
if (e.ctrlKey || e.key === "/") eid("url-input-area").focus(); if (e.metaKey || e.ctrlKey || e.key === "/") eid("url-input-area").focus();
if (e.key === "Escape" || e.key === "Clear") clearInput(); if (e.key === "Escape" || e.key === "Clear") clearInput();
if (e.target === eid("url-input-area")) return;
// top buttons // top buttons
if (e.key === "D") pasteClipboard(); if (e.key === "D") pasteClipboard();
if (e.key === "K") changeSwitcher('audioMode', 'false'); if (e.key === "K") changeSwitcher('audioMode', 'false');

View file

@ -0,0 +1,338 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_iiii_18_20100)">
<rect x="2.09375" y="4.96875" width="27.875" height="22.0312" rx="0.8" fill="#5E4F86" />
</g>
<g filter="url(#filter1_f_18_20100)">
<path d="M2.57935 21.9844V10.0156H5.96764C6.92614 10.0156 7.70317 10.8663 7.70317 11.9156V20.0844C7.70317 21.1337 6.92614 21.9844 5.96764 21.9844H2.57935Z" stroke="#463875" stroke-width="0.65" />
<path d="M2.57935 21.9844V10.0156H5.96764C6.92614 10.0156 7.70317 10.8663 7.70317 11.9156V20.0844C7.70317 21.1337 6.92614 21.9844 5.96764 21.9844H2.57935Z" stroke="url(#paint0_linear_18_20100)" stroke-width="0.65" />
</g>
<g filter="url(#filter2_f_18_20100)">
<path d="M29.4983 10.0156V21.9844H26.1C25.1387 21.9844 24.3594 21.1337 24.3594 20.0844V11.9156C24.3594 10.8663 25.1387 10.0156 26.1 10.0156H29.4983Z" stroke="#463875" stroke-width="0.65" />
<path d="M29.4983 10.0156V21.9844H26.1C25.1387 21.9844 24.3594 21.1337 24.3594 20.0844V11.9156C24.3594 10.8663 25.1387 10.0156 26.1 10.0156H29.4983Z" stroke="url(#paint1_linear_18_20100)" stroke-width="0.65" />
</g>
<g filter="url(#filter3_f_18_20100)">
<path d="M9.94531 11.9156C9.94531 10.8663 10.796 10.0156 11.8453 10.0156H20.1547C21.204 10.0156 22.0547 10.8663 22.0547 11.9156V20.0844C22.0547 21.1337 21.204 21.9844 20.1547 21.9844H11.8453C10.796 21.9844 9.94531 21.1337 9.94531 20.0844V11.9156Z" stroke="#463875" stroke-width="0.65" />
<path d="M9.94531 11.9156C9.94531 10.8663 10.796 10.0156 11.8453 10.0156H20.1547C21.204 10.0156 22.0547 10.8663 22.0547 11.9156V20.0844C22.0547 21.1337 21.204 21.9844 20.1547 21.9844H11.8453C10.796 21.9844 9.94531 21.1337 9.94531 20.0844V11.9156Z" stroke="url(#paint2_linear_18_20100)" stroke-width="0.65" />
</g>
<g filter="url(#filter4_i_18_20100)">
<path d="M29.9688 10.0156V21.9844H26.2594C25.21 21.9844 24.3594 21.1337 24.3594 20.0844V11.9156C24.3594 10.8663 25.21 10.0156 26.2594 10.0156H29.9688Z" fill="#7BBEE4" />
</g>
<path d="M29.9688 10.0156V21.9844H26.2594C25.21 21.9844 24.3594 21.1337 24.3594 20.0844V11.9156C24.3594 10.8663 25.21 10.0156 26.2594 10.0156H29.9688Z" fill="url(#paint3_linear_18_20100)" />
<g filter="url(#filter5_i_18_20100)">
<path d="M9.94531 11.9156C9.94531 10.8663 10.796 10.0156 11.8453 10.0156H20.1547C21.204 10.0156 22.0547 10.8663 22.0547 11.9156V20.0844C22.0547 21.1337 21.204 21.9844 20.1547 21.9844H11.8453C10.796 21.9844 9.94531 21.1337 9.94531 20.0844V11.9156Z" fill="#7BBEE4" />
</g>
<g filter="url(#filter6_i_18_20100)">
<path d="M2.09375 21.9844V10.0156H5.80313C6.85246 10.0156 7.70312 10.8663 7.70312 11.9156V20.0844C7.70312 21.1337 6.85246 21.9844 5.80313 21.9844H2.09375Z" fill="#7BBEE4" />
</g>
<g filter="url(#filter7_f_18_20100)">
<path d="M4.0625 6.75781C4.0625 6.61974 4.17443 6.50781 4.3125 6.50781H5.85938C5.99745 6.50781 6.10938 6.61974 6.10938 6.75781V8.30469C6.10938 8.44276 5.99745 8.55469 5.85938 8.55469H4.3125C4.17443 8.55469 4.0625 8.44276 4.0625 8.30469V6.75781Z" stroke="#43346D" stroke-width="0.6" />
<path d="M4.0625 6.75781C4.0625 6.61974 4.17443 6.50781 4.3125 6.50781H5.85938C5.99745 6.50781 6.10938 6.61974 6.10938 6.75781V8.30469C6.10938 8.44276 5.99745 8.55469 5.85938 8.55469H4.3125C4.17443 8.55469 4.0625 8.44276 4.0625 8.30469V6.75781Z" stroke="url(#paint4_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter8_f_18_20100)">
<path d="M4.0625 6.75781C4.0625 6.61974 4.17443 6.50781 4.3125 6.50781H5.85938C5.99745 6.50781 6.10938 6.61974 6.10938 6.75781V8.30469C6.10938 8.44276 5.99745 8.55469 5.85938 8.55469H4.3125C4.17443 8.55469 4.0625 8.44276 4.0625 8.30469V6.75781Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter9_f_18_20100)">
<path d="M9.53125 6.75781C9.53125 6.61974 9.64318 6.50781 9.78125 6.50781H11.3281C11.4662 6.50781 11.5781 6.61974 11.5781 6.75781V8.30469C11.5781 8.44276 11.4662 8.55469 11.3281 8.55469H9.78125C9.64318 8.55469 9.53125 8.44276 9.53125 8.30469V6.75781Z" stroke="#43346D" stroke-width="0.6" />
<path d="M9.53125 6.75781C9.53125 6.61974 9.64318 6.50781 9.78125 6.50781H11.3281C11.4662 6.50781 11.5781 6.61974 11.5781 6.75781V8.30469C11.5781 8.44276 11.4662 8.55469 11.3281 8.55469H9.78125C9.64318 8.55469 9.53125 8.44276 9.53125 8.30469V6.75781Z" stroke="url(#paint5_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter10_f_18_20100)">
<path d="M9.53125 6.75781C9.53125 6.61974 9.64318 6.50781 9.78125 6.50781H11.3281C11.4662 6.50781 11.5781 6.61974 11.5781 6.75781V8.30469C11.5781 8.44276 11.4662 8.55469 11.3281 8.55469H9.78125C9.64318 8.55469 9.53125 8.44276 9.53125 8.30469V6.75781Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter11_f_18_20100)">
<path d="M15.0078 6.75781C15.0078 6.61974 15.1197 6.50781 15.2578 6.50781H16.8047C16.9428 6.50781 17.0547 6.61974 17.0547 6.75781V8.30469C17.0547 8.44276 16.9428 8.55469 16.8047 8.55469H15.2578C15.1197 8.55469 15.0078 8.44276 15.0078 8.30469V6.75781Z" stroke="#43346D" stroke-width="0.6" />
<path d="M15.0078 6.75781C15.0078 6.61974 15.1197 6.50781 15.2578 6.50781H16.8047C16.9428 6.50781 17.0547 6.61974 17.0547 6.75781V8.30469C17.0547 8.44276 16.9428 8.55469 16.8047 8.55469H15.2578C15.1197 8.55469 15.0078 8.44276 15.0078 8.30469V6.75781Z" stroke="url(#paint6_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter12_f_18_20100)">
<path d="M15.0078 6.75781C15.0078 6.61974 15.1197 6.50781 15.2578 6.50781H16.8047C16.9428 6.50781 17.0547 6.61974 17.0547 6.75781V8.30469C17.0547 8.44276 16.9428 8.55469 16.8047 8.55469H15.2578C15.1197 8.55469 15.0078 8.44276 15.0078 8.30469V6.75781Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter13_f_18_20100)">
<path d="M20.4688 6.75781C20.4688 6.61974 20.5807 6.50781 20.7188 6.50781H22.2656C22.4037 6.50781 22.5156 6.61974 22.5156 6.75781V8.30469C22.5156 8.44276 22.4037 8.55469 22.2656 8.55469H20.7188C20.5807 8.55469 20.4688 8.44276 20.4688 8.30469V6.75781Z" stroke="#43346D" stroke-width="0.6" />
<path d="M20.4688 6.75781C20.4688 6.61974 20.5807 6.50781 20.7188 6.50781H22.2656C22.4037 6.50781 22.5156 6.61974 22.5156 6.75781V8.30469C22.5156 8.44276 22.4037 8.55469 22.2656 8.55469H20.7188C20.5807 8.55469 20.4688 8.44276 20.4688 8.30469V6.75781Z" stroke="url(#paint7_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter14_f_18_20100)">
<path d="M20.4688 6.75781C20.4688 6.61974 20.5807 6.50781 20.7188 6.50781H22.2656C22.4037 6.50781 22.5156 6.61974 22.5156 6.75781V8.30469C22.5156 8.44276 22.4037 8.55469 22.2656 8.55469H20.7188C20.5807 8.55469 20.4688 8.44276 20.4688 8.30469V6.75781Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter15_f_18_20100)">
<path d="M25.9375 6.75781C25.9375 6.61974 26.0494 6.50781 26.1875 6.50781H27.7344C27.8724 6.50781 27.9844 6.61974 27.9844 6.75781V8.30469C27.9844 8.44276 27.8724 8.55469 27.7344 8.55469H26.1875C26.0494 8.55469 25.9375 8.44276 25.9375 8.30469V6.75781Z" stroke="#43346D" stroke-width="0.6" />
<path d="M25.9375 6.75781C25.9375 6.61974 26.0494 6.50781 26.1875 6.50781H27.7344C27.8724 6.50781 27.9844 6.61974 27.9844 6.75781V8.30469C27.9844 8.44276 27.8724 8.55469 27.7344 8.55469H26.1875C26.0494 8.55469 25.9375 8.44276 25.9375 8.30469V6.75781Z" stroke="url(#paint8_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter16_f_18_20100)">
<path d="M25.9375 6.75781C25.9375 6.61974 26.0494 6.50781 26.1875 6.50781H27.7344C27.8724 6.50781 27.9844 6.61974 27.9844 6.75781V8.30469C27.9844 8.44276 27.8724 8.55469 27.7344 8.55469H26.1875C26.0494 8.55469 25.9375 8.44276 25.9375 8.30469V6.75781Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter17_f_18_20100)">
<path d="M4.0625 23.6953C4.0625 23.5572 4.17443 23.4453 4.3125 23.4453H5.85938C5.99745 23.4453 6.10938 23.5572 6.10938 23.6953V25.2422C6.10938 25.3803 5.99745 25.4922 5.85938 25.4922H4.3125C4.17443 25.4922 4.0625 25.3803 4.0625 25.2422V23.6953Z" stroke="#43346D" stroke-width="0.6" />
<path d="M4.0625 23.6953C4.0625 23.5572 4.17443 23.4453 4.3125 23.4453H5.85938C5.99745 23.4453 6.10938 23.5572 6.10938 23.6953V25.2422C6.10938 25.3803 5.99745 25.4922 5.85938 25.4922H4.3125C4.17443 25.4922 4.0625 25.3803 4.0625 25.2422V23.6953Z" stroke="url(#paint9_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter18_f_18_20100)">
<path d="M9.53125 23.6953C9.53125 23.5572 9.64318 23.4453 9.78125 23.4453H11.3281C11.4662 23.4453 11.5781 23.5572 11.5781 23.6953V25.2422C11.5781 25.3803 11.4662 25.4922 11.3281 25.4922H9.78125C9.64318 25.4922 9.53125 25.3803 9.53125 25.2422V23.6953Z" stroke="#43346D" stroke-width="0.6" />
<path d="M9.53125 23.6953C9.53125 23.5572 9.64318 23.4453 9.78125 23.4453H11.3281C11.4662 23.4453 11.5781 23.5572 11.5781 23.6953V25.2422C11.5781 25.3803 11.4662 25.4922 11.3281 25.4922H9.78125C9.64318 25.4922 9.53125 25.3803 9.53125 25.2422V23.6953Z" stroke="url(#paint10_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter19_f_18_20100)">
<path d="M15 23.6953C15 23.5572 15.1119 23.4453 15.25 23.4453H16.7969C16.9349 23.4453 17.0469 23.5572 17.0469 23.6953V25.2422C17.0469 25.3803 16.9349 25.4922 16.7969 25.4922H15.25C15.1119 25.4922 15 25.3803 15 25.2422V23.6953Z" stroke="#43346D" stroke-width="0.6" />
<path d="M15 23.6953C15 23.5572 15.1119 23.4453 15.25 23.4453H16.7969C16.9349 23.4453 17.0469 23.5572 17.0469 23.6953V25.2422C17.0469 25.3803 16.9349 25.4922 16.7969 25.4922H15.25C15.1119 25.4922 15 25.3803 15 25.2422V23.6953Z" stroke="url(#paint11_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter20_f_18_20100)">
<path d="M20.4688 23.6953C20.4688 23.5572 20.5807 23.4453 20.7188 23.4453H22.2656C22.4037 23.4453 22.5156 23.5572 22.5156 23.6953V25.2422C22.5156 25.3803 22.4037 25.4922 22.2656 25.4922H20.7188C20.5807 25.4922 20.4688 25.3803 20.4688 25.2422V23.6953Z" stroke="#43346D" stroke-width="0.6" />
<path d="M20.4688 23.6953C20.4688 23.5572 20.5807 23.4453 20.7188 23.4453H22.2656C22.4037 23.4453 22.5156 23.5572 22.5156 23.6953V25.2422C22.5156 25.3803 22.4037 25.4922 22.2656 25.4922H20.7188C20.5807 25.4922 20.4688 25.3803 20.4688 25.2422V23.6953Z" stroke="url(#paint12_linear_18_20100)" stroke-width="0.6" />
</g>
<g filter="url(#filter21_f_18_20100)">
<path d="M25.9375 23.6953C25.9375 23.5572 26.0494 23.4453 26.1875 23.4453H27.7344C27.8724 23.4453 27.9844 23.5572 27.9844 23.6953V25.2422C27.9844 25.3803 27.8724 25.4922 27.7344 25.4922H26.1875C26.0494 25.4922 25.9375 25.3803 25.9375 25.2422V23.6953Z" stroke="#43346D" stroke-width="0.6" />
<path d="M25.9375 23.6953C25.9375 23.5572 26.0494 23.4453 26.1875 23.4453H27.7344C27.8724 23.4453 27.9844 23.5572 27.9844 23.6953V25.2422C27.9844 25.3803 27.8724 25.4922 27.7344 25.4922H26.1875C26.0494 25.4922 25.9375 25.3803 25.9375 25.2422V23.6953Z" stroke="url(#paint13_linear_18_20100)" stroke-width="0.6" />
</g>
<path d="M4.0625 6.75781C4.0625 6.61974 4.17443 6.50781 4.3125 6.50781H5.85938C5.99745 6.50781 6.10938 6.61974 6.10938 6.75781V8.30469C6.10938 8.44276 5.99745 8.55469 5.85938 8.55469H4.3125C4.17443 8.55469 4.0625 8.44276 4.0625 8.30469V6.75781Z" fill="#E5E5E5" />
<path d="M9.53125 6.75781C9.53125 6.61974 9.64318 6.50781 9.78125 6.50781H11.3281C11.4662 6.50781 11.5781 6.61974 11.5781 6.75781V8.30469C11.5781 8.44276 11.4662 8.55469 11.3281 8.55469H9.78125C9.64318 8.55469 9.53125 8.44276 9.53125 8.30469V6.75781Z" fill="#E5E5E5" />
<path d="M15 6.75781C15 6.61974 15.1119 6.50781 15.25 6.50781H16.7969C16.9349 6.50781 17.0469 6.61974 17.0469 6.75781V8.30469C17.0469 8.44276 16.9349 8.55469 16.7969 8.55469H15.25C15.1119 8.55469 15 8.44276 15 8.30469V6.75781Z" fill="#E5E5E5" />
<path d="M20.4688 6.75781C20.4688 6.61974 20.5807 6.50781 20.7188 6.50781H22.2656C22.4037 6.50781 22.5156 6.61974 22.5156 6.75781V8.30469C22.5156 8.44276 22.4037 8.55469 22.2656 8.55469H20.7188C20.5807 8.55469 20.4688 8.44276 20.4688 8.30469V6.75781Z" fill="#E5E5E5" />
<path d="M25.9375 6.75781C25.9375 6.61974 26.0494 6.50781 26.1875 6.50781H27.7344C27.8724 6.50781 27.9844 6.61974 27.9844 6.75781V8.30469C27.9844 8.44276 27.8724 8.55469 27.7344 8.55469H26.1875C26.0494 8.55469 25.9375 8.44276 25.9375 8.30469V6.75781Z" fill="#E5E5E5" />
<g filter="url(#filter22_f_18_20100)">
<path d="M4.0625 23.6953C4.0625 23.5572 4.17443 23.4453 4.3125 23.4453H5.85938C5.99745 23.4453 6.10938 23.5572 6.10938 23.6953V25.2422C6.10938 25.3803 5.99745 25.4922 5.85938 25.4922H4.3125C4.17443 25.4922 4.0625 25.3803 4.0625 25.2422V23.6953Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter23_f_18_20100)">
<path d="M9.53125 23.6953C9.53125 23.5572 9.64318 23.4453 9.78125 23.4453H11.3281C11.4662 23.4453 11.5781 23.5572 11.5781 23.6953V25.2422C11.5781 25.3803 11.4662 25.4922 11.3281 25.4922H9.78125C9.64318 25.4922 9.53125 25.3803 9.53125 25.2422V23.6953Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter24_f_18_20100)">
<path d="M15.0078 23.6953C15.0078 23.5572 15.1197 23.4453 15.2578 23.4453H16.8047C16.9428 23.4453 17.0547 23.5572 17.0547 23.6953V25.2422C17.0547 25.3803 16.9428 25.4922 16.8047 25.4922H15.2578C15.1197 25.4922 15.0078 25.3803 15.0078 25.2422V23.6953Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter25_f_18_20100)">
<path d="M20.4688 23.6953C20.4688 23.5572 20.5807 23.4453 20.7188 23.4453H22.2656C22.4037 23.4453 22.5156 23.5572 22.5156 23.6953V25.2422C22.5156 25.3803 22.4037 25.4922 22.2656 25.4922H20.7188C20.5807 25.4922 20.4688 25.3803 20.4688 25.2422V23.6953Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<g filter="url(#filter26_f_18_20100)">
<path d="M25.9375 23.6953C25.9375 23.5572 26.0494 23.4453 26.1875 23.4453H27.7344C27.8724 23.4453 27.9844 23.5572 27.9844 23.6953V25.2422C27.9844 25.3803 27.8724 25.4922 27.7344 25.4922H26.1875C26.0494 25.4922 25.9375 25.3803 25.9375 25.2422V23.6953Z" stroke="#422E6F" stroke-width="0.1" />
</g>
<path d="M4.0625 23.6953C4.0625 23.5572 4.17443 23.4453 4.3125 23.4453H5.85938C5.99745 23.4453 6.10938 23.5572 6.10938 23.6953V25.2422C6.10938 25.3803 5.99745 25.4922 5.85938 25.4922H4.3125C4.17443 25.4922 4.0625 25.3803 4.0625 25.2422V23.6953Z" fill="#E5E5E5" />
<path d="M9.53125 23.6953C9.53125 23.5572 9.64318 23.4453 9.78125 23.4453H11.3281C11.4662 23.4453 11.5781 23.5572 11.5781 23.6953V25.2422C11.5781 25.3803 11.4662 25.4922 11.3281 25.4922H9.78125C9.64318 25.4922 9.53125 25.3803 9.53125 25.2422V23.6953Z" fill="#E5E5E5" />
<path d="M15 23.6953C15 23.5572 15.1119 23.4453 15.25 23.4453H16.7969C16.9349 23.4453 17.0469 23.5572 17.0469 23.6953V25.2422C17.0469 25.3803 16.9349 25.4922 16.7969 25.4922H15.25C15.1119 25.4922 15 25.3803 15 25.2422V23.6953Z" fill="#E5E5E5" />
<path d="M20.4688 23.6953C20.4688 23.5572 20.5807 23.4453 20.7188 23.4453H22.2656C22.4037 23.4453 22.5156 23.5572 22.5156 23.6953V25.2422C22.5156 25.3803 22.4037 25.4922 22.2656 25.4922H20.7188C20.5807 25.4922 20.4688 25.3803 20.4688 25.2422V23.6953Z" fill="#E5E5E5" />
<path d="M25.9375 23.6953C25.9375 23.5572 26.0494 23.4453 26.1875 23.4453H27.7344C27.8724 23.4453 27.9844 23.5572 27.9844 23.6953V25.2422C27.9844 25.3803 27.8724 25.4922 27.7344 25.4922H26.1875C26.0494 25.4922 25.9375 25.3803 25.9375 25.2422V23.6953Z" fill="#E5E5E5" />
<defs>
<filter id="filter0_iiii_18_20100" x="1.79375" y="4.66875" width="28.575" height="22.6313" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="0.4" />
<feGaussianBlur stdDeviation="0.15" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.356863 0 0 0 0 0.317647 0 0 0 0 0.501961 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_20100" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="-0.4" />
<feGaussianBlur stdDeviation="0.15" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.447059 0 0 0 0 0.407843 0 0 0 0 0.572549 0 0 0 1 0" />
<feBlend mode="normal" in2="effect1_innerShadow_18_20100" result="effect2_innerShadow_18_20100" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="-0.4" />
<feGaussianBlur stdDeviation="0.15" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.376471 0 0 0 0 0.231373 0 0 0 0 0.584314 0 0 0 1 0" />
<feBlend mode="normal" in2="effect2_innerShadow_18_20100" result="effect3_innerShadow_18_20100" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="0.4" />
<feGaussianBlur stdDeviation="0.2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.313726 0 0 0 0 0.262745 0 0 0 0 0.466667 0 0 0 1 0" />
<feBlend mode="normal" in2="effect3_innerShadow_18_20100" result="effect4_innerShadow_18_20100" />
</filter>
<filter id="filter1_f_18_20100" x="1.95439" y="9.39067" width="6.3738" height="13.2187" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.15" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter2_f_18_20100" x="23.7344" y="9.39067" width="6.38894" height="13.2187" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.15" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter3_f_18_20100" x="9.32036" y="9.39067" width="13.3593" height="13.2187" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.15" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter4_i_18_20100" x="24.3594" y="10.0156" width="6.60938" height="11.9688" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="1" />
<feGaussianBlur stdDeviation="1.5" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.321569 0 0 0 0 0.615686 0 0 0 0 0.764706 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_20100" />
</filter>
<filter id="filter5_i_18_20100" x="9.94531" y="10.0156" width="12.1094" height="11.9688" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.321569 0 0 0 0 0.615686 0 0 0 0 0.764706 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_20100" />
</filter>
<filter id="filter6_i_18_20100" x="1.09375" y="10.0156" width="6.60938" height="11.9688" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="-1" />
<feGaussianBlur stdDeviation="1.5" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.321569 0 0 0 0 0.615686 0 0 0 0 0.764706 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_20100" />
</filter>
<filter id="filter7_f_18_20100" x="3.51245" y="5.95776" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter8_f_18_20100" x="3.86245" y="6.30776" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter9_f_18_20100" x="8.9812" y="5.95776" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter10_f_18_20100" x="9.3312" y="6.30776" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter11_f_18_20100" x="14.4578" y="5.95776" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter12_f_18_20100" x="14.8078" y="6.30776" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter13_f_18_20100" x="19.9187" y="5.95776" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter14_f_18_20100" x="20.2687" y="6.30776" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter15_f_18_20100" x="25.3875" y="5.95776" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter16_f_18_20100" x="25.7375" y="6.30776" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter17_f_18_20100" x="3.51245" y="22.8953" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter18_f_18_20100" x="8.9812" y="22.8953" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter19_f_18_20100" x="14.45" y="22.8953" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter20_f_18_20100" x="19.9187" y="22.8953" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter21_f_18_20100" x="25.3875" y="22.8953" width="3.14697" height="3.14697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter22_f_18_20100" x="3.86245" y="23.2453" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter23_f_18_20100" x="9.3312" y="23.2453" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter24_f_18_20100" x="14.8078" y="23.2453" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter25_f_18_20100" x="20.2687" y="23.2453" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<filter id="filter26_f_18_20100" x="25.7375" y="23.2453" width="2.44697" height="2.44697" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.075" result="effect1_foregroundBlur_18_20100" />
</filter>
<linearGradient id="paint0_linear_18_20100" x1="2.76942" y1="21.4319" x2="6.13388" y2="21.1993" gradientUnits="userSpaceOnUse">
<stop stop-color="#564D7C" />
<stop offset="1" stop-color="#564D7C" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint1_linear_18_20100" x1="24.55" y1="21.4319" x2="25.7998" y2="21.3309" gradientUnits="userSpaceOnUse">
<stop stop-color="#7D769C" />
<stop offset="1" stop-color="#7D769C" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint2_linear_18_20100" x1="10.3945" y1="21.4319" x2="13.255" y2="20.8871" gradientUnits="userSpaceOnUse">
<stop stop-color="#7D769C" />
<stop offset="1" stop-color="#7D769C" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint3_linear_18_20100" x1="30.8125" y1="21.9844" x2="26.6875" y2="21.9844" gradientUnits="userSpaceOnUse">
<stop stop-color="#87CDED" />
<stop offset="1" stop-color="#87CDED" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint4_linear_18_20100" x1="3.90625" y1="8.55469" x2="4.8125" y2="8.45312" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint5_linear_18_20100" x1="9.375" y1="8.55469" x2="10.2812" y2="8.45312" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint6_linear_18_20100" x1="14.8516" y1="8.55469" x2="15.7578" y2="8.45312" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint7_linear_18_20100" x1="20.3125" y1="8.55469" x2="21.2187" y2="8.45312" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint8_linear_18_20100" x1="25.7812" y1="8.55469" x2="26.6875" y2="8.45312" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint9_linear_18_20100" x1="3.90625" y1="25.4922" x2="4.8125" y2="25.3906" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint10_linear_18_20100" x1="9.375" y1="25.4922" x2="10.2812" y2="25.3906" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint11_linear_18_20100" x1="14.8437" y1="25.4922" x2="15.75" y2="25.3906" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint12_linear_18_20100" x1="20.3125" y1="25.4922" x2="21.2187" y2="25.3906" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint13_linear_18_20100" x1="25.7812" y1="25.4922" x2="26.6875" y2="25.3906" gradientUnits="userSpaceOnUse">
<stop stop-color="#7A7294" />
<stop offset="1" stop-color="#7A7294" stop-opacity="0" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,186 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_18_18382)">
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint0_radial_18_18382)" />
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint1_radial_18_18382)" />
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint2_radial_18_18382)" />
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint3_radial_18_18382)" />
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint4_radial_18_18382)" />
<path d="M8.97094 7.24512C6.29539 9.30811 5.9423 11.5837 5.9873 12.9995C6.05991 14.2784 6.22043 15.1674 6.34761 15.8717L6.36089 15.9453C6.51034 16.7742 6.17759 17.5816 5.56055 18.0928L4.91841 18.6248C4.8743 18.6613 4.86067 18.7232 4.88534 18.7749L6.09659 21.3137L4.51715 22.0672L3.3059 19.5285C2.93592 18.753 3.14034 17.8253 3.80197 17.2772L4.44412 16.7452C4.60682 16.6104 4.66887 16.4233 4.63866 16.2557L4.62452 16.1775C4.56451 15.8452 4.27047 13.8177 4.25247 13.0581C4.22614 11.9474 4.29703 8.81201 6.58812 6.15918C9.26 3.06543 13.4866 2.08105 15.9553 2.08105C18.5881 2.08105 22.7402 3.06543 25.4121 6.15918C27.7032 8.81201 27.7741 11.9474 27.7478 13.0581C27.7856 13.7659 27.4357 15.8452 27.3757 16.1775L27.3616 16.2557C27.3314 16.4233 27.3934 16.6104 27.5561 16.7452L28.1983 17.2772C28.8599 17.8253 29.0643 18.753 28.6943 19.5285L27.4831 22.0672L25.9036 21.3137L27.1149 18.7749C27.1396 18.7232 27.1259 18.6613 27.0818 18.6248L26.4397 18.0928C25.8226 17.5816 25.4899 16.7742 25.6393 15.9453L25.6526 15.8717C25.7798 15.1674 25.9403 14.2785 26.0129 12.9997C26.0579 11.5838 25.7049 9.30811 23.0293 7.24512C20.219 5.07819 17.1662 4.90137 15.9553 4.90137C14.7444 4.90137 11.7812 5.07819 8.97094 7.24512Z" fill="url(#paint5_linear_18_18382)" />
</g>
<path d="M3.72032 22.4236C3.5802 21.9624 3.78897 21.4669 4.21686 21.2451L7.50131 19.5423C7.94222 19.3137 8.4827 19.5379 8.63234 20.0115L11.1429 27.9561C11.2933 28.432 10.9759 28.928 10.4808 28.9908L6.72997 29.4669C6.24636 29.5283 5.78896 29.232 5.64724 28.7656L3.72032 22.4236Z" fill="url(#paint6_linear_18_18382)" />
<path d="M3.72032 22.4236C3.5802 21.9624 3.78897 21.4669 4.21686 21.2451L7.50131 19.5423C7.94222 19.3137 8.4827 19.5379 8.63234 20.0115L11.1429 27.9561C11.2933 28.432 10.9759 28.928 10.4808 28.9908L6.72997 29.4669C6.24636 29.5283 5.78896 29.232 5.64724 28.7656L3.72032 22.4236Z" fill="url(#paint7_linear_18_18382)" />
<path d="M3.72032 22.4236C3.5802 21.9624 3.78897 21.4669 4.21686 21.2451L7.50131 19.5423C7.94222 19.3137 8.4827 19.5379 8.63234 20.0115L11.1429 27.9561C11.2933 28.432 10.9759 28.928 10.4808 28.9908L6.72997 29.4669C6.24636 29.5283 5.78896 29.232 5.64724 28.7656L3.72032 22.4236Z" fill="url(#paint8_radial_18_18382)" />
<path d="M3.72032 22.4236C3.5802 21.9624 3.78897 21.4669 4.21686 21.2451L7.50131 19.5423C7.94222 19.3137 8.4827 19.5379 8.63234 20.0115L11.1429 27.9561C11.2933 28.432 10.9759 28.928 10.4808 28.9908L6.72997 29.4669C6.24636 29.5283 5.78896 29.232 5.64724 28.7656L3.72032 22.4236Z" fill="url(#paint9_linear_18_18382)" />
<path d="M3.72032 22.4236C3.5802 21.9624 3.78897 21.4669 4.21686 21.2451L7.50131 19.5423C7.94222 19.3137 8.4827 19.5379 8.63234 20.0115L11.1429 27.9561C11.2933 28.432 10.9759 28.928 10.4808 28.9908L6.72997 29.4669C6.24636 29.5283 5.78896 29.232 5.64724 28.7656L3.72032 22.4236Z" fill="url(#paint10_linear_18_18382)" />
<g filter="url(#filter1_i_18_18382)">
<path d="M28.263 22.4236C28.4031 21.9624 28.1943 21.4669 27.7664 21.2451L24.482 19.5423C24.0411 19.3137 23.5006 19.5379 23.3509 20.0115L20.8404 27.9561C20.69 28.432 21.0074 28.928 21.5025 28.9908L25.2533 29.4669C25.7369 29.5283 26.1943 29.232 26.336 28.7656L28.263 22.4236Z" fill="url(#paint11_linear_18_18382)" />
<path d="M28.263 22.4236C28.4031 21.9624 28.1943 21.4669 27.7664 21.2451L24.482 19.5423C24.0411 19.3137 23.5006 19.5379 23.3509 20.0115L20.8404 27.9561C20.69 28.432 21.0074 28.928 21.5025 28.9908L25.2533 29.4669C25.7369 29.5283 26.1943 29.232 26.336 28.7656L28.263 22.4236Z" fill="url(#paint12_linear_18_18382)" />
<path d="M28.263 22.4236C28.4031 21.9624 28.1943 21.4669 27.7664 21.2451L24.482 19.5423C24.0411 19.3137 23.5006 19.5379 23.3509 20.0115L20.8404 27.9561C20.69 28.432 21.0074 28.928 21.5025 28.9908L25.2533 29.4669C25.7369 29.5283 26.1943 29.232 26.336 28.7656L28.263 22.4236Z" fill="url(#paint13_radial_18_18382)" />
<path d="M28.263 22.4236C28.4031 21.9624 28.1943 21.4669 27.7664 21.2451L24.482 19.5423C24.0411 19.3137 23.5006 19.5379 23.3509 20.0115L20.8404 27.9561C20.69 28.432 21.0074 28.928 21.5025 28.9908L25.2533 29.4669C25.7369 29.5283 26.1943 29.232 26.336 28.7656L28.263 22.4236Z" fill="url(#paint14_radial_18_18382)" />
</g>
<path d="M6.46752 19.7017C6.34016 19.2787 6.57989 18.8324 7.00296 18.7051L9.338 18.0022C9.76108 17.8748 10.2073 18.1146 10.3346 18.5376L13.2486 28.2178C13.376 28.6409 13.1362 29.0871 12.7132 29.2144L10.3781 29.9173C9.95505 30.0447 9.50884 29.805 9.38149 29.3819L6.46752 19.7017Z" fill="url(#paint15_linear_18_18382)" />
<path d="M6.46752 19.7017C6.34016 19.2787 6.57989 18.8324 7.00296 18.7051L9.338 18.0022C9.76108 17.8748 10.2073 18.1146 10.3346 18.5376L13.2486 28.2178C13.376 28.6409 13.1362 29.0871 12.7132 29.2144L10.3781 29.9173C9.95505 30.0447 9.50884 29.805 9.38149 29.3819L6.46752 19.7017Z" fill="url(#paint16_radial_18_18382)" />
<path d="M6.46752 19.7017C6.34016 19.2787 6.57989 18.8324 7.00296 18.7051L9.338 18.0022C9.76108 17.8748 10.2073 18.1146 10.3346 18.5376L13.2486 28.2178C13.376 28.6409 13.1362 29.0871 12.7132 29.2144L10.3781 29.9173C9.95505 30.0447 9.50884 29.805 9.38149 29.3819L6.46752 19.7017Z" fill="url(#paint17_radial_18_18382)" />
<path d="M6.46752 19.7017C6.34016 19.2787 6.57989 18.8324 7.00296 18.7051L9.338 18.0022C9.76108 17.8748 10.2073 18.1146 10.3346 18.5376L13.2486 28.2178C13.376 28.6409 13.1362 29.0871 12.7132 29.2144L10.3781 29.9173C9.95505 30.0447 9.50884 29.805 9.38149 29.3819L6.46752 19.7017Z" fill="url(#paint18_linear_18_18382)" />
<path d="M25.3283 19.7017C25.4556 19.2787 25.2159 18.8324 24.7928 18.7051L22.4578 18.0022C22.0347 17.8748 21.5885 18.1146 21.4611 18.5376L18.5472 28.2178C18.4198 28.6409 18.6595 29.0871 19.0826 29.2144L21.4176 29.9173C21.8407 30.0447 22.2869 29.805 22.4143 29.3819L25.3283 19.7017Z" fill="url(#paint19_linear_18_18382)" />
<path d="M25.3283 19.7017C25.4556 19.2787 25.2159 18.8324 24.7928 18.7051L22.4578 18.0022C22.0347 17.8748 21.5885 18.1146 21.4611 18.5376L18.5472 28.2178C18.4198 28.6409 18.6595 29.0871 19.0826 29.2144L21.4176 29.9173C21.8407 30.0447 22.2869 29.805 22.4143 29.3819L25.3283 19.7017Z" fill="url(#paint20_linear_18_18382)" />
<path d="M25.3283 19.7017C25.4556 19.2787 25.2159 18.8324 24.7928 18.7051L22.4578 18.0022C22.0347 17.8748 21.5885 18.1146 21.4611 18.5376L18.5472 28.2178C18.4198 28.6409 18.6595 29.0871 19.0826 29.2144L21.4176 29.9173C21.8407 30.0447 22.2869 29.805 22.4143 29.3819L25.3283 19.7017Z" fill="url(#paint21_radial_18_18382)" />
<path d="M25.3283 19.7017C25.4556 19.2787 25.2159 18.8324 24.7928 18.7051L22.4578 18.0022C22.0347 17.8748 21.5885 18.1146 21.4611 18.5376L18.5472 28.2178C18.4198 28.6409 18.6595 29.0871 19.0826 29.2144L21.4176 29.9173C21.8407 30.0447 22.2869 29.805 22.4143 29.3819L25.3283 19.7017Z" fill="url(#paint22_radial_18_18382)" />
<path d="M5.98718 12.9995C5.94219 11.5837 6.29527 9.30811 8.97082 7.24512C11.7811 5.07819 14.7443 4.90137 15.9552 4.90137C17.1661 4.90137 20.2189 5.07819 23.0292 7.24512C25.7048 9.30811 26.0578 11.5838 26.0128 12.9997C25.9402 14.2785 25.7797 15.1674 25.6525 15.8717L25.6392 15.9453C25.5195 16.6093 25.7093 17.2597 26.1068 17.7549L26.7731 14.0724C26.7903 13.986 26.806 13.8988 26.8203 13.8115C26.8873 13.4016 26.9219 12.983 26.9219 12.5576C26.9219 7.50073 22.032 3.40137 16 3.40137C9.96801 3.40137 5.07812 7.50073 5.07812 12.5576C5.07812 12.9883 5.1136 13.4121 5.18224 13.8271L5.17969 13.8271L5.89691 17.7503C6.2919 17.2556 6.48014 16.6073 6.36077 15.9453L6.34749 15.8717C6.22031 15.1674 6.05979 14.2784 5.98718 12.9995Z" fill="url(#paint23_linear_18_18382)" />
<path d="M5.98718 12.9995C5.94219 11.5837 6.29527 9.30811 8.97082 7.24512C11.7811 5.07819 14.7443 4.90137 15.9552 4.90137C17.1661 4.90137 20.2189 5.07819 23.0292 7.24512C25.7048 9.30811 26.0578 11.5838 26.0128 12.9997C25.9402 14.2785 25.7797 15.1674 25.6525 15.8717L25.6392 15.9453C25.5195 16.6093 25.7093 17.2597 26.1068 17.7549L26.7731 14.0724C26.7903 13.986 26.806 13.8988 26.8203 13.8115C26.8873 13.4016 26.9219 12.983 26.9219 12.5576C26.9219 7.50073 22.032 3.40137 16 3.40137C9.96801 3.40137 5.07812 7.50073 5.07812 12.5576C5.07812 12.9883 5.1136 13.4121 5.18224 13.8271L5.17969 13.8271L5.89691 17.7503C6.2919 17.2556 6.48014 16.6073 6.36077 15.9453L6.34749 15.8717C6.22031 15.1674 6.05979 14.2784 5.98718 12.9995Z" fill="url(#paint24_radial_18_18382)" />
<path d="M5.98718 12.9995C5.94219 11.5837 6.29527 9.30811 8.97082 7.24512C11.7811 5.07819 14.7443 4.90137 15.9552 4.90137C17.1661 4.90137 20.2189 5.07819 23.0292 7.24512C25.7048 9.30811 26.0578 11.5838 26.0128 12.9997C25.9402 14.2785 25.7797 15.1674 25.6525 15.8717L25.6392 15.9453C25.5195 16.6093 25.7093 17.2597 26.1068 17.7549L26.7731 14.0724C26.7903 13.986 26.806 13.8988 26.8203 13.8115C26.8873 13.4016 26.9219 12.983 26.9219 12.5576C26.9219 7.50073 22.032 3.40137 16 3.40137C9.96801 3.40137 5.07812 7.50073 5.07812 12.5576C5.07812 12.9883 5.1136 13.4121 5.18224 13.8271L5.17969 13.8271L5.89691 17.7503C6.2919 17.2556 6.48014 16.6073 6.36077 15.9453L6.34749 15.8717C6.22031 15.1674 6.05979 14.2784 5.98718 12.9995Z" fill="url(#paint25_linear_18_18382)" />
<path d="M5.98718 12.9995C5.94219 11.5837 6.29527 9.30811 8.97082 7.24512C11.7811 5.07819 14.7443 4.90137 15.9552 4.90137C17.1661 4.90137 20.2189 5.07819 23.0292 7.24512C25.7048 9.30811 26.0578 11.5838 26.0128 12.9997C25.9402 14.2785 25.7797 15.1674 25.6525 15.8717L25.6392 15.9453C25.5195 16.6093 25.7093 17.2597 26.1068 17.7549L26.7731 14.0724C26.7903 13.986 26.806 13.8988 26.8203 13.8115C26.8873 13.4016 26.9219 12.983 26.9219 12.5576C26.9219 7.50073 22.032 3.40137 16 3.40137C9.96801 3.40137 5.07812 7.50073 5.07812 12.5576C5.07812 12.9883 5.1136 13.4121 5.18224 13.8271L5.17969 13.8271L5.89691 17.7503C6.2919 17.2556 6.48014 16.6073 6.36077 15.9453L6.34749 15.8717C6.22031 15.1674 6.05979 14.2784 5.98718 12.9995Z" fill="url(#paint26_radial_18_18382)" />
<g filter="url(#filter2_f_18_18382)">
<path d="M26.1724 17.316C26.1896 17.2295 26.806 13.8988 26.8203 13.8115C26.8873 13.4016 26.9219 12.983 26.9219 12.5576C26.9219 7.50073 22.032 3.40137 16 3.40137C9.96801 3.40137 5.07812 7.50073 5.07812 12.5576C5.07812 12.9883 5.1136 13.4121 5.18224 13.8271L5.17969 13.8271L5.83019 17.4014" stroke="url(#paint27_linear_18_18382)" stroke-width="0.1" />
</g>
<g filter="url(#filter3_f_18_18382)">
<path d="M27.0589 16.0014C26.9997 16.2263 26.9622 16.7367 27.2856 16.9788C27.609 17.2208 27.8068 17.3878 27.8653 17.441C28.1661 17.6417 28.6741 18.257 28.299 19.1125" stroke="#D1D1D3" stroke-width="0.3" />
</g>
<defs>
<filter id="filter0_i_18_18382" x="3.12305" y="1.83105" width="26.0042" height="20.2361" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="0.25" dy="-0.25" />
<feGaussianBlur stdDeviation="0.25" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.52549 0 0 0 0 0.513726 0 0 0 0 0.541176 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_18382" />
</filter>
<filter id="filter1_i_18_18382" x="20.6027" y="19.4521" width="7.70354" height="10.2228" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dx="-0.2" dy="0.2" />
<feGaussianBlur stdDeviation="0.2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0.690196 0 0 0 0 0.686275 0 0 0 0 0.698039 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_18_18382" />
</filter>
<filter id="filter2_f_18_18382" x="4.82808" y="3.15132" width="22.3438" height="14.459" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.1" result="effect1_foregroundBlur_18_18382" />
</filter>
<filter id="filter3_f_18_18382" x="26.5693" y="15.6633" width="2.31326" height="3.80947" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="0.15" result="effect1_foregroundBlur_18_18382" />
</filter>
<radialGradient id="paint0_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.6188 4.48184) rotate(114.311) scale(16.9353 23.4245)">
<stop stop-color="#DADADB" />
<stop offset="1" stop-color="#B9B5BD" />
</radialGradient>
<radialGradient id="paint1_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(3.12305 10.8068) rotate(19.2062) scale(9.61161 32.0782)">
<stop offset="0.095751" stop-color="#969499" />
<stop offset="1" stop-color="#969499" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint2_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(5.95906 20.563) rotate(-117.087) scale(3.0608 1.02371)">
<stop offset="0.256074" stop-color="#C6C4C9" />
<stop offset="1" stop-color="#C6C4C9" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint3_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(5.83425 20.7294) rotate(-117.35) scale(2.03754 0.586159)">
<stop offset="0.166979" stop-color="#8D8D8F" />
<stop offset="1" stop-color="#8D8D8F" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint4_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(22.3511 8.14414) rotate(-54.3815) scale(3.429 6.67803)">
<stop offset="0.56676" stop-color="#EBEBED" />
<stop offset="1" stop-color="#EBEBED" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint5_linear_18_18382" x1="27.8844" y1="21.4159" x2="27.094" y2="18.67" gradientUnits="userSpaceOnUse">
<stop offset="0.326903" stop-color="#B1B0B4" />
<stop offset="1" stop-color="#B1B0B4" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint6_linear_18_18382" x1="5.06449" y1="21.4326" x2="7.42872" y2="29.4749" gradientUnits="userSpaceOnUse">
<stop stop-color="#A6A0AA" />
<stop offset="1" stop-color="#9D94A2" />
</linearGradient>
<linearGradient id="paint7_linear_18_18382" x1="8.15118" y1="29.4749" x2="7.42872" y2="26.9552" gradientUnits="userSpaceOnUse">
<stop stop-color="#998E9E" />
<stop offset="1" stop-color="#998E9E" stop-opacity="0" />
</linearGradient>
<radialGradient id="paint8_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(3.677 22.0576) rotate(75.1074) scale(8.92479 1.06286)">
<stop stop-color="#817F83" />
<stop offset="1" stop-color="#817F83" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint9_linear_18_18382" x1="4.53324" y1="19.9951" x2="5.87699" y2="23.1513" gradientUnits="userSpaceOnUse">
<stop stop-color="#7F7E80" />
<stop offset="1" stop-color="#7F7E80" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint10_linear_18_18382" x1="8.55733" y1="24.1461" x2="7.00043" y2="24.6537" gradientUnits="userSpaceOnUse">
<stop offset="0.181858" stop-color="#807D84" />
<stop offset="1" stop-color="#807D84" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint11_linear_18_18382" x1="27.1583" y1="20.8076" x2="24.5546" y2="29.4749" gradientUnits="userSpaceOnUse">
<stop stop-color="#BAB9BC" />
<stop offset="1" stop-color="#9F96A4" />
</linearGradient>
<linearGradient id="paint12_linear_18_18382" x1="23.252" y1="29.4749" x2="25.1583" y2="26.4013" gradientUnits="userSpaceOnUse">
<stop stop-color="#9A8F9F" />
<stop offset="1" stop-color="#9A8F9F" stop-opacity="0" />
</linearGradient>
<radialGradient id="paint13_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.6739 22.7295) rotate(106.376) scale(3.71313 0.681309)">
<stop stop-color="#C7C6C8" />
<stop offset="1" stop-color="#BAB8BC" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint14_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.6895 22.6826) rotate(19.7468) scale(3.88469 2.56034)">
<stop stop-color="#C1BFC3" />
<stop offset="1" stop-color="#C1BFC3" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint15_linear_18_18382" x1="7.97071" y1="18.4013" x2="11.7207" y2="30.9013" gradientUnits="userSpaceOnUse">
<stop stop-color="#B7B6B8" />
<stop offset="0.321101" stop-color="#D9D5DD" />
<stop offset="0.681651" stop-color="#D9D5DD" />
<stop offset="1" stop-color="#CDC1D4" />
</linearGradient>
<radialGradient id="paint16_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.18946 18.8701) rotate(72.755) scale(16.655 1.37978)">
<stop stop-color="#A4A2A6" />
<stop offset="1" stop-color="#C5BACF" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint17_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.5332 22.6201) rotate(162.646) scale(1.04769 9.65758)">
<stop offset="0.261566" stop-color="#F6F4F7" />
<stop offset="1" stop-color="#E6E1EA" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint18_linear_18_18382" x1="12.1426" y1="24.1201" x2="11.6113" y2="24.2607" gradientUnits="userSpaceOnUse">
<stop offset="0.133145" stop-color="#DBD9DE" />
<stop offset="1" stop-color="#DBD9DE" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint19_linear_18_18382" x1="24.377" y1="18.3076" x2="20.0958" y2="31.1201" gradientUnits="userSpaceOnUse">
<stop stop-color="#DAD9DB" />
<stop offset="0.399322" stop-color="#DEDDE1" />
<stop offset="0.961979" stop-color="#C7B9CF" />
</linearGradient>
<linearGradient id="paint20_linear_18_18382" x1="23.9395" y1="18.1826" x2="23.2833" y2="20.2763" gradientUnits="userSpaceOnUse">
<stop offset="0.10868" stop-color="#C9C9CA" />
<stop offset="1" stop-color="#C9C9CA" stop-opacity="0" />
</linearGradient>
<radialGradient id="paint21_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.377 21.6513) rotate(15.7807) scale(1.4938 15.9576)">
<stop offset="0.205252" stop-color="#A4A3A6" />
<stop offset="1" stop-color="#CECAD3" stop-opacity="0" />
</radialGradient>
<radialGradient id="paint22_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.2364 21.7763) rotate(105.977) scale(3.91692 0.624606)">
<stop offset="0.151268" stop-color="#F5F3F7" />
<stop offset="1" stop-color="#E8E4EC" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint23_linear_18_18382" x1="6.09582" y1="16.9951" x2="17.4083" y2="2.49512" gradientUnits="userSpaceOnUse">
<stop stop-color="#CDCBCF" />
<stop offset="1" stop-color="#BEB5C7" />
</linearGradient>
<radialGradient id="paint24_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(22.6271 8.58887) rotate(-88.0363) scale(5.47196 16.8348)">
<stop offset="0.172043" stop-color="#AFA7B6" />
<stop offset="1" stop-color="#AFA7B6" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint25_linear_18_18382" x1="26.3146" y1="17.7549" x2="22.8771" y2="9.33887" gradientUnits="userSpaceOnUse">
<stop stop-color="#9B99A0" />
<stop offset="1" stop-color="#9B99A0" stop-opacity="0" />
</linearGradient>
<radialGradient id="paint26_radial_18_18382" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.28332 18.5264) rotate(-88.9949) scale(3.56305 5.42238)">
<stop stop-color="#A5A2A9" />
<stop offset="1" stop-color="#A5A2A9" stop-opacity="0" />
</radialGradient>
<linearGradient id="paint27_linear_18_18382" x1="10.0646" y1="7.08887" x2="25.1271" y2="7.15137" gradientUnits="userSpaceOnUse">
<stop stop-color="#CFC9D4" />
<stop offset="1" stop-color="#DFDDE3" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,30 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5 18C25.6421 18 29 14.6421 29 10.5C29 6.35786 25.6421 3 21.5 3C17.3579 3 14 6.35786 14 10.5C14 14.6421 17.3579 18 21.5 18Z" fill="#E0AEF8"/>
<path d="M10.75 14.375C10.75 16.6532 8.90317 18.5 6.625 18.5C4.34683 18.5 2.5 16.6532 2.5 14.375C2.5 12.0968 4.34683 10.25 6.625 10.25C8.90317 10.25 10.75 12.0968 10.75 14.375Z" fill="#E0AEF8"/>
<path d="M19.5989 24.5C19.5989 27.316 17.316 29.5989 14.5 29.5989C11.684 29.5989 9.40112 27.316 9.40112 24.5C9.40112 21.684 11.684 19.4011 14.5 19.4011C17.316 19.4011 19.5989 21.684 19.5989 24.5Z" fill="#E0AEF8"/>
<path d="M29 10.5C29 14.0281 26.5639 16.9872 23.2821 17.787C24.9278 16.6077 26 14.6791 26 12.5C26 8.91015 23.0899 6 19.5 6C17.321 6 15.3923 7.07225 14.213 8.71792C15.0128 5.43607 17.9719 3 21.5 3C25.6421 3 29 6.35786 29 10.5Z" fill="#C4BBF9"/>
<path d="M19.5 24.5C19.5 26.0548 18.7903 27.4439 17.6772 28.3609C17.8861 27.7797 18 27.1532 18 26.5C18 23.4624 15.5376 21 12.5 21C11.8468 21 11.2203 21.1139 10.639 21.3228C11.5561 20.2097 12.9452 19.5 14.5 19.5C17.2614 19.5 19.5 21.7386 19.5 24.5Z" fill="#C4BBF9"/>
<path d="M29.5 10.5C29.5 13.9844 27.2723 16.9485 24.1635 18.0459C26.4529 16.7619 28 14.3116 28 11.5C28 7.35786 24.6421 4 20.5 4C17.6883 4 15.2381 5.54715 13.954 7.83654C15.0514 4.72769 18.0155 2.5 21.5 2.5C25.9182 2.5 29.5 6.08172 29.5 10.5Z" fill="#AEDDFF"/>
<path d="M19.7969 24.2162C19.7969 25.3601 19.3936 26.4109 18.7201 27.237C18.9017 26.691 19 26.107 19 25.5C19 22.4624 16.5376 20 13.5 20C13.116 20 12.7412 20.0394 12.3794 20.1142C13.121 19.662 13.9946 19.4011 14.9297 19.4011C17.6178 19.4011 19.7969 21.5569 19.7969 24.2162Z" fill="#AEDDFF"/>
<path d="M29.4999 8C29.7959 8.78448 29.9347 10.1243 29.9855 10.9996C29.8755 12.897 29.1432 14.6275 27.9895 15.9899C26.5085 17.2439 24.5925 18.5 22.4999 18.5C22.3229 18.5 22.147 18.3576 21.9726 18.2164C21.8138 18.0878 21.6562 17.9602 21.4999 17.9418C25.7231 17.4469 28.9999 13.8562 28.9999 9.50003C28.9999 8.53244 28.8383 7.60261 28.5404 6.73608C28.9999 7 29.2635 7.3734 29.4999 8Z" fill="#FCD53F"/>
<path d="M10.9931 14.7496C10.9875 13.826 10.6534 12.8627 10.2458 12.5056C10.4104 12.9732 10.4999 13.4762 10.4999 14.0001C10.4999 16.3164 8.74995 18.2239 6.5 18.4726C6.5773 18.4812 6.6458 18.5222 6.71428 18.5633C6.79122 18.6094 6.86816 18.6556 6.95756 18.6556C8.07293 18.6556 9.06514 18.2854 9.92215 17.4223C10.7144 16.6244 11 15.874 10.9931 14.7496Z" fill="#FCD53F"/>
<path d="M19.1466 22.0559C19.1949 22.132 19.2416 22.203 19.2866 22.2714C19.4726 22.5544 19.6291 22.7924 19.745 23.1541C19.8905 23.608 19.9721 24.2532 19.9944 24.7499C19.9365 26.0435 19.4317 27.2208 18.6307 28.1312C17.662 28.9832 16.3395 29.6941 14.948 29.6941C14.8603 29.6941 14.7871 29.6397 14.7138 29.5853C14.646 29.5349 14.5781 29.4845 14.4987 29.4774C17.3026 29.2253 19.4999 26.8691 19.4999 23.9997C19.4999 23.3154 19.375 22.6603 19.1466 22.0559Z" fill="#FCD53F"/>
<path d="M29.9856 11C29.9952 10.8346 30 10.6679 30 10.5C30 8.04319 28.9577 5.82978 27.291 4.27795C25.9269 3.34543 24.2772 2.80005 22.5 2.80005C21.749 2.80005 21.0207 2.89745 20.3271 3.08031C20.7105 3.02739 21.1021 3.00005 21.5 3.00005C26.0266 3.00005 29.7267 6.53836 29.9856 11Z" fill="#FF6DC6"/>
<path d="M11 14.5001C11 14.5838 10.9977 14.667 10.9932 14.7495C10.8677 12.4636 9.03606 10.6321 6.74999 10.5069C6.79014 10.5047 6.82851 10.4836 6.86696 10.4624C6.90775 10.44 6.94863 10.4175 6.99181 10.4175C8.66187 10.5069 9.13592 10.9059 9.92216 11.5779C10.5942 12.3641 11 13.3847 11 14.5001Z" fill="#FF6DC6"/>
<path d="M19.9944 24.7502C19.9981 24.6673 20 24.5839 20 24.5001C20 23.1085 19.4832 21.8377 18.6311 20.869C17.6773 19.9687 16.5385 19.3779 15.1229 19.3181C15.0807 19.3181 15.0065 19.366 14.9331 19.4133C14.8626 19.4588 14.7929 19.5038 14.7527 19.5055C17.5902 19.6339 19.8676 21.9123 19.9944 24.7502Z" fill="#FF6DC6"/>
<path d="M15 29.5C16.3916 29.5 17.6625 28.9832 18.6312 28.131C17.6233 29.2769 16.1461 30 14.5 30C13.0491 30 11.7294 29.4382 10.7467 28.5203C9.49997 26.8685 9.26615 25.8102 9.49997 24C9.49997 27.0376 11.9624 29.5 15 29.5Z" fill="#FF6DC6"/>
<path d="M6.99998 18.5C8.11535 18.5 9.13594 18.0942 9.92218 17.4222C9.09682 18.3879 7.86988 19 6.49998 19C5.18777 19 4.00675 18.4383 3.18419 17.5423C2.60279 16.559 2.36462 15.6482 2.36462 14.4751C2.38643 14.3988 2.39759 14.3397 2.40768 14.2863C2.42512 14.1941 2.43937 14.1187 2.49998 14C2.49998 16.4853 4.51469 18.5 6.99998 18.5Z" fill="#FF6DC6"/>
<path d="M16.0102 16.9896C17.4912 18.2438 19.4073 19.0001 21.5 19.0001C24.1018 19.0001 26.4305 17.8311 27.9897 15.9898C26.5087 17.2438 24.5926 18.0001 22.5 18.0001C18.1439 18.0001 14.5531 14.7232 14.0582 10.5001C14.0399 10.6564 13.9122 10.814 13.7836 10.9728C13.6424 11.1471 13.5 11.3229 13.5 11.5C13.5 13.5926 14.7562 15.5086 16.0102 16.9896Z" fill="#FF6DC6"/>
<path d="M24.9686 10.9687C25.2209 10.9348 25.4701 10.8735 25.7115 10.7846C25.7859 10.7572 25.8596 10.7272 25.9324 10.6946C26.46 9.42491 26.2075 7.9076 25.1749 6.87498C24.1184 5.81849 22.5545 5.57861 21.2676 6.15534C21.1502 6.43779 21.0715 6.7325 21.0313 7.0314C22.076 6.89103 23.1719 7.2223 23.9748 8.02519C24.7777 8.82809 25.109 9.92403 24.9686 10.9687Z" fill="#EBCAFF"/>
<path d="M8.82323 14.8232C8.70434 14.877 8.57925 14.9195 8.44933 14.9493C8.48249 14.8049 8.5 14.6545 8.5 14.5C8.5 13.3954 7.60457 12.5 6.5 12.5C6.3455 12.5 6.21094 12.5195 6.05066 12.5507C6.09766 12.3281 6.17676 12.1767 6.17676 12.1767C6.42782 12.0632 6.70653 12 6.99999 12C8.10456 12 8.99999 12.8954 8.99999 14C8.99999 14.2935 8.93678 14.5722 8.82323 14.8232Z" fill="#EBCAFF"/>
<path d="M16.0598 24.9944C16.34 24.9236 16.5909 24.8093 16.8104 24.6618C16.9804 23.6993 16.4826 22.8173 15.7467 22.3141C15.0109 21.8109 13.7989 21.5999 13.0531 22.1033C13.0184 22.2655 13 22.4356 13 22.6125C13 22.7045 13.0051 22.7952 13.0149 22.8846C13.1498 22.8637 13.2879 22.8529 13.4286 22.8529C14.7256 22.8529 15.8079 23.772 16.0598 24.9944Z" fill="#EBCAFF"/>
<path d="M26.1249 5.87498C24.903 4.6531 23.0025 4.52352 21.6367 5.48622C21.4704 5.7221 21.3366 5.97393 21.2355 6.23547C22.4884 5.75093 23.9639 6.01414 24.9749 7.02511C25.9859 8.03608 26.2491 9.51165 25.7645 10.7645C26.0259 10.6635 26.2776 10.5298 26.5133 10.3637C27.4764 8.9978 27.3469 7.09699 26.1249 5.87498Z" fill="#EFD5FF"/>
<path d="M9.14202 14.6421C9.04203 14.7118 8.93539 14.7726 8.82323 14.8233C8.93678 14.5722 8.99999 14.2935 8.99999 14C8.99999 12.8955 8.10456 12 6.99999 12C6.70653 12 6.42782 12.0633 6.17676 12.1768C6.24219 12.0469 6.29688 11.9493 6.35797 11.8579C6.68176 11.6323 7.06816 11.4113 7.49272 11.4113C8.0001 11.4113 8.61197 11.5466 8.96458 11.8579C9.37959 12.2244 9.6122 12.8748 9.6122 13.472C9.6122 13.8966 9.36766 14.3183 9.14202 14.6421Z" fill="#EFD5FF"/>
<path d="M16.7415 24.7069C17.0113 24.5411 17.2466 24.3247 17.4341 24.0708C17.9701 23.3453 17.371 21.9121 16.7423 21.3271C15.9331 20.5743 14.2913 20.4843 13.4795 21.155C13.2655 21.4449 13.1137 21.7835 13.0439 22.1511C13.3562 22.024 13.6978 21.954 14.0558 21.954C15.5395 21.954 16.7423 23.1567 16.7423 24.6404C16.7423 24.6627 16.742 24.6848 16.7415 24.7069Z" fill="#EFD5FF"/>
<path d="M26.9748 9.97487C26.8036 10.1462 26.6189 10.296 26.4243 10.4243C27.3202 9.06595 27.1704 7.22067 25.9748 6.02513C24.7793 4.82958 22.934 4.67976 21.5756 5.57566C21.704 5.38104 21.8538 5.19641 22.0251 5.02513C23.3919 3.65829 25.608 3.65829 26.9748 5.02513C28.3417 6.39196 28.3417 8.60804 26.9748 9.97487Z" fill="white"/>
<path d="M9.14195 14.6421C9.66057 14.2808 9.99995 13.68 9.99995 13C9.99995 11.8954 9.10452 11 7.99995 11C7.31998 11 6.71927 11.3393 6.35791 11.8579C6.6817 11.6323 7.07536 11.5 7.49991 11.5C8.60448 11.5 9.49991 12.3954 9.49991 13.5C9.49991 13.9246 9.3676 14.3183 9.14195 14.6421Z" fill="white"/>
<path d="M17.3629 24.1618C17.7068 23.7391 17.913 23.1999 17.913 22.6125C17.913 21.2558 16.8131 20.156 15.4564 20.156C14.6246 20.156 13.8893 20.5695 13.4449 21.2022C13.8303 20.9885 14.2737 20.8668 14.7456 20.8668C16.2293 20.8668 17.4321 22.0696 17.4321 23.5533C17.4321 23.7626 17.4082 23.9663 17.3629 24.1618Z" fill="white"/>
<path d="M22.5 3C24.5926 3 26.5087 3.75622 27.9897 5.0103C26.4305 3.16895 24.1018 2 21.5 2C16.8056 2 13 5.80558 13 10.5C13 13.1018 14.1689 15.4305 16.0103 16.9897C14.7562 15.5087 14 13.5926 14 11.5C14 6.80558 17.8056 3 22.5 3Z" fill="#26C9FC"/>
<path d="M9.9222 11.5778C9.13597 10.9058 8.11537 10.5 7 10.5C4.51472 10.5 2.5 12.5147 2.5 15C2.5 16.1154 2.90579 17.136 3.5778 17.9222C2.61213 17.0968 2 15.8699 2 14.5C2 12.0147 4.01472 10 6.5 10C7.86991 10 9.09685 10.6121 9.9222 11.5778Z" fill="#26C9FC"/>
<path d="M18.6311 20.8689C17.6624 20.0168 16.3916 19.5 15 19.5C11.9624 19.5 9.5 21.9624 9.5 25C9.5 26.3916 10.0168 27.6624 10.8689 28.6311C9.72307 27.6231 9 26.146 9 24.5C9 21.4624 11.4624 19 14.5 19C16.146 19 17.6231 19.7231 18.6311 20.8689Z" fill="#26C9FC"/>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -0,0 +1,9 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="5" width="30" height="22" rx="1.5" fill="#B4ACBC" />
<rect x="2" y="7" width="28" height="18" rx="1" fill="#CDC4D6" />
<path d="M30 23.4001L17.029 15.6175C16.3956 15.2375 15.6044 15.2375 14.971 15.6175L2 23.4001V25.0001C2 25.5524 2.44771 26.0001 3 26.0001L29 26.0001C29.5523 26.0001 30 25.5524 30 25.0001V23.4001Z" fill="#E1D8EC" />
<path d="M2 9.76619V8H30V9.76619L17.5435 17.2401C16.5934 17.8101 15.4066 17.8101 14.4565 17.2401L2 9.76619Z" fill="#998EA4" />
<path d="M2 8.6V7C2 6.44772 2.44772 6 3 6H29C29.5523 6 30 6.44772 30 7V8.6L17.029 16.3826C16.3956 16.7626 15.6044 16.7626 14.971 16.3826L2 8.6Z" fill="#F3EEF8" />
<path d="M16 23C19.866 23 23 19.866 23 16C23 12.134 19.866 9 16 9C12.134 9 9 12.134 9 16C9 19.866 12.134 23 16 23Z" fill="#00A6ED" />
<path d="M15.9999 11.5001C14.7899 11.4801 13.6399 11.9401 12.7799 12.8001C11.9099 13.6501 11.4399 14.7901 11.4399 16.0001C11.4399 18.4801 13.4599 20.5001 15.9399 20.5001C16.1999 20.5001 16.4099 20.2901 16.4099 20.0301C16.4099 19.7701 16.1999 19.5601 15.9399 19.5601C13.9799 19.5601 12.3799 17.9601 12.3799 16.0001C12.3799 15.0401 12.7599 14.1401 13.4399 13.4701C14.1199 12.8001 15.0299 12.4401 15.9899 12.4401C17.9199 12.4701 19.4999 14.0901 19.4999 16.0601V16.8701C19.4999 17.2401 19.1999 17.5401 18.8299 17.5401C18.4599 17.5401 18.1599 17.2401 18.1599 16.8701V13.7901C18.1599 13.5301 17.9499 13.3201 17.6899 13.3201C17.4299 13.3201 17.1999 13.5301 17.1999 13.7901V13.8801C16.7599 13.5301 16.2099 13.3101 15.5999 13.3101C14.1999 13.3101 13.0599 14.4501 13.0599 15.8501C13.0599 17.2501 14.1999 18.3901 15.5999 18.3901C16.2999 18.3901 16.9399 18.1001 17.3999 17.6401C17.6799 18.1401 18.2099 18.4801 18.8199 18.4801C19.7099 18.4801 20.4399 17.7501 20.4399 16.8601V16.0501C20.4399 13.5801 18.4499 11.5301 15.9999 11.5001ZM15.6099 17.4601C14.7299 17.4601 14.0099 16.7401 14.0099 15.8601C14.0099 14.9801 14.7299 14.2601 15.6099 14.2601C16.4899 14.2601 17.2099 14.9801 17.2099 15.8601C17.2099 16.7401 16.4899 17.4601 15.6099 17.4601Z" fill="#F4F4F4" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,4 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30 8.91016H2V22.9802H30V8.91016Z" fill="#83CBFF" />
<path d="M30 10.02V5.82C30 5.37 29.63 5 29.18 5H2.82C2.37 5 2 5.37 2 5.82V10.02H5.74C6.8 10.02 7.66 10.88 7.66 11.94V20C7.66 21.06 6.8 21.92 5.74 21.92H2V26.12C2 26.57 2.37 26.94 2.82 26.94H29.18C29.63 26.94 30 26.57 30 26.12V21.92H26.26C25.2 21.92 24.34 21.06 24.34 20V11.94C24.34 10.88 25.2 10.02 26.26 10.02H30ZM20.11 21.92H11.89C10.83 21.92 9.97 21.06 9.97 20V11.94C9.97 10.88 10.83 10.02 11.89 10.02H20.11C21.17 10.02 22.03 10.88 22.03 11.94V20C22.03 21.06 21.17 21.92 20.11 21.92ZM5.77001 23.39C5.95001 23.39 6.09001 23.53 6.09001 23.71V25.09C6.09001 25.27 5.95001 25.41 5.77001 25.41H4.39001C4.21001 25.41 4.07001 25.27 4.07001 25.09V23.71C4.07001 23.53 4.21001 23.39 4.39001 23.39H5.77001ZM11.23 23.39C11.41 23.39 11.55 23.53 11.55 23.71V25.09C11.55 25.27 11.41 25.41 11.23 25.41H9.85003C9.67003 25.41 9.53003 25.27 9.53003 25.09V23.71C9.53003 23.53 9.67003 23.39 9.85003 23.39H11.23ZM15.31 23.39H16.69C16.87 23.39 17.01 23.53 17.01 23.71V25.09C17.01 25.27 16.87 25.41 16.69 25.41H15.31C15.13 25.41 14.99 25.27 14.99 25.09V23.71C14.99 23.53 15.13 23.39 15.31 23.39ZM22.15 23.39C22.32 23.39 22.47 23.53 22.47 23.71V25.09C22.47 25.27 22.33 25.41 22.15 25.41H20.77C20.59 25.41 20.45 25.27 20.45 25.09V23.71C20.45 23.53 20.59 23.39 20.77 23.39H22.15ZM26.23 23.39H27.61C27.78 23.39 27.93 23.53 27.93 23.71V25.09C27.93 25.27 27.79 25.41 27.61 25.41H26.23C26.05 25.41 25.91 25.27 25.91 25.09V23.71C25.91 23.53 26.05 23.39 26.23 23.39ZM4.39001 6.47998H5.77001C5.95001 6.47998 6.09001 6.62998 6.09001 6.79998V8.17998C6.09001 8.35998 5.95001 8.49998 5.77001 8.49998H4.39001C4.21001 8.49998 4.07001 8.35998 4.07001 8.17998V6.79998C4.07001 6.61998 4.21001 6.47998 4.39001 6.47998ZM9.85003 6.47998H11.23C11.41 6.47998 11.55 6.62998 11.55 6.79998V8.17998C11.55 8.35998 11.41 8.49998 11.23 8.49998H9.85003C9.67003 8.49998 9.53003 8.35998 9.53003 8.17998V6.79998C9.53003 6.61998 9.67003 6.47998 9.85003 6.47998ZM16.69 6.47998C16.87 6.47998 17.01 6.62998 17.01 6.79998V8.17998C17.01 8.35998 16.87 8.49998 16.69 8.49998H15.31C15.13 8.49998 14.99 8.35998 14.99 8.17998V6.79998C14.99 6.61998 15.13 6.47998 15.31 6.47998H16.69ZM20.77 6.47998H22.15C22.32 6.47998 22.47 6.62998 22.47 6.79998V8.17998C22.47 8.35998 22.33 8.49998 22.15 8.49998H20.77C20.59 8.49998 20.45 8.35998 20.45 8.17998V6.79998C20.45 6.61998 20.59 6.47998 20.77 6.47998ZM27.61 6.47998C27.78 6.47998 27.93 6.62998 27.93 6.79998V8.17998C27.93 8.35998 27.79 8.49998 27.61 8.49998H26.23C26.05 8.49998 25.91 8.35998 25.91 8.17998V6.79998C25.91 6.61998 26.05 6.47998 26.23 6.47998H27.61Z" fill="#433B6B" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,7 @@
<svg width="100%" height="100%" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99032 29.1445L4.05032 22.9645C3.89032 22.4345 4.12031 21.8545 4.62031 21.5745L7.14032 20.2045L10.0903 29.6345L7.24032 29.9545C6.68032 30.0245 6.16032 29.6845 5.99032 29.1445Z" fill="#998EA4" />
<path d="M26.5702 29.1445L28.5102 22.9645C28.6802 22.4345 28.4502 21.8545 27.9402 21.5745L25.4202 20.2045L22.4702 29.6345L25.3202 29.9545C25.8802 30.0245 26.4002 29.6845 26.5702 29.1445Z" fill="#998EA4" />
<path d="M26.7201 16.1106C26.6201 16.4106 26.7501 16.7406 27.0201 16.8906C28.0601 17.4406 28.4601 18.7106 27.9401 19.7806L27.3801 20.9106C27.2501 21.1806 26.9801 21.3306 26.7001 21.3306C26.5601 21.3306 26.4101 21.2806 26.2701 21.1906C25.9501 20.9806 25.8601 20.5406 26.0301 20.2006L26.5801 19.1006C26.7401 18.7806 26.6001 18.4106 26.2901 18.2406C25.3801 17.7406 24.9501 16.6506 25.2701 15.6506C25.5601 14.7406 25.7101 13.7806 25.7101 12.8106V12.7906C25.7101 11.6806 25.4101 10.5906 24.8501 9.64058C23.1301 6.71058 19.9401 4.7506 16.3001 4.7506C12.6501 4.7506 9.47007 6.71058 7.74007 9.64058C7.18007 10.5906 6.88007 11.6806 6.88007 12.7906V12.8106C6.88007 13.7806 7.03007 14.7406 7.32007 15.6506C7.65007 16.6606 7.22007 17.7406 6.30007 18.2406C5.99007 18.4106 5.85006 18.7806 6.01006 19.1006L6.56006 20.2006C6.73006 20.5406 6.64007 20.9706 6.32007 21.1906C6.19007 21.2906 6.04006 21.3306 5.89006 21.3306C5.61006 21.3306 5.34007 21.1806 5.21007 20.9106L4.65006 19.7806C4.12006 18.7206 4.53007 17.4606 5.57007 16.9006C5.84007 16.7606 5.97006 16.4306 5.87006 16.1206C5.44006 14.7906 5.28007 13.4006 5.38007 11.9906C5.57007 9.42059 6.69006 7.00059 8.50006 5.18059C10.5701 3.09059 13.3401 1.9706 16.2801 2.0006H16.3001C19.2201 1.9706 21.9501 3.07058 24.0201 5.11058C26.1001 7.15058 27.2401 9.89059 27.2401 12.8006C27.2401 13.9306 27.0601 15.0406 26.7201 16.1106Z" fill="#CDC4D6" />
<path d="M9.58021 18.3745L7.65021 18.9745C7.12021 19.1445 6.83022 19.7045 6.99022 20.2345L9.92021 29.6045C10.0902 30.1345 10.6502 30.4245 11.1802 30.2645L13.1102 29.6645C13.6402 29.4945 13.9302 28.9345 13.7702 28.4045L10.8402 19.0345C10.6802 18.5045 10.1102 18.2145 9.58021 18.3745Z" fill="#CDC4D6" />
<path d="M22.9803 18.3745L24.9103 18.9745C25.4403 19.1445 25.7303 19.7045 25.5703 20.2345L22.6403 29.6045C22.4703 30.1345 21.9103 30.4245 21.3803 30.2645L19.4503 29.6645C18.9203 29.4945 18.6303 28.9345 18.7903 28.4045L21.7203 19.0345C21.8903 18.5045 22.4503 18.2145 22.9803 18.3745Z" fill="#CDC4D6" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.18 19.61C28.2345 19.61 29.9 17.9445 29.9 15.89C29.9 13.8355 28.2345 12.17 26.18 12.17C24.1255 12.17 22.46 13.8355 22.46 15.89C22.46 17.9445 24.1255 19.61 26.18 19.61Z" fill="#212121"/>
<path d="M10.9999 11L11.6799 9.99997C12.9299 9.99997 14.1699 9.70997 15.2899 9.16997L21.5499 6.08997V25.71L15.2899 22.63C14.1699 22.08 12.9299 21.79 11.6799 21.79L10.9999 20V11ZM6.21586 29.0083H8.78989C9.45989 29.0083 9.99989 28.4683 9.99989 27.7983V19.89H5.00586V27.7983C5.00586 28.4683 5.54586 29.0083 6.21586 29.0083Z" fill="#D3D3D3"/>
<path d="M24.07 3C22.38 3 21 4.37 21 6.07V25.72C21 27.41 22.37 28.79 24.07 28.79C25.76 28.79 27.14 27.42 27.14 25.72V6.07C27.13 4.37 25.76 3 24.07 3Z" fill="#F8312F"/>
<path d="M3.72662 10H12V21.78H3.72662C2.77081 21.78 2 21.03 2 20.11V11.68C2 10.75 2.77081 10 3.72662 10Z" fill="#CA0B4A"/>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View file

@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.81 30.04V23.4L14.5 22L11.83 23.4V30.04H16.81Z" fill="#7D4533"/>
<path d="M21.65 7H7.84L11 23.61H15.81H21.65H25.99C26.8184 23.61 27.49 22.9384 27.49 22.11V12.84C27.48 9.61 24.87 7 21.65 7Z" fill="#5092FF"/>
<path d="M7.84 7C4.61 7 2 9.61 2 12.84V22.11C2 22.9384 2.67157 23.61 3.5 23.61H12.17C12.9984 23.61 13.67 22.9384 13.67 22.11V12.84C13.67 9.61 11.06 7 7.84 7Z" fill="#3F5FFF"/>
<path d="M7.84 8C5.16228 8 3 10.1623 3 12.84V22.11C3 22.3861 3.22386 22.6 3.5 22.6H4.63253L12.5232 11.6492C11.9942 9.54876 10.0974 8 7.84 8Z" fill="#321B41"/>
<path d="M24.1315 2L18.8685 2C18.3929 2 18 2.36893 18 2.81553L18 5.18447C18 5.63107 18.3929 6 18.8685 6L24.1315 6C24.6071 6 25 5.63107 25 5.18447L25 2.81553C25 2.36893 24.6071 2 24.1315 2Z" fill="#F92F60"/>
<path d="M17.21 2.58C17.21 1.91 17.75 1.37 18.42 1.37C19.09 1.37 19.63 1.91 19.63 2.58L19.63 11.4838C20.3533 11.9022 20.84 12.6843 20.84 13.58C20.84 14.9166 19.7565 16 18.42 16C17.0835 16 16 14.9166 16 13.58C16 12.6843 16.4866 11.9022 17.21 11.4838L17.21 2.58Z" fill="#D3D3D3"/>
<path d="M12.6071 12.0555H4.57714C4.25714 12.0555 4 12.303 4 12.611V21.5C4 21.808 4.25714 22.0555 4.57714 22.0555H12.67V12.84C12.67 12.5728 12.6485 12.3108 12.6071 12.0555Z" fill="#E1D8EC"/>
<path d="M12.6229 12.16H4.57714C4.25714 12.16 4 12.4075 4 12.7155V13.2875L11.3086 17.77C11.7243 18.0242 12.2505 18.0285 12.67 17.7829V12.84C12.67 12.6091 12.6539 12.3821 12.6229 12.16Z" fill="#CDC4D6"/>
<path d="M12.5131 11.61H4.57714C4.25714 11.61 4 11.8575 4 12.1655V12.7375L11.3086 17.22C11.7243 17.4741 12.2505 17.4784 12.67 17.2328V12.84C12.67 12.4148 12.6155 12.0026 12.5131 11.61Z" fill="#F3EEF8"/>
<path d="M12.67 17.7829C12.2505 18.0285 11.7243 18.0242 11.3086 17.7701L10.6678 17.3771L4 21.4661V22.0436C4 22.3461 4.25714 22.5936 4.57714 22.5991H12.2743C12.5004 22.5511 12.67 22.3504 12.67 22.11V17.7829Z" fill="#F3EEF8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.2359 5.89697L6.00835 8.66735C6.26317 8.92198 6.66069 8.92198 6.91551 8.66735L8.66867 6.91549C8.92349 6.66086 8.92349 6.26364 8.66867 6.00901L5.89623 3.23862C5.53948 2.88214 5.71276 2.28121 6.19182 2.15899C7.96537 1.72102 9.92239 2.18954 11.329 3.54418C12.8928 5.05575 13.3617 7.28456 12.7243 9.22843L22.7212 19.2909C24.6418 18.6406 26.8528 19.0802 28.387 20.6132C29.7936 22.0188 30.2828 24.0049 29.8445 25.8178C29.7222 26.2864 29.1208 26.4595 28.7641 26.103L25.9917 23.3326C25.7368 23.078 25.3393 23.078 25.0845 23.3326L23.3313 25.0845C23.0765 25.3391 23.0765 25.7364 23.3313 25.991L26.1038 28.7614C26.4605 29.1179 26.2872 29.7188 25.8082 29.841C24.0346 30.279 22.0776 29.8105 20.671 28.4558C19.1243 26.9608 18.6487 24.7642 19.2552 22.8355L9.2093 12.7321C7.30512 13.3486 5.12872 12.9014 3.61304 11.3868C2.20643 9.98124 1.71717 7.99512 2.15546 6.18215C2.27778 5.71363 2.87915 5.54048 3.2359 5.89697Z" fill="#B4ACBC"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,2 +1,8 @@
User-Agent: * User-Agent: *
Disallow: /icons/ /fonts/ *.js *.css Disallow: /emoji/
Disallow: /fonts/
Disallow: /icons/
Disallow: /sponsors/
Disallow: /updateBanners/
Disallow: /*.js
Disallow: /*.css

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,14 +1,14 @@
{ {
"name": "english", "name": "english",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>" "ContactLink": "check the <a class=\"text-backdrop link\" href=\"{statusPage}\" target=\"_blank\">status page</a> or <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>."
}, },
"strings": { "strings": {
"AppTitleCobalt": "cobalt", "AppTitleCobalt": "cobalt",
"LinkInput": "paste the link here", "LinkInput": "paste the link here",
"AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!", "AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
"EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.", "EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.",
"MadeWithLove": "made with <3 by wukko", "MadeWithLove": "made with &lt;3 by wukko",
"AccessibilityInputArea": "link input area", "AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup", "AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button", "AccessibilityDownloadButton": "download button",
@ -45,7 +45,6 @@
"SettingsEnableDownloadPopup": "ask how to save", "SettingsEnableDownloadPopup": "ask how to save",
"AccessibilityEnableDownloadPopup": "ask what to do with downloads", "AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github",
"NoScriptMessage": "cobalt uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", "NoScriptMessage": "cobalt 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": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request, and press \"always allow\".\n\nalternative method:\npress 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\".", "DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request, and press \"always allow\".\n\nalternative method:\npress 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.", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
@ -77,12 +76,12 @@
"ImagePickerExplanationPhone": "press and hold an image to save it.", "ImagePickerExplanationPhone": "press and hold an image to save it.",
"ErrorNoUrlReturned": "i didn't get a download link from the server. this should never happen. try again, but if it still doesn't work, {ContactLink}.", "ErrorNoUrlReturned": "i didn't get a download link from the server. this should never happen. try again, but if it still doesn't work, {ContactLink}.",
"ErrorUnknownStatus": "i received a response i can't process. this should never happen. try again, but if it still doesn't work, {ContactLink}.", "ErrorUnknownStatus": "i received a response i can't process. this should never happen. try again, but if it still doesn't work, {ContactLink}.",
"PasteFromClipboard": "paste and download", "PasteFromClipboard": "paste",
"ChangelogOlder": "previous versions", "ChangelogOlder": "previous versions",
"ChangelogPressToExpand": "expand", "ChangelogPressToExpand": "expand",
"Miscellaneous": "miscellaneous", "Miscellaneous": "miscellaneous",
"ModeToggleAuto": "auto mode", "ModeToggleAuto": "auto",
"ModeToggleAudio": "audio mode", "ModeToggleAudio": "audio",
"SettingsDisableNotifications": "hide notifications", "SettingsDisableNotifications": "hide notifications",
"MediaPickerTitle": "pick what to save", "MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.", "MediaPickerExplanationPC": "click or right click to download what you want.",
@ -102,14 +101,13 @@
"CollapseSupport": "support & source code", "CollapseSupport": "support & source code",
"CollapsePrivacy": "privacy policy", "CollapsePrivacy": "privacy policy",
"ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!", "ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!",
"FollowSupport": "keep in touch with cobalt for support, polls, news, and more:", "FollowSupport": "keep in touch with cobalt for news, support, and more:",
"SupportNote": "please note that response may take a while, there's only one person managing everything.", "SourceCode": "explore source code, report issues, star or fork the repo:",
"SourceCode": "report issues, explore source code, star or fork the repo:", "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for <span class=\"text-backdrop\">20 seconds</span> and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
"ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!",
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.", "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api sometimes acts unexpectedly. try again or try another settings.",
"SettingsCodecSubtitle": "youtube codec", "SettingsCodecSubtitle": "youtube codec",
"SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: poor player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.",
"SettingsAudioDub": "youtube audio track", "SettingsAudioDub": "youtube audio track",
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.", "SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.",
"SettingsDubDefault": "original", "SettingsDubDefault": "original",
@ -119,31 +117,44 @@
"ShareURL": "share", "ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!", "ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
"ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.", "ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.",
"UrgentDonate": "cobalt needs your help!",
"PopupCloseDone": "done", "PopupCloseDone": "done",
"Accessibility": "accessibility", "Accessibility": "accessibility",
"SettingsReduceTransparency": "reduce transparency", "SettingsReduceTransparency": "reduce transparency",
"SettingsDisableAnimations": "disable animations", "SettingsDisableAnimations": "disable animations",
"FeatureErrorGeneric": "your browser doesn't allow or support this feature. check if there are any updates available and try again!", "FeatureErrorGeneric": "your browser doesn't allow or support this feature. check if there are any updates available and try again!",
"ClipboardErrorFirefox": "you're using firefox where all clipboard reading functionality is disabled.\n\nyou can fix this by following steps listed <a class=\"text-backdrop link\" href=\"{repo}/wiki/Troubleshooting#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">here!</a>\n\n...or you can paste the link manually instead.", "ClipboardErrorFirefox": "you're using firefox where all clipboard reading functionality is disabled.\n\nyou can fix this by following steps listed <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/troubleshooting.md#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">here!</a>\n\n...or you can paste the link manually instead.",
"ClipboardErrorNoPermission": "cobalt can't access the most recent item in your clipboard without your permission.\n\nif you don't want to give access, just paste the link manually instead.\n\nif you do, go to site settings and enable the clipboard permission.", "ClipboardErrorNoPermission": "cobalt can't access the most recent item in your clipboard without your permission.\n\nif you don't want to give access, just paste the link manually instead.\n\nif you do, go to site settings and enable the clipboard permission.",
"SupportSelfTroubleshooting": "experiencing issues? try <a class=\"text-backdrop link\" href=\"{repo}/wiki/Troubleshooting\" target=\"_blank\">self-troubleshooting guide</a> first!", "SupportSelfTroubleshooting": "experiencing issues? try one of these first:",
"AccessibilityGoBack": "go back and close the popup", "AccessibilityGoBack": "go back and close the popup",
"CollapseKeyboard": "keyboard shortcuts", "CollapseKeyboard": "keyboard shortcuts",
"KeyboardShortcutsIntro": "use cobalt even faster with keyboard shortcuts:", "KeyboardShortcutsIntro": "use cobalt even faster with keyboard shortcuts:",
"KeyboardShortcutQuickPaste": "paste the link", "KeyboardShortcutQuickPaste": "paste the link",
"KeyboardShortcutClear": "clear link input area", "KeyboardShortcutClear": "clear link input area",
"KeyboardShortcutClosePopup": "close all popups", "KeyboardShortcutClosePopup": "close all popups",
"CollapseLegal": "legal stuff", "CollapseLegal": "terms and ethics",
"FairUse": "cobalt is a tool for easing content downloads from internet and takes <span class=\"text-backdrop\">zero liability</span>. 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.", "FairUse": "cobalt is a web tool that makes it easier to download content from the internet and takes <span class=\"text-backdrop\">zero liability</span>. processing servers work like <span class=\"text-backdrop\">limited proxies</span>, so no media content is ever cached or stored.\n\nyou (end user) are responsible for what you download, how you use and distribute that content. 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!",
"UrgentThanks": "thank you for support!",
"SettingsDisableMetadata": "don't add metadata", "SettingsDisableMetadata": "don't add metadata",
"UrgentNewDomain": "new domain, same cobalt",
"NewDomainWelcomeTitle": "hey there!", "NewDomainWelcomeTitle": "hey there!",
"NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\n<span class=\"text-backdrop\">cobalt.tools</span> is the new main domain, aka where you are now. make sure to update your bookmarks and reinstall the web app!", "NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\n<span class=\"text-backdrop\">cobalt.tools</span> 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 :)", "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.", "DataTransferError": "something went wrong when transferring your preferences. you'll have to open settings and configure cobalt by hand.",
"SupportNotAffiliated": "cobalt is <span class=\"text-backdrop\">not affiliated</span> with any services listed above." "SupportNotAffiliated": "cobalt is <span class=\"text-backdrop\">not affiliated</span> with any services listed above.",
"SponsoredBy": "sponsored by",
"FilenameTitle": "file name style",
"FilenamePatternClassic": "classic",
"FilenamePatternPretty": "pretty",
"FilenamePatternBasic": "basic",
"FilenamePatternNerdy": "nerdy",
"FilenameDescription": "classic: default cobalt file name pattern.\nbasic: title and basic info in brackets.\npretty: title and info in brackets.\nnerdy: title and all info in brackets.\n\nsome services dont support rich file names and always use the classic style.",
"Preview": "preview",
"FilenamePreviewVideoTitle": "Video Title",
"FilenamePreviewAudioTitle": "Audio Title",
"FilenamePreviewAudioAuthor": "Audio Author",
"StatusPage": "service status page",
"TroubleshootingGuide": "self-troubleshooting guide",
"DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly",
"SettingsTwitterGif": "convert gifs to .gif",
"SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.",
"UpdateTwitterGif": "twitter gifs and pinterest"
} }
} }

View file

@ -1,14 +1,14 @@
{ {
"name": "русский", "name": "русский",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">напиши об этом на github (можно на русском)</a>" "ContactLink": "глянь <a class=\"text-backdrop link\" href=\"{statusPage}\" target=\"_blank\">статус серверов</a> или <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">напиши о проблеме на github (можно на русском)</a>"
}, },
"strings": { "strings": {
"AppTitleCobalt": "кобальт", "AppTitleCobalt": "кобальт",
"LinkInput": "вставь ссылку сюда", "LinkInput": "вставь ссылку сюда",
"AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", "AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.", "EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано wukko, с <3", "MadeWithLove": "сделано с любовью &lt;3",
"AccessibilityInputArea": "зона вставки ссылки", "AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания", "AccessibilityDownloadButton": "кнопка скачивания",
@ -45,7 +45,6 @@
"SettingsEnableDownloadPopup": "выбор метода скачивания", "SettingsEnableDownloadPopup": "выбор метода скачивания",
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github",
"NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", "NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop link\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
@ -77,12 +76,12 @@
"ImagePickerExplanationPhone": "зажми и удерживай картинку, чтобы её сохранить.", "ImagePickerExplanationPhone": "зажми и удерживай картинку, чтобы её сохранить.",
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.", "ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.", "ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. попробуй ещё раз, а если не поможет, то {ContactLink}.",
"PasteFromClipboard": "вставить и скачать", "PasteFromClipboard": "вставить",
"ChangelogOlder": "предыдущие версии (тоже на английском)", "ChangelogOlder": "предыдущие версии (тоже на английском)",
"ChangelogPressToExpand": "раскрыть", "ChangelogPressToExpand": "раскрыть",
"Miscellaneous": "разное", "Miscellaneous": "разное",
"ModeToggleAuto": "авто режим", "ModeToggleAuto": "авто",
"ModeToggleAudio": "аудио режим", "ModeToggleAudio": "аудио",
"SettingsDisableNotifications": "cкрыть уведомления", "SettingsDisableNotifications": "cкрыть уведомления",
"MediaPickerTitle": "выбери, что сохранить", "MediaPickerTitle": "выбери, что сохранить",
"MediaPickerExplanationPC": "кликни то, что хочешь скачать. также можно скачать правой кнопки мыши.", "MediaPickerExplanationPC": "кликни то, что хочешь скачать. также можно скачать правой кнопки мыши.",
@ -103,10 +102,9 @@
"CollapseSupport": "поддержка и исходный код", "CollapseSupport": "поддержка и исходный код",
"CollapsePrivacy": "политика конфиденциальности", "CollapsePrivacy": "политика конфиденциальности",
"ServicesNote": "этот список далеко не финальный и постоянно пополняется, заглядывай сюда почаще!", "ServicesNote": "этот список далеко не финальный и постоянно пополняется, заглядывай сюда почаще!",
"FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:", "FollowSupport": "подписывайся на соц.сети кобальта для новостей и поддержки:",
"SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SourceCode": "шарься в исходнике, пиши о проблемах, или же форкай репозиторий:",
"SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">90 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">20 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube", "SettingsCodecSubtitle": "кодек для видео с youtube",
@ -120,32 +118,45 @@
"ShareURL": "поделиться", "ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
"UrgentDonate": "нужна твоя помощь!",
"PopupCloseDone": "готово", "PopupCloseDone": "готово",
"Accessibility": "общедоступность", "Accessibility": "общедоступность",
"SettingsReduceTransparency": "уменьшить прозрачность", "SettingsReduceTransparency": "уменьшить прозрачность",
"SettingsDisableAnimations": "убрать анимации", "SettingsDisableAnimations": "убрать анимации",
"FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!", "FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!",
"ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным <a class=\"text-backdrop link\" href=\"{repo}/wiki/Troubleshooting#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">здесь</a>\n\n...или же ты можешь просто вставить ссылку вручную.", "ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/troubleshooting.md#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">здесь</a>\n\n...или же ты можешь просто вставить ссылку вручную.",
"ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.", "ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.",
"SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала исправить всё сам <a class=\"text-backdrop link\" href=\"{repo}/wiki/Troubleshooting\" target=\"_blank\">по этому гиду!</a>", "SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала что-то из этого:",
"AccessibilityGoBack": "вернуться назад и закрыть окно", "AccessibilityGoBack": "вернуться назад и закрыть окно",
"CollapseKeyboard": "горячие клавиши", "CollapseKeyboard": "горячие клавиши",
"KeyboardShortcutsIntro": "пользуйся кобальтом ещё быстрее с горячими клавишами:", "KeyboardShortcutsIntro": "пользуйся кобальтом ещё быстрее с горячими клавишами:",
"KeyboardShortcutQuickPaste": "вставить ссылку", "KeyboardShortcutQuickPaste": "вставить ссылку",
"KeyboardShortcutClear": "очистить зону вставки ссылки", "KeyboardShortcutClear": "очистить зону вставки ссылки",
"KeyboardShortcutClosePopup": "закрыть все окна", "KeyboardShortcutClosePopup": "закрыть все окна",
"CollapseLegal": "правовые штучки", "CollapseLegal": "принципы и этика",
"FairUse": "кобальт - это инструмент для облегчения скачивания контента из интернета, и он <span class=\"text-backdrop\">не несёт никакой ответственности</span>. ты несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент.\n\nкобальт не собирает никакой информации о тебе, и не может донести на тебя, но, пожалуйста, будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nпри использовании в образовательных целях (лекции, домашние задания и т.д.), пожалуйста, прикладывай ссылку на источник.\n\nчестное использование и указание авторства выгодно всем.", "FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.",
"UrgentFeatureUpdate71": "расширение поддержки сервисов!",
"UrgentThanks": "спасибо за поддержку!",
"SettingsDisableMetadata": "не добавлять метаданные", "SettingsDisableMetadata": "не добавлять метаданные",
"UrgentNewDomain": "новый домен, тот же кобальт",
"NewDomainWelcomeTitle": "привет!", "NewDomainWelcomeTitle": "привет!",
"NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!", "NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!",
"DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)", "DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)",
"DataTransferError": "при переносе настроек что-то пошло не так. придётся зайти в настройки и настроить кобальт вручную.", "DataTransferError": "при переносе настроек что-то пошло не так. придётся зайти в настройки и настроить кобальт вручную.",
"SupportNotAffiliated": "кобальт <span class=\"text-backdrop\">не аффилирован</span> ни с одним из перечисленных выше сервисов.", "SupportNotAffiliated": "кобальт <span class=\"text-backdrop\">не аффилирован</span> ни с одним из перечисленных выше сервисов.",
"SupportMetaNoticeRU": "деятельность meta platforms inc. (владелец instagram) запрещена на территории россии." "SupportMetaNoticeRU": "деятельность meta platforms inc. (владелец instagram) запрещена на территории россии.",
"SponsoredBy": "спонсируется",
"FilenameTitle": "стиль названий файлов",
"FilenamePatternClassic": "классический",
"FilenamePatternPretty": "красивый",
"FilenamePatternBasic": "простой",
"FilenamePatternNerdy": "полный",
"FilenameDescription": "классический: стандартный стиль названия файлов кобальта.\nпростой: название и основная инфа в скобках.\nкрасивый: название и инфа в скобках.\nполный: название и вся инфа в скобках.\n\nнекоторые сервисы не поддерживают красивые имена файлов и всегда используют классический стиль.",
"Preview": "превью",
"FilenamePreviewVideoTitle": "Название Видео",
"FilenamePreviewAudioTitle": "Название Аудио",
"FilenamePreviewAudioAuthor": "Автор Аудио",
"StatusPage": "статус серверов",
"TroubleshootingGuide": "гайд по устранению проблем",
"DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
"SettingsTwitterGif": "конвертировать гифки в .gif",
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
"UpdateTwitterGif": "гифки с твиттера и одноклассники"
} }
} }

View file

@ -1,6 +1,6 @@
import * as fs from "fs"; import * as fs from "fs";
import { links, repo } from "../modules/config.js"; import { links, repo } from "../modules/config.js";
import loadJson from "../modules/sub/loadJSON.js"; import { loadJSON } from "../modules/sub/loadFromFs.js";
const locPath = './src/localization/languages'; const locPath = './src/localization/languages';
@ -10,13 +10,18 @@ let languages = [];
export async function loadLoc() { export async function loadLoc() {
const files = await fs.promises.readdir(locPath).catch((e) => { return [] }); const files = await fs.promises.readdir(locPath).catch((e) => { return [] });
files.forEach(file => { files.forEach(file => {
loc[file.split('.')[0]] = loadJson(`${locPath}/${file}`); loc[file.split('.')[0]] = loadJSON(`${locPath}/${file}`);
languages.push(file.split('.')[0]) languages.push(file.split('.')[0])
}); });
} }
export function replaceBase(s) { export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{repo}/g, repo).replace(/\*;/g, "&bull;"); return s
.replace(/\n/g, '<br>')
.replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut)
.replace(/{repo}/g, repo)
.replace(/{statusPage}/g, links.statusPage)
.replace(/\*;/g, "&bull;");
} }
export function replaceAll(lang, str, string, replacement) { export function replaceAll(lang, str, string, replacement) {
let s = replaceBase(str[string]) let s = replaceBase(str[string])

View file

@ -1,33 +1,30 @@
import UrlPattern from "url-pattern"; import { services } from "./config.js";
import { services as patterns } from "./config.js"; import { apiJSON } from "./sub/utils.js";
import { cleanURL, apiJSON } from "./sub/utils.js";
import { errorUnsupported } from "./sub/errors.js"; import { errorUnsupported } from "./sub/errors.js";
import loc from "../localization/manager.js"; import loc from "../localization/manager.js";
import match from "./processing/match.js"; import match from "./processing/match.js";
import hostOverrides from "./processing/hostOverrides.js"; import { getHostIfValid } from "./processing/url.js";
export async function getJSON(originalURL, lang, obj) { export async function getJSON(url, lang, obj) {
try { try {
let patternMatch, url = decodeURIComponent(originalURL), const host = getHostIfValid(url);
hostname = new URL(url).hostname.split('.'),
host = hostname[hostname.length - 2];
if (!url.startsWith('https://')) return apiJSON(0, { t: errorUnsupported(lang) }); if (!host || !services[host].enabled) {
return apiJSON(0, { t: errorUnsupported(lang) });
let overrides = hostOverrides(host, url); }
host = overrides.host;
url = overrides.url; let patternMatch;
for (const pattern of services[host].patterns) {
if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) return apiJSON(0, { t: errorUnsupported(lang) }); patternMatch = pattern.match(
url.pathname.substring(1) + url.search
let pathToMatch = cleanURL(url, host).split(`.${patterns[host]['tld'] ? patterns[host]['tld'] : "com"}/`)[1].replace('.', ''); );
for (let i in patterns[host]["patterns"]) { if (patternMatch) break;
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(pathToMatch); }
if (patternMatch) break
if (!patternMatch) {
return apiJSON(0, { t: errorUnsupported(lang) });
} }
if (!patternMatch) return apiJSON(0, { t: errorUnsupported(lang) });
return await match(host, patternMatch, url, lang, obj) return await match(host, patternMatch, url, lang, obj)
} catch (e) { } catch (e) {

View file

@ -1,21 +1,67 @@
{ {
"current": { "current": {
"version": "7.9",
"date": "January 17, 2024",
"title": "twitter gifs, pinterest, ok.ru, and more!",
"banner": {
"file": "meowthball.webp",
"alt": "meowth rolling on a big catnip ball",
"width": 478,
"height": 350
},
"content": "yes, you read that right. cobalt now lets you convert any twitter gif to an actual .gif file! (finally)\njust go to settings and enable this feature :)\n\nservice improvements:\n*; added an option to <a class=\"text-backdrop link\" href=\"{repo}/issues/250\" target=\"_blank\">convert gifs from twitter</a> into actual .gif format. files will be bigger and lower quality, but maybe you want that.\n*; pinterest support has been completely redone, now all videos (<a class=\"text-backdrop link\" href=\"{repo}/issues/160\" target=\"_blank\">and even pin.it links</a>) are supported.\n*; added <a class=\"text-backdrop link\" href=\"{repo}/issues/322\" target=\"_blank\">support for ok.ru</a> in case you're a russian grandma.\n*; now processing <a class=\"text-backdrop link\" href=\"{repo}/issues/318\" target=\"_blank\">all reddit links</a> (including old.reddit.com).\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/316\" target=\"_blank\">instagram live vods</a> are now supported.\n*; fixed a <a class=\"text-backdrop link\" href=\"{repo}/issues/289\" target=\"_blank\">rare vimeo bug</a> related to 1440p videos.\n\nother improvements:\n*; ui fade in animation is no longer present if you've disabled animations.\n*; all images now have alt descriptions.\n*; cobalt html is now <a class=\"text-backdrop link\" href=\"{repo}/issues/317\" target=\"_blank\">biblically correct</a> and follows the html spec.\n*; lots of cleaning up.\n\npatches since 7.8:\n*; shift+key <a class=\"text-backdrop link\" href=\"{repo}/issues/288\" target=\"_blank\">shortcuts are now ignored</a> if url bar is focused.\n*; longer soundcloud links are now supported, also catching more tiktok-related errors.\n*; removed mastodon from support links as that account is no longer active.\n*; added ability to download a specific video from multi media tweets and support for /mediaViewer links.\n*; fixed <a class=\"text-backdrop link\" href=\"{repo}/issues/309\" target=\"_blank\">modal blurriness</a> in chromium.\n*; minor html changes (road to biblically correct one).\n\nlots of long-awaited updates (especially twitter gifs), hope you enjoy them and have a great day :D"
},
"history": [{
"version": "7.8",
"date": "December 25, 2023",
"title": "new years clean up! bug fixes and fresh look for the home page",
"banner": {
"file": "catroomba.webp",
"alt": "a cat riding a roomba vacuum",
"width": 300,
"height": 168
},
"content": "merry christmas and happy new year! this update fixes several (very annoying) bugs to help you enjoy your holidays better.\n\nyou might have already noticed, but we've refreshed the home page on desktop and mobile! less space wasted, more pleasant to look at. let us know if you like it or not :D\n\nservice improvements:\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/264\" target=\"_blank\">#264</a> anything that includes a period in the url should be possible to download (including instagram stories).\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/273\" target=\"_blank\">#73</a> soundcloud: falling back to mp3 instead of refusing to download the song at all.\n*; <a class=\"text-backdrop link\" href=\"{repo}/issues/275\" target=\"_blank\">#275</a> youtube: query parameters are parsed and handled correctly, all links should be supported, no matter where v query is located.\n*; tlds are parsed and validated correctly (e.g. \"pinterest.co.uk\" works now).\n*; fixvx.com links are now supported.\n\ninterface improvements:\n*; cleaner and more consistent home page layout.\n*; cleaned up support section in \"about\". also includes a link to the status page.\n\ninternal improvements:\n*; urls, subdomains, and tlds are properly validated.\n*; minor clean up.\n\nchanges since 7.7:\n*; made terms and ethics more descriptive.\n*; fix only affected twitter videos.\n*; fixed quick ⌘+V pasting on mac.\n*; now catching even more youtube-related errors.\n\nthis might not seem like a lot, but even smaller changes make a difference!\n\nenjoy this update and the rest of your day :D"
}, {
"version": "7.7",
"date": "December 2, 2023",
"title": "bugfixes and better downloads!",
"banner": {
"file": "meowthpolishegg.webp",
"alt": "meowth polishing a togepi egg",
"width": 640,
"height": 480
},
"content": "this update fixes various issues with supported services. no new features yet, but twitter fix is surely something good to have in the meantime!\n\nservice improvements:\n*; broken twitter videos are now automatically fixed by cobalt.\n*; all vimeo videos and audios should now be possible to download.\n*; vimeo: fixed short resolution displayed in \"basic\" and \"pretty\" filename styles.\n\ninterface improvements:\n*; streamables are now easier to save on ios.\n\ninternal improvements:\n*; port env variable is now not strictly necessary for cobalt to run.\n*; minor clean up.\n\nchanges since 7.6:\n*; fix for an issue related to youtube dubs.\n*; fixed a memory leak related to live renders.\n*; handling all errors related to twitter downloads.\n*; fixed support for reddit links in various languages.\n*; added rich filenames support for twitch clips.\n*; updated support and donation lists.\n\nstay tuned for future updates and have a great day :D"
}, {
"version": "7.6",
"date": "October 15, 2023",
"title": "customizable file names, instagram stories, and first cobalt sponsor!",
"banner": {
"file": "meowthcenter.webp",
"alt": "meowth plush in a datacenter wearing a hardhat, wielding a hammer",
"width": 851,
"height": 640
},
"content": "as many have (very) often requested, cobalt now lets you pick between several file name format styles!\ngo to <span class=\"text-backdrop\">settings > other</span> and change it to whichever you like! there's a preview of each style, so you know how exactly files are gonna look like.\n\nif you liked file names the way they were before, don't worry: classic style is still the default :)\n\non a different but not any less important note: cobalt is now sponsored by <a class=\"text-backdrop link\" href=\"https://royalehosting.net/\" target=\"_blank\">royalehosting.net</a>!\noverall service performance and stability is gonna be better, but also more content will be possible to download thanks to geniuine server locations. and yes, still no ads or trackers.\n\nthis update also includes a bunch of other changes, check them out:\n\nservice improvements:\n*; added support for instagram stories thanks to <a class=\"text-backdrop link\" href=\"{repo}/pull/194\" target=\"_blank\">#194</a>.\n*; fixed reddit support thanks to <a class=\"text-backdrop link\" href=\"{repo}/pull/221\" target=\"_blank\">#221</a>.\n*; added support for rich file names for youtube, vimeo, soundcloud, rutube, and vk.\n*; numbers and emoji no longer disappear from file name and metadata.\n*; mute and audio dub file name tags don't appear together anymore.\n*; youtube: dub file name tag doesn't appear anymore if audio track is default.\n\ninterface improvements:\n*; added a list of sponsors to about tab. if you host an instance, it's disabled by default, but can be enabled with showSponsors env variable.\n*; about button now opens about tab when no new changelog is available.\n*; fixed download button thickness on ios.\n\nyou now can reach out to cobalt via email for support! it's located in the about tab along with other socials, such as discord.\n\ni hope you enjoy this long-awaited update and have a blissful day :D"
}, {
"version": "7.5", "version": "7.5",
"date": "September 16, 2023", "date": "September 16, 2023",
"title": "support for twitch clips and rutube!", "title": "support for twitch clips and rutube!",
"banner": { "banner": {
"file": "twitchupdate.webp", "file": "twitchupdate.webp",
"alt": "meowth plush staring into the camera, laptop with generic purple service in the background",
"width": 851, "width": 851,
"height": 640 "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!" "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", "version": "7.4",
"date": "September 9, 2023", "date": "September 9, 2023",
"title": "new domain, what's coming in future, bug fixes, and more!", "title": "new domain, what's coming in future, bug fixes, and more!",
"banner": { "banner": {
"file": "newdomain.webp", "file": "newdomain.webp",
"alt": "text: new domain, same cobalt",
"width": 960, "width": 960,
"height": 540 "height": 540
}, },
@ -26,26 +72,29 @@
"title": "extended video length limit, metadata toggle, ui improvements, and more!", "title": "extended video length limit, metadata toggle, ui improvements, and more!",
"banner": { "banner": {
"file": "meowthsnap.webp", "file": "meowthsnap.webp",
"alt": "cartoon meowth pointing paw dramatically and saying something",
"width": 500, "width": 500,
"height": 280 "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 <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pkgs/container/cobalt\" target=\"_blank\">github</a>.\n\nthank you for being here. i hope you have a great day :D" "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 <a class=\"text-backdrop link\" href=\"{repo}/pkgs/container/cobalt\" target=\"_blank\">github</a>.\n\nthank you for being here. i hope you have a great day :D"
}, { }, {
"version": "7.1", "version": "7.1",
"date": "August 20, 2023", "date": "August 20, 2023",
"title": "instagram, streamable, video metadata, and more!", "title": "instagram, streamable, video metadata, and more!",
"banner": { "banner": {
"file": "meowthproductions.webp", "file": "meowthproductions.webp",
"alt": "meowth roaring in a fancy circle, à la MGM studios intro",
"width": 640, "width": 640,
"height": 358 "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 <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/179\" target=\"_blank\">#179</a>)\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 <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/177\" target=\"_blank\">#177</a>)\n*; replaced got with undici (thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/182\" target=\"_blank\">#182</a>). 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" "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 <a class=\"text-backdrop link\" href=\"{repo}/pull/179\" target=\"_blank\">#179</a>)\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 <a class=\"text-backdrop link\" href=\"{repo}/pull/177\" target=\"_blank\">#177</a>)\n*; replaced got with undici (thanks to <a class=\"text-backdrop link\" href=\"{repo}/pull/182\" target=\"_blank\">#182</a>). 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"
}, { }, {
"version": "7.0", "version": "7.0",
"date": "August 15, 2023", "date": "August 15, 2023",
"title": "biggest ui refresh yet!", "title": "biggest ui refresh yet!",
"banner": { "banner": {
"file": "meowthcooking.webp", "file": "meowthcooking.webp",
"alt": "meowth handling orders in a restaurant",
"width": 640, "width": 640,
"height": 360 "height": 360
}, },
@ -56,6 +105,7 @@
"title": "all network issues have been fixed!", "title": "all network issues have been fixed!",
"banner": { "banner": {
"file": "meowthhammer.webp", "file": "meowthhammer.webp",
"alt": "meowth plush holding a hammer in real life",
"width": 1280, "width": 1280,
"height": 827 "height": 827
}, },
@ -66,15 +116,17 @@
"title": "better reliability, new infrastructure, pinterest support, and way more!", "title": "better reliability, new infrastructure, pinterest support, and way more!",
"banner": { "banner": {
"file": "catswitchboxes.webp", "file": "catswitchboxes.webp",
"alt": "a cat climbing into two empty boxes of asahi beer",
"width": 600, "width": 600,
"height": 314 "height": 314
}, },
"content": "hey! long time no see, hopefully over 40 changes will make up for it :)\n\ncobalt now has an official community discord server. you can go there for news, support, or just to chat. <a class=\"text-backdrop link\" href=\"https://discord.gg/pQPt8HBUPu\" target=\"_blank\">go check it out!</a>\n\n<span class='text-backdrop'>tl;dr</span>\n*; new infra, new hosting structure, new main instance api url. developers, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/blob/current/docs/API.md\" target=\"_blank\">get it here.</a>\n*; added support for pinterest, vine archive, tumblr audio, youtube vr videos.\n*; better web app performance and look.\n*; better stability thanks to load balancing.\n*; (hopefully) no more random video/audio download drops.\n\nservice improvements:\n*; added support for pinterest videos and stories (pr by <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/commit/40291c4d24cb5f441cdddfd26104f149bc4ee27c\" target=\"_blank\">@Snazzah</a>).\n*; added support for tumblr audio. sorry, tumblr.\n*; added support for youtube vr videos. please note that they're in youtube's proprietary ratio.\n*; added support for vine archive.\n*; added support for ancient vk videos in 240p.\n*; fixed an issue related to muted video downloads from tumblr.\n*; moved to twitter v2 api.\n*; soundcloud share links are now processed without errors.\n\nui improvements:\n*; lazy image loading. should significantly speed up the page load.\n*; fixed checkbox width on mobile devices.\n*; addition of a temporary urgent notice.\n*; added hover border to all buttons.\n*; less annoying donation button highlight.\n*; more consistent color scheme.\n*; added link to a discord server into about popup.\n*; remember celebratory emoji changes? they've been fixed, and are now dynamically loaded!\n*; changelog history now lets you try to load it again if first attempt failed for whatever reason.\n*; padding (everywhere) has been slightly reduced to fit in more content and be consistent across ui.\n*; added more info to the \"how to save\" popup for ios devices.\n*; crypto wallet press-to-copy buttons now look like buttons.\n*; improved ui layout for smallest screens (iphone 5, 5s, se, etc).\n*; removed partial translations for sake of clarity and consistency.\n\ninternal improvements:\n*; separated web and api servers. they're now completely independent and therefore more stress-resistant.\n*; added a dedicated script for building the web app if you don't want to reload the frontend server.\n*; web app building improvements.\n*; async localization preloading.\n*; consistent server start time reporting.\n*; dynamic stream and ip hashing salt generation.\n\ninfrastructure improvements:\n*; load balancing: your api requests are now sent to the least busy server. yes, there are now several of them with more to come in the future.\n*; when possible, server in closest region is used instead of a far-away one. this should help with download speeds.\n*; currently there are multiple servers in europe. i will let you know when (and if) i manage to get an american one.\n\nupdates for developers and instance hosters:\n*; server info api endpoint: you can now check up on the api server of choice. it reports all the basic info you may need. <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/blob/current/docs/API.md#get-apiserverinfo\" target=\"_blank\">check the api docs</a> for more info.\n*; api names: each and every api instance should have a distinctive name. this will be useful in the future :)\n*; added docker compose sample config.\n*; updated and more granular setup script.\n*; better api scalability and faster server start up thanks to web and api separation.\n*; added ability to specify ffmpeg threads. simply add ffmpegThreads to your environment variables!\n\ni'm still in awe from how popular cobalt has become. there are now over 200k of unique users monthly, and that number only keeps growing. i even had to come up with something to accommodate for larger traffic, it's absolutely insane.\n\nlove you all, have a great day :D" "content": "hey! long time no see, hopefully over 40 changes will make up for it :)\n\ncobalt now has an official community discord server. you can go there for news, support, or just to chat. <a class=\"text-backdrop link\" href=\"https://discord.gg/pQPt8HBUPu\" target=\"_blank\">go check it out!</a>\n\n<span class='text-backdrop'>tl;dr</span>\n*; new infra, new hosting structure, new main instance api url. developers, <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/API.md\" target=\"_blank\">get it here.</a>\n*; added support for pinterest, vine archive, tumblr audio, youtube vr videos.\n*; better web app performance and look.\n*; better stability thanks to load balancing.\n*; (hopefully) no more random video/audio download drops.\n\nservice improvements:\n*; added support for pinterest videos and stories (pr by <a class=\"text-backdrop link\" href=\"{repo}/commit/40291c4d24cb5f441cdddfd26104f149bc4ee27c\" target=\"_blank\">@Snazzah</a>).\n*; added support for tumblr audio. sorry, tumblr.\n*; added support for youtube vr videos. please note that they're in youtube's proprietary ratio.\n*; added support for vine archive.\n*; added support for ancient vk videos in 240p.\n*; fixed an issue related to muted video downloads from tumblr.\n*; moved to twitter v2 api.\n*; soundcloud share links are now processed without errors.\n\nui improvements:\n*; lazy image loading. should significantly speed up the page load.\n*; fixed checkbox width on mobile devices.\n*; addition of a temporary urgent notice.\n*; added hover border to all buttons.\n*; less annoying donation button highlight.\n*; more consistent color scheme.\n*; added link to a discord server into about popup.\n*; remember celebratory emoji changes? they've been fixed, and are now dynamically loaded!\n*; changelog history now lets you try to load it again if first attempt failed for whatever reason.\n*; padding (everywhere) has been slightly reduced to fit in more content and be consistent across ui.\n*; added more info to the \"how to save\" popup for ios devices.\n*; crypto wallet press-to-copy buttons now look like buttons.\n*; improved ui layout for smallest screens (iphone 5, 5s, se, etc).\n*; removed partial translations for sake of clarity and consistency.\n\ninternal improvements:\n*; separated web and api servers. they're now completely independent and therefore more stress-resistant.\n*; added a dedicated script for building the web app if you don't want to reload the frontend server.\n*; web app building improvements.\n*; async localization preloading.\n*; consistent server start time reporting.\n*; dynamic stream and ip hashing salt generation.\n\ninfrastructure improvements:\n*; load balancing: your api requests are now sent to the least busy server. yes, there are now several of them with more to come in the future.\n*; when possible, server in closest region is used instead of a far-away one. this should help with download speeds.\n*; currently there are multiple servers in europe. i will let you know when (and if) i manage to get an american one.\n\nupdates for developers and instance hosters:\n*; server info api endpoint: you can now check up on the api server of choice. it reports all the basic info you may need. <a class=\"text-backdrop link\" href=\"{repo}/blob/current/docs/API.md#get-apiserverinfo\" target=\"_blank\">check the api docs</a> for more info.\n*; api names: each and every api instance should have a distinctive name. this will be useful in the future :)\n*; added docker compose sample config.\n*; updated and more granular setup script.\n*; better api scalability and faster server start up thanks to web and api separation.\n*; added ability to specify ffmpeg threads. simply add ffmpegThreads to your environment variables!\n\ni'm still in awe from how popular cobalt has become. there are now over 200k of unique users monthly, and that number only keeps growing. i even had to come up with something to accommodate for larger traffic, it's absolutely insane.\n\nlove you all, have a great day :D"
}, { }, {
"version": "5.4", "version": "5.4",
"title": "instagram support, docker, and more!", "title": "instagram support, docker, and more!",
"banner": { "banner": {
"file": "catphonestand.webp", "file": "catphonestand.webp",
"alt": "a cat holding a phone under its chin while a person plays clash of clans on it",
"width": 451, "width": 451,
"height": 272 "height": 272
}, },
@ -84,6 +136,7 @@
"title": "better looks, better feel", "title": "better looks, better feel",
"banner": { "banner": {
"file": "cattired.webp", "file": "cattired.webp",
"alt": "a cat laying on a sofa face down, wiggling its tail",
"width": 640, "width": 640,
"height": 286 "height": 286
}, },
@ -93,6 +146,7 @@
"title": "fastest one in the game", "title": "fastest one in the game",
"banner": { "banner": {
"file": "catspeed.webp", "file": "catspeed.webp",
"alt": "a cat running very fast in an exercise wheel",
"width": 640, "width": 640,
"height": 356 "height": 356
}, },
@ -102,6 +156,7 @@
"title": "the evil has been defeated", "title": "the evil has been defeated",
"banner": { "banner": {
"file": "happymeowth.webp", "file": "happymeowth.webp",
"alt": "meowth jumping up into the sky very excitedly",
"width": 500, "width": 500,
"height": 330 "height": 330
}, },
@ -111,6 +166,7 @@
"title": "it's all about attention to detail!", "title": "it's all about attention to detail!",
"banner": { "banner": {
"file": "valentines.webp", "file": "valentines.webp",
"alt": "relaxed meowth with sakura petals falling in front of them",
"width": 489, "width": 489,
"height": 374 "height": 374
}, },
@ -120,6 +176,7 @@
"title": "prettier than ever", "title": "prettier than ever",
"banner": { "banner": {
"file": "catmakeup.webp", "file": "catmakeup.webp",
"alt": "a cat being brushed with a powder makeup brush",
"width": 394, "width": 394,
"height": 266 "height": 266
}, },
@ -129,6 +186,7 @@
"title": "we're better together! thank you for bug reports.", "title": "we're better together! thank you for bug reports.",
"banner": { "banner": {
"file": "bettertogether.webp", "file": "bettertogether.webp",
"alt": "various different pokémon jumping in happiness",
"width": 640, "width": 640,
"height": 358 "height": 358
}, },
@ -138,6 +196,7 @@
"title": "mute videos and proper soundcloud support", "title": "mute videos and proper soundcloud support",
"banner": { "banner": {
"file": "shutup.webp", "file": "shutup.webp",
"alt": "a cat yawning, with a crossed out loudspeaker icon next to it",
"width": 1024, "width": 1024,
"height": 665 "height": 665
}, },
@ -147,15 +206,17 @@
"title": "better, faster, stronger, stable", "title": "better, faster, stronger, stable",
"banner": { "banner": {
"file": "meowthstrong.webp", "file": "meowthstrong.webp",
"alt": "meowth stretching",
"width": 500, "width": 500,
"height": 280 "height": 280
}, },
"content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop link\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop link\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me." "content": "your favorite social media downloader just got even better! this update includes a ton of improvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n*; vimeo module has been revamped, all sorts of videos should now be supported.\n*; vimeo audio downloads! you now can download audios from more recent videos.\n*; cobalt now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n*; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n*; youtube videos with community warnings should now be possible to download.\nuser interface improvements:\n*; list of supported services is now MUCH easier to read.\n*; banners in changelog history should no longer overlap each other.\n*; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.\ninternal improvements:\n*; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n*; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n*; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop link\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop link\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n*; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n*; \"got\" package has been (mostly) replaced by native fetch api. this should greatly reduce ram usage.\n*; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n*; other code optimizations. there's less clutter overall.\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop link\" href=\"{repo}/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
}, { }, {
"version": "4.4", "version": "4.4",
"title": "over 1 million monthly requests. thank you.", "title": "over 1 million monthly requests. thank you.",
"banner": { "banner": {
"file": "onemillionr.webp", "file": "onemillionr.webp",
"alt": "cobalt logo and a confetti emoji",
"width": 1441, "width": 1441,
"height": 1441 "height": 1441
}, },
@ -169,10 +230,11 @@
"title": "developers, developers, developers, developers", "title": "developers, developers, developers, developers",
"banner": { "banner": {
"file": "developers.webp", "file": "developers.webp",
"alt": "steve ballmer going \"developers, developers, developers\"",
"width": 640, "width": 640,
"height": 360 "height": 360
}, },
"content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop link\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop link\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made." "content": "this update features a TON of improvements.\n\n<a class=\"text-backdrop link\" href=\"https://www.youtube.com/watch?v=SaVTHG-Ev4k\" target=\"_blank\">developers</a>, you now can rely on cobalt for getting content from social media. the api has been revamped and <a class=\"text-backdrop link\" href=\"{repo}/tree/current/docs/API.md\" target=\"_blank\">documentation</a> is now available. you can read more about API changes down below. go crazy, and have fun :D\n\nif you're not a developer, here's a list of changes that you probably care about:\n- rate limit is now approximately 8 times bigger. no more waiting, even if you want to download entirety of your tiktok \"for you\" page.\n- some updates will now have expressive banners, just like this one.\n- fixed what was causing an error when a youtube video had no description.\n- mp4 format button text should now be displayed properly, no matter if you touched the switcher or not.\n\nnext, the star of this update — improved api!\n- main endpoint now uses POST method instead of GET.\n- internal variables for preferences have been updated to be consistent and easier to understand.\n- ip address is now hashed right upon request, not somewhere deep inside the code.\n- global stream salt variable is no longer unnecessarily passed over a billion functions.\n- url and picker keys are now separate in the json response.\n- cobalt web app now correctly processes responses with \"success\" status.\n\nif you currently have a siri shortcut or some other script that uses the GET method, make sure to update it soon. this method is deprecated, limited, and will be removed entirely in coming updates.\n\nif you ever make something using cobalt's api, make sure to mention <a class=\"text-backdrop link\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a> on twitter, i would absolutely love to see what you made."
}, { }, {
"version": "4.2", "version": "4.2",
"title": "optimized quality picking and 8k video support", "title": "optimized quality picking and 8k video support",
@ -184,7 +246,7 @@
}, { }, {
"version": "4.0", "version": "4.0",
"title": "better and faster than ever", "title": "better and faster than ever",
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>." "content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">you can do it on github</a>."
}, { }, {
"version": "3.7", "version": "3.7",
"title": "support for multi media tweets is here!", "title": "support for multi media tweets is here!",

View file

@ -1,37 +1,39 @@
import { replaceBase } from "../../localization/manager.js"; import { replaceBase } from "../../localization/manager.js";
import loadJSON from "../sub/loadJSON.js"; import { loadJSON } from "../sub/loadFromFs.js";
let changelog = loadJSON('./src/modules/changelog/changelog.json') let changelog = loadJSON('./src/modules/changelog/changelog.json')
export default function(string) { export default function(string) {
try { try {
const currentChangelog = changelog.current;
switch (string) { switch (string) {
case "version": case "version":
return `<span class="text-backdrop changelog-tag-version">v.${changelog["current"]["version"]}</span>${ return `<span class="text-backdrop changelog-tag-version">v.${currentChangelog.version}</span>${
changelog["current"]["date"] ? `<span class="changelog-tag-date">· ${changelog["current"]["date"]}</span>` : '' currentChangelog.date ? `<span class="changelog-tag-date">· ${currentChangelog.date}</span>` : ''
}` }`
case "title": case "title":
return replaceBase(changelog["current"]["title"]); return replaceBase(currentChangelog.title);
case "banner": case "banner":
return changelog["current"]["banner"] ? { const currentBanner = changelog.current.banner;
url: `updateBanners/${changelog["current"]["banner"]["file"]}`, return currentBanner ? {
width: changelog["current"]["banner"]["width"], ...currentBanner,
height: changelog["current"]["banner"]["height"] url: `updateBanners/${currentBanner.file}`
} : false; } : false;
case "content": case "content":
return replaceBase(changelog["current"]["content"]); return replaceBase(currentChangelog.content);
case "history": case "history":
return changelog["history"].map((i) => { return changelog.history.map((log) => {
const banner = log.banner;
return { return {
title: replaceBase(i["title"]), title: replaceBase(log.title),
version: `<span class="text-backdrop changelog-tag-version">v.${i["version"]}</span>${ version: `<span class="text-backdrop changelog-tag-version">v.${log.version}</span>${
i["date"] ? `<span class="changelog-tag-date">· ${i["date"]}</span>` : '' log.date ? `<span class="changelog-tag-date">· ${log.date}</span>` : ''
}`, }`,
content: replaceBase(i["content"]), content: replaceBase(log.content),
banner: i["banner"] ? { banner: banner ? {
url: `updateBanners/${i["banner"]["file"]}`, ...banner,
width: i["banner"]["width"], url: `updateBanners/${banner.file}`
height: i["banner"]["height"]
} : false, } : false,
} }
}); });

View file

@ -1,7 +1,16 @@
import loadJson from "./sub/loadJSON.js"; import UrlPattern from "url-pattern";
const config = loadJson("./src/config.json"); import { loadJSON } from "./sub/loadFromFs.js";
const packageJson = loadJson("./package.json"); const config = loadJSON("./src/config.json");
const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.json"); const packageJson = loadJSON("./package.json");
const servicesConfigJson = loadJSON("./src/modules/processing/servicesConfig.json");
Object.values(servicesConfigJson.config).forEach(service => {
service.patterns = service.patterns.map(
pattern => new UrlPattern(pattern, {
segmentValueCharset: UrlPattern.defaultOptions.segmentValueCharset + '@\\.'
})
)
})
export const export const
services = servicesConfigJson.config, services = servicesConfigJson.config,
@ -16,4 +25,5 @@ export const
ffmpegArgs = config.ffmpegArgs, ffmpegArgs = config.ffmpegArgs,
supportedAudio = config.supportedAudio, supportedAudio = config.supportedAudio,
celebrations = config.celebrations, celebrations = config.celebrations,
links = config.links links = config.links,
sponsors = config.sponsors

View file

@ -35,12 +35,20 @@ const names = {
"📑": "boring_document", "📑": "boring_document",
"🧮": "abacus", "🧮": "abacus",
"😸": "cat_grin", "😸": "cat_grin",
"📰": "newspaper" "📰": "newspaper",
"🎞️": "film_frames",
"🎧": "headphone",
"📧": "email",
"📬": "mailbox",
"📢": "loudspeaker",
"🔧": "wrench",
"🫧": "bubbles"
} }
let sizing = { let sizing = {
18: 0.8, 18: 0.8,
22: 0.4, 22: 0.4,
30: 0.7, 30: 0.7,
32: 0.8,
48: 0.9, 48: 0.9,
64: 0.9, 64: 0.9,
78: 0.9 78: 0.9
@ -54,5 +62,5 @@ export default function(emoji, size, disablePadding, fluent) {
let filePath = `emoji/${names[emoji]}.svg`; let filePath = `emoji/${names[emoji]}.svg`;
if (fluent) filePath = `emoji/3d/${names[emoji]}.svg`; if (fluent) filePath = `emoji/3d/${names[emoji]}.svg`;
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}"` : ''}alt="${emoji}" src="${filePath}" loading="lazy">` return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}" ` : ''}alt="${emoji}" src="${filePath}" loading="lazy">`
} }

View file

@ -1,5 +1,6 @@
import { authorInfo, celebrations } from "../config.js"; import { authorInfo, celebrations, sponsors } from "../config.js";
import emoji from "../emoji.js"; import emoji from "../emoji.js";
import { loadFile } from "../sub/loadFromFs.js";
export const backButtonSVG = `<svg width="22" height="22" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> export const backButtonSVG = `<svg width="22" height="22" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.1905 28.5L2 16L14.1905 3.5L16.2857 5.62054L7.65986 14.4654H30V17.5346H7.65986L16.2857 26.3516L14.1905 28.5Z" fill="#E1E1E1"/> <path d="M14.1905 28.5L2 16L14.1905 3.5L16.2857 5.62054L7.65986 14.4654H30V17.5346H7.65986L16.2857 26.3516L14.1905 28.5Z" fill="#E1E1E1"/>
@ -9,6 +10,8 @@ export const dropdownSVG = `<svg width="18" height="18" viewBox="0 0 32 32" fill
<path d="M28 12.0533L16 24L4 12.0533L6.03571 10L14.7188 18.4104L16.25 19.9348L17.7813 18.4104L25.9375 10L28 12.0533Z" fill="#E1E1E1"/> <path d="M28 12.0533L16 24L4 12.0533L6.03571 10L14.7188 18.4104L16.25 19.9348L17.7813 18.4104L25.9375 10L28 12.0533Z" fill="#E1E1E1"/>
</svg>` </svg>`
export const linkSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M137.54 186.36a8 8 0 0 1 0 11.31l-9.94 10a56 56 0 0 1-79.22-79.27l24.12-24.12a56 56 0 0 1 76.81-2.28a8 8 0 1 1-10.64 12a40 40 0 0 0-54.85 1.63L59.7 139.72a40 40 0 0 0 56.58 56.58l9.94-9.94a8 8 0 0 1 11.32 0Zm70.08-138a56.08 56.08 0 0 0-79.22 0l-9.94 9.95a8 8 0 0 0 11.32 11.31l9.94-9.94a40 40 0 0 1 56.58 56.58l-24.12 24.14a40 40 0 0 1-54.85 1.6a8 8 0 1 0-10.64 12a56 56 0 0 0 76.81-2.26l24.12-24.12a56.08 56.08 0 0 0 0-79.24Z"/></svg>'
export function switcher(obj) { export function switcher(obj) {
let items = ``; let items = ``;
if (obj.name === "download") { if (obj.name === "download") {
@ -16,8 +19,6 @@ export function switcher(obj) {
} else { } else {
for (let i = 0; i < obj.items.length; i++) { for (let i = 0; i < obj.items.length; i++) {
let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : []; let classes = obj.items[i]["classes"] ? obj.items[i]["classes"] : [];
if (i === 0) classes.push("first");
if (i === (obj.items.length - 1)) classes.push("last");
items += `<button id="${obj.name}-${obj.items[i]["action"]}" class="switch${classes.length > 0 ? ' ' + classes.join(' ') : ''}" onclick="changeSwitcher('${obj.name}', '${obj.items[i]["action"]}')">${obj.items[i]["text"] ? obj.items[i]["text"] : obj.items[i]["action"]}</button>` items += `<button id="${obj.name}-${obj.items[i]["action"]}" class="switch${classes.length > 0 ? ' ' + classes.join(' ') : ''}" onclick="changeSwitcher('${obj.name}', '${obj.items[i]["action"]}')">${obj.items[i]["text"] ? obj.items[i]["text"] : obj.items[i]["action"]}</button>`
} }
} }
@ -58,35 +59,35 @@ export function popup(obj) {
body = `` body = ``
for (let i = 0; i < obj.body.length; i++) { for (let i = 0; i < obj.body.length; i++) {
if (obj.body[i]["text"].length > 0) { if (obj.body[i]["text"].length > 0) {
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : [] classes = obj.body[i]["classes"] ?? []
if (i !== obj.body.length - 1 && !obj.body[i]["nopadding"]) { if (i !== obj.body.length - 1 && !obj.body[i]["nopadding"]) {
classes.push("desc-padding") classes.push("desc-padding")
} }
body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>` body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div class="${['popup-desc', ...classes].join(' ')}">${obj.body[i]["text"]}</div>`
} }
} }
} }
return ` return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box": ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''} ${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box" : ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''}
<div id="popup-header" class="popup-header"> <div class="popup-header">
<div id="popup-header-contents"> <div class="popup-header-contents">
${obj.buttonOnly ? obj.header.emoji : ``} ${obj.buttonOnly ? obj.header.emoji : ``}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div> </div>
${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>`: ''} ${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''}
</div> </div>
<div id="popup-content" class="popup-content-inner"> <div class="popup-content popup-content-inner">
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''} ${body}${obj.buttonOnly ? `<button class="close-error switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div> </div>
${classes.includes("small") ? `<div class="glass-bkg small"></div>`: ''} ${classes.includes("small") ? `<div class="glass-bkg small"></div>` : ''}
${obj.standalone ? `</div>` : ''}` ${obj.standalone ? `</div>` : ''}`
} }
export function multiPagePopup(obj) { export function multiPagePopup(obj) {
let tabs = ` let tabs = `
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}> <button class="back-button switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG} ${backButtonSVG}
</button>`; </button>`;
@ -98,16 +99,16 @@ export function multiPagePopup(obj) {
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content"> <div class="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header"> ${obj.header ? `<div class="popup-header">
<div id="popup-header-contents"> <div class="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div> </div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div>` : ''}${tabContent}</div> </div>` : ''}${tabContent}</div>
<div id="popup-tabs" class="switches popup-tabs"> <div class="switches popup-tabs">
<div class="switches popup-tabs-child">${tabs}</div> <div class="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div> </div>
@ -118,8 +119,6 @@ export function collapsibleList(arr) {
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
let classes = arr[i]["classes"] ? arr[i]["classes"] : []; let classes = arr[i]["classes"] ? arr[i]["classes"] : [];
if (i === 0) classes.push("first");
if (i === (arr.length - 1)) classes.push("last");
items += `<div id="${arr[i]["name"]}-collapse" class="collapse-list${classes.length > 0 ? ' ' + classes.join(' ') : ''}"> items += `<div id="${arr[i]["name"]}-collapse" class="collapse-list${classes.length > 0 ? ' ' + classes.join(' ') : ''}">
<div class="collapse-header" onclick="expandCollapsible(event)"> <div class="collapse-header" onclick="expandCollapsible(event)">
<div class="collapse-title">${arr[i]["title"]}</div> <div class="collapse-title">${arr[i]["title"]}</div>
@ -132,7 +131,7 @@ export function collapsibleList(arr) {
} }
export function popupWithBottomButtons(obj) { export function popupWithBottomButtons(obj) {
let tabs = ` let tabs = `
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}> <button class="back-button switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG} ${backButtonSVG}
</button>` </button>`
@ -141,38 +140,38 @@ export function popupWithBottomButtons(obj) {
} }
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content"> <div class="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header"> ${obj.header ? `<div class="popup-header">
<div id="popup-header-contents"> <div class="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''} ${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}
</div> </div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div>` : ''}${obj.content}</div> </div>` : ''}${obj.content}</div>
<div id="popup-tabs" class="switches popup-tabs"> <div class="switches popup-tabs">
<div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div> <div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div> </div>
</div>` </div>`
} }
export function socialLink(emji, name, handle, url) { export function socialLink(emji, name, url) {
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop link" href="${url}" target="_blank">${handle}</a></div>` return `<div class="cobalt-support-link">${emji} <a class="text-backdrop link" href="${url}" target="_blank">${name}</a></div>`
} }
export function socialLinks(lang) { export function socialLinks(lang) {
let links = authorInfo.support[lang] ? authorInfo.support[lang] : authorInfo.support.default; let links = authorInfo.support[lang] ? authorInfo.support[lang] : authorInfo.support.default;
let r = ``; let r = ``;
for (let i in links) { for (let i in links) {
r += socialLink( r += socialLink(
emoji(links[i].emoji), i, links[i].handle, links[i].url emoji(links[i].emoji), links[i].name, links[i].url
) )
} }
return r return r
} }
export function settingsCategory(obj) { export function settingsCategory(obj) {
return `<div id="settings-${obj.name}" class="settings-category"> return `<div id="settings-${obj.name}" class="settings-category">
<div class="category-title">${obj.title ? obj.title : obj.name}</div> <div class="category-title">${obj.title ?? obj.name}</div>
<div class="category-content">${obj.body}</div> <div class="category-content">${obj.body}</div>
</div>` </div>`
} }
@ -180,32 +179,22 @@ export function settingsCategory(obj) {
export function footerButtons(obj) { export function footerButtons(obj) {
let items = `` let items = ``
for (let i = 0; i < obj.length; i++) { for (let i = 0; i < obj.length; i++) {
switch (obj[i]["type"]) { let buttonName = obj[i]["context"] ? `${obj[i]["name"]}-${obj[i]["context"]}` : obj[i]["name"],
case "toggle": context = obj[i]["context"] ? `, '${obj[i]["context"]}'` : '',
items += `<button id="${obj[i]["name"]}-footer" class="switch footer-button" onclick="toggle('${obj[i]["name"]}')" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>`; buttonName2,
break; context2;
case "action":
items += `<button id="${obj[i]["name"]}-footer" class="switch footer-button" onclick="${obj[i]["action"]}()" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>`;
break;
case "popup":
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]) { if (obj[i + 1]) {
buttonName2 = obj[i+1]["context"] ? `${obj[i+1]["name"]}-${obj[i+1]["context"]}` : obj[i+1]["name"]; 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"]}'` : ''; context2 = obj[i + 1]["context"] ? `, '${obj[i + 1]["context"]}'` : '';
}
items += `
<div class="footer-pair">
<button id="${buttonName}-footer" class="switch footer-button" onclick="popup('${obj[i]["name"]}', 1${context})" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>
${obj[i+1] ? `<button id="${buttonName2}-footer" class="switch footer-button" onclick="popup('${obj[i+1]["name"]}', 1${context2})" aria-label="${obj[i+1]["aria"]}">${obj[i+1]["text"]}</button>`: ''}
</div>`;
i++;
break;
} }
items +=
`<div class="footer-pair">
<button id="${buttonName}-footer" class="switch footer-button" onclick="popup('${obj[i]["name"]}', 1${context})" aria-label="${obj[i]["aria"]}">${obj[i]["text"]}</button>
${obj[i + 1] ? `<button id="${buttonName2}-footer" class="switch footer-button" onclick="popup('${obj[i + 1]["name"]}', 1${context2})" aria-label="${obj[i + 1]["aria"]}">${obj[i + 1]["text"]}</button>` : ''}
</div>`;
i++;
} }
return ` return `
<div id="footer-buttons">${items}</div>` <div id="footer-buttons">${items}</div>`
@ -255,3 +244,25 @@ export function webLoc(t, arr) {
} }
return `{${base}};` return `{${base}};`
} }
export function sponsoredList() {
let base = ``;
let altText = ``
for (let i = 0; i < sponsors.length; i++) {
let s = sponsors[i];
let loadedLogo = loadFile(`./src/front/sponsors/${s.name}.svg`);
altText += `${s.fullName ? s.fullName : s.name}, `;
base +=
`<a class="sponsored-logo ${s.name}"
href="${s.url}" target="_blank"
style="width: calc(${s.logo.width}px / ${s.logo.scale}); height: calc(${s.logo.height}px / ${s.logo.scale});">
${loadedLogo}
</a>`
}
return `<div id="sponsored-logos" aria-label="${altText.slice(0, -2)}">${base}</div>`
}
export function betaTag() {
return process.env.isBeta ? '<span class="logo-sub">β</span>' : ''
}

View file

@ -17,14 +17,15 @@ export function changelogHistory() { // blockId 0
`<div class="changelog-banner"> `<div class="changelog-banner">
<img class="changelog-img" ` + <img class="changelog-img" ` +
`src="${history[i]["banner"]["url"]}" ` + `src="${history[i]["banner"]["url"]}" ` +
`alt="${history[i]["banner"]["alt"].replaceAll('"', '&quot;')}" ` +
`width="${history[i]["banner"]["width"]}" ` + `width="${history[i]["banner"]["width"]}" ` +
`height="${history[i]["banner"]["height"]}" ` + `height="${history[i]["banner"]["height"]}" ` +
`onerror="this.style.opacity=0" loading="lazy">`+ `onerror="this.style.opacity=0" loading="lazy">`+
`</img> `
</div>` : ''} </div>` : ''}
<div id="popup-desc" class="changelog-tags">${history[i]["version"]}</div> <div class="popup-desc changelog-tags">${history[i]["version"]}</div>
<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div> <div class="popup-desc changelog-subtitle">${history[i]["title"]}</div>
<div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>` <div class="popup-desc desc-padding">${history[i]["content"]}</div>`
} }
render = cleanHTML(render); render = cleanHTML(render);
cache['0'] = render; cache['0'] = render;

View file

@ -1,5 +1,5 @@
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc } from "./elements.js"; import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink, socialLinks, urgentNotice, keyboardShortcuts, webLoc, sponsoredList, betaTag, linkSVG } from "./elements.js";
import { services as s, authorInfo, version, repo, donations, supportedAudio } from "../config.js"; import { services as s, authorInfo, version, repo, donations, supportedAudio, links } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js"; import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import emoji from "../emoji.js"; import emoji from "../emoji.js";
@ -43,40 +43,40 @@ export default function(obj) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="${obj.lang}"> <html lang="${obj.lang}">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" /> <meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}">
<title>${t("AppTitleCobalt")}</title> <title>${t("AppTitleCobalt")}</title>
<meta property="og:url" content="${process.env.webURL || process.env.selfURL}" /> <meta property="og:url" content="${process.env.webURL}">
<meta property="og:title" content="${t("AppTitleCobalt")}" /> <meta property="og:title" content="${t("AppTitleCobalt")}">
<meta property="og:description" content="${t('EmbedBriefDescription')}" /> <meta property="og:description" content="${t('EmbedBriefDescription')}">
<meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" /> <meta property="og:image" content="${process.env.webURL}icons/generic.png">
<meta name="title" content="${t("AppTitleCobalt")}" /> <meta name="title" content="${t("AppTitleCobalt")}">
<meta name="description" content="${t('AboutSummary')}" /> <meta name="description" content="${t('AboutSummary')}">
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000">
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="${t("AppTitleCobalt")}"> <meta name="apple-mobile-web-app-title" content="${t("AppTitleCobalt")}">
<link rel="icon" type="image/x-icon" href="icons/favicon.ico" /> <link rel="icon" type="image/x-icon" href="icons/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest" /> <link rel="manifest" href="manifest.webmanifest">
<link rel="stylesheet" href="fonts/notosansmono.css" rel="preload" /> <link rel="preload" href="fonts/notosansmono.css" as="style">
<link rel="stylesheet" href="cobalt.css" /> <link rel="stylesheet" href="fonts/notosansmono.css">
<link rel="stylesheet" href="cobalt.css">
<link rel="me" href="${authorInfo.support.default.mastodon.url}">
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
</head> </head>
<body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''} data-nosnippet ontouchstart> <body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''}>
<body id="notification-area"></div> <noscript>
<div style="margin: 2rem;">${t('NoScriptMessage')}</div>
</noscript>
${multiPagePopup({ ${multiPagePopup({
name: "about", name: "about",
closeAria: t('AccessibilityGoBack'), closeAria: t('AccessibilityGoBack'),
@ -122,7 +122,7 @@ export default function(obj) {
}] }]
}, { }, {
items: [{ items: [{
combo: "Ctrl+V", combo: "⌘/Ctrl+V",
name: t("KeyboardShortcutQuickPaste") name: t("KeyboardShortcutQuickPaste")
}, { }, {
combo: "Esc", combo: "Esc",
@ -146,15 +146,15 @@ export default function(obj) {
}, { }, {
name: "support", name: "support",
title: `${emoji("❤️‍🩹")} ${t("CollapseSupport")}`, title: `${emoji("❤️‍🩹")} ${t("CollapseSupport")}`,
body: body: `${t("SupportSelfTroubleshooting")}`
`${t("SupportSelfTroubleshooting")}<br/><br/>` + `${socialLink(emoji("📢"), t("StatusPage"), links.statusPage)}`
+ `${t("FollowSupport")}<br/>` + `${socialLink(emoji("🔧"), t("TroubleshootingGuide"), links.troubleshootingGuide)}`
+ `${socialLinks(obj.lang)}<br/>` + `<br>`
+ `${t("SourceCode")}<br/>` + `${t("FollowSupport")}`
+ `${socialLink( + `${socialLinks(obj.lang)}`
emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo + `<br>`
)}<br/> + `${t("SourceCode")}`
${t("SupportNote")}` + `${socialLink(emoji("🐙"), repo.replace("https://github.com/", ''), repo)}`
}, { }, {
name: "privacy", name: "privacy",
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`, title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
@ -164,7 +164,17 @@ export default function(obj) {
title: `${emoji("📑")} ${t("CollapseLegal")}`, title: `${emoji("📑")} ${t("CollapseLegal")}`,
body: t("FairUse") body: t("FairUse")
}]) }])
}] },
...(process.env.showSponsors ?
[{
text: t("SponsoredBy"),
classes: ["sponsored-by-text"],
nopadding: true
}, {
text: sponsoredList(),
raw: true
}] : []
)]
}) })
}, { }, {
name: "changelog", name: "changelog",
@ -179,15 +189,18 @@ export default function(obj) {
text: `<div class="category-title">${t('ChangelogLastMajor')}</div>`, text: `<div class="category-title">${t('ChangelogLastMajor')}</div>`,
raw: true raw: true
}, { }, {
text: changelogManager("banner") ? text: (() => {
`<div class="changelog-banner"> const banner = changelogManager('banner');
<img class="changelog-img" ` + if (!banner) return '';
`src="${changelogManager("banner")["url"]}" ` + return `<div class="changelog-banner">
`width="${changelogManager("banner")["width"]}" ` + <img class="changelog-img" ` +
`height="${changelogManager("banner")["height"]}" ` + `src="${banner.url}" ` +
`onerror="this.style.opacity=0" loading="lazy">`+ `alt="${banner.alt.replaceAll('"', '&quot;')}" ` +
`</img> `width="${banner.width}" ` +
</div>`: '', `height="${banner.height}" ` +
`onerror="this.style.opacity=0" loading="lazy">
</div>`;
})(),
raw: true raw: true
}, { }, {
text: changelogManager("version"), text: changelogManager("version"),
@ -236,13 +249,14 @@ export default function(obj) {
text: `<div class="category-title">${t('DonateSub')}</div>`, text: `<div class="category-title">${t('DonateSub')}</div>`,
raw: true raw: true
}, { }, {
text: `<div class="changelog-banner"> text: `
<div class="changelog-banner">
<img class="changelog-img" ` + <img class="changelog-img" ` +
`src="updateBanners/catsleep.webp"` + `src="updateBanners/catsleep.webp" ` +
`alt="${t("DonateImageDescription")}" ` +
`width="480" ` + `width="480" ` +
`height="270" ` + `height="270" ` +
`onerror="this.style.opacity=0" loading="lazy">`+ `onerror="this.style.opacity=0" loading="lazy">
`</img>
</div>`, </div>`,
raw: true raw: true
}, { }, {
@ -313,7 +327,7 @@ export default function(obj) {
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: "tiktok", name: "tiktok-watermark",
title: "tiktok", title: "tiktok",
body: checkbox([{ body: checkbox([{
action: "disableTikTokWatermark", action: "disableTikTokWatermark",
@ -322,7 +336,18 @@ export default function(obj) {
}]) }])
}) })
+ settingsCategory({ + settingsCategory({
name: t('SettingsCodecSubtitle'), name: "twitter",
title: "twitter",
body: checkbox([{
action: "twitterGif",
name: t("SettingsTwitterGif"),
padding: "no-margin"
}])
+ explanation(t('SettingsTwitterGifDescription'))
})
+ settingsCategory({
name: "codec",
title: t('SettingsCodecSubtitle'),
body: switcher({ body: switcher({
name: "vCodec", name: "vCodec",
explanation: t('SettingsCodecDescription'), explanation: t('SettingsCodecDescription'),
@ -339,7 +364,8 @@ export default function(obj) {
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: t('SettingsVimeoPrefer'), name: "vimeo",
title: t('SettingsVimeoPrefer'),
body: switcher({ body: switcher({
name: "vimeoDash", name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'), explanation: t('SettingsVimeoPreferDescription'),
@ -387,7 +413,7 @@ export default function(obj) {
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: "tiktok", name: "tiktok-audio",
title: "tiktok", title: "tiktok",
body: checkbox([{ body: checkbox([{
action: "fullTikTokAudio", action: "fullTikTokAudio",
@ -416,6 +442,43 @@ export default function(obj) {
}] }]
}) })
}) })
+ settingsCategory({
name: "filename",
title: t('FilenameTitle'),
body: switcher({
name: "filenamePattern",
items: [{
action: "classic",
text: t('FilenamePatternClassic')
}, {
action: "basic",
text: t('FilenamePatternBasic')
}, {
action: "pretty",
text: t('FilenamePatternPretty')
}, {
action: "nerdy",
text: t('FilenamePatternNerdy')
}]
})
+ `<div id="filename-preview">
<div id="video-filename" class="filename-item line">
${emoji('🎞️', 32, 1, 1)}
<div class="filename-container">
<div class="filename-label">${t('Preview')}</div>
<div id="video-filename-text"></div>
</div>
</div>
<div id="audio-filename" class="filename-item">
${emoji('🎧', 32, 1, 1)}
<div class="filename-container">
<div class="filename-label">${t('Preview')}</div>
<div id="audio-filename-text"></div>
</div>
</div>
</div>`
+ explanation(t('FilenameDescription'))
})
+ settingsCategory({ + settingsCategory({
name: "accessibility", name: "accessibility",
title: t('Accessibility'), title: t('Accessibility'),
@ -513,18 +576,19 @@ export default function(obj) {
<div id="popup-backdrop" onclick="hideAllPopups()"></div> <div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home" style="visibility:hidden"> <div id="home" style="visibility:hidden">
${urgentNotice({ ${urgentNotice({
emoji: "👾", emoji: "🎬",
text: t("UrgentFeatureUpdate71"), text: t("UpdateTwitterGif"),
visible: true, visible: true,
action: "popup('about', 1, 'changelog')" action: "popup('about', 1, 'changelog')"
})} })}
<div id="cobalt-main-box" class="center"> <div id="cobalt-main-box" class="center">
<div id="logo">${t("AppTitleCobalt")}</div> <div id="logo">${t("AppTitleCobalt")}${betaTag()}</div>
<div id="download-area"> <div id="download-area">
<div id="top"> <div id="top">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input> <div id="link-icon">${linkSVG}</div>
<input id="url-input-area" class="mono" type="text" autocomplete="off" spellcheck="false" maxlength="256" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()">
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button> <button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}"> <input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled aria-label="${t('AccessibilityDownloadButton')}">
</div> </div>
<div id="bottom"> <div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button> <button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
@ -562,29 +626,29 @@ export default function(obj) {
}])} }])}
</footer> </footer>
</div> </div>
<script>
let defaultApiUrl = '${process.env.apiURL ? process.env.apiURL : ''}';
const loc = ${webLoc(t,
[
'ErrorNoInternet',
'ErrorNoUrlReturned',
'ErrorUnknownStatus',
'ChangelogPressToHide',
'MediaPickerTitle',
'MediaPickerExplanationPhone',
'MediaPickerExplanationPC',
'FeatureErrorGeneric',
'ClipboardErrorNoPermission',
'ClipboardErrorFirefox',
'DataTransferSuccess',
'DataTransferError',
'FilenamePreviewVideoTitle',
'FilenamePreviewAudioTitle',
'FilenamePreviewAudioAuthor'
])}
</script>
<script src="cobalt.js"></script>
</body> </body>
<script type="text/javascript">
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
const loc = ${webLoc(t,
[
'ErrorNoInternet',
'ErrorNoUrlReturned',
'ErrorUnknownStatus',
'ChangelogPressToHide',
'MediaPickerTitle',
'MediaPickerExplanationPhone',
'MediaPickerExplanationPC',
'ImagePickerTitle',
'ImagePickerExplanationPhone',
'ImagePickerExplanationPC',
'FeatureErrorGeneric',
'ClipboardErrorNoPermission',
'ClipboardErrorFirefox',
'DataTransferSuccess',
'DataTransferError'
])}
</script>
<script type="text/javascript" src="cobalt.js"></script>
</html> </html>
` `
} catch (err) { } catch (err) {

View file

@ -56,7 +56,10 @@ export function updateCookie(cookie, headers) {
cookie.unset(parsed.filter(c => c.expires < new Date()).map(c => c.name)); cookie.unset(parsed.filter(c => c.expires < new Date()).map(c => c.name));
parsed.filter(c => !c.expires || c.expires > new Date()).forEach(c => values[c.name] = c.value); parsed.filter(c => !c.expires || c.expires > new Date()).forEach(c => values[c.name] = c.value);
updateCookieValues(cookie, values);
}
export function updateCookieValues(cookie, values) {
cookie.set(values); cookie.set(values);
if (Object.keys(values).length) dirty = true if (Object.keys(values).length) dirty = true
} }

View file

@ -0,0 +1,78 @@
export default function(f, template, isAudioOnly, isAudioMuted) {
let filename = '';
switch(template) {
default:
case "classic":
// youtube_MMK3L4W70g4_1920x1080_h264_mute.mp4
// youtube_MMK3L4W70g4_audio.mp3
filename += `${f.service}_${f.id}`;
if (!isAudioOnly) {
if (f.resolution) filename += `_${f.resolution}`;
if (f.youtubeFormat) filename += `_${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `_${f.youtubeDubName}`;
if (isAudioMuted) filename += '_mute';
filename += `.${f.extension}`
} else {
filename += `_audio`;
if (f.youtubeDubName) filename += `_${f.youtubeDubName}`;
}
break;
case "pretty":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, mute, youtube).mp4
// How secure is 256 bit security? - 3Blue1Brown (es, youtube).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}`;
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service})`
}
break;
case "basic":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru).mp4
// How secure is 256 bit security? - 3Blue1Brown (es).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `, ${f.youtubeDubName}`;
if (isAudioMuted) filename += ', mute';
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author}`;
if (f.youtubeDubName) filename += ` (${f.youtubeDubName})`;
}
break;
case "nerdy":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru, youtube, MMK3L4W70g4).mp4
// Loossemble (루셈블) - 'Sensitive' MV - Loossemble (ru, youtube, MMK3L4W70g4).mp4
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}, ${f.id}`;
filename += ')'
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service}, ${f.id})`
}
break;
}
return filename.replace(' ,', '').replace(', )', ')').replace(',)', ')')
}

View file

@ -1,48 +0,0 @@
export default function (inHost, inURL) {
let host = String(inHost);
let url = String(inURL);
switch(host) {
case "youtube":
if (url.startsWith("https://youtube.com/live/") || url.startsWith("https://www.youtube.com/live/")) {
url = url.split("?")[0].replace("www.", "");
url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
}
if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v=');
}
break;
case "youtu":
if (url.startsWith("https://youtu.be/")) {
host = "youtube";
url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}`
}
break;
case "vxtwitter":
case "x":
if (url.startsWith("https://x.com/")) {
host = "twitter";
url = url.replace("https://x.com/", "https://twitter.com/")
}
if (url.startsWith("https://vxtwitter.com/")) {
host = "twitter";
url = url.replace("https://vxtwitter.com/", "https://twitter.com/")
}
break;
case "tumblr":
if (!url.includes("blog/view")) {
if (url.slice(-1) === '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], '')
}
break;
case "twitch":
if (url.includes('clips.twitch.tv')) {
url = url.split('?')[0].replace('clips.twitch.tv/', 'twitch.tv/_/clip/');
}
break;
}
return {
host: host,
url: url
}
}

View file

@ -1,3 +1,5 @@
import { strict as assert } from "node:assert";
import { apiJSON } from "../sub/utils.js"; import { apiJSON } from "../sub/utils.js";
import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js"; import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js";
@ -11,6 +13,7 @@ import reddit from "./services/reddit.js";
import twitter from "./services/twitter.js"; import twitter from "./services/twitter.js";
import youtube from "./services/youtube.js"; import youtube from "./services/youtube.js";
import vk from "./services/vk.js"; import vk from "./services/vk.js";
import ok from "./services/ok.js";
import tiktok from "./services/tiktok.js"; import tiktok from "./services/tiktok.js";
import tumblr from "./services/tumblr.js"; import tumblr from "./services/tumblr.js";
import vimeo from "./services/vimeo.js"; import vimeo from "./services/vimeo.js";
@ -22,7 +25,9 @@ import streamable from "./services/streamable.js";
import twitch from "./services/twitch.js"; import twitch from "./services/twitch.js";
import rutube from "./services/rutube.js"; import rutube from "./services/rutube.js";
export default async function (host, patternMatch, url, lang, obj) { export default async function(host, patternMatch, url, lang, obj) {
assert(url instanceof URL);
try { try {
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata; let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
@ -32,52 +37,59 @@ export default async function (host, patternMatch, url, lang, obj) {
switch (host) { switch (host) {
case "twitter": case "twitter":
r = await twitter({ r = await twitter({
id: patternMatch["id"] ? patternMatch["id"] : false, id: patternMatch.id,
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false index: patternMatch.index - 1,
toGif: !!obj.twitterGif
}); });
break; break;
case "vk": case "vk":
r = await vk({ r = await vk({
url: url, userId: patternMatch.userId,
userId: patternMatch["userId"], videoId: patternMatch.videoId,
videoId: patternMatch["videoId"], quality: obj.vQuality
});
break;
case "ok":
r = await ok({
id: patternMatch.id,
quality: obj.vQuality quality: obj.vQuality
}); });
break; break;
case "bilibili": case "bilibili":
r = await bilibili({ r = await bilibili({
id: patternMatch["id"].slice(0, 12) id: patternMatch.id.slice(0, 12)
}); });
break; break;
case "youtube": case "youtube":
let fetchInfo = { let fetchInfo = {
id: patternMatch["id"].slice(0, 11), id: patternMatch.id.slice(0, 11),
quality: obj.vQuality, quality: obj.vQuality,
format: obj.vCodec, format: obj.vCodec,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
isAudioMuted: obj.isAudioMuted, isAudioMuted: obj.isAudioMuted,
dubLang: obj.dubLang dubLang: obj.dubLang
} }
if (url.match('music.youtube.com') || isAudioOnly === true) {
if (url.hostname === 'music.youtube.com' || isAudioOnly === true) {
fetchInfo.quality = "max"; fetchInfo.quality = "max";
fetchInfo.format = "vp9"; fetchInfo.format = "vp9";
fetchInfo.isAudioOnly = true fetchInfo.isAudioOnly = true
} }
r = await youtube(fetchInfo); r = await youtube(fetchInfo);
break; break;
case "reddit": case "reddit":
r = await reddit({ r = await reddit({
sub: patternMatch["sub"], sub: patternMatch.sub,
id: patternMatch["id"], id: patternMatch.id
title: patternMatch["title"]
}); });
break; break;
case "douyin": case "douyin":
case "tiktok": case "tiktok":
r = await tiktok({ r = await tiktok({
host: host, host: host,
postId: patternMatch["postId"], postId: patternMatch.postId,
id: patternMatch["id"], id: patternMatch.id,
noWatermark: obj.isNoTTWatermark, noWatermark: obj.isNoTTWatermark,
fullAudio: obj.isTTFullAudio, fullAudio: obj.isTTFullAudio,
isAudioOnly: isAudioOnly isAudioOnly: isAudioOnly
@ -85,14 +97,14 @@ export default async function (host, patternMatch, url, lang, obj) {
break; break;
case "tumblr": case "tumblr":
r = await tumblr({ r = await tumblr({
id: patternMatch["id"], id: patternMatch.id,
url: url, user: patternMatch.user,
user: patternMatch["user"] ? patternMatch["user"] : false url
}); });
break; break;
case "vimeo": case "vimeo":
r = await vimeo({ r = await vimeo({
id: patternMatch["id"].slice(0, 11), id: patternMatch.id.slice(0, 11),
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash forceDash: isAudioOnly ? true : obj.vimeoDash
@ -101,39 +113,47 @@ export default async function (host, patternMatch, url, lang, obj) {
case "soundcloud": case "soundcloud":
isAudioOnly = true; isAudioOnly = true;
r = await soundcloud({ r = await soundcloud({
author: patternMatch["author"], url,
song: patternMatch["song"], url: url, author: patternMatch.author,
shortLink: patternMatch["shortLink"] ? patternMatch["shortLink"] : false, song: patternMatch.song,
accessKey: patternMatch["accessKey"] ? patternMatch["accessKey"] : false, shortLink: patternMatch.shortLink || false,
format: obj.aFormat accessKey: patternMatch.accessKey || false
}); });
break; break;
case "instagram": case "instagram":
r = await instagram({ id: patternMatch["id"] }); r = await instagram({
...patternMatch,
quality: obj.vQuality
})
break; break;
case "vine": case "vine":
r = await vine({ id: patternMatch["id"] }); r = await vine({
id: patternMatch.id
});
break; break;
case "pinterest": case "pinterest":
r = await pinterest({ id: patternMatch["id"] }); r = await pinterest({
id: patternMatch.id,
shortLink: patternMatch.shortLink || false
});
break; break;
case "streamable": case "streamable":
r = await streamable({ r = await streamable({
id: patternMatch["id"], id: patternMatch.id,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
}); });
break; break;
case "twitch": case "twitch":
r = await twitch({ r = await twitch({
clipId: patternMatch["clip"] ? patternMatch["clip"] : false, clipId: patternMatch.clip || false,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: obj.isAudioOnly isAudioOnly: obj.isAudioOnly
}); });
break; break;
case "rutube": case "rutube":
r = await rutube({ r = await rutube({
id: patternMatch["id"], id: patternMatch.id,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly isAudioOnly: isAudioOnly
}); });
@ -145,9 +165,21 @@ export default async function (host, patternMatch, url, lang, obj) {
if (r.isAudioOnly) isAudioOnly = true; if (r.isAudioOnly) isAudioOnly = true;
let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted; let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted;
if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) }); if (r.error && r.critical)
return apiJSON(6, { t: loc(lang, r.error) })
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata); if (r.error)
return apiJSON(0, {
t: Array.isArray(r.error)
? loc(lang, r.error[0], r.error[1])
: loc(lang, r.error)
})
return matchActionDecider(
r, host, obj.aFormat, isAudioOnly,
lang, isAudioMuted, disableMetadata,
obj.filenamePattern, obj.twitterGif
)
} catch (e) { } catch (e) {
return apiJSON(0, { t: genericError(lang, host) }) return apiJSON(0, { t: genericError(lang, host) })
} }

View file

@ -1,74 +1,54 @@
import { audioIgnore, services, supportedAudio } from "../config.js"; import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js"; import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js";
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) { export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) {
let action, let action,
responseType = 2, responseType = 2,
defaultParams = { defaultParams = {
u: r.urls, u: r.urls,
service: host, service: host,
filename: r.filename, filename: r.filenameAttributes ?
createFilename(r.filenameAttributes, filenamePattern, isAudioOnly, isAudioMuted) : r.filename,
fileMetadata: !disableMetadata ? r.fileMetadata : false fileMetadata: !disableMetadata ? r.fileMetadata : false
}, },
params = {} params = {},
audioFormat = String(userFormat);
if (r.isPhoto) action = "photo"; if (r.isPhoto) action = "photo";
else if (r.picker) action = "picker" else if (r.picker) action = "picker"
else if (r.isGif && toGif) action = "gif";
else if (isAudioMuted) action = "muteVideo"; else if (isAudioMuted) action = "muteVideo";
else if (isAudioOnly) action = "audio"; else if (isAudioOnly) action = "audio";
else if (r.isM3U8) action = "singleM3U8"; else if (r.isM3U8) action = "singleM3U8";
else action = "video"; else action = "video";
if (action === "picker" || action === "audio") { if (action === "picker" || action === "audio") {
defaultParams.filename = r.audioFilename; if (!r.filenameAttributes) defaultParams.filename = r.audioFilename;
defaultParams.isAudioOnly = true; defaultParams.isAudioOnly = true;
defaultParams.audioFormat = audioFormat; defaultParams.audioFormat = audioFormat;
} }
if (isAudioMuted && !r.filenameAttributes) {
defaultParams.filename = r.filename.replace('.', '_mute.')
}
switch (action) { switch (action) {
default:
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
case "photo": case "photo":
responseType = 1; responseType = 1;
break; break;
case "video":
switch (host) { case "gif":
case "bilibili": params = { type: "gif" }
params = { type: "render" };
break;
case "youtube":
params = { type: r.type };
break;
case "reddit":
responseType = r.typeId;
params = { type: r.type };
break;
case "vimeo":
if (Array.isArray(r.urls)) {
params = { type: "render" }
} else {
responseType = 1;
}
break;
case "vk":
case "douyin":
case "tiktok":
params = { type: "bridge" };
break;
case "vine":
case "instagram":
case "tumblr":
case "twitter":
case "pinterest":
case "streamable":
responseType = 1;
break;
}
break; break;
case "singleM3U8": case "singleM3U8":
params = { type: "videoM3U8" } params = { type: "remux" }
break; break;
case "muteVideo": case "muteVideo":
params = { params = {
type: Array.isArray(r.urls) ? "bridge" : "mute", type: Array.isArray(r.urls) ? "bridge" : "mute",
@ -101,45 +81,106 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
} }
break; break;
case "audio": case "video":
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') }); switch (host) {
case "bilibili":
let processType = "render"; params = { type: "render" };
let copy = false; break;
case "youtube":
if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; params = { type: r.type };
break;
if ((host === "tiktok" || host === "douyin") && services.tiktok.audioFormats.includes(audioFormat)) { case "reddit":
if (r.isMp3) { responseType = r.typeId;
if (audioFormat === "mp3" || audioFormat === "best") { params = { type: r.type };
audioFormat = "mp3"; break;
processType = "bridge" case "vimeo":
if (Array.isArray(r.urls)) {
params = { type: "render" }
} else {
responseType = 1;
} }
} else if (audioFormat === "best") { break;
case "twitter":
if (r.type === "remux") {
params = { type: r.type };
} else {
responseType = 1;
}
break;
case "vk":
case "douyin":
case "tiktok":
params = { type: "bridge" };
break;
case "vine":
case "instagram":
case "tumblr":
case "pinterest":
case "streamable":
responseType = 1;
break;
}
break;
case "audio":
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) {
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') })
}
let processType = "render",
copy = false;
if (!supportedAudio.includes(audioFormat)) {
audioFormat = "best"
}
const isBestAudio = audioFormat === "best";
const isBestOrMp3 = audioFormat === "mp3" || isBestAudio;
const isBestAudioDefined = isBestAudio && services[host]["bestAudio"];
const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]);
const isTikTok = host === "tiktok" || host === "douyin";
const isTumblr = host === "tumblr" && !r.filename;
const isSoundCloud = host === "soundcloud";
if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3 && isBestOrMp3) {
audioFormat = "mp3";
processType = "bridge"
} else if (isBestAudio) {
audioFormat = "m4a"; audioFormat = "m4a";
processType = "bridge" processType = "bridge"
} }
} }
if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) {
if (isSoundCloud && services.soundcloud.audioFormats.includes(audioFormat)) {
if (r.isMp3 && isBestOrMp3) {
audioFormat = "mp3";
processType = "render"
copy = true
} else if (isBestAudio || audioFormat === "opus") {
audioFormat = "opus";
processType = "render"
copy = true
}
}
if (isTumblr && isBestOrMp3) {
audioFormat = "mp3"; audioFormat = "mp3";
processType = "bridge" processType = "bridge"
} }
if ((audioFormat === "best" && services[host]["bestAudio"]) || (services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]))) {
if (isBestAudioDefined || isBestHostAudio) {
audioFormat = services[host]["bestAudio"]; audioFormat = services[host]["bestAudio"];
if (host === "soundcloud") { processType = "bridge";
processType = "render" } else if (isBestAudio && !isSoundCloud) {
copy = true
} else {
processType = "bridge"
}
} else if (audioFormat === "best") {
audioFormat = "m4a"; audioFormat = "m4a";
copy = true; copy = true
if (r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
} }
if (r.isM3U8 || host === "vimeo") { if (r.isM3U8 || host === "vimeo") {
copy = false; copy = false;
processType = "render" processType = "render"
@ -152,8 +193,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
copy: copy copy: copy
} }
break; break;
default:
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
} }
return apiJSON(responseType, {...defaultParams, ...params}) return apiJSON(responseType, {...defaultParams, ...params})

View file

@ -1,10 +1,43 @@
import { createStream } from "../../stream/manage.js"; import { createStream } from "../../stream/manage.js";
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
import { getCookie, updateCookie } from '../cookie/manager.js'; import { getCookie, updateCookie } from "../cookie/manager.js";
export default async function(obj) { const commonInstagramHeaders = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'User-Agent': genericUserAgent,
'X-Ig-App-Id': '936619743392459',
'X-Asbd-Id': '129477',
'x-requested-with': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'upgrade-insecure-requests': '1',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,en;q=0.8',
}
async function request(url, cookie) {
const data = await fetch(url, {
headers: {
...commonInstagramHeaders,
'x-ig-www-claim': cookie?._wwwClaim || '0',
'x-csrftoken': cookie?.values()?.csrftoken,
cookie
}
})
if (data.headers.get('X-Ig-Set-Www-Claim') && cookie)
cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim');
updateCookie(cookie, data.headers);
return data.json();
}
async function getPost(id) {
let data; let data;
try { try {
const cookie = getCookie('instagram');
const url = new URL('https://www.instagram.com/graphql/query/'); const url = new URL('https://www.instagram.com/graphql/query/');
url.searchParams.set('query_hash', 'b3055c01b4b222b8a47dc12b090e4e64') url.searchParams.set('query_hash', 'b3055c01b4b222b8a47dc12b090e4e64')
url.searchParams.set('variables', JSON.stringify({ url.searchParams.set('variables', JSON.stringify({
@ -12,91 +45,105 @@ export default async function(obj) {
fetch_comment_count: 40, fetch_comment_count: 40,
has_threaded_comments: true, has_threaded_comments: true,
parent_comment_count: 24, parent_comment_count: 24,
shortcode: obj.id shortcode: id
})) }))
const cookie = getCookie('instagram'); data = (await request(url, cookie)).data;
data = await fetch(url, { } catch {}
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'User-Agent': genericUserAgent,
'X-Ig-App-Id': '936619743392459',
'X-Asbd-Id': '129477',
'x-ig-www-claim': cookie?._wwwClaim || '0',
'x-csrftoken': cookie?.values()?.csrftoken,
'x-requested-with': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'upgrade-insecure-requests': '1',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,en;q=0.8',
cookie
}
})
if (data.headers.get('X-Ig-Set-Www-Claim') && cookie) {
cookie._wwwClaim = data.headers.get('X-Ig-Set-Www-Claim');
}
updateCookie(cookie, data.headers);
data = (await data.json()).data;
} catch (e) {
data = false;
}
if (!data) return { error: 'ErrorCouldntFetch' }; if (!data) return { error: 'ErrorCouldntFetch' };
let single, multiple = [];
const sidecar = data?.shortcode_media?.edge_sidecar_to_children; const sidecar = data?.shortcode_media?.edge_sidecar_to_children;
if (sidecar) { if (sidecar) {
sidecar.edges.forEach(e => { const picker = sidecar.edges.filter(e => e.node?.display_url)
if (e.node?.is_video) { .map(e => {
multiple.push({ const type = e.node?.is_video ? "video" : "photo";
type: "video", const url = type === "video" ? e.node?.video_url : e.node?.display_url;
// thumbnails have `Cross-Origin-Resource-Policy` set to `same-origin`, so we need to proxy them
return {
type, url,
/* thumbnails have `Cross-Origin-Resource-Policy`
** set to `same-origin`, so we need to proxy them */
thumb: createStream({ thumb: createStream({
service: "instagram", service: "instagram",
type: "default", type: "default",
u: e.node?.display_url, u: e.node?.display_url,
filename: "image.jpg" filename: "image.jpg"
}), })
url: e.node?.video_url }
}) });
} else {
multiple.push({ if (picker.length) return { picker }
type: "photo",
thumb: createStream({
service: "instagram",
type: "default",
u: e.node?.display_url,
filename: "image.jpg"
}),
url: e.node?.display_url
})
}
})
} else if (data?.shortcode_media?.video_url) { } else if (data?.shortcode_media?.video_url) {
single = data.shortcode_media.video_url return {
urls: data.shortcode_media.video_url,
filename: `instagram_${id}.mp4`,
audioFilename: `instagram_${id}_audio`
}
} else if (data?.shortcode_media?.display_url) { } else if (data?.shortcode_media?.display_url) {
return { return {
urls: data?.shortcode_media?.display_url, urls: data.shortcode_media.display_url,
isPhoto: true isPhoto: true
} }
} else {
return { error: 'ErrorEmptyDownload' }
} }
if (single) { return { error: 'ErrorEmptyDownload' }
return { }
urls: single,
filename: `instagram_${obj.id}.mp4`, async function usernameToId(username, cookie) {
audioFilename: `instagram_${obj.id}_audio` const url = new URL('https://www.instagram.com/api/v1/users/web_profile_info/');
} url.searchParams.set('username', username);
} else if (multiple.length) {
return { picker: multiple } try {
} else { const data = await request(url, cookie);
return { error: 'ErrorEmptyDownload' } return data?.data?.user?.id;
} } catch {}
}
async function getStory(username, id) {
const cookie = getCookie('instagram');
if (!cookie) return { error: 'ErrorUnsupported' }
const userId = await usernameToId(username, cookie);
if (!userId) return { error: 'ErrorEmptyDownload' }
const url = new URL('https://www.instagram.com/api/v1/feed/reels_media/');
url.searchParams.set('reel_ids', userId);
url.searchParams.set('media_id', id);
let media;
try {
const data = await request(url, cookie);
media = data?.reels_media?.find(m => m.id === userId);
} catch {}
const item = media.items[media.media_ids.indexOf(id)];
if (!item) return { error: 'ErrorEmptyDownload' };
if (item.video_versions) {
const video = item.video_versions.reduce((a, b) => a.width * a.height < b.width * b.height ? b : a)
return {
urls: video.url,
filename: `instagram_${id}.mp4`,
audioFilename: `instagram_${id}_audio`
}
}
if (item.image_versions2?.candidates) {
return {
urls: item.image_versions2.candidates[0].url,
isPhoto: true
}
}
return { error: 'ErrorCouldntFetch' };
}
export default function(obj) {
const { postId, storyId, username } = obj;
if (postId) return getPost(postId);
if (username && storyId) return getStory(username, storyId);
return { error: 'ErrorUnsupported' }
} }

View file

@ -0,0 +1,56 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { cleanString } from "../../sub/utils.js";
const resolutions = {
"ultra": "2160",
"quad": "1440",
"full": "1080",
"hd": "720",
"sd": "480",
"low": "360",
"lowest": "240",
"mobile": "144"
}
export default async function(o) {
let quality = o.quality === "max" ? "2160" : o.quality;
let html = await fetch(`https://ok.ru/video/${o.id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes(`<div data-module="OKVideo" data-options="{`)) {
return { error: 'ErrorEmptyDownload' };
}
let videoData = html.split(`<div data-module="OKVideo" data-options="`)[1].split('" data-')[0].replaceAll("&quot;", '"');
videoData = JSON.parse(JSON.parse(videoData).flashvars.metadata);
if (videoData.provider !== "UPLOADED_ODKL") return { error: 'ErrorUnsupported' };
if (videoData.movie.is_live) return { error: 'ErrorLiveVideo' };
if (videoData.movie.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let videos = videoData.videos.filter(v => !v.disallowed);
let bestVideo = videos.find(v => resolutions[v.name] === quality) || videos[videos.length - 1];
let fileMetadata = {
title: cleanString(videoData.movie.title.trim()),
author: cleanString(videoData.author.name.trim()),
}
if (bestVideo) return {
urls: bestVideo.url,
filenameAttributes: {
service: "ok",
id: o.id,
title: fileMetadata.title,
author: fileMetadata.author,
resolution: `${resolutions[bestVideo.name]}p`,
qualityLabel: `${resolutions[bestVideo.name]}p`,
extension: "mp4"
}
}
return { error: 'ErrorEmptyDownload' }
}

View file

@ -1,24 +1,36 @@
import { maxVideoDuration } from "../../config.js"; import { genericUserAgent } from "../../config.js";
export default async function(obj) { const videoLinkBase = {
const pinId = obj.id.split('--').reverse()[0]; "regular": "https://v1.pinimg.com/videos/mc/720p/",
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' }; "story": "https://v1.pinimg.com/videos/mc/720p/"
let data = await fetch(`https://www.pinterest.com/resource/PinResource/get?data=${encodeURIComponent(JSON.stringify({ }
options: {
field_set_key: "unauth_react_main_pin", export default async function(o) {
id: pinId let id = o.id, type = "regular";
}
}))}`).then((r) => { return r.json() }).catch(() => { return false }); if (!o.id && o.shortLink) {
if (!data) return { error: 'ErrorCouldntFetch' }; id = await fetch(`https://api.pinterest.com/url_shortener/${o.shortLink}/redirect/`, { redirect: "manual" }).then((r) => {
return r.headers.get("location").split('pin/')[1].split('/')[0]
data = data["resource_response"]["data"]; }).catch(() => {});
}
let video = null; if (id.includes("--")) {
id = id.split("--")[1];
if (data.videos !== null) video = data.videos.video_list.V_720P; type = "story";
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7; }
if (!id) return { error: 'ErrorCouldntFetch' };
if (!video) return { error: 'ErrorEmptyDownload' };
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; let html = await fetch(`https://www.pinterest.com/pin/${id}/`, {
return { urls: video.url, filename: `pinterest_${pinId}.mp4`, audioFilename: `pinterest_${pinId}_audio` } headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0];
if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' };
return {
urls: `${videoLinkBase[type]}${videoLink}`,
filename: `pinterest_${o.id}.mp4`,
audioFilename: `pinterest_${o.id}_audio`
}
} }

View file

@ -1,7 +1,61 @@
import { maxVideoDuration } from "../../config.js"; import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { getCookie, updateCookieValues } from "../cookie/manager.js";
async function getAccessToken() {
/* "cookie" in cookiefile needs to contain:
* client_id, client_secret, refresh_token
* e.g. client_id=bla; client_secret=bla; refresh_token=bla
*
* you can get these by making a reddit app and
* authenticating an account against reddit's oauth2 api
* see: https://github.com/reddit-archive/reddit/wiki/OAuth2
*
* any additional cookie fields are managed by this code and you
* should not touch them unless you know what you're doing. **/
const cookie = await getCookie('reddit');
if (!cookie) return;
const values = cookie.values(),
needRefresh = !values.access_token
|| !values.expiry
|| Number(values.expiry) < new Date().getTime();
if (!needRefresh) return values.access_token;
const data = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(
[values.client_id, values.client_secret].join(':')
).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
'user-agent': genericUserAgent,
'accept': 'application/json'
},
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(values.refresh_token)}`
}).then(r => r.json()).catch(_ => {});
if (!data) return;
const { access_token, refresh_token, expires_in } = data;
if (!access_token) return;
updateCookieValues(cookie, {
...cookie.values(),
access_token, refresh_token,
expiry: new Date().getTime() + (expires_in * 1000),
});
return access_token;
}
export default async function(obj) { export default async function(obj) {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false }); const url = new URL(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}.json`);
const accessToken = await getAccessToken();
if (accessToken) url.hostname = 'oauth.reddit.com';
let data = await fetch(
url, { headers: accessToken && { authorization: `Bearer ${accessToken}` } }
).then((r) => { return r.json() }).catch(() => { return false });
if (!data) return { error: 'ErrorCouldntFetch' }; if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"]; data = data[0]["data"]["children"][0]["data"];
@ -17,7 +71,7 @@ export default async function(obj) {
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false }); await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
// fallback for videos with differentiating audio quality // fallback for videos with variable audio quality
if (!audio) { if (!audio) {
audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4` audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4`
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false }); await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });

View file

@ -1,5 +1,6 @@
import HLS from 'hls-parser'; import HLS from 'hls-parser';
import { maxVideoDuration } from "../../config.js"; import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
export default async function(obj) { export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality; let quality = obj.quality === "max" ? "9000" : obj.quality;
@ -20,11 +21,23 @@ export default async function(obj) {
if (Number(quality) < bestQuality.resolution.height) { if (Number(quality) < bestQuality.resolution.height) {
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height)); bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
} }
let fileMetadata = {
title: cleanString(play.title.trim()),
artist: cleanString(play.author.name.trim()),
}
return { return {
urls: bestQuality.uri, urls: bestQuality.uri,
isM3U8: true, isM3U8: true,
audioFilename: `rutube_${play.id}_audio`, filenameAttributes: {
filename: `rutube_${play.id}_${bestQuality.resolution.width}x${bestQuality.resolution.height}.mp4` service: "rutube",
id: play.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
qualityLabel: `${bestQuality.resolution.height}p`,
extension: "mp4"
},
fileMetadata: fileMetadata
} }
} }

View file

@ -39,17 +39,18 @@ export default async function(obj) {
if (!clientId) return { error: 'ErrorSoundCloudNoClientId' }; if (!clientId) return { error: 'ErrorSoundCloudNoClientId' };
let link; let link;
if (obj.shortLink && !obj.author && !obj.song) { if (obj.url.hostname === 'on.soundcloud.com' && obj.shortLink) {
link = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`, { redirect: "manual" }).then((r) => { link = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`, { redirect: "manual" }).then((r) => {
if (r.status === 302 && r.headers.get("location").startsWith("https://soundcloud.com/")) { if (r.status === 302 && r.headers.get("location").startsWith("https://soundcloud.com/")) {
return r.headers.get("location").split('?', 1)[0] return r.headers.get("location").split('?', 1)[0]
} }
return false }).catch(() => {});
}).catch(() => { return false });
} }
if (!link && obj.author && obj.song) { if (!link && obj.author && obj.song) {
link = `https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}` link = `https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`
} }
if (!link) return { error: 'ErrorCouldntFetch' }; if (!link) return { error: 'ErrorCouldntFetch' };
let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => { let json = await fetch(`https://api-v2.soundcloud.com/resolve?url=${link}&client_id=${clientId}`).then((r) => {
@ -59,8 +60,16 @@ export default async function(obj) {
if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' }; if (!json["media"]["transcodings"]) return { error: 'ErrorEmptyDownload' };
let fileUrlBase = json.media.transcodings.filter(v => v.preset === "opus_0_0")[0]["url"], let isMp3,
fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; selectedStream = json.media.transcodings.filter(v => v.preset === "opus_0_0")
// fall back to mp3 if no opus is available
if (selectedStream.length === 0) {
selectedStream = json.media.transcodings.filter(v => v.preset === "mp3_0_0")
isMp3 = true
}
let fileUrlBase = selectedStream[0]["url"];
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' }; if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' };
@ -69,12 +78,20 @@ export default async function(obj) {
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false }); let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
if (!file) return { error: 'ErrorCouldntFetch' }; if (!file) return { error: 'ErrorCouldntFetch' };
let fileMetadata = {
title: cleanString(json.title.trim()),
artist: cleanString(json.user.username.trim()),
}
return { return {
urls: file, urls: file,
audioFilename: `soundcloud_${json.id}`, filenameAttributes: {
fileMetadata: { service: "soundcloud",
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()), id: json.id,
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()), title: fileMetadata.title,
} author: fileMetadata.artist
},
isMp3,
fileMetadata
} }
} }

View file

@ -23,7 +23,7 @@ function selector(j, h, id) {
t = j['aweme_detail']; t = j['aweme_detail'];
break; break;
} }
if (t.length < 3) return false; if (t?.length < 3) return false;
return t; return t;
} }
@ -35,10 +35,11 @@ export default async function(obj) {
redirect: "manual", redirect: "manual",
headers: { "user-agent": userAgent } headers: { "user-agent": userAgent }
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) { if (html.slice(0, 17) === '<a href="https://') {
postId = html.split('/video/')[1].split('?')[0].replace("/", '') postId = html.split('<a href="https://')[1].split('?')[0].split('/')[3]
} else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) { } else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) {
postId = html.split('/v/')[1].split('.html')[0].replace("/", '') postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
} }

View file

@ -1,9 +1,16 @@
import psl from "psl";
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
export default async function(obj) { export default async function(obj) {
let html = await fetch(`https://${ let { subdomain } = psl.parse(obj.url.hostname);
obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '')
}.tumblr.com/post/${obj.id}`, { if (subdomain?.includes('.')) {
return { error: ['ErrorBrokenLink', 'tumblr'] }
} else if (subdomain === 'www' || subdomain === 'at') {
subdomain = undefined
}
let html = await fetch(`https://${subdomain ?? obj.user}.tumblr.com/post/${obj.id}`, {
headers: { "user-agent": genericUserAgent } headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
@ -24,5 +31,5 @@ export default async function(obj) {
} }
} else r = { error: 'ErrorEmptyDownload' }; } else r = { error: 'ErrorEmptyDownload' };
return r; return r
} }

View file

@ -1,4 +1,5 @@
import { maxVideoDuration } from "../../config.js"; import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
const gqlURL = "https://gql.twitch.tv/gql"; const gqlURL = "https://gql.twitch.tv/gql";
const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" }; const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" };
@ -67,9 +68,17 @@ export default async function (obj) {
token: req_token[0].data.clip.playbackAccessToken.value token: req_token[0].data.clip.playbackAccessToken.value
})}`, })}`,
fileMetadata: { fileMetadata: {
title: clipMetadata.title, title: cleanString(clipMetadata.title.trim()),
artist: `Twitch Clip by @${clipMetadata.broadcaster.login}, clipped by @${clipMetadata.curator.login}`, artist: `Twitch Clip by @${clipMetadata.broadcaster.login}, clipped by @${clipMetadata.curator.login}`,
}, },
filenameAttributes: {
service: "twitch",
id: clipMetadata.id,
title: cleanString(clipMetadata.title.trim()),
author: `${clipMetadata.broadcaster.login}, clipped by ${clipMetadata.curator.login}`,
qualityLabel: `${format.quality}p`,
extension: 'mp4'
},
filename: `twitchclip_${clipMetadata.id}_${format.quality}p.mp4`, filename: `twitchclip_${clipMetadata.id}_${format.quality}p.mp4`,
audioFilename: `twitchclip_${clipMetadata.id}_audio` audioFilename: `twitchclip_${clipMetadata.id}_audio`
} }

View file

@ -1,117 +1,138 @@
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
import { createStream } from "../../stream/manage.js";
const graphqlURL = 'https://twitter.com/i/api/graphql/5GOHgZe-8U2j5sVHQzEm9A/TweetResultByRestId';
const tokenURL = 'https://api.twitter.com/1.1/guest/activate.json';
const tweetFeatures = JSON.stringify({ "creator_subscriptions_tweet_preview_api_enabled": true, "c9s_tweet_anatomy_moderator_badge_enabled": true, "tweetypie_unmention_optimization_enabled": true, "responsive_web_edit_tweet_api_enabled": true, "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, "view_counts_everywhere_api_enabled": true, "longform_notetweets_consumption_enabled": true, "responsive_web_twitter_article_tweet_consumption_enabled": false, "tweet_awards_web_tipping_enabled": false, "responsive_web_home_pinned_timelines_enabled": true, "freedom_of_speech_not_reach_fetch_enabled": true, "standardized_nudges_misinfo": true, "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, "longform_notetweets_rich_text_read_enabled": true, "longform_notetweets_inline_media_enabled": true, "responsive_web_graphql_exclude_directive_enabled": true, "verified_phone_label_enabled": false, "responsive_web_media_download_video_enabled": false, "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, "responsive_web_graphql_timeline_navigation_enabled": true, "responsive_web_enhance_cards_enabled": false });
const commonHeaders = {
"user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
"accept-language": "en"
}
// fix all videos affected by the container bug in twitter muxer (took them over two weeks to fix it????)
const TWITTER_EPOCH = 1288834974657n;
const badContainerStart = new Date(1701446400000);
const badContainerEnd = new Date(1702605600000);
function needsFixing(media) {
const representativeId = media.source_status_id_str ?? media.id_str;
const mediaTimestamp = new Date(
Number((BigInt(representativeId) >> 22n) + TWITTER_EPOCH)
);
return mediaTimestamp > badContainerStart && mediaTimestamp < badContainerEnd
}
function bestQuality(arr) { function bestQuality(arr) {
return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"] return arr.filter(v => v.content_type === "video/mp4")
.reduce((a, b) => Number(a?.bitrate) > Number(b?.bitrate) ? a : b)
.url
} }
export default async function(obj) { let _cachedToken;
let _headers = { const getGuestToken = async (forceReload = false) => {
"user-agent": genericUserAgent, if (_cachedToken && !forceReload) {
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", return _cachedToken;
"host": "api.twitter.com",
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
"accept-language": "en"
};
let activateURL = `https://api.twitter.com/1.1/guest/activate.json`;
let graphqlTweetURL = `https://twitter.com/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId`;
let graphqlSpaceURL = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById`;
let req_act = await fetch(activateURL, {
method: "POST",
headers: _headers
}).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["host"] = "twitter.com";
_headers["content-type"] = "application/json";
_headers["x-guest-token"] = req_act["guest_token"];
_headers["cookie"] = `guest_id=v1%3A${req_act["guest_token"]}`;
if (obj.id) {
let query = {
variables: {"tweetId": obj.id, "withCommunity": false, "includePromotedContent": false, "withVoice": false},
features: {"creator_subscriptions_tweet_preview_api_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_media_download_video_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false}
}
query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1);
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
query = `${graphqlTweetURL}?variables=${query.variables}&features=${query.features}`;
let TweetResultByRestId = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
if (!TweetResultByRestId || TweetResultByRestId.data.tweetResult.result.__typename !== "Tweet") return { error: 'ErrorTweetUnavailable' };
let baseMedia,
baseTweet = TweetResultByRestId.data.tweetResult.result.legacy;
if (baseTweet.retweeted_status_result && baseTweet.retweeted_status_result.result.legacy.extended_entities.media) {
baseMedia = baseTweet.retweeted_status_result.result.legacy.extended_entities
} else if (baseTweet.extended_entities && baseTweet.extended_entities.media) {
baseMedia = baseTweet.extended_entities
}
if (!baseMedia) return { error: 'ErrorNoVideosInTweet' };
let single, multiple = [], media = baseMedia["media"];
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true });
if (media.length > 1) {
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
} else if (media.length === 1) {
single = bestQuality(media[0]["video_info"]["variants"])
} else {
return { error: 'ErrorNoVideosInTweet' }
}
if (single) {
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) {
return { picker: multiple }
} else {
return { error: 'ErrorNoVideosInTweet' }
}
} }
// spaces no longer work with guest authorization
if (obj.spaceId) {
_headers["host"] = "twitter.com";
_headers["content-type"] = "application/json";
let query = { const tokenResponse = await fetch(tokenURL, {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withReplays":true}, method: 'POST',
features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"responsive_web_twitter_blue_verified_badge_is_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":false,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"longform_notetweets_richtext_consumption_enabled":false,"responsive_web_enhance_cards_enabled":false} headers: commonHeaders
} }).then(r => r.status === 200 && r.json()).catch(() => {})
query.variables = new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1);
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
query = `${graphqlSpaceURL}?variables=${query.variables}&features=${query.features}`;
let AudioSpaceById = await fetch(query, { headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false }); if (tokenResponse?.guest_token) {
if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' }; return _cachedToken = tokenResponse.guest_token
}
if (!AudioSpaceById.data.audioSpace.metadata) return { error: 'ErrorEmptyDownload' }; }
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay !== true) return { error: 'TwitterSpaceWasntRecorded' };
const requestTweet = (tweetId, token) => {
let streamStatus = await fetch( const graphqlTweetURL = new URL(graphqlURL);
`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }
).then((r) =>{ return r.status === 200 ? r.json() : false }).catch(() => { return false }); graphqlTweetURL.searchParams.set('variables',
if (!streamStatus) return { error: 'ErrorCouldntFetch' }; JSON.stringify({
tweetId,
let participants = AudioSpaceById.data.audioSpace.participants.speakers, withCommunity: false,
listOfParticipants = `Twitter Space speakers: `; includePromotedContent: false,
for (let i in participants) { listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` } withVoice: false
listOfParticipants = listOfParticipants.slice(0, -2); })
);
return { graphqlTweetURL.searchParams.set('features', tweetFeatures);
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`, return fetch(graphqlTweetURL, {
isAudioOnly: true, headers: {
fileMetadata: { ...commonHeaders,
title: AudioSpaceById.data.audioSpace.metadata.title, 'content-type': 'application/json',
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`, 'x-guest-token': token,
comment: listOfParticipants, cookie: `guest_id=${encodeURIComponent(`v1:${token}`)}`
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") }
} })
} }
export default async function({ id, index, toGif }) {
let guestToken = await getGuestToken();
if (!guestToken) return { error: 'ErrorCouldntFetch' };
let tweet = await requestTweet(id, guestToken);
if ([403, 429].includes(tweet.status)) { // get new token & retry
guestToken = await getGuestToken(true);
tweet = await requestTweet(id, guestToken)
}
tweet = await tweet.json();
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
if (tweet?.data?.tweetResult?.result?.__typename !== "Tweet") {
return { error: 'ErrorTweetUnavailable' }
}
const baseTweet = tweet.data.tweetResult.result.legacy,
repostedTweet = baseTweet.retweeted_status_result?.result.legacy.extended_entities;
let media = (repostedTweet?.media || baseTweet.extended_entities.media);
media = media?.filter(m => m.video_info?.variants?.length);
// check if there's a video at given index (/video/<index>)
if ([0, 1, 2, 3].includes(index) && index < media?.length) {
media = [media[index]]
}
switch (media?.length) {
case undefined:
case 0:
return { error: 'ErrorNoVideosInTweet' };
case 1:
return {
type: needsFixing(media[0]) ? "remux" : "normal",
urls: bestQuality(media[0].video_info.variants),
filename: `twitter_${id}.mp4`,
audioFilename: `twitter_${id}_audio`,
isGif: media[0].type === "animated_gif"
};
default:
const picker = media.map((content, i) => {
let url = bestQuality(content.video_info.variants);
const shouldRenderGif = content.type === 'animated_gif' && toGif;
if (needsFixing(content) || shouldRenderGif) {
url = createStream({
service: 'twitter',
type: shouldRenderGif ? 'gif' : 'remux',
u: url,
filename: `twitter_${id}_${i + 1}.mp4`
})
}
return {
type: 'video',
url,
thumb: content.media_url_https,
}
});
return { picker };
} }
} }

View file

@ -1,9 +1,10 @@
import { maxVideoDuration } from "../../config.js"; import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
// vimeo you're fucked in the head for this
const resolutionMatch = { const resolutionMatch = {
"3840": "2160", "3840": "2160",
"2732": "1440", "2732": "1440",
"2560": "1440",
"2048": "1080", "2048": "1080",
"1920": "1080", "1920": "1080",
"1366": "720", "1366": "720",
@ -33,6 +34,11 @@ export default async function(obj) {
let downloadType = "dash"; let downloadType = "dash";
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive"; if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
let fileMetadata = {
title: cleanString(api.video.title.trim()),
artist: cleanString(api.video.owner.name.trim()),
}
if (downloadType !== "dash") { if (downloadType !== "dash") {
if (qualityMatch[quality]) quality = qualityMatch[quality]; if (qualityMatch[quality]) quality = qualityMatch[quality];
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width)); let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
@ -43,7 +49,11 @@ export default async function(obj) {
if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality); if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality);
if (!best) return { error: 'ErrorEmptyDownload' }; if (!best) return { error: 'ErrorEmptyDownload' };
return { urls: best["url"], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4` } return {
urls: best["url"],
audioFilename: `vimeo_${obj.id}_audio`,
filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4`
}
} }
if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
@ -54,32 +64,26 @@ export default async function(obj) {
if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' }; if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let type = "parcel"; let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => ["dash", "mp42"].includes(a['format'])),
if (masterJSON.base_url === "../") type = "chop";
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)),
bestVideo = masterJSON_Video[0]; bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality); if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality)
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
switch (type) {
case "parcel":
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter(a => a['mime_type'] === "audio/mp4"),
bestAudio = masterJSON_Audio[0];
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
break;
case "chop":
videoUrl = `${baseUrl}/sep/video/${bestVideo.id}/master.m3u8`;
break;
} }
if (videoUrl) {
return { let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`;
urls: audioUrl ? [videoUrl, audioUrl] : videoUrl,
isM3U8: audioUrl ? false : true, return {
audioFilename: `vimeo_${obj.id}_audio`, urls: masterM3U8,
filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` isM3U8: true,
fileMetadata: fileMetadata,
filenameAttributes: {
service: "vimeo",
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${resolutionMatch[bestVideo["width"]]}p`,
extension: "mp4"
} }
} }
return { error: 'ErrorEmptyDownload' }
} }

View file

@ -2,7 +2,11 @@ export default async function(obj) {
let post = await fetch(`https://archive.vine.co/posts/${obj.id}.json`).then((r) => { return r.json() }).catch(() => { return false }); let post = await fetch(`https://archive.vine.co/posts/${obj.id}.json`).then((r) => { return r.json() }).catch(() => { return false });
if (!post) return { error: 'ErrorEmptyDownload' }; if (!post) return { error: 'ErrorEmptyDownload' };
if (post.videoUrl) return { urls: post.videoUrl.replace("http://", "https://"), filename: `vine_${obj.id}.mp4`, audioFilename: `vine_${obj.id}_audio` }; if (post.videoUrl) return {
urls: post.videoUrl.replace("http://", "https://"),
filename: `vine_${obj.id}.mp4`,
audioFilename: `vine_${obj.id}_audio`
}
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
} }

View file

@ -1,17 +1,21 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js"; import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { cleanString } from "../../sub/utils.js";
const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"]; const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"];
export default async function(o) { export default async function(o) {
let html, url, let html, url, quality = o.quality === "max" ? 2160 : o.quality;
quality = o.quality === "max" ? 2160 : o.quality,
filename = `vk_${o.userId}_${o.videoId}_`;
html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, { html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, {
headers: { "user-agent": genericUserAgent } headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.arrayBuffer() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
// decode cyrillic from windows-1251 because vk still uses apis from prehistoric times
let decoder = new TextDecoder('windows-1251');
html = decoder.decode(html);
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' }; if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
@ -28,11 +32,23 @@ export default async function(o) {
if (Number(quality) > Number(o.quality)) quality = o.quality; if (Number(quality) > Number(o.quality)) quality = o.quality;
url = js.player.params[0][`url${quality}`]; url = js.player.params[0][`url${quality}`];
filename += `${quality}p.mp4`
if (url && filename) return { let fileMetadata = {
title: cleanString(js.player.params[0].md_title.trim()),
author: cleanString(js.player.params[0].md_author.trim()),
}
if (url) return {
urls: url, urls: url,
filename: filename filenameAttributes: {
service: "vk",
id: `${o.userId}_${o.videoId}`,
title: fileMetadata.title,
author: fileMetadata.author,
resolution: `${quality}p`,
qualityLabel: `${quality}p`,
extension: "mp4"
}
} }
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
} }

View file

@ -54,16 +54,17 @@ export default async function(o) {
audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]); audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]);
if (o.dubLang) { if (o.dubLang) {
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang); let dubbedAudio = adaptive_formats.find(i =>
checkBestAudio(i) && i["language"] === o.dubLang && i["audio_track"] && !i["audio_track"].audio_is_default
);
if (dubbedAudio) { if (dubbedAudio) {
audio = dubbedAudio; audio = dubbedAudio;
isDubbed = true isDubbed = true
} }
} }
let fileMetadata = { let fileMetadata = {
title: cleanString(info.basic_info.title.replace(/\p{Emoji}/gu, '').trim()), title: cleanString(info.basic_info.title.trim()),
artist: cleanString(info.basic_info.author.replace("- Topic", "").replace(/\p{Emoji}/gu, '').trim()), artist: cleanString(info.basic_info.author.replace("- Topic", "").trim()),
} }
if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) { if (info.basic_info.short_description && info.basic_info.short_description.startsWith("Provided to YouTube by")) {
let descItems = info.basic_info.short_description.split("\n\n"); let descItems = info.basic_info.short_description.split("\n\n");
@ -72,13 +73,27 @@ export default async function(o) {
if (descItems[4].startsWith("Released on:")) { if (descItems[4].startsWith("Released on:")) {
fileMetadata.date = descItems[4].replace("Released on: ", '').trim() fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
} }
}; }
let filenameAttributes = {
service: "youtube",
id: o.id,
title: fileMetadata.title,
author: fileMetadata.artist,
youtubeDubName: isDubbed ? o.dubLang : false
}
if (filenameAttributes.title === "Video Not Available" && filenameAttributes.author === "YouTube Viewers")
return {
error: 'ErrorCantConnectToServiceAPI',
critical: true
}
if (hasAudio && o.isAudioOnly) return { if (hasAudio && o.isAudioOnly) return {
type: "render", type: "render",
isAudioOnly: true, isAudioOnly: true,
urls: audio.url, urls: audio.url,
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`, filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata fileMetadata: fileMetadata
} }
let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)), let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
@ -87,21 +102,33 @@ export default async function(o) {
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') { if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
let single = info.streaming_data.formats.find(i => checkSingle(i)); let single = info.streaming_data.formats.find(i => checkSingle(i));
if (single) return { if (single) {
type: "bridge", filenameAttributes.qualityLabel = single.quality_label;
urls: single.url, filenameAttributes.resolution = `${single.width}x${single.height}`;
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`, filenameAttributes.extension = c[o.format].container;
fileMetadata: fileMetadata filenameAttributes.youtubeFormat = o.format;
return {
type: "bridge",
urls: single.url,
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
} }
}; }
let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i))); let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i)));
if (video && audio) return { if (video && audio) {
type: "render", filenameAttributes.qualityLabel = video.quality_label;
urls: [video.url, audio.url], filenameAttributes.resolution = `${video.width}x${video.height}`;
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`, filenameAttributes.extension = c[o.format].container;
fileMetadata: fileMetadata filenameAttributes.youtubeFormat = o.format;
}; return {
type: "render",
urls: [video.url, audio.url],
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
}
return { error: 'ErrorYTTryOtherCodec' } return { error: 'ErrorYTTryOtherCodec' }
} }

View file

@ -1,5 +1,5 @@
{ {
"audioIgnore": ["vk"], "audioIgnore": ["vk", "ok"],
"config": { "config": {
"bilibili": { "bilibili": {
"alias": "bilibili.com videos", "alias": "bilibili.com videos",
@ -9,11 +9,19 @@
"reddit": { "reddit": {
"alias": "reddit videos & gifs", "alias": "reddit videos & gifs",
"patterns": ["r/:sub/comments/:id/:title"], "patterns": ["r/:sub/comments/:id/:title"],
"subdomains": "*",
"enabled": true "enabled": true
}, },
"twitter": { "twitter": {
"alias": "twitter videos & voice", "alias": "twitter videos & voice",
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"], "altDomains": ["x.com", "vxtwitter.com", "fixvx.com"],
"subdomains": ["mobile"],
"patterns": [
":user/status/:id",
":user/status/:id/video/:index",
":user/status/:id/mediaviewer",
":user/status/:id/mediaViewer"
],
"enabled": true "enabled": true
}, },
"vk": { "vk": {
@ -21,25 +29,35 @@
"patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"], "patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"],
"enabled": true "enabled": true
}, },
"ok": {
"alias": "ok video",
"tld": "ru",
"patterns": ["video/:id"],
"enabled": true
},
"youtube": { "youtube": {
"alias": "youtube videos, shorts & music", "alias": "youtube videos, shorts & music",
"patterns": ["watch?v=:id", "embed/:id", "watch/:id"], "patterns": ["watch?v=:id", "embed/:id", "watch/:id"],
"subdomains": ["music", "m"],
"bestAudio": "opus", "bestAudio": "opus",
"enabled": true "enabled": true
}, },
"tumblr": { "tumblr": {
"patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"], "patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"],
"subdomains": "*",
"enabled": true "enabled": true
}, },
"tiktok": { "tiktok": {
"alias": "tiktok videos, photos & audio", "alias": "tiktok videos, photos & audio",
"patterns": [":user/video/:postId", ":id", "t/:id"], "patterns": [":user/video/:postId", ":id", "t/:id", ":user/photo/:postId"],
"subdomains": ["vt", "vm"],
"audioFormats": ["best", "m4a", "mp3"], "audioFormats": ["best", "m4a", "mp3"],
"enabled": true "enabled": true
}, },
"douyin": { "douyin": {
"alias": "douyin videos & audio", "alias": "douyin videos & audio",
"patterns": ["video/:postId", ":id"], "patterns": ["video/:postId", ":id"],
"subdomains": ["v"],
"enabled": false "enabled": false
}, },
"vimeo": { "vimeo": {
@ -49,12 +67,16 @@
}, },
"soundcloud": { "soundcloud": {
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"], "patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
"bestAudio": "opus", "subdomains": ["on"],
"audioFormats": ["best", "opus", "mp3"],
"enabled": true "enabled": true
}, },
"instagram": { "instagram": {
"alias": "instagram reels & posts", "alias": "instagram reels, posts & stories",
"patterns": ["reels/:id", "reel/:id", "p/:id"], "patterns": [
"reels/:postId", "reel/:postId", "p/:postId",
"tv/:postId", "stories/:username/:storyId"
],
"enabled": true "enabled": true
}, },
"vine": { "vine": {
@ -65,7 +87,7 @@
}, },
"pinterest": { "pinterest": {
"alias": "pinterest videos & stories", "alias": "pinterest videos & stories",
"patterns": ["pin/:id"], "patterns": ["pin/:id", "pin/:id/:garbage", "url_shortener/:shortLink"],
"enabled": true "enabled": true
}, },
"streamable": { "streamable": {

View file

@ -1,40 +1,52 @@
export const testers = { export const testers = {
"twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20) "bilibili": (patternMatch) =>
|| (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13), patternMatch.id?.length <= 12,
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] "instagram": (patternMatch) =>
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length <= 10), patternMatch.postId?.length <= 12
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
"ok": (patternMatch) =>
patternMatch.id?.length <= 16,
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12), "pinterest": (patternMatch) =>
patternMatch.id?.length <= 128 || patternMatch.shortLink?.length <= 32,
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 11), "reddit": (patternMatch) =>
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,
"reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] "rutube": (patternMatch) =>
&& patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96), patternMatch.id?.length === 32,
"tiktok": (patternMatch) => ((patternMatch["user"] && patternMatch["postId"] && patternMatch["postId"].length <= 21) "soundcloud": (patternMatch) =>
|| (patternMatch["id"] && patternMatch["id"].length <= 13)), (patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255)
|| patternMatch.shortLink?.length <= 32,
"douyin": (patternMatch) => ((patternMatch["postId"] && patternMatch["postId"].length <= 21) "streamable": (patternMatch) =>
|| (patternMatch["id"] && patternMatch["id"].length <= 13)), patternMatch.id?.length === 6,
"tiktok": (patternMatch) =>
patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13,
"tumblr": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length < 21) "tumblr": (patternMatch) =>
|| (patternMatch["id"] && patternMatch["id"].length < 21 && patternMatch["user"] && patternMatch["user"].length <= 32)), patternMatch.id?.length < 21
|| (patternMatch.id?.length < 21 && patternMatch.user?.length <= 32),
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)), "twitch": (patternMatch) =>
patternMatch.channel && patternMatch.clip?.length <= 100,
"soundcloud": (patternMatch) => (patternMatch["author"]?.length <= 25 && patternMatch["song"]?.length <= 255) "twitter": (patternMatch) =>
|| (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32), patternMatch.id?.length < 20,
"instagram": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12), "vimeo": (patternMatch) =>
patternMatch.id?.length <= 11,
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12), "vine": (patternMatch) =>
patternMatch.id?.length <= 12,
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128), "vk": (patternMatch) =>
patternMatch.userId?.length <= 10 && patternMatch.videoId?.length <= 10,
"streamable": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length === 6), "youtube": (patternMatch) =>
patternMatch.id?.length <= 11,
"twitch": (patternMatch) => ((patternMatch["channel"] && patternMatch["clip"] && patternMatch["clip"].length <= 100)),
"rutube": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length === 32)),
} }

View file

@ -0,0 +1,103 @@
import { services } from "../config.js";
import { strict as assert } from "node:assert";
import psl from "psl";
export function aliasURL(url) {
assert(url instanceof URL);
const host = psl.parse(url.hostname);
const parts = url.pathname.split('/');
switch (host.sld) {
case "youtube":
if (url.pathname.startsWith('/live/') || url.pathname.startsWith('/shorts/')) {
url.pathname = '/watch';
// parts := ['', 'live' || 'shorts', id, ...rest]
url.search = `?v=${encodeURIComponent(parts[2])}`
}
break;
case "youtu":
if (url.hostname === 'youtu.be' && parts.length >= 2) {
/* youtu.be urls can be weird, e.g. https://youtu.be/<id>//asdasd// still works
** but we only care about the 1st segment of the path */
url = new URL(`https://youtube.com/watch?v=${
encodeURIComponent(parts[1])
}`)
}
break;
case "pin":
if (url.hostname === 'pin.it' && parts.length === 2) {
url = new URL(`https://pinterest.com/url_shortener/${
encodeURIComponent(parts[1])
}`)
}
break;
case "vxtwitter":
case "fixvx":
case "x":
if (services.twitter.altDomains.includes(url.hostname)) {
url.hostname = 'twitter.com'
}
break;
case "twitch":
if (url.hostname === 'clips.twitch.tv' && parts.length >= 2) {
url = new URL(`https://twitch.tv/_/clip/${parts[1]}`);
}
break;
}
return url
}
export function cleanURL(url) {
assert(url instanceof URL);
const host = psl.parse(url.hostname).sld;
let stripQuery = true;
if (host === 'pinterest') {
url.hostname = 'pinterest.com'
} else if (host === 'vk' && url.pathname.includes('/clip')) {
if (url.searchParams.get('z'))
url.search = '?z=' + encodeURIComponent(url.searchParams.get('z'));
stripQuery = false;
} else if (host === 'youtube' && url.searchParams.get('v')) {
url.search = '?v=' + encodeURIComponent(url.searchParams.get('v'));
stripQuery = false;
}
if (stripQuery) {
url.search = ''
}
url.username = url.password = url.port = url.hash = ''
if (url.pathname.endsWith('/'))
url.pathname = url.pathname.slice(0, -1);
return url
}
export function normalizeURL(url) {
return cleanURL(
aliasURL(
new URL(url.replace(/^https\/\//, 'https://'))
)
);
}
export function getHostIfValid(url) {
const host = psl.parse(url.hostname);
if (host.error) return;
const service = services[host.sld];
if (!service) return;
if ((service.tld ?? 'com') !== host.tld) return;
const anySubdomainAllowed = service.subdomains === '*';
const validSubdomain = [null, 'www', ...(service.subdomains ?? [])].includes(host.subdomain);
if (!validSubdomain && !anySubdomainAllowed) return;
return host.sld;
}

View file

@ -1,9 +1,10 @@
import { existsSync, unlinkSync, appendFileSync } from "fs"; import { existsSync, unlinkSync, appendFileSync } from "fs";
import { createInterface } from "readline"; import { createInterface } from "readline";
import { Cyan, Bright } from "./sub/consoleText.js"; import { Cyan, Bright } from "./sub/consoleText.js";
import { loadJSON } from "./sub/loadFromFs.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { version } from "../modules/config.js"; const { version } = loadJSON("./package.json");
let envPath = './.env'; let envPath = './.env';
let q = `${Cyan('?')} \x1b[1m`; let q = `${Cyan('?')} \x1b[1m`;
@ -29,9 +30,6 @@ console.log(
`${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}` `${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
) )
console.log(
`\n${Bright("⚠️ Please notice that since v.6.0 cobalt is hosted in two parts. API and web app are now separate.\nMerged hosting is no longer available.")}`
)
function setup() { function setup() {
console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web.")); console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web."));

View file

@ -5,12 +5,17 @@ import { nanoid } from 'nanoid';
import { sha256 } from "../sub/crypto.js"; import { sha256 } from "../sub/crypto.js";
import { streamLifespan } from "../config.js"; import { streamLifespan } from "../config.js";
const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true }); const streamCache = new NodeCache({
const streamSalt = randomBytes(64).toString('hex'); stdTTL: streamLifespan/1000,
checkperiod: 10,
deleteOnExpire: true
})
streamCache.on("expired", (key) => { streamCache.on("expired", (key) => {
streamCache.del(key); streamCache.del(key);
}); })
const streamSalt = randomBytes(64).toString('hex');
export function createStream(obj) { export function createStream(obj) {
let streamID = nanoid(), let streamID = nanoid(),
@ -38,20 +43,26 @@ export function createStream(obj) {
exp = streamInfo.exp; exp = streamInfo.exp;
ghmac = streamInfo.hmac; ghmac = streamInfo.hmac;
} }
return `${process.env.apiURL || process.env.selfURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`; return `${process.env.apiURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
} }
export function verifyStream(id, hmac, exp) { export function verifyStream(id, hmac, exp) {
try { try {
let streamInfo = streamCache.get(id.toString()); let streamInfo = streamCache.get(id.toString());
if (!streamInfo) return { error: "this download link has expired or doesn't exist. go back and try again!", status: 400 }; if (!streamInfo) return {
error: "this download link has expired or doesn't exist. go back and try again!",
status: 400
}
let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt); let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt);
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac) if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
&& Number(exp) > Math.floor(new Date().getTime())) { && Number(exp) > Math.floor(new Date().getTime())) {
return streamInfo; return streamInfo;
} }
return { error: "i couldn't verify if you have access to this download. go back and try again!", status: 401 }; return {
error: "i couldn't verify if you have access to this stream. go back and try again!",
status: 401
}
} catch (e) { } catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } }; return { status: 500, body: { status: "error", text: "Internal Server Error" } };
} }

View file

@ -1,4 +1,4 @@
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js"; import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js";
export default async function(res, streamInfo) { export default async function(res, streamInfo) {
try { try {
@ -10,7 +10,10 @@ export default async function(res, streamInfo) {
case "render": case "render":
await streamLiveRender(streamInfo, res); await streamLiveRender(streamInfo, res);
break; break;
case "videoM3U8": case "gif":
convertToGif(streamInfo, res);
break;
case "remux":
case "mute": case "mute":
streamVideoOnly(streamInfo, res); streamVideoOnly(streamInfo, res);
break; break;

View file

@ -1,47 +1,91 @@
import { spawn } from "child_process"; import { spawn } from "child_process";
import ffmpeg from "ffmpeg-static"; import ffmpeg from "ffmpeg-static";
import { ffmpegArgs, genericUserAgent } from "../config.js"; import { ffmpegArgs, genericUserAgent } from "../config.js";
import { getThreads, metadataManager } from "../sub/utils.js"; import { metadataManager } from "../sub/utils.js";
import { request } from 'undici'; import { request } from "undici";
import { create as contentDisposition } from "content-disposition-header";
import { AbortController } from "abort-controller"
function fail(res) { function closeRequest(controller) {
try { controller.abort() } catch {}
}
function closeResponse(res) {
if (!res.headersSent) res.sendStatus(500); if (!res.headersSent) res.sendStatus(500);
return res.destroy(); return res.destroy();
} }
function killProcess(p) {
// ask the process to terminate itself gracefully
p?.kill('SIGTERM');
setTimeout(() => {
if (p?.exitCode === null)
// brutally murder the process if it didn't quit
p?.kill('SIGKILL');
}, 5000);
}
function pipe(from, to, done) {
from.on('error', done)
.on('close', done);
to.on('error', done)
.on('close', done);
from.pipe(to);
}
function getCommand(args) {
if (process.env.PROCESSING_PRIORITY && process.platform !== "win32") {
return ['nice', ['-n', process.env.PROCESSING_PRIORITY, ffmpeg, ...args]]
}
return [ffmpeg, args]
}
export async function streamDefault(streamInfo, res) { export async function streamDefault(streamInfo, res) {
const abortController = new AbortController();
const shutdown = () => (closeRequest(abortController), closeResponse(res));
try { try {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; let filename = streamInfo.filename;
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`; if (streamInfo.isAudioOnly) {
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`); filename = `${streamInfo.filename}.${streamInfo.audioFormat}`
}
res.setHeader('Content-disposition', contentDisposition(filename));
const { body: stream, headers } = await request(streamInfo.urls, { const { body: stream, headers } = await request(streamInfo.urls, {
headers: { 'user-agent': genericUserAgent }, headers: { 'user-agent': genericUserAgent },
signal: abortController.signal,
maxRedirections: 16 maxRedirections: 16
}); });
res.setHeader('content-type', headers['content-type']); res.setHeader('content-type', headers['content-type']);
res.setHeader('content-length', headers['content-length']); res.setHeader('content-length', headers['content-length']);
stream.pipe(res).on('error', () => fail(res)); pipe(stream, res, shutdown);
stream.on('error', () => fail(res)); } catch {
stream.on('aborted', () => fail(res)); shutdown();
} catch (e) {
fail(res);
} }
} }
export async function streamLiveRender(streamInfo, res) {
try {
if (streamInfo.urls.length !== 2) return fail(res);
let { body: audio } = await request(streamInfo.urls[1], { export async function streamLiveRender(streamInfo, res) {
maxRedirections: 16 let abortController = new AbortController(), process;
const shutdown = () => (
closeRequest(abortController),
killProcess(process),
closeResponse(res)
);
try {
if (streamInfo.urls.length !== 2) return shutdown();
const { body: audio } = await request(streamInfo.urls[1], {
maxRedirections: 16, signal: abortController.signal
}); });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1],
args = [ args = [
'-loglevel', '-8', '-loglevel', '-8',
'-threads', `${getThreads()}`,
'-i', streamInfo.urls[0], '-i', streamInfo.urls[0],
'-i', 'pipe:3', '-i', 'pipe:3',
'-map', '0:v', '-map', '0:v',
@ -49,133 +93,166 @@ export async function streamLiveRender(streamInfo, res) {
]; ];
args = args.concat(ffmpegArgs[format]); args = args.concat(ffmpegArgs[format]);
if (streamInfo.metadata) args = args.concat(metadataManager(streamInfo.metadata)); if (streamInfo.metadata) {
args = args.concat(metadataManager(streamInfo.metadata))
}
args.push('-f', format, 'pipe:4'); args.push('-f', format, 'pipe:4');
let ffmpegProcess = spawn(ffmpeg, args, {
process = spawn(...getCommand(args), {
windowsHide: true, windowsHide: true,
stdio: [ stdio: [
'inherit', 'inherit', 'inherit', 'inherit', 'inherit', 'inherit',
'pipe', 'pipe' 'pipe', 'pipe'
], ],
}); });
const [,,, audioInput, muxOutput] = process.stdio;
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename));
res.on('error', () => {
ffmpegProcess.kill();
fail(res);
});
ffmpegProcess.stdio[4].pipe(res).on('error', () => {
ffmpegProcess.kill();
fail(res);
});
audio.pipe(ffmpegProcess.stdio[3]).on('error', () => {
ffmpegProcess.kill();
fail(res);
});
audio.on('error', () => {
ffmpegProcess.kill();
fail(res);
});
audio.on('aborted', () => {
ffmpegProcess.kill();
fail(res);
});
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); audio.on('error', shutdown);
ffmpegProcess.on('close', () => ffmpegProcess.kill()); audioInput.on('error', shutdown);
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); audio.pipe(audioInput);
res.on('finish', () => ffmpegProcess.kill()); pipe(muxOutput, res, shutdown);
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => {
ffmpegProcess.kill();
fail(res);
});
} catch (e) { process.on('close', shutdown);
fail(res); res.on('finish', shutdown);
} catch {
shutdown();
} }
} }
export function streamAudioOnly(streamInfo, res) { export function streamAudioOnly(streamInfo, res) {
let process;
const shutdown = () => (killProcess(process), closeResponse(res));
try { try {
let args = [ let args = [
'-loglevel', '-8', '-loglevel', '-8'
'-threads', `${getThreads()}`,
'-i', streamInfo.urls
] ]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push(
'-i', streamInfo.urls,
'-vn'
)
if (streamInfo.metadata) { if (streamInfo.metadata) {
if (streamInfo.metadata.cover) { // currently corrupts the audio
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0')
} else {
args.push('-vn')
}
args = args.concat(metadataManager(streamInfo.metadata)) args = args.concat(metadataManager(streamInfo.metadata))
} else {
args.push('-vn')
} }
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"];
args = args.concat(arg); args = args.concat(arg);
if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]); if (ffmpegArgs[streamInfo.audioFormat]) {
args = args.concat(ffmpegArgs[streamInfo.audioFormat])
}
args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, { process = spawn(...getCommand(args), {
windowsHide: true, windowsHide: true,
stdio: [ stdio: [
'inherit', 'inherit', 'inherit', 'inherit', 'inherit', 'inherit',
'pipe' 'pipe'
], ],
}); });
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); const [,,, muxOutput] = process.stdio;
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); res.setHeader('Connection', 'keep-alive');
res.on('finish', () => ffmpegProcess.kill()); res.setHeader('Content-Disposition', contentDisposition(`${streamInfo.filename}.${streamInfo.audioFormat}`));
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => { pipe(muxOutput, res, shutdown);
ffmpegProcess.kill(); res.on('finish', shutdown);
fail(res); } catch {
}); shutdown();
} catch (e) {
fail(res);
} }
} }
export function streamVideoOnly(streamInfo, res) { export function streamVideoOnly(streamInfo, res) {
let process;
const shutdown = () => (killProcess(process), closeResponse(res));
try { try {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ let args = [
'-loglevel', '-8', '-loglevel', '-8'
'-threads', `${getThreads()}`, ]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push(
'-i', streamInfo.urls, '-i', streamInfo.urls,
'-c', 'copy' '-c', 'copy'
] )
if (streamInfo.mute) args.push('-an'); if (streamInfo.mute) {
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") args.push('-bsf:a', 'aac_adtstoasc'); args.push('-an')
if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov'); }
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") {
args.push('-bsf:a', 'aac_adtstoasc')
}
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
if (format === "mp4") {
args.push('-movflags', 'faststart+frag_keyframe+empty_moov')
}
args.push('-f', format, 'pipe:3'); args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
process = spawn(...getCommand(args), {
windowsHide: true, windowsHide: true,
stdio: [ stdio: [
'inherit', 'inherit', 'inherit', 'inherit', 'inherit', 'inherit',
'pipe' 'pipe'
], ],
}); });
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}${streamInfo.mute ? '_mute' : ''}.${format}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); const [,,, muxOutput] = process.stdio;
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); res.setHeader('Connection', 'keep-alive');
res.on('finish', () => ffmpegProcess.kill()); res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename));
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', () => { pipe(muxOutput, res, shutdown);
ffmpegProcess.kill();
fail(res); process.on('close', shutdown);
}); res.on('finish', shutdown);
} catch (e) { } catch {
fail(res); shutdown();
}
}
export function convertToGif(streamInfo, res) {
let process;
const shutdown = () => (killProcess(process), closeResponse(res));
try {
let args = [
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push('-i', streamInfo.urls)
args = args.concat(ffmpegArgs["gif"]);
args.push('-f', "gif", 'pipe:3');
process = spawn(...getCommand(args), {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
const [,,, muxOutput] = process.stdio;
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename.split('.')[0] + ".gif"));
pipe(muxOutput, res, shutdown);
process.on('close', shutdown);
res.on('finish', shutdown);
} catch {
shutdown();
} }
} }

View file

@ -0,0 +1,16 @@
import * as fs from "fs";
export function loadJSON(path) {
try {
return JSON.parse(fs.readFileSync(path, 'utf-8'))
} catch(e) {
return false
}
}
export function loadFile(path) {
try {
return fs.readFileSync(path, 'utf-8')
} catch(e) {
return false
}
}

View file

@ -1,9 +0,0 @@
import * as fs from "fs";
export default function(path) {
try {
return JSON.parse(fs.readFileSync(path, 'utf-8'))
} catch(e) {
return false
}
}

View file

@ -1,14 +1,16 @@
import { normalizeURL } from "../processing/url.js";
import { createStream } from "../stream/manage.js"; import { createStream } from "../stream/manage.js";
const apiVar = { const apiVar = {
allowed: { allowed: {
vCodec: ["h264", "av1", "vp9"], vCodec: ["h264", "av1", "vp9"],
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"], vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"] aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
}, },
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"] booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
} }
const forbiddenChars = ['}', '{', '(', ')', '\\', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '==']; const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
export function apiJSON(type, obj) { export function apiJSON(type, obj) {
@ -34,11 +36,13 @@ export function apiJSON(type, obj) {
break; break;
} }
return { status: 200, body: { status: "picker", pickerType: pickerType, picker: obj.picker, audio: audio } }; return { status: 200, body: { status: "picker", pickerType: pickerType, picker: obj.picker, audio: audio } };
case 6: // critical error, action should be taken by balancer/other server software
return { status: 500, body: { status: "error", text: obj.t, critical: true } };
default: default:
return { status: 400, body: { status: "error", text: "Bad Request" } }; return { status: 400, body: { status: "error", text: "Bad Request" } };
} }
} catch (e) { } catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } }; return { status: 500, body: { status: "error", text: "Internal Server Error", critical: true } };
} }
} }
export function metadataManager(obj) { export function metadataManager(obj) {
@ -49,32 +53,10 @@ export function metadataManager(obj) {
for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) } for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) }
return commands; return commands;
} }
export function cleanURL(url, host) {
switch (host) {
case "vk":
url = url.includes('clip') ? url.split('&')[0] : url.split('?')[0];
break;
case "youtube":
url = url.split('&')[0];
break;
case "tiktok":
url = url.replace(/@([a-zA-Z]+(\.[a-zA-Z]+)+)/, "@a")
case "pinterest":
url = url.replace(/:\/\/(?:www.)pinterest(?:\.[a-z.]+)/, "://pinterest.com")
default:
url = url.split('?')[0];
if (url.substring(url.length - 1) === "/") url = url.substring(0, url.length - 1);
break;
}
for (let i in forbiddenChars) {
url = url.replaceAll(forbiddenChars[i], '')
}
url = url.replace('https//', 'https://')
return url.slice(0, 128)
}
export function cleanString(string) { export function cleanString(string) {
for (let i in forbiddenCharsString) { for (let i in forbiddenCharsString) {
string = string.replaceAll(forbiddenCharsString[i], '') string = string.replaceAll("/", "_").replaceAll(forbiddenCharsString[i], '')
} }
return string; return string;
} }
@ -91,16 +73,19 @@ export function unicodeDecode(str) {
} }
export function checkJSONPost(obj) { export function checkJSONPost(obj) {
let def = { let def = {
url: normalizeURL(decodeURIComponent(obj.url)),
vCodec: "h264", vCodec: "h264",
vQuality: "720", vQuality: "720",
aFormat: "mp3", aFormat: "mp3",
filenamePattern: "classic",
isAudioOnly: false, isAudioOnly: false,
isNoTTWatermark: false, isNoTTWatermark: false,
isTTFullAudio: false, isTTFullAudio: false,
isAudioMuted: false, isAudioMuted: false,
disableMetadata: false, disableMetadata: false,
dubLang: false, dubLang: false,
vimeoDash: false vimeoDash: false,
twitterGif: false
} }
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);
@ -117,12 +102,8 @@ export function checkJSONPost(obj) {
} }
} }
if (def.dubLang) def.dubLang = verifyLanguageCode(obj.dubLang); if (def.dubLang)
def.dubLang = verifyLanguageCode(obj.dubLang);
obj["url"] = decodeURIComponent(String(obj["url"]));
let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2];
def["url"] = encodeURIComponent(cleanURL(obj["url"], host));
return def return def
} catch (e) { } catch (e) {
@ -132,18 +113,6 @@ export function checkJSONPost(obj) {
export function getIP(req) { export function getIP(req) {
return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip; 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'
}
}
export function cleanHTML(html) { export function cleanHTML(html) {
let clean = html.replace(/ {4}/g, ''); let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, ''); clean = clean.replace(/\n/g, '');

View file

@ -2,7 +2,7 @@ import "dotenv/config";
import { getJSON } from "../modules/api.js"; import { getJSON } from "../modules/api.js";
import { services } from "../modules/config.js"; import { services } from "../modules/config.js";
import loadJSON from "../modules/sub/loadJSON.js"; import { loadJSON } from "../modules/sub/loadFromFs.js";
import { checkJSONPost } from "../modules/sub/utils.js"; import { checkJSONPost } from "../modules/sub/utils.js";
let tests = loadJSON('./src/test/tests.json'); let tests = loadJSON('./src/test/tests.json');

View file

@ -0,0 +1,70 @@
import createFilename from "../modules/processing/createFilename.js";
let tests = [
{
f: {
service: 'youtube',
id: 'MMK3L4W70g4',
title: "Loossemble (루셈블) - 'Sensitive' MV",
author: 'Loossemble',
youtubeDubName: false,
qualityLabel: '2160p',
resolution: '3840x2160',
extension: 'webm',
youtubeFormat: 'vp9'
},
isAudioOnly: false,
isAudioMuted: false
},
{
f: {
service: 'youtube',
id: 'MMK3L4W70g4',
title: "Loossemble (루셈블) - 'Sensitive' MV",
author: 'Loossemble',
youtubeDubName: false,
qualityLabel: '2160p',
resolution: '3840x2160',
extension: 'webm',
youtubeFormat: 'vp9'
},
isAudioOnly: true,
isAudioMuted: false
},
{
f: {
service: 'youtube',
id: 'MMK3L4W70g4',
title: "Loossemble (루셈블) - 'Sensitive' MV",
author: 'Loossemble',
youtubeDubName: false,
qualityLabel: '2160p',
resolution: '3840x2160',
extension: 'webm',
youtubeFormat: 'vp9'
},
isAudioOnly: false,
isAudioMuted: true
},
{
f: {
service: 'vimeo',
id: 'MMK3L4W70g4',
title: "Loossemble (루셈블) - 'Sensitive' MV",
author: 'Loossemble',
qualityLabel: '2160p',
resolution: '3840x2160',
extension: 'mp4'
},
isAudioOnly: false,
isAudioMuted: true
}
]
for (let i = 0; i < tests.length; i++) {
console.log(`---${i}---`)
console.log(createFilename(tests[i].f, "classic", tests[i].isAudioOnly, tests[i].isAudioMuted))
console.log(createFilename(tests[i].f, "basic", tests[i].isAudioOnly, tests[i].isAudioMuted))
console.log(createFilename(tests[i].f, "pretty", tests[i].isAudioOnly, tests[i].isAudioMuted))
console.log(createFilename(tests[i].f, "nerdy", tests[i].isAudioOnly, tests[i].isAudioMuted))
}

View file

@ -11,6 +11,15 @@
"code": 200, "code": 200,
"status": "redirect" "status": "redirect"
} }
},
{
"name": "video with mobile web mediaviewer",
"url": "https://twitter.com/XSpaces/status/1526955853743546372/mediaViewer?currentTweet=1526955853743546372&currentTweetUser=XSpaces&currentTweet=1526955853743546372&currentTweetUser=XSpaces",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, { }, {
"name": "embedded twitter video", "name": "embedded twitter video",
"url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20", "url": "https://twitter.com/dustbin_nie/status/1624596567188717568?s=20",
@ -304,6 +313,14 @@
"code": 200, "code": 200,
"status": "stream" "status": "stream"
} }
}, {
"name": "no opus audio, fallback to mp3",
"url": "https://soundcloud.com/frums/credits",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}], }],
"youtube": [{ "youtube": [{
"name": "4k video (h264, 1440)", "name": "4k video (h264, 1440)",
@ -1154,5 +1171,14 @@
"code": 200, "code": 200,
"status": "stream" "status": "stream"
} }
}],
"ok": [{
"name": "regular video",
"url": "https://ok.ru/video/7204071410346",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}] }]
} }