From 9f4f03ec6c7264bb4e78f17c18e17599d55ee4a9 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 12 Oct 2024 11:06:14 +0000 Subject: [PATCH 001/379] docs/examples/cookies: add youtube_oauth to examples --- docs/examples/cookies.example.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/examples/cookies.example.json b/docs/examples/cookies.example.json index 73f3378d..7996adeb 100644 --- a/docs/examples/cookies.example.json +++ b/docs/examples/cookies.example.json @@ -10,5 +10,8 @@ ], "twitter": [ "auth_token=; ct0=" + ], + "youtube_oauth": [ + "" ] } From 7c0fb16fdb8b2ff2ea9bf5c4b7b0a6f5321d03fe Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 12 Oct 2024 11:24:29 +0000 Subject: [PATCH 002/379] api/keys: fix prefix size calculation for individual ipv6 addresses --- api/src/security/api-keys.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/security/api-keys.js b/api/src/security/api-keys.js index eee48da3..72063099 100644 --- a/api/src/security/api-keys.js +++ b/api/src/security/api-keys.js @@ -99,7 +99,9 @@ const formatKeys = (keyData) => { if (data.ips) { formatted[key].ips = data.ips.map(addr => { if (ip.isValid(addr)) { - return [ ip.parse(addr), 32 ]; + const parsed = ip.parse(addr); + const range = parsed.kind() === 'ipv6' ? 128 : 32; + return [ parsed, range ]; } return ip.parseCIDR(addr); From 1c9685922f2c9139054d6f9831d79cddb060e54a Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 12 Oct 2024 12:01:27 +0000 Subject: [PATCH 003/379] docs/api: add information about auth header --- docs/api.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index fc09a441..e3f3bf99 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,7 +3,42 @@ this document provides info about methods and acceptable variables for all cobal > if you are looking for the documentation for the old (7.x) api, you can find > it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md) - + +## authentication +an api instance may be configured to require you to authenticate yourself. +if this is the case, you will typically receive an [error response](#error-response) +with a **`api.auth..missing`** code, which tells you that a particular method +of authentication is required. + +authentication is done by passing the `Authorization` header, containing +the authentication scheme and the token: +``` +Authorization: +``` + +currently, cobalt supports two ways of authentication. an instance can +choose to configure both, or neither: +- [`Api-Key`](#api-key-authentication) +- [`Bearer`](#bearer-authentication) + +### api-key authentication +the api key authentication is the most straightforward. the instance owner +will assign you an api key which you can then use to authenticate like so: +``` +Authorization: Api-Key aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +``` + +if you are an instance owner and wish to configure api key authentication, +see the [instance](run-an-instance.md#api-key-file-format) documentation! + +### bearer authentication +the cobalt server may be configured to issue JWT bearers, which are short-lived +tokens intended for use by regular users (e.g. after passing a challenge). +currently, cobalt can issue tokens for successfully solved [turnstile](run-an-instance.md#list-of-all-environment-variables) +challenge, if the instance has turnstile configured. the resulting token is passed like so: +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` ## POST: `/` cobalt's main processing endpoint. From 52c24ab1a3127a62e2d4be04fcbc5b99f3e4adfb Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 12 Oct 2024 12:15:07 +0000 Subject: [PATCH 004/379] docs/run-an-instance: add undocumented turnstile envs --- docs/run-an-instance.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 08654d9f..272fbd35 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -72,11 +72,17 @@ sudo service nscd start | `RATELIMIT_MAX` | `20` | `30` | max requests per time window. requests above this amount will be blocked for the rate limit window duration. | | `DURATION_LIMIT` | `10800` | `18000` | max allowed video duration in **seconds**. | | `TUNNEL_LIFESPAN` | `90` | `120` | the duration for which tunnel info is stored in ram, **in seconds**. | +| `TURNSTILE_SITEKEY` | ➖ | `1x00000000000000000000BB` | [cloudflare turnstile](https://www.cloudflare.com/products/turnstile/) sitekey used by browser clients to request a challenge.\*\* | +| `TURNSTILE_SECRET` | ➖ | `1x0000000000000000000000000000000AA` | [cloudflare turnstile](https://www.cloudflare.com/products/turnstile/) secret used by cobalt to verify the client successfully solved the challenge.\*\* | +| `JWT_SECRET` | ➖ | ➖ | the secret used for issuing JWT tokens for request authentication. to choose a value, generate a random, secure, long string (ideally >=16 characters).\*\* | +| `JWT_EXPIRY` | `120` | `240` | the duration of how long a cobalt-issued JWT token will remain valid, in seconds. | | `API_KEY_URL` | ➖ | `file://keys.json` | the location of the api key database. for loading API keys, cobalt supports HTTP(S) urls, or local files by specifying a local path using the `file://` protocol. see the "api key file format" below for more details. | | `API_AUTH_REQUIRED` | ➖ | `1` | when set to `1`, the user always needs to be authenticated in some way before they can access the API (either via an api key or via turnstile, if enabled). | \* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)). +\*\* in order to enable turnstile bot protection, all three **`TURNSTILE_SITEKEY`, `TURNSTILE_SECRET` and `JWT_SECRET`** need to be set. + #### FREEBIND_CIDR setting a `FREEBIND_CIDR` allows cobalt to pick a random IP for every download and use it for all requests it makes for that particular download. to use freebind in cobalt, you need to follow its [setup instructions](https://github.com/imputnet/freebind.js?tab=readme-ov-file#setup) first. if you configure this option while running cobalt From 6cc895c3952df1291d1ba2371b6222fb17f12ce2 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 12 Oct 2024 12:20:15 +0000 Subject: [PATCH 005/379] docs/api: document `/session` endpoint --- docs/api.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api.md b/docs/api.md index e3f3bf99..39b17209 100644 --- a/docs/api.md +++ b/docs/api.md @@ -143,3 +143,18 @@ response body type: `application/json` | `commit` | `string` | commit hash | | `branch` | `string` | git branch | | `remote` | `string` | git remote | + +## POST: `/session` + +used for generating JWT tokens, if enabled. currently, cobalt only supports +generating tokens when a [turnstile](run-an-instance.md#list-of-all-environment-variables) challenge solution +is submitted by the client. + +the turnstile challenge response is submitted via the `cf-turnstile-response` header. +### response body +| key | type | description | +|:----------------|:-----------|:-------------------------------------------------------| +| `token` | `string` | a `Bearer` token used for later request authentication | +| `exp` | `number` | number in seconds indicating the token lifetime | + +on failure, an [error response](#error-response) is returned. From ebf157862a1df96b676a07cfac2a9a6a01511e8d Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 12 Oct 2024 19:06:11 +0600 Subject: [PATCH 006/379] web/about/community: redesign the page, add descriptions --- web/i18n/en/about.json | 17 +-- web/src/components/about/AboutSupport.svelte | 116 +++++++++++++++++++ web/src/routes/about/community/+page.svelte | 103 +++++++++------- 3 files changed, 185 insertions(+), 51 deletions(-) create mode 100644 web/src/components/about/AboutSupport.svelte diff --git a/web/i18n/en/about.json b/web/i18n/en/about.json index cfe9129f..10d31ec2 100644 --- a/web/i18n/en/about.json +++ b/web/i18n/en/about.json @@ -8,12 +8,6 @@ "page.terms": "terms and ethics", "page.credits": "thanks & licenses", - "community.discord": "community discord server", - "community.twitter": "news account on twitter", - "community.github": "github repo", - "community.email": "support email", - "community.telegram": "news channel on telegram", - "heading.general": "general terms", "heading.licenses": "licenses", "heading.summary": "best way to save what you love", @@ -27,5 +21,14 @@ "heading.responsibility": "user responsibilities", "heading.abuse": "reporting abuse", "heading.motivation": "motivation", - "heading.testers": "beta testers" + "heading.testers": "beta testers", + + "support.github": "check out cobalt's source code, contribute changes, or report issues", + "support.discord": "chat with the community and developers about cobalt or ask for help", + "support.twitter": "follow cobalt's updates and development on your twitter timeline", + "support.telegram": "stay up to date with latest cobalt updates via a telegram channel", + + "support.description.issue": "if you want to report a bug or some other recurring issue, please do it on github.", + "support.description.help": "use discord for any other questions. describe the issue properly in #cobalt-support or else no one will be able help you.", + "support.description.best-effort": "all support is best effort and not guaranteed, a reply might take some time." } diff --git a/web/src/components/about/AboutSupport.svelte b/web/src/components/about/AboutSupport.svelte new file mode 100644 index 00000000..2d4727f4 --- /dev/null +++ b/web/src/components/about/AboutSupport.svelte @@ -0,0 +1,116 @@ + + + + + diff --git a/web/src/routes/about/community/+page.svelte b/web/src/routes/about/community/+page.svelte index e0638015..f5c20bf4 100644 --- a/web/src/routes/about/community/+page.svelte +++ b/web/src/routes/about/community/+page.svelte @@ -4,61 +4,76 @@ import { contacts } from "$lib/env"; import { t } from "$lib/i18n/translations"; - import DonateAltItem from "$components/donate/DonateAltItem.svelte"; + import AboutSupport from "$components/about/AboutSupport.svelte"; + + let buttonContainerWidth: number; - -
- {#if $locale !== "ru"} - +
+ - - {/if} + {#if $locale === "ru"} + + {:else} + + + {/if} +
- +
+ {$t("about.support.description.issue")} - + {#if $locale !== "ru"} + {$t("about.support.description.help")} + {/if} - {#if $locale === "ru"} - - {/if} + {$t("about.support.description.best-effort")} +
From e34b8dd89c365abf03c9c413a7cb0488962fa456 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 12 Oct 2024 19:07:05 +0600 Subject: [PATCH 007/379] web/Switcher: add a gap between items --- web/src/components/buttons/Switcher.svelte | 1 + web/src/routes/settings/audio/+page.svelte | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/components/buttons/Switcher.svelte b/web/src/components/buttons/Switcher.svelte index a72c4e77..2ead0caf 100644 --- a/web/src/components/buttons/Switcher.svelte +++ b/web/src/components/buttons/Switcher.svelte @@ -47,6 +47,7 @@ background: var(--button); box-shadow: var(--button-box-shadow); padding: var(--switcher-padding); + gap: calc(var(--switcher-padding) - 1.5px); } .switcher :global(.button.active) { diff --git a/web/src/routes/settings/audio/+page.svelte b/web/src/routes/settings/audio/+page.svelte index eb0ac679..06555a42 100644 --- a/web/src/routes/settings/audio/+page.svelte +++ b/web/src/routes/settings/audio/+page.svelte @@ -24,8 +24,8 @@ - From d5ea154ed8266cb72f6112000840dd4ee0dc8dfd Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 12 Oct 2024 19:08:01 +0600 Subject: [PATCH 008/379] web/Omnibox: reduce gap by 2px --- web/src/components/save/Omnibox.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index a900def7..eed0ad71 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -225,7 +225,7 @@ flex-direction: column; max-width: 640px; width: 100%; - gap: 10px; + gap: 8px; } #input-container { From c10652b8c4586dbe14ae5d52c9dcb2e19363cddd Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 12 Oct 2024 19:10:31 +0600 Subject: [PATCH 009/379] web/AboutSupport: replace duplicated type --- web/src/components/about/AboutSupport.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/about/AboutSupport.svelte b/web/src/components/about/AboutSupport.svelte index 2d4727f4..1e9170b6 100644 --- a/web/src/components/about/AboutSupport.svelte +++ b/web/src/components/about/AboutSupport.svelte @@ -9,9 +9,6 @@ import IconBrandDiscord from "@tabler/icons-svelte/IconBrandDiscord.svelte"; import IconBrandTelegram from "@tabler/icons-svelte/IconBrandTelegram.svelte"; - export let platform: "github" | "discord" | "twitter" | "telegram"; - export let externalLink: string; - const platformIcons = { github: { icon: IconBrandGithub, @@ -30,6 +27,9 @@ color: "#1c9efb", }, }; + + export let platform: keyof typeof platformIcons; + export let externalLink: string; - +{#if button.link} + + {button.text} + +{:else} + +{/if} From c33017283d20ee9a998b7c19ad5f24fc17bdede7 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 13 Oct 2024 09:59:52 +0600 Subject: [PATCH 020/379] api/twitter: fix gifs having a wrong file extension in a picker --- api/src/processing/services/twitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/processing/services/twitter.js b/api/src/processing/services/twitter.js index 18866b49..229415eb 100644 --- a/api/src/processing/services/twitter.js +++ b/api/src/processing/services/twitter.js @@ -208,7 +208,7 @@ export default async function({ id, index, toGif, dispatcher, alwaysProxy }) { let url = bestQuality(content.video_info.variants); const shouldRenderGif = content.type === "animated_gif" && toGif; - const videoFilename = `twitter_${id}_${i + 1}.mp4`; + const videoFilename = `twitter_${id}_${i + 1}.${shouldRenderGif ? "gif" : "mp4"}`; let type = "video"; if (shouldRenderGif) type = "gif"; From 1ab94eb11d7f4499acc87ce5ca1171f93586b7e5 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 16 Oct 2024 16:53:20 +0600 Subject: [PATCH 021/379] web/i18n: update data management strings --- web/i18n/en/dialog.json | 4 ++-- web/i18n/en/settings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/i18n/en/dialog.json b/web/i18n/en/dialog.json index a2688f6d..3e6f5dec 100644 --- a/web/i18n/en/dialog.json +++ b/web/i18n/en/dialog.json @@ -1,6 +1,6 @@ { - "reset.title": "reset all settings?", - "reset.body": "are you sure you want to reset all settings? this action is immediate and irreversible.", + "reset.title": "reset all data?", + "reset.body": "are you sure you want to reset all data? this action is immediate and irreversible.", "picker.title": "select what to save", "picker.description.desktop": "click an item to save it. images can also be saved via the right click menu.", diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 0537982f..8d003439 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -106,7 +106,7 @@ "advanced.debug.title": "enable debug features", "advanced.debug.description": "gives you access to a page with various info that can be useful for debugging.", - "advanced.data": "settings data", + "advanced.data": "data management", "processing.override": "default instance override", "processing.override.title": "use the instance-provided processing server", From 0e52e1f8b091c2c48d7851a0185ecf8087115733 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 16 Oct 2024 17:03:34 +0600 Subject: [PATCH 022/379] web/safety-warning: reduce continue button timeout --- web/src/lib/api/safety-warning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/api/safety-warning.ts b/web/src/lib/api/safety-warning.ts index 175ddd67..26ad6aa4 100644 --- a/web/src/lib/api/safety-warning.ts +++ b/web/src/lib/api/safety-warning.ts @@ -98,7 +98,7 @@ export const customInstanceWarning = async () => { text: get(t)("button.continue"), color: "red", main: true, - timeout: 15000, + timeout: 5000, action: () => { _actions.resolve(); updateSetting({ From d55dddea2e7928bd5cac2c4c02d876d6a093ed9e Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 10:00:00 +0000 Subject: [PATCH 023/379] core/api: normalize bearer authorization --- api/src/core/api.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api/src/core/api.js b/api/src/core/api.js index b11d689a..b3123033 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -158,19 +158,20 @@ export const runAPI = (express, app, __dirname) => { return fail(res, "error.api.auth.jwt.missing"); } - if (!authorization.startsWith("Bearer ") || authorization.length > 256) { + if (authorization.length >= 256) { return fail(res, "error.api.auth.jwt.invalid"); } - const verifyJwt = jwt.verify( - authorization.split("Bearer ", 2)[1] - ); - - if (!verifyJwt) { + const [ type, token, ...rest ] = authorization.split(" "); + if (!token || type.toLowerCase() !== 'bearer' || rest.length) { return fail(res, "error.api.auth.jwt.invalid"); } - req.rateLimitKey = generateHmac(req.header("Authorization"), ipSalt); + if (!jwt.verify(token)) { + return fail(res, "error.api.auth.jwt.invalid"); + } + + req.rateLimitKey = generateHmac(token, ipSalt); } catch { return fail(res, "error.api.generic"); } From f5d09f86db73ed82b5d18b2ff0f8ca09c716f4cd Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 10:18:51 +0000 Subject: [PATCH 024/379] tests/soundcloud: replace private link --- api/src/util/tests.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/util/tests.json b/api/src/util/tests.json index 94005c0a..402a2baf 100644 --- a/api/src/util/tests.json +++ b/api/src/util/tests.json @@ -238,7 +238,7 @@ }, { "name": "private song", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90", "params": { "audioFormat": "mp3" }, @@ -249,7 +249,7 @@ }, { "name": "private song (wav, isAudioMuted)", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90", "params": { "downloadMode": "mute", "audioFormat": "wav" @@ -261,7 +261,7 @@ }, { "name": "private song (ogg, isAudioMuted, isAudioOnly)", - "url": "https://soundcloud.com/4kayy/unhappy-new-year-prod4kay/s-9bKbvwLdRWG", + "url": "https://soundcloud.com/user-798052861/asdasdasdsdsd/s-9TqZ7edLJ90", "params": { "downloadMode": "audio", "audioFormat": "ogg" From 1cf82e4d69fb667518c09b23c447ac923d80f703 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 16:23:09 +0600 Subject: [PATCH 025/379] docs: add a tutorial document for protecting an instance --- docs/images/protect-an-instance/add.png | Bin 0 -> 15590 bytes docs/images/protect-an-instance/created.png | Bin 0 -> 29173 bytes docs/images/protect-an-instance/domain.png | Bin 0 -> 13460 bytes docs/images/protect-an-instance/mode.png | Bin 0 -> 27968 bytes docs/images/protect-an-instance/name.png | Bin 0 -> 14816 bytes docs/images/protect-an-instance/sidebar.png | Bin 0 -> 6662 bytes docs/protect-an-instance.md | 58 ++++++++++++++++++++ docs/run-an-instance.md | 2 +- 8 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 docs/images/protect-an-instance/add.png create mode 100644 docs/images/protect-an-instance/created.png create mode 100644 docs/images/protect-an-instance/domain.png create mode 100644 docs/images/protect-an-instance/mode.png create mode 100644 docs/images/protect-an-instance/name.png create mode 100644 docs/images/protect-an-instance/sidebar.png create mode 100644 docs/protect-an-instance.md diff --git a/docs/images/protect-an-instance/add.png b/docs/images/protect-an-instance/add.png new file mode 100644 index 0000000000000000000000000000000000000000..e186a65ce99e5a3b31d98b524348af049fba3a38 GIT binary patch literal 15590 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 literal 0 HcmV?d00001 diff --git a/docs/images/protect-an-instance/created.png b/docs/images/protect-an-instance/created.png new file mode 100644 index 0000000000000000000000000000000000000000..546a68978caa3b604e5ef283c178aae7112d16e3 GIT binary patch literal 29173 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 literal 0 HcmV?d00001 diff --git a/docs/images/protect-an-instance/domain.png b/docs/images/protect-an-instance/domain.png new file mode 100644 index 0000000000000000000000000000000000000000..249a8a9250c51e5939bc6067c11d43436f2f3ec7 GIT binary patch literal 13460 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 NRgh5uS4o)$|1W`nisJwP literal 0 HcmV?d00001 diff --git a/docs/images/protect-an-instance/mode.png b/docs/images/protect-an-instance/mode.png new file mode 100644 index 0000000000000000000000000000000000000000..242b35a5b00c4f293e7cddb480d9660f131ffd88 GIT binary patch literal 27968 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 X0`LXhsc+ZC{sk$@s>#$#nT7r@@y`EQ literal 0 HcmV?d00001 diff --git a/docs/images/protect-an-instance/name.png b/docs/images/protect-an-instance/name.png new file mode 100644 index 0000000000000000000000000000000000000000..fd39dc954e9925f2f97b58ccdcfb4baf7d65e867 GIT binary patch literal 14816 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 H@aO*p%%Qsl literal 0 HcmV?d00001 diff --git a/docs/images/protect-an-instance/sidebar.png b/docs/images/protect-an-instance/sidebar.png new file mode 100644 index 0000000000000000000000000000000000000000..8294c4a059a39b5f922bd40521998cb6a20bdd4b GIT binary patch literal 6662 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 F{{buvy|@4X literal 0 HcmV?d00001 diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md new file mode 100644 index 00000000..650f155e --- /dev/null +++ b/docs/protect-an-instance.md @@ -0,0 +1,58 @@ +# how to protect your cobalt instance +if you keep getting a ton of unknown traffic that hurts the performance of your instance, then it might be a good idea to enable bot protection. + +``` +⚠️ this tutorial will work reliably on the latest official version of cobalt 10. we can't promise full compatibility with anything else. +``` + +## configure cloudflare turnstile +turnstile is a free, safe, and privacy-respecting alternative to captcha. +cobalt uses it automatically to weed out bots and automated scripts. +your instance doesn't have to be proxied by cloudflare to use turnstile. +all you need is a free cloudflare account to get started. + +cloudflare dashboard interface might change over time, but basics should stay the same. + +1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account. +2. once logged in, select `turnstile` in the sidebar. +![](images/protect-an-instance/sidebar.png) +3. press `add widget`. +![](images/protect-an-instance/add.png) +4. enter the widget name (can be anything, such as "cobalt"). +![](images/protect-an-instance/name.png) +5. add cobalt frontend domains you want the widget to work with. you can change this list later at any time. + - if you want to use your processing instance with [cobalt.tools](https://cobalt.tools/) frontend, then add `cobalt.tools` to the list. +![](images/protect-an-instance/domain.png) +6. select `invisible` widget mode. +![](images/protect-an-instance/mode.png) +7. press `create`. +8. keep the page with sitekey and secret key open, you'll need them later. +if you closed it, no worries! +just open the same turnstile page and press "settings" on your freshly made turnstile widget. +**never share your secret turnstile key with anyone.** +![](images/protect-an-instance/created.png) + +you've successfully created a turnstile widget! time to add it to your processing instance. + +### enable turnstile on your processing instance +this tutorial assumes that you only have `API_URL` in your `environment` variables list. +if you have other variables there, just add new ones after existing ones. +**example values in the tutorial should never be used**. + +1. open your `docker-compose.yml` config file in any text editor of choice. +2. copy the turnstile sitekey & secret key and paste them to their respective variables. `TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key: +```yml +environment: + API_URL: "https://your.instance.url.here.local/" + TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key + TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key +``` +3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters. this string will be used as salt for all JWT keys. **do NOT use the example secret**. + +```yml +environment: + API_URL: "https://your.instance.url.here.local/" + TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key + TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key + JWT_SECRET: "bgBmF4efNCKPirDqTc4FMmbX8P22I31oCj5R1zDiDi5sy8CWPnfLUct7rk5RlZUS" # create a new secret, NEVER use this one +``` diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 272fbd35..1d4dcdc0 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -1,4 +1,4 @@ -# how to host a cobalt instance yourself +# how to run a cobalt instance ## 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. From 90114bdbea2d757681e7c78686b5e45acec22e18 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 16:28:22 +0600 Subject: [PATCH 026/379] docs/protect-an-instance: update the note to show up as such --- docs/protect-an-instance.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 650f155e..7c9bec7b 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -1,9 +1,9 @@ # how to protect your cobalt instance if you keep getting a ton of unknown traffic that hurts the performance of your instance, then it might be a good idea to enable bot protection. -``` -⚠️ this tutorial will work reliably on the latest official version of cobalt 10. we can't promise full compatibility with anything else. -``` +> [!NOTE] +> this tutorial will work reliably on the latest official version of cobalt 10. +we can't promise full compatibility with anything else. ## configure cloudflare turnstile turnstile is a free, safe, and privacy-respecting alternative to captcha. From 5ce3a941f9ef345a74439bfffdb33fb9c417ff39 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 16:31:55 +0600 Subject: [PATCH 027/379] docs/protect-an-instance: emphasize a warning in env variable section --- docs/protect-an-instance.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 7c9bec7b..c95bf135 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -37,7 +37,9 @@ you've successfully created a turnstile widget! time to add it to your processin ### enable turnstile on your processing instance this tutorial assumes that you only have `API_URL` in your `environment` variables list. if you have other variables there, just add new ones after existing ones. -**example values in the tutorial should never be used**. + +> [!IMPORTANT] +> never use any of the values from the tutorial, especially `JWT_SECRET`! 1. open your `docker-compose.yml` config file in any text editor of choice. 2. copy the turnstile sitekey & secret key and paste them to their respective variables. `TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key: @@ -47,7 +49,7 @@ environment: TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key ``` -3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters. this string will be used as salt for all JWT keys. **do NOT use the example secret**. +3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters. this string will be used as salt for all JWT keys. ```yml environment: From c3f3499a42e04a24c8a63c3f174bd68aff9b4e5d Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 10:35:05 +0000 Subject: [PATCH 028/379] api/util: add script to generate secure `JWT_SECRET` --- api/package.json | 3 ++- api/src/util/generate-jwt-secret.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 api/src/util/generate-jwt-secret.js diff --git a/api/package.json b/api/package.json index 339d383f..ba346f39 100644 --- a/api/package.json +++ b/api/package.json @@ -12,7 +12,8 @@ "start": "node src/cobalt", "setup": "node src/util/setup", "test": "node src/util/test", - "token:youtube": "node src/util/generate-youtube-tokens" + "token:youtube": "node src/util/generate-youtube-tokens", + "token:jwt": "node src/util/generate-jwt-secret" }, "repository": { "type": "git", diff --git a/api/src/util/generate-jwt-secret.js b/api/src/util/generate-jwt-secret.js new file mode 100644 index 00000000..83f0aa5b --- /dev/null +++ b/api/src/util/generate-jwt-secret.js @@ -0,0 +1,13 @@ +// run with `pnpm -r token:jwt` + +const makeSecureString = (length = 64) => { + const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'; + const out = []; + + for (const byte of crypto.getRandomValues(new Uint8Array(length))) + out.push(alphabet[byte % alphabet.length]); + + return out.join(''); +} + +console.log(`JWT_SECRET: ${JSON.stringify(makeSecureString(64))}`) From 7515204bb73a858a82853a456cef576dd64cd038 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 16:51:38 +0600 Subject: [PATCH 029/379] docs/api: update warnings --- docs/api.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index 39b17209..dfcac1ed 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,8 +1,8 @@ # cobalt api documentation this document provides info about methods and acceptable variables for all cobalt api requests. -> if you are looking for the documentation for the old (7.x) api, you can find -> it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md) +> [!IMPORTANT] +> hosted api instances (such as api.cobalt.tools) use bot protection and are not intended to be used in other projects without explicit permission. if you want to access the cobalt api reliably, you should [host your own instance](/docs/run-an-instance.md). ## authentication an api instance may be configured to require you to authenticate yourself. @@ -46,9 +46,10 @@ 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 /` request. +> [!IMPORTANT] +> you must include `Accept` and `Content-Type` headers with every `POST /` request. +``` Accept: application/json Content-Type: application/json ``` From 67ffcdc5047ef314a6f042c0634ee7539ba98449 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 16:52:59 +0600 Subject: [PATCH 030/379] docs/api: update the general api warning --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index dfcac1ed..dc7e9c59 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,7 +2,7 @@ this document provides info about methods and acceptable variables for all cobalt api requests. > [!IMPORTANT] -> hosted api instances (such as api.cobalt.tools) use bot protection and are not intended to be used in other projects without explicit permission. if you want to access the cobalt api reliably, you should [host your own instance](/docs/run-an-instance.md). +> hosted api instances (such as `api.cobalt.tools`) use bot protection and are **not** intended to be used in other projects without explicit permission. if you want to access the cobalt api reliably, you should [host your own instance](/docs/run-an-instance.md) or ask an instance owner for access. ## authentication an api instance may be configured to require you to authenticate yourself. From 51adfc85cd03bd24fbf4b535f7a6c5b220dbc4db Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 17:20:38 +0600 Subject: [PATCH 031/379] api: update readme --- api/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/README.md b/api/README.md index 5c281246..05664321 100644 --- a/api/README.md +++ b/api/README.md @@ -11,12 +11,14 @@ as long as you: ## running your own instance if you want to run your own instance for whatever purpose, [follow this guide](/docs/run-an-instance.md). -it's *highly* recommended to use a docker compose method unless you run for developing/debugging purposes. +we recommend to use docker compose unless you intend to run cobalt for developing/debugging purposes. ## accessing the api -currently, there is no publicly accessible main api. we plan on providing a public api for -cobalt 10 in some form in the future. we recommend deploying your own instance if you wish -to use the latest api. you can access [the documentation](/docs/api.md) for it here. +there is currently no publicly available pre-hosted api. +we recommend [deploying your own instance](/docs/run-an-instance.md) if you wish to use the cobalt api. -if you are looking for the documentation for the old (7.x) api, you can find -it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md) \ No newline at end of file +you can read [the api documentation here](/docs/api.md). + +> [!WARNING] +> the v7 public api (/api/json) will be shut down on **november 11th, 2024**. +> you can access documentation for it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md). From c494850cfff1d4f3977e735b73cc690c39ad065b Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 17:45:10 +0600 Subject: [PATCH 032/379] repo: update readme & remove old docs --- README.md | 114 ++++-------------- .../troubleshooting/clipboard/config.png | Bin 4154 -> 0 bytes .../images/troubleshooting/clipboard/risk.png | Bin 17944 -> 0 bytes .../troubleshooting/clipboard/search.png | Bin 6867 -> 0 bytes .../troubleshooting/clipboard/toggle.png | Bin 18725 -> 0 bytes .../troubleshooting/clipboard/toggled.png | Bin 17312 -> 0 bytes docs/troubleshooting.md | 37 ------ 7 files changed, 24 insertions(+), 127 deletions(-) delete mode 100644 docs/images/troubleshooting/clipboard/config.png delete mode 100644 docs/images/troubleshooting/clipboard/risk.png delete mode 100644 docs/images/troubleshooting/clipboard/search.png delete mode 100644 docs/images/troubleshooting/clipboard/toggle.png delete mode 100644 docs/images/troubleshooting/clipboard/toggled.png delete mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index 5bf8ae8e..ca108a86 100644 --- a/README.md +++ b/README.md @@ -15,108 +15,42 @@ 💬 community discord server - 🐦 twitter/x + 🐦 twitter


-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 paywalls***. +cobalt is a media downloader that doesn't piss you off. it's friendly, efficient, and doesn't have ads, trackers, paywalls or other nonsense. -paste the link, get the file, move on. it's that simple. just how it should be. +paste the link, get the file, move on. that simple, just how it should be. -### supported services -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 👀). +### cobalt monorepo +this monorepo includes source code for api, frontend, and related packages: +- [api tree](/api/) +- [web tree](/web/) +- [packages tree](/packages/) -| service | video + audio | only audio | only video | metadata | rich file names | -| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: | -| bilibili | ✅ | ✅ | ✅ | ➖ | ➖ | -| bluesky | ✅ | ✅ | ✅ | ➖ | ➖ | -| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ | -| instagram | ✅ | ✅ | ✅ | ➖ | ➖ | -| facebook | ✅ | ❌ | ✅ | ➖ | ➖ | -| loom | ✅ | ❌ | ✅ | ✅ | ➖ | -| ok.ru | ✅ | ❌ | ✅ | ✅ | ✅ | -| pinterest | ✅ | ✅ | ✅ | ➖ | ➖ | -| reddit | ✅ | ✅ | ✅ | ❌ | ❌ | -| rutube | ✅ | ✅ | ✅ | ✅ | ✅ | -| snapchat | ✅ | ✅ | ✅ | ➖ | ➖ | -| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ | -| streamable | ✅ | ✅ | ✅ | ➖ | ➖ | -| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ | -| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ | -| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ | -| twitter/x | ✅ | ✅ | ✅ | ➖ | ➖ | -| vimeo | ✅ | ✅ | ✅ | ✅ | ✅ | -| vine | ✅ | ✅ | ✅ | ➖ | ➖ | -| vk videos & clips | ✅ | ❌ | ✅ | ✅ | ✅ | -| youtube | ✅ | ✅ | ✅ | ✅ | ✅ | - -| emoji | meaning | -| :-----: | :---------------------- | -| ✅ | supported | -| ➖ | impossible/unreasonable | -| ❌ | not supported | - -### additional notes or features (per service) -| service | notes or features | -| :-------- | :----- | -| instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. | -| facebook | supports public accessible videos content only. | -| pinterest | supports photos, gifs, videos and stories. | -| reddit | supports gifs and videos. | -| snapchat | supports spotlights and stories. lets you pick what to save from stories. | -| rutube | supports yappy & private links. | -| 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. | +it also includes documentation in the [docs tree](/docs/): +- [how to run a cobalt instance](/docs/run-an-instance.md) +- [how to protect a cobalt instance](/docs/protect-an-instance.md) +- [cobalt api documentation](/docs/api.md) ### partners -cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt), all main instances are currently hosted on their network :) +cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt) and the main processing instance is hosted on their network. we really appreciate their kindness! -### ethics and disclaimer -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. +### ethics +cobalt is a tool that makes downloading public content easier. it takes **zero liability**. +the end user is responsible for what they download, how they use and distribute that content. +cobalt never caches any content, it [works like a fancy proxy](/api/src/stream/). -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. +cobalt is in no way a piracy tool and cannot be used as such. +it can only download free & publicly accessible content. +same content can be downloaded via dev tools of any modern web browser. -### cobalt license +### contributing +thank you for considering making a contribution to cobalt! please check the [contributing guidelines here](/CONTRIBUTING.md) before making a pull request. + +### licenses for relevant licensing information, see the [api](api/README.md) and [web](web/README.md) READMEs. unless specified otherwise, the remainder of this repository is licensed under [AGPL-3.0](LICENSE). - -## acknowledgements -### ffmpeg -cobalt heavily relies on ffmpeg for converting and merging media files. it's an absolutely amazing piece of software offered for anyone for free, yet doesn't receive as much credit as it should. - -you can [support ffmpeg here](https://ffmpeg.org/donations.html)! - -#### ffmpeg-static -we use [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static) to get binaries for ffmpeg depending on the platform. - -you can support the developer via various methods listed on their github page! (linked above) - -### youtube.js -cobalt relies on [youtube.js](https://github.com/LuanRT/YouTube.js) for interacting with the innertube api, it wouldn't have been possible without it. - -you can support the developer via various methods listed on their github page! (linked above) - -### many others -cobalt also depends on: - -- [content-disposition-header](https://www.npmjs.com/package/content-disposition-header) to simplify the provision of `content-disposition` headers. -- [cors](https://www.npmjs.com/package/cors) to manage cross-origin resource sharing within expressjs. -- [dotenv](https://www.npmjs.com/package/dotenv) to load environment variables from the `.env` file. -- [esbuild](https://www.npmjs.com/package/esbuild) to minify the frontend files. -- [express](https://www.npmjs.com/package/express) as the backbone of cobalt servers. -- [express-rate-limit](https://www.npmjs.com/package/express-rate-limit) to rate limit api endpoints. -- [hls-parser](https://www.npmjs.com/package/hls-parser) to parse `m3u8` playlists for certain services. -- [ipaddr.js](https://www.npmjs.com/package/ipaddr.js) to parse ip addresses (for rate limiting). -- [nanoid](https://www.npmjs.com/package/nanoid) to generate unique (temporary) identifiers for each requested stream. -- [node-cache](https://www.npmjs.com/package/node-cache) to cache stream info in server ram for a limited amount of time. -- [psl](https://www.npmjs.com/package/psl) as the domain name parser. -- [set-cookie-parser](https://www.npmjs.com/package/set-cookie-parser) to parse cookies that cobalt receives from certain services. -- [undici](https://www.npmjs.com/package/undici) for making http requests. -- [url-pattern](https://www.npmjs.com/package/url-pattern) to match provided links with supported patterns. - -...and many other packages that these packages rely on. diff --git a/docs/images/troubleshooting/clipboard/config.png b/docs/images/troubleshooting/clipboard/config.png deleted file mode 100644 index b0c0a04803397f14957aef152d84e3449757f3d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4154 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 OK>9i+=yKHc$o~VoUOguO diff --git a/docs/images/troubleshooting/clipboard/risk.png b/docs/images/troubleshooting/clipboard/risk.png deleted file mode 100644 index 1948f0eb48baad049c38a83297ecf8709d94c275..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17944 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 cYpKC;r0VcLr9Su7T2?+_Es)~Xx2?;3z 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 diff --git a/docs/images/troubleshooting/clipboard/toggle.png b/docs/images/troubleshooting/clipboard/toggle.png deleted file mode 100644 index 32060dc7f9e01dd447d66db8b6e493e47997e3a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18725 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 diff --git a/docs/images/troubleshooting/clipboard/toggled.png b/docs/images/troubleshooting/clipboard/toggled.png deleted file mode 100644 index 6afa0ace793db75be3e30184f2a2a2d96ea9b4bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17312 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 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 4c97511f..00000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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 older versions of firefox -``` -🎉 firefox finally supports pasting by default starting from version 125. - -👍 you don't need to follow this tutorial if you're on the latest version of 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](images/troubleshooting/clipboard/config.png) - -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"](images/troubleshooting/clipboard/risk.png) - -3. search for `dom.events.asyncClipboard.readText` - - ![screenshot showing "dom.events.asyncclipboard.readtext" entered into search on about:config page](images/troubleshooting/clipboard/search.png) - -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](images/troubleshooting/clipboard/toggle.png) - -5. "false" should change to "true". - - ![screenshot showing "dom.events.asyncclipboard.readtext" preference on about:config page, this one with "true" text highlighted](images/troubleshooting/clipboard/toggled.png) - -6. go back to cobalt, reload the page, press `paste` button again. this time it works! enjoy simpler downloading experience :) From 71c3d64331a148250510dd9837c198b8a4b24fc1 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 17:45:37 +0600 Subject: [PATCH 033/379] repo: update contribution guidelines --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2668242b..073d0fb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ if you're reading this, you are probably interested in contributing to cobalt, w this document serves as a guide to help you make contributions that we can merge into the cobalt codebase. ## translations -currently, we are **not accepting** translations of cobalt. this is because we are making significant changes to the frontend, and the currently used localization structure is being completely reworked. if this changes, this document will be updated. +currently, we are **not accepting** translations of cobalt. we're working on changing this soon! ## adding features or support for services before putting in the effort to implement a feature, it's worth considering whether it would be appropriate to add it to cobalt. the cobalt api is built to assist people **only with downloading freely accessible content**. other functionality, such as: @@ -22,9 +22,9 @@ when contributing code to cobalt, there are a few guidelines in place to ensure ### clean commit messages internally, we use a format similar to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) - the first part signifies which part of the code you are changing (the *scope*), and the second part explains the change. for inspiration on how to write appropriate commit titles, you can take a look at the [commit history](https://github.com/imputnet/cobalt/commits/). -the scope is not strictly defined, you can write whatever you find most fitting for the particular change. suppose you are changing a small part of a more significant part of the codebase. in that case, you can specify both the larger and smaller scopes in the commit message for clarity (e.g., if you were changing something in internal streams, the commit could be something like `stream/internal: fix object not being handled properly`). +the scope is not strictly defined, you can write whatever you find most fitting for the particular change. suppose you are changing a small part of a more significant part of the codebase. in that case, you can specify both the larger and smaller scopes in the commit message for clarity (e.g., if you were changing something in internal streams, the commit could be something like `api/stream: fix object not being handled properly`). -if you think a change deserves further explanation, we encourage you to write a short explanation in the commit message ([example](https://github.com/imputnet/cobalt/commit/d2e5b6542f71f3809ba94d56c26f382b5cb62762)), which will save both you and us time having to enquire about the change, and you explaining the reason behind it. +if you think a change deserves further explanation, we encourage you to write a short explanation in the commit message ([example](https://github.com/imputnet/cobalt/commit/31be60484de8eaf63bba8a4f508e16438aa7ba6e)), which will save both you and us time having to enquire about the change, and you explaining the reason behind it. if your contribution has uninformative commit titles, you may be asked to interactively rebase your branch and amend each commit to include a meaningful title. From 6499d079ef869585d4687b6d9b0eac1a218b8f5f Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 17:49:37 +0600 Subject: [PATCH 034/379] api/readme: add supported services & acknowledgements --- api/README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/api/README.md b/api/README.md index 05664321..38104626 100644 --- a/api/README.md +++ b/api/README.md @@ -1,13 +1,5 @@ # cobalt api - -## license -cobalt api code is licensed under [AGPL-3.0](LICENSE). - -this license allows you to modify, distribute and use the code for any purpose -as long as you: -- give appropriate credit to the original repo when using or modifying any parts of the code, -- provide a link to the license and indicate if changes to the code were made, and -- release the code under the **same license** +this directory includes the source code for cobalt api. it's made with [express.js](https://www.npmjs.com/package/express) and love! ## running your own instance if you want to run your own instance for whatever purpose, [follow this guide](/docs/run-an-instance.md). @@ -22,3 +14,96 @@ you can read [the api documentation here](/docs/api.md). > [!WARNING] > the v7 public api (/api/json) will be shut down on **november 11th, 2024**. > you can access documentation for it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md). + +## supported services +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 👀). + +| service | video + audio | only audio | only video | metadata | rich file names | +| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: | +| bilibili | ✅ | ✅ | ✅ | ➖ | ➖ | +| bluesky | ✅ | ✅ | ✅ | ➖ | ➖ | +| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ | +| instagram | ✅ | ✅ | ✅ | ➖ | ➖ | +| facebook | ✅ | ❌ | ✅ | ➖ | ➖ | +| loom | ✅ | ❌ | ✅ | ✅ | ➖ | +| ok.ru | ✅ | ❌ | ✅ | ✅ | ✅ | +| pinterest | ✅ | ✅ | ✅ | ➖ | ➖ | +| reddit | ✅ | ✅ | ✅ | ❌ | ❌ | +| rutube | ✅ | ✅ | ✅ | ✅ | ✅ | +| snapchat | ✅ | ✅ | ✅ | ➖ | ➖ | +| soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ | +| streamable | ✅ | ✅ | ✅ | ➖ | ➖ | +| tiktok | ✅ | ✅ | ✅ | ❌ | ❌ | +| tumblr | ✅ | ✅ | ✅ | ➖ | ➖ | +| twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ | +| twitter/x | ✅ | ✅ | ✅ | ➖ | ➖ | +| vimeo | ✅ | ✅ | ✅ | ✅ | ✅ | +| vine | ✅ | ✅ | ✅ | ➖ | ➖ | +| vk videos & clips | ✅ | ❌ | ✅ | ✅ | ✅ | +| youtube | ✅ | ✅ | ✅ | ✅ | ✅ | + +| emoji | meaning | +| :-----: | :---------------------- | +| ✅ | supported | +| ➖ | impossible/unreasonable | +| ❌ | not supported | + +### additional notes or features (per service) +| service | notes or features | +| :-------- | :----- | +| instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. | +| facebook | supports public accessible videos content only. | +| pinterest | supports photos, gifs, videos and stories. | +| reddit | supports gifs and videos. | +| snapchat | supports spotlights and stories. lets you pick what to save from stories. | +| rutube | supports yappy & private links. | +| 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. | + +## license +cobalt api code is licensed under [AGPL-3.0](LICENSE). + +this license allows you to modify, distribute and use the code for any purpose +as long as you: +- give appropriate credit to the original repo when using or modifying any parts of the code, +- provide a link to the license and indicate if changes to the code were made, and +- release the code under the **same license** + +## acknowledgements +### ffmpeg +cobalt heavily relies on ffmpeg for converting and merging media files. it's an absolutely amazing piece of software offered for anyone for free, yet doesn't receive as much credit as it should. + +you can [support ffmpeg here](https://ffmpeg.org/donations.html)! + +#### ffmpeg-static +we use [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static) to get binaries for ffmpeg depending on the platform. + +you can support the developer via various methods listed on their github page! (linked above) + +### youtube.js +cobalt relies on [youtube.js](https://github.com/LuanRT/YouTube.js) for interacting with the innertube api, it wouldn't have been possible without it. + +you can support the developer via various methods listed on their github page! (linked above) + +### many others +cobalt also depends on: + +- [content-disposition-header](https://www.npmjs.com/package/content-disposition-header) to simplify the provision of `content-disposition` headers. +- [cors](https://www.npmjs.com/package/cors) to manage cross-origin resource sharing within expressjs. +- [dotenv](https://www.npmjs.com/package/dotenv) to load environment variables from the `.env` file. +- [esbuild](https://www.npmjs.com/package/esbuild) to minify the frontend files. +- [express](https://www.npmjs.com/package/express) as the backbone of cobalt servers. +- [express-rate-limit](https://www.npmjs.com/package/express-rate-limit) to rate limit api endpoints. +- [hls-parser](https://www.npmjs.com/package/hls-parser) to parse `m3u8` playlists for certain services. +- [ipaddr.js](https://www.npmjs.com/package/ipaddr.js) to parse ip addresses (for rate limiting). +- [nanoid](https://www.npmjs.com/package/nanoid) to generate unique (temporary) identifiers for each requested stream. +- [node-cache](https://www.npmjs.com/package/node-cache) to cache stream info in server ram for a limited amount of time. +- [psl](https://www.npmjs.com/package/psl) as the domain name parser. +- [set-cookie-parser](https://www.npmjs.com/package/set-cookie-parser) to parse cookies that cobalt receives from certain services. +- [undici](https://www.npmjs.com/package/undici) for making http requests. +- [url-pattern](https://www.npmjs.com/package/url-pattern) to match provided links with supported patterns. + +...and many other packages that these packages rely on. From b837f291b5bb8ce21a53334deb4d5afa0329b171 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 17:59:38 +0600 Subject: [PATCH 035/379] docs/protect-an-instance: fix image sizes, add a secret warning --- docs/protect-an-instance.md | 57 ++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index c95bf135..5b9aea1f 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -13,32 +13,69 @@ all you need is a free cloudflare account to get started. cloudflare dashboard interface might change over time, but basics should stay the same. +> [!CAUTION] +> never share the turnstile secret key, always keep it private. if accidentally exposed, rotate it in widget settings. + 1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account. + 2. once logged in, select `turnstile` in the sidebar. -![](images/protect-an-instance/sidebar.png) +
+

+ +

+
+ 3. press `add widget`. -![](images/protect-an-instance/add.png) +
+

+ +

+
+ 4. enter the widget name (can be anything, such as "cobalt"). -![](images/protect-an-instance/name.png) +
+

+ +

+
+ 5. add cobalt frontend domains you want the widget to work with. you can change this list later at any time. - if you want to use your processing instance with [cobalt.tools](https://cobalt.tools/) frontend, then add `cobalt.tools` to the list. -![](images/protect-an-instance/domain.png) +
+

+ +

+
+ 6. select `invisible` widget mode. -![](images/protect-an-instance/mode.png) +
+

+ +

+
+ 7. press `create`. + 8. keep the page with sitekey and secret key open, you'll need them later. if you closed it, no worries! just open the same turnstile page and press "settings" on your freshly made turnstile widget. -**never share your secret turnstile key with anyone.** -![](images/protect-an-instance/created.png) - -you've successfully created a turnstile widget! time to add it to your processing instance. +
+

+ +

+
+
+

+ you've successfully created a turnstile widget! + time to add it to your processing instance. +

+
### enable turnstile on your processing instance this tutorial assumes that you only have `API_URL` in your `environment` variables list. if you have other variables there, just add new ones after existing ones. -> [!IMPORTANT] +> [!CAUTION] > never use any of the values from the tutorial, especially `JWT_SECRET`! 1. open your `docker-compose.yml` config file in any text editor of choice. From 722223f6d337304d3a464b8f9bce9f4243a69239 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 18:02:24 +0600 Subject: [PATCH 036/379] docs/protect-an-instance: fix image alignment --- docs/protect-an-instance.md | 38 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 5b9aea1f..96f3a309 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -16,60 +16,58 @@ cloudflare dashboard interface might change over time, but basics should stay th > [!CAUTION] > never share the turnstile secret key, always keep it private. if accidentally exposed, rotate it in widget settings. -1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account. +1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account -2. once logged in, select `turnstile` in the sidebar. -
+2. once logged in, select `Turnstile` in the sidebar +

-3. press `add widget`. -
+3. press `Add widget` +

-4. enter the widget name (can be anything, such as "cobalt"). -
+4. enter the widget name (can be anything, such as "cobalt") +

-5. add cobalt frontend domains you want the widget to work with. you can change this list later at any time. - - if you want to use your processing instance with [cobalt.tools](https://cobalt.tools/) frontend, then add `cobalt.tools` to the list. -
+5. add cobalt frontend domains you want the widget to work with, you can change this list later at any time + - if you want to use your processing instance with [cobalt.tools](https://cobalt.tools/) frontend, then add `cobalt.tools` to the list +

-6. select `invisible` widget mode. -
+6. select `invisible` widget mode +

-7. press `create`. +7. press `create` 8. keep the page with sitekey and secret key open, you'll need them later. if you closed it, no worries! just open the same turnstile page and press "settings" on your freshly made turnstile widget. -
+ +

-
-

- you've successfully created a turnstile widget! - time to add it to your processing instance. -

-
+ +you've successfully created a turnstile widget! +time to add it to your processing instance. ### enable turnstile on your processing instance this tutorial assumes that you only have `API_URL` in your `environment` variables list. From a58684f314b6a539e948d6423b5c6d00de41d4ba Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 18:05:50 +0600 Subject: [PATCH 037/379] docs/protect-an-instance: update the tuto value warning --- docs/protect-an-instance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 96f3a309..59c7fa27 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -74,7 +74,7 @@ this tutorial assumes that you only have `API_URL` in your `environment` variabl if you have other variables there, just add new ones after existing ones. > [!CAUTION] -> never use any of the values from the tutorial, especially `JWT_SECRET`! +> never use any values from the tutorial, especially `JWT_SECRET`! 1. open your `docker-compose.yml` config file in any text editor of choice. 2. copy the turnstile sitekey & secret key and paste them to their respective variables. `TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key: From 4c43a00e884b3eba06d9d2931dfdad3a6bae5a9d Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 11:46:09 +0000 Subject: [PATCH 038/379] web/api/session: replace writable with normal variable --- web/src/lib/api/session.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/src/lib/api/session.ts b/web/src/lib/api/session.ts index f7974393..f627b6f2 100644 --- a/web/src/lib/api/session.ts +++ b/web/src/lib/api/session.ts @@ -1,12 +1,11 @@ import turnstile from "$lib/api/turnstile"; -import { writable, get } from "svelte/store"; import { currentApiURL } from "$lib/api/api-url"; import type { CobaltSession, CobaltErrorResponse, CobaltSessionResponse } from "$lib/types/api"; -const cachedSession = writable(); +let cache: CobaltSession | undefined; -export const requestSession = async() => { +export const requestSession = async () => { const apiEndpoint = `${currentApiURL()}/session`; let requestHeaders = {}; @@ -43,7 +42,6 @@ export const requestSession = async() => { export const getSession = async () => { const currentTime = () => Math.floor(new Date().getTime() / 1000); - const cache = get(cachedSession); if (cache?.token && cache?.exp - 2 > currentTime()) { return cache; @@ -60,7 +58,7 @@ export const getSession = async () => { if (!("status" in newSession)) { newSession.exp = currentTime() + newSession.exp; - cachedSession.set(newSession); + cache = newSession; } return newSession; } From be7c09bd07e2f8e2c9b3767de0803e03d44889b3 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 11:50:39 +0000 Subject: [PATCH 039/379] web/lib: move `dialogs` to `state` folder --- web/src/components/dialog/DialogContainer.svelte | 2 +- web/src/components/dialog/DialogHolder.svelte | 2 +- web/src/components/save/Omnibox.svelte | 2 +- web/src/components/save/buttons/DownloadButton.svelte | 2 +- web/src/components/settings/ManageSettings.svelte | 2 +- web/src/components/settings/ResetSettingsButton.svelte | 2 +- web/src/lib/api/safety-warning.ts | 2 +- web/src/lib/download.ts | 2 +- web/src/lib/{ => state}/dialogs.ts | 0 web/src/routes/remux/+page.svelte | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename web/src/lib/{ => state}/dialogs.ts (100%) diff --git a/web/src/components/dialog/DialogContainer.svelte b/web/src/components/dialog/DialogContainer.svelte index 993efd7c..363395ba 100644 --- a/web/src/components/dialog/DialogContainer.svelte +++ b/web/src/components/dialog/DialogContainer.svelte @@ -1,6 +1,6 @@ {#if $settings.advanced.debug} From 6933daf0463e44b406b304c1ff78a0e832ed1e85 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 18:56:23 +0600 Subject: [PATCH 041/379] docs: add configure-for-youtube document --- README.md | 3 ++- docs/configure-for-youtube.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 docs/configure-for-youtube.md diff --git a/README.md b/README.md index ca108a86..434b5943 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,10 @@ this monorepo includes source code for api, frontend, and related packages: - [packages tree](/packages/) it also includes documentation in the [docs tree](/docs/): +- [cobalt api documentation](/docs/api.md) - [how to run a cobalt instance](/docs/run-an-instance.md) - [how to protect a cobalt instance](/docs/protect-an-instance.md) -- [cobalt api documentation](/docs/api.md) +- [how to configure a cobalt instance for youtube](/docs/configure-for-youtube.md) ### partners cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt) and the main processing instance is hosted on their network. we really appreciate their kindness! diff --git a/docs/configure-for-youtube.md b/docs/configure-for-youtube.md new file mode 100644 index 00000000..345776a4 --- /dev/null +++ b/docs/configure-for-youtube.md @@ -0,0 +1,33 @@ +# how to configure a cobalt instance for youtube +if you get various errors when attempting to download videos that are: +publicly available, not region locked, and not age-restricted; +then your instance's ip address may have bad reputation. + +in this case you have to use disposable google accounts. +there's no other known workaround as of time of writing this document. + +> [!CAUTION] +> **NEVER** use your personal google account for downloading videos via any means. +> you can use any google accounts that you're willing to sacrifice, +> but be prepared to have them **permanently suspended**. +> +> we recommend that you use accounts that don't link back to your personal google account or identity, just in case. +> +> use incognito mode when signing in. +> we also recommend using vpn/proxy services (such as [mullvad](https://mullvad.net/)). + +1. if you haven't done it already, clone the cobalt repo, go to the cloned directory, and run `pnpm install` + +2. run `pnpm -r token:youtube` + +3. follow instructions, use incognito mode in your browser when signing in. +i cannot stress this enough, but again, **DO NOT USE YOUR PERSONAL GOOGLE ACCOUNT**. + +4. once you have the oauth token, add it to your cookies file. +you can see an [example here](/docs/examples/cookies.example.json). +you can have several account tokens in this file, if you like. + +5. all done! enjoy freedom. + +### liability +you're responsible for any damage done to any of your google accounts or any other damages. you do this by yourself and at your own risk. From f33cf12fd3f13a4ac5d62e17782206950fc69d50 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 18:56:37 +0600 Subject: [PATCH 042/379] docs/run-an-instance: update headings --- docs/run-an-instance.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 1d4dcdc0..63841625 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -54,8 +54,7 @@ sudo apt install nscd sudo service nscd start ``` -## list of all environment variables -### variables for api +## list of environment variables for api | variable name | default | example | description | |:----------------------|:----------|:------------------------|:------------| | `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. | @@ -89,7 +88,7 @@ requests it makes for that particular download. to use freebind in cobalt, you n in a docker container, you also need to set the `API_LISTEN_ADDRESS` env to `127.0.0.1`, and set `network_mode` for the container to `host`. -#### api key file format +## api key file format the file is a JSON-serialized object with the following structure: ```typescript From 155322a47bc44f5b77c7fc0796928f887e11ce49 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 18:59:07 +0600 Subject: [PATCH 043/379] docs/configure-for-youtube: clarify where to put the token --- docs/configure-for-youtube.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configure-for-youtube.md b/docs/configure-for-youtube.md index 345776a4..5a397f92 100644 --- a/docs/configure-for-youtube.md +++ b/docs/configure-for-youtube.md @@ -23,7 +23,7 @@ there's no other known workaround as of time of writing this document. 3. follow instructions, use incognito mode in your browser when signing in. i cannot stress this enough, but again, **DO NOT USE YOUR PERSONAL GOOGLE ACCOUNT**. -4. once you have the oauth token, add it to your cookies file. +4. once you have the oauth token, add it to `youtube_oauth` in your cookies file. you can see an [example here](/docs/examples/cookies.example.json). you can have several account tokens in this file, if you like. From 9d68247523890c83dc8ce13f150f11fddd12f051 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 19:06:48 +0600 Subject: [PATCH 044/379] api: remove the outdated setup script --- api/package.json | 1 - api/src/util/setup.js | 105 ------------------------------------------ 2 files changed, 106 deletions(-) delete mode 100644 api/src/util/setup.js diff --git a/api/package.json b/api/package.json index ba346f39..948f8180 100644 --- a/api/package.json +++ b/api/package.json @@ -10,7 +10,6 @@ }, "scripts": { "start": "node src/cobalt", - "setup": "node src/util/setup", "test": "node src/util/test", "token:youtube": "node src/util/generate-youtube-tokens", "token:jwt": "node src/util/generate-jwt-secret" diff --git a/api/src/util/setup.js b/api/src/util/setup.js deleted file mode 100644 index 34b870cb..00000000 --- a/api/src/util/setup.js +++ /dev/null @@ -1,105 +0,0 @@ -import { existsSync, unlinkSync, appendFileSync } from "fs"; -import { createInterface } from "readline"; -import { Cyan, Bright } from "../misc/console-text.js"; -import { loadJSON } from "../misc/load-from-fs.js"; -import { execSync } from "child_process"; - -const { version } = loadJSON("./package.json"); - -let envPath = './.env'; -let q = `${Cyan('?')} \x1b[1m`; -let ob = {}; -let rl = createInterface({ input: process.stdin, output: process.stdout }); - -let final = () => { - if (existsSync(envPath)) unlinkSync(envPath); - - for (let i in ob) { - appendFileSync(envPath, `${i}=${ob[i]}\n`) - } - console.log(Bright("\nAwesome! I've created a fresh .env file for you.")); - console.log(`${Bright("Now I'll run")} ${Cyan("npm install")} ${Bright("to install all dependencies. It shouldn't take long.\n\n")}`); - execSync('pnpm install', { stdio: [0, 1, 2] }); - console.log(`\n\n${Cyan("All done!\n")}`); - console.log(Bright("You can re-run this script at any time to update the configuration.")); - console.log(Bright("\nYou're now ready to start cobalt. Simply run ") + Cyan("npm start") + Bright('!\nHave fun :)')); - rl.close() -} - -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.")}` -) - -function setup() { - console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web.")); - - rl.question(q, r1 => { - switch (r1.toLowerCase()) { - case 'api': - console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: api.cobalt.tools")); - - rl.question(q, apiURL => { - ob.API_URL = `http://localhost:9000/`; - ob.API_PORT = 9000; - if (apiURL && apiURL !== "localhost") ob.API_URL = `https://${apiURL.toLowerCase()}/`; - - console.log(Bright("\nGreat! Now, what port will it be running on? (9000)")); - - rl.question(q, apiPort => { - if (apiPort) ob.API_PORT = apiPort; - if (apiPort && (apiURL === "localhost" || !apiURL)) ob.API_URL = `http://localhost:${apiPort}/`; - - console.log(Bright("\nWhat will your instance's name be? Usually it's something like eu-nl aka region-country. (local)")); - - rl.question(q, apiName => { - ob.API_NAME = apiName.toLowerCase(); - if (!apiName || apiName === "local") ob.API_NAME = "local"; - - console.log(Bright("\nOne last thing: would you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)")); - - rl.question(q, apiCors => { - let answCors = apiCors.toLowerCase().trim(); - if (answCors !== "y" && answCors !== "yes") ob.CORS_WILDCARD = '0' - final() - }) - }) - }); - - }) - break; - case 'web': - console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: cobalt.tools")); - - rl.question(q, webURL => { - ob.WEB_URL = `http://localhost:9001/`; - ob.WEB_PORT = 9001; - if (webURL && webURL !== "localhost") ob.WEB_URL = `https://${webURL.toLowerCase()}/`; - - console.log( - Bright("\nGreat! Now, what port will it be running on? (9001)") - ) - rl.question(q, webPort => { - if (webPort) ob.WEB_PORT = webPort; - if (webPort && (webURL === "localhost" || !webURL)) ob.WEB_URL = `http://localhost:${webPort}/`; - - console.log( - Bright("\nOne last thing: what default API domain should be used? (api.cobalt.tools)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000") - ); - - rl.question(q, apiURL => { - ob.API_URL = `https://${apiURL.toLowerCase()}/`; - if (apiURL.includes(':')) ob.API_URL = `http://${apiURL.toLowerCase()}/`; - if (!apiURL) ob.API_URL = "https://api.cobalt.tools/"; - final() - }) - }); - - }); - break; - default: - console.log(Bright("\nThis is not an option. Try again.")); - setup() - } - }) -} -setup() From 16c5450d408b73e8ca683805aa02f9aef9b9c83e Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 19:07:42 +0600 Subject: [PATCH 045/379] api/cobalt: update api url error message --- api/src/cobalt.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/api/src/cobalt.js b/api/src/cobalt.js index c548e792..363930ba 100644 --- a/api/src/cobalt.js +++ b/api/src/cobalt.js @@ -2,26 +2,24 @@ import "dotenv/config"; import express from "express"; -import path from 'path'; -import { fileURLToPath } from 'url'; +import path from "path"; +import { fileURLToPath } from "url"; import { env } from "./config.js" -import { Bright, Green, Red } from "./misc/console-text.js"; +import { Red } from "./misc/console-text.js"; const app = express(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename).slice(0, -4); -app.disable('x-powered-by'); +app.disable("x-powered-by"); if (env.apiURL) { - const { runAPI } = await import('./core/api.js'); + const { runAPI } = await import("./core/api.js"); runAPI(express, app, __dirname) } else { console.log( - Red(`cobalt wasn't configured yet or configuration is invalid.\n`) - + Bright(`please run the setup script to fix this: `) - + Green(`npm run setup`) + Red("API_URL env variable is missing, cobalt api can't start.") ) } From a81a19de6825d6c4199a1f4989322328332c50d4 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 19:26:19 +0600 Subject: [PATCH 046/379] docs/protect-an-instance: add a command for generating a secret --- docs/protect-an-instance.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 59c7fa27..5604ddec 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -77,14 +77,18 @@ if you have other variables there, just add new ones after existing ones. > never use any values from the tutorial, especially `JWT_SECRET`! 1. open your `docker-compose.yml` config file in any text editor of choice. -2. copy the turnstile sitekey & secret key and paste them to their respective variables. `TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key: +2. copy the turnstile sitekey & secret key and paste them to their respective variables. +`TURNSTILE_SITEKEY` for the sitekey and `TURNSTILE_SECRET` for the secret key: ```yml environment: API_URL: "https://your.instance.url.here.local/" TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key ``` -3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters. this string will be used as salt for all JWT keys. +3. generate a `JWT_SECRET`. we recommend using an alphanumeric collection with a length of at least 64 characters. +this string will be used as salt for all JWT keys. + + you can generate a random secret with `pnpm -r token:jwt` or use any other that you like. ```yml environment: From 9790179e29e498494e01d7d300ff2203aa681df2 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 19:51:35 +0600 Subject: [PATCH 047/379] docs/protect-an-instance: add api keys configuration --- docs/protect-an-instance.md | 52 ++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 5604ddec..e0ba7f3b 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -13,7 +13,7 @@ all you need is a free cloudflare account to get started. cloudflare dashboard interface might change over time, but basics should stay the same. -> [!CAUTION] +> [!WARNING] > never share the turnstile secret key, always keep it private. if accidentally exposed, rotate it in widget settings. 1. open [the cloudflare dashboard](https://dash.cloudflare.com/) and log into your account @@ -97,3 +97,53 @@ environment: TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key JWT_SECRET: "bgBmF4efNCKPirDqTc4FMmbX8P22I31oCj5R1zDiDi5sy8CWPnfLUct7rk5RlZUS" # create a new secret, NEVER use this one ``` +4. restart the docker container. + +## configure api keys +if you want to use your instance outside of web interface, you'll need an api key! + +> [!NOTE] +> this tutorial assumes that you'll keep your keys file locally, on the instance server. +> if you wish to upload your file to a remote location, +> replace the value for `API_KEYS_URL` with a direct url to the file. + +> [!WARNING] +> when storing keys file remotely, make sure that it's not publicly accessible +> and that link to it is either authenticated (via query) or impossible to guess. +> +> if api keys leak, you'll have to update/remove all UUIDs to revoke them. + +1. create a `keys.json` file following [the schema and example here](/docs//run-an-instance.md#api-key-file-format). + +2. expose the `keys.json` to the docker container: +```yml +volumes: + - ./keys.json:/keys.json:ro # ro - read-only +``` + +3. add a path to the keys file to container environment: +```yml +environment: + # ... other variables here ... + API_KEY_URL: "file:///keys.json" +``` + +4. restart the docker container. + +## limit access to an instance with api keys but no turnstile +by default, api keys are additional, meaning that they're not *required*, +but work alongside with turnstile or no auth (regular ip hash rate limiting). + +to always require auth (via keys or turnstile, if configured), set `API_AUTH_REQUIRED` to 1: +```yml +environment: + # ... other variables here ... + API_AUTH_REQUIRED: 1 +``` + +- if both keys and turnstile are enabled, then nothing will change. +- if only keys are configured, then all requests without a valid api key will be refused. + +### why not make keys exclusive by default? +keys may be useful for going around rate limiting, +while keeping the rest of api rate limited, with no turnstile in place. From 43b3139b4a318ecc27a33fead946fe990f19a89e Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 19:53:17 +0600 Subject: [PATCH 048/379] docs/protect-an-instance: skip second step of api keys config if remote --- docs/protect-an-instance.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index e0ba7f3b..2cfcd808 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -105,7 +105,8 @@ if you want to use your instance outside of web interface, you'll need an api ke > [!NOTE] > this tutorial assumes that you'll keep your keys file locally, on the instance server. > if you wish to upload your file to a remote location, -> replace the value for `API_KEYS_URL` with a direct url to the file. +> replace the value for `API_KEYS_URL` with a direct url to the file +> and skip the second step. > [!WARNING] > when storing keys file remotely, make sure that it's not publicly accessible From 4efe6d9350bb6770757632d2ddf81b24fcc90e8a Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 14:14:47 +0000 Subject: [PATCH 049/379] api/config: disallow `JWT_SECRET`s shorter than 16 chars --- api/src/config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/config.js b/api/src/config.js index 3a28d7ce..02584212 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -54,6 +54,10 @@ const env = { const genericUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"; const cobaltUserAgent = `cobalt/${version} (+https://github.com/imputnet/cobalt)`; +if (env.sessionEnabled && env.jwtSecret.length < 16) { + throw new Error("JWT_SECRET env is too short (must be at least 16 characters long)"); +} + export { env, genericUserAgent, From 4b1ea6ed8034818b79af9cf01425454184b409b0 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 20 Oct 2024 20:18:50 +0600 Subject: [PATCH 050/379] docs/protect-an-instance: update the template secret to fail --- docs/protect-an-instance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/protect-an-instance.md b/docs/protect-an-instance.md index 2cfcd808..9b4131c1 100644 --- a/docs/protect-an-instance.md +++ b/docs/protect-an-instance.md @@ -95,7 +95,7 @@ environment: API_URL: "https://your.instance.url.here.local/" TURNSTILE_SITEKEY: "2x00000000000000000000BB" # use your key TURNSTILE_SECRET: "2x0000000000000000000000000000000AA" # use your key - JWT_SECRET: "bgBmF4efNCKPirDqTc4FMmbX8P22I31oCj5R1zDiDi5sy8CWPnfLUct7rk5RlZUS" # create a new secret, NEVER use this one + JWT_SECRET: "bgBmF4efNCKPirD" # create a new secret, NEVER use this one ``` 4. restart the docker container. From 429b7c85aa91b284c01435dd49ea8d2e417a8d9c Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 23:12:07 +0200 Subject: [PATCH 051/379] docs/configure-for-youtube: change pnpm command --- docs/configure-for-youtube.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configure-for-youtube.md b/docs/configure-for-youtube.md index 5a397f92..65795912 100644 --- a/docs/configure-for-youtube.md +++ b/docs/configure-for-youtube.md @@ -18,7 +18,7 @@ there's no other known workaround as of time of writing this document. 1. if you haven't done it already, clone the cobalt repo, go to the cloned directory, and run `pnpm install` -2. run `pnpm -r token:youtube` +2. run `pnpm run -C api token:youtube` 3. follow instructions, use incognito mode in your browser when signing in. i cannot stress this enough, but again, **DO NOT USE YOUR PERSONAL GOOGLE ACCOUNT**. From 1b9855206ee5861f48262a390f5dcd7e8949482f Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 20 Oct 2024 23:12:35 +0200 Subject: [PATCH 052/379] docs/configure-for-youtube: omit `run` from pnpm command --- docs/configure-for-youtube.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configure-for-youtube.md b/docs/configure-for-youtube.md index 65795912..fe286d86 100644 --- a/docs/configure-for-youtube.md +++ b/docs/configure-for-youtube.md @@ -18,7 +18,7 @@ there's no other known workaround as of time of writing this document. 1. if you haven't done it already, clone the cobalt repo, go to the cloned directory, and run `pnpm install` -2. run `pnpm run -C api token:youtube` +2. run `pnpm -C api token:youtube` 3. follow instructions, use incognito mode in your browser when signing in. i cannot stress this enough, but again, **DO NOT USE YOUR PERSONAL GOOGLE ACCOUNT**. From 9d59a2f5d20e59e1918a2df6b01773d5c396c668 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 22 Oct 2024 14:16:10 +0600 Subject: [PATCH 053/379] web/about/terms: point out even more that safety email is not support --- web/i18n/en/about/terms.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/i18n/en/about/terms.md b/web/i18n/en/about/terms.md index a134ab84..aa32fd65 100644 --- a/web/i18n/en/about/terms.md +++ b/web/i18n/en/about/terms.md @@ -48,9 +48,10 @@ fair use and credits benefit everyone. sectionId="abuse" /> -we have no way of detecting abusive behavior automatically, as cobalt is 100% anonymous. -however, you can report such activities to us and we will do our best to comply manually: [safety@imput.net](mailto:safety@imput.net) +we have no way of detecting abusive behavior automatically because cobalt is 100% anonymous. +however, you can report such activities to us and we will do our best to comply manually: **safety@imput.net** + +**this email is not intended for user support, you will not get a response if your concern is not related to abuse.** -please note that this email is not intended for user support. if you're experiencing issues, contact us via any preferred method on [the support page](/about/community). From a3ee3d9c16bf8b636c3cab773d48e26543055cc4 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 23 Oct 2024 14:01:10 +0600 Subject: [PATCH 054/379] api/youtube: catch one more age limit error --- api/src/processing/services/youtube.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 46f72a5b..a6cdc9a2 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -170,6 +170,10 @@ export default async function(o) { } } + if (playability.status === "AGE_VERIFICATION_REQUIRED") { + return { error: "content.video.age" } + } + if (playability.status !== "OK") { return { error: "content.video.unavailable" }; } From ae271fd3c631c1b0a90cd0bb7dca5c8660802e83 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 23 Oct 2024 18:08:50 +0600 Subject: [PATCH 055/379] api/youtube: refactor playability status handling --- api/src/processing/services/youtube.js | 79 +++++++++++++------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index a6cdc9a2..95581863 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -1,5 +1,4 @@ import { fetch } from "undici"; - import { Innertube, Session } from "youtubei.js"; import { env } from "../../config.js"; @@ -146,41 +145,49 @@ export default async function(o) { const playability = info.playability_status; const basicInfo = info.basic_info; - if (playability.status === "LOGIN_REQUIRED") { - if (playability.reason.endsWith("bot")) { - return { error: "youtube.login" } - } - if (playability.reason.endsWith("age")) { - return { error: "content.video.age" } - } - if (playability?.error_screen?.reason?.text === "Private video") { - return { error: "content.video.private" } - } + switch(playability.status) { + case "OK": + break; + + case "LOGIN_REQUIRED": + if (playability.reason.endsWith("bot")) { + return { error: "youtube.login" } + } + if (playability.reason.endsWith("age")) { + return { error: "content.video.age" } + } + if (playability?.error_screen?.reason?.text === "Private video") { + return { error: "content.video.private" } + } + break; + + case "UNPLAYABLE": + if (playability?.reason?.endsWith("request limit.")) { + return { error: "fetch.rate" } + } + if (playability?.error_screen?.subreason?.text?.endsWith("in your country")) { + return { error: "content.video.region" } + } + if (playability?.error_screen?.reason?.text === "Private video") { + return { error: "content.video.private" } + } + break; + + case "AGE_VERIFICATION_REQUIRED": + return { error: "content.video.age" }; + + default: + return { error: "content.video.unavailable" }; } - if (playability.status === "UNPLAYABLE") { - if (playability?.reason?.endsWith("request limit.")) { - return { error: "fetch.rate" } - } - if (playability?.error_screen?.subreason?.text?.endsWith("in your country")) { - return { error: "content.video.region" } - } - if (playability?.error_screen?.reason?.text === "Private video") { - return { error: "content.video.private" } - } - } - - if (playability.status === "AGE_VERIFICATION_REQUIRED") { - return { error: "content.video.age" } - } - - if (playability.status !== "OK") { - return { error: "content.video.unavailable" }; - } if (basicInfo.is_live) { return { error: "content.video.live" }; } + if (basicInfo.duration > env.durationLimit) { + return { error: "content.too_long" }; + } + // return a critical error if returned video is "Video Not Available" // or a similar stub by youtube if (basicInfo.id !== o.id) { @@ -212,11 +219,9 @@ export default async function(o) { if (bestVideo) bestQuality = qual(bestVideo); - if ((!bestQuality && !o.isAudioOnly) || !hasAudio) + if ((!bestQuality && !o.isAudioOnly) || !hasAudio) { return { error: "youtube.codec" }; - - if (basicInfo.duration > env.durationLimit) - return { error: "content.too_long" }; + } const checkBestAudio = (i) => (i.has_audio && !i.has_video); @@ -226,9 +231,7 @@ export default async function(o) { if (o.dubLang) { let dubbedAudio = adaptive_formats.find(i => - checkBestAudio(i) - && i.language === o.dubLang - && i.audio_track + checkBestAudio(i) && i.language === o.dubLang && i.audio_track ) if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) { @@ -313,5 +316,5 @@ export default async function(o) { } } - return { error: "fetch.fail" } + return { error: "fetch.fail" }; } From cfb05282c351185b44f7768e75f2354f4ff72cb7 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 23 Oct 2024 19:56:59 +0600 Subject: [PATCH 056/379] api/youtube: refactor, fallback codecs, don't return premuxed videos --- api/src/processing/services/youtube.js | 99 ++++++++++---------------- 1 file changed, 39 insertions(+), 60 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 95581863..e2f83c5d 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -9,7 +9,7 @@ const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms let innertube, lastRefreshedAt; -const codecMatch = { +const codecList = { h264: { videoCodec: "avc1", audioCodec: "mp4a", @@ -120,12 +120,12 @@ export default async function(o) { let info, isDubbed, format = o.format || "h264"; - function qual(i) { + const qual = (i) => { if (!i.quality_label) { return; } - return i.quality_label.split('p')[0].split('s')[0] + return i.quality_label.split('p', 2)[0].split('s', 2)[0] } try { @@ -198,36 +198,31 @@ export default async function(o) { } const filterByCodec = (formats) => - formats - .filter(e => - e.mime_type.includes(codecMatch[format].videoCodec) - || e.mime_type.includes(codecMatch[format].audioCodec) - ) - .sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); + formats.filter(e => + e.mime_type.includes(codecList[format].videoCodec) + || e.mime_type.includes(codecList[format].audioCodec) + ).sort((a, b) => + Number(b.bitrate) - Number(a.bitrate) + ); let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); - if (adaptive_formats.length === 0 && format === "vp9") { - format = "h264" - adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats) + if (adaptive_formats.length === 0 && ["vp9", "av1"].includes(format)) { + format = "h264"; + adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); } - let bestQuality; - const bestVideo = adaptive_formats.find(i => i.has_video && i.content_length); const hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); - if (bestVideo) bestQuality = qual(bestVideo); - - if ((!bestQuality && !o.isAudioOnly) || !hasAudio) { - return { error: "youtube.codec" }; + if (!bestVideo || (!hasAudio && o.isAudioOnly)) { + return { error: "fetch.empty" }; } + const bestQuality = qual(bestVideo); const checkBestAudio = (i) => (i.has_audio && !i.has_video); - let audio = adaptive_formats.find(i => - checkBestAudio(i) && i.is_original - ); + let audio = adaptive_formats.find(i => checkBestAudio(i) && i.is_original); if (o.dubLang) { let dubbedAudio = adaptive_formats.find(i => @@ -244,13 +239,14 @@ export default async function(o) { audio = adaptive_formats.find(i => checkBestAudio(i)); } - let fileMetadata = { + const fileMetadata = { title: cleanString(basicInfo.title.trim()), - artist: cleanString(basicInfo.author.replace("- Topic", "").trim()), + artist: cleanString(basicInfo.author.replace("- Topic", "").trim()) } if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) { - let descItems = basicInfo.short_description.split("\n\n", 5); + const descItems = basicInfo.short_description.split("\n\n", 5); + if (descItems.length === 5) { fileMetadata.album = descItems[2]; fileMetadata.copyright = descItems[3]; @@ -260,7 +256,7 @@ export default async function(o) { } } - let filenameAttributes = { + const filenameAttributes = { service: "youtube", id: o.id, title: fileMetadata.title, @@ -271,46 +267,29 @@ export default async function(o) { if (audio && o.isAudioOnly) return { type: "audio", isAudioOnly: true, - urls: audio.decipher(yt.session.player), - filenameAttributes: filenameAttributes, - fileMetadata: fileMetadata, - bestAudio: format === "h264" ? "m4a" : "opus" + urls: audio.url, + filenameAttributes, + fileMetadata, + bestAudio: format === "h264" ? "m4a" : "opus", } - const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality, - checkSingle = i => - qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].videoCodec), - checkRender = i => - qual(i) === matchingQuality && i.has_video && !i.has_audio; + const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality; + const video = adaptive_formats.find(i => + qual(i) === matchingQuality && i.has_video && !i.has_audio + ); - let match, type, urls; - - // prefer good premuxed videos if available - if (!o.isAudioOnly && !o.isAudioMuted && format === "h264" && bestVideo.fps <= 30) { - match = info.streaming_data.formats.find(checkSingle); - type = "proxy"; - urls = match?.decipher(yt.session.player); - } - - const video = adaptive_formats.find(checkRender); - - if (!match && video && audio) { - match = video; - type = "merge"; - urls = [ - video.decipher(yt.session.player), - audio.decipher(yt.session.player) - ] - } - - if (match) { - filenameAttributes.qualityLabel = match.quality_label; - filenameAttributes.resolution = `${match.width}x${match.height}`; - filenameAttributes.extension = codecMatch[format].container; + if (video && audio) { + filenameAttributes.qualityLabel = video.quality_label; + filenameAttributes.resolution = `${video.width}x${video.height}`; + filenameAttributes.extension = codecList[format].container; filenameAttributes.youtubeFormat = format; + return { - type, - urls, + type: "merge", + urls: [ + video.url, + audio.url + ], filenameAttributes, fileMetadata } From 52c171460833215ca2af0bb33a049c40ac267363 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 26 Oct 2024 22:38:42 +0600 Subject: [PATCH 057/379] web/i18n/settings: fix typo in youtube codec description --- web/i18n/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 8d003439..2b34eb62 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -30,7 +30,7 @@ "video.quality.description": "if preferred video quality isn't available, next best is picked instead.", "video.youtube.codec": "youtube video codec and container", - "video.youtube.codec.description": "h264: best compatibility, average bitrate. max quality is 1080p. \nav1: best quality, efficiency, and bitrate. supports 8k & HDR. \nvp9: same quality & bitrate as av1, but file is approximately two times bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264.", + "video.youtube.codec.description": "h264: best compatibility, average bitrate. max quality is 1080p. \nav1: best quality, efficiency, and bitrate. supports 8k & HDR. \nvp9: same quality as av1, but file is approximately two times bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264.", "video.twitter.gif": "twitter/x", "video.twitter.gif.title": "convert looping videos to GIF", From 3907697fa708e5c1d19b4d23c9c1809e86801f9f Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 26 Oct 2024 22:45:16 +0600 Subject: [PATCH 058/379] web/i18n/settings: rephrase the youtube codec desc also added info about fallback --- web/i18n/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 2b34eb62..8e5a402c 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -30,7 +30,7 @@ "video.quality.description": "if preferred video quality isn't available, next best is picked instead.", "video.youtube.codec": "youtube video codec and container", - "video.youtube.codec.description": "h264: best compatibility, average bitrate. max quality is 1080p. \nav1: best quality, efficiency, and bitrate. supports 8k & HDR. \nvp9: same quality as av1, but file is approximately two times bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264.", + "video.youtube.codec.description": "h264: best compatibility, average quality. max quality is 1080p. \nav1: best quality and efficiency. supports 8k & HDR. \nvp9: same quality as av1, but file is ~2x bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264. if av1 or vp9 isn't available, h264 is used instead.", "video.twitter.gif": "twitter/x", "video.twitter.gif.title": "convert looping videos to GIF", From 8b15fe78637588a71c34dc712ef6c689e114d340 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 26 Oct 2024 22:49:16 +0600 Subject: [PATCH 059/379] api/youtube: check if playability is ok after the status switch --- api/src/processing/services/youtube.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index e2f83c5d..7517c351 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -146,9 +146,6 @@ export default async function(o) { const basicInfo = info.basic_info; switch(playability.status) { - case "OK": - break; - case "LOGIN_REQUIRED": if (playability.reason.endsWith("bot")) { return { error: "youtube.login" } @@ -175,9 +172,10 @@ export default async function(o) { case "AGE_VERIFICATION_REQUIRED": return { error: "content.video.age" }; + } - default: - return { error: "content.video.unavailable" }; + if (playability.status !== "OK") { + return { error: "content.video.unavailable" }; } if (basicInfo.is_live) { From 66bb76e1c7410e62146abc5f52cf0c097806e4d6 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 26 Oct 2024 23:06:43 +0600 Subject: [PATCH 060/379] web/i18n/settings: update preferred language description --- web/i18n/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 8e5a402c..ba2fc886 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -91,7 +91,7 @@ "language.auto.title": "automatic selection", "language.auto.description": "cobalt will use your browser's default language if translation is available. if not, english will be used instead.", "language.preferred.title": "preferred language", - "language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.\n\nwe use community-sourced translations for languages other than english, russian, and czech. they may be inaccurate or incomplete.", + "language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.\n\nsome languages use community-sourced translations, they may be inaccurate or incomplete.", "privacy.analytics": "anonymous traffic analytics", "privacy.analytics.title": "don't contribute to analytics", From fb7325f3b2a9ebcbe60061a10bf343800cff0a90 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 26 Oct 2024 23:53:43 +0600 Subject: [PATCH 061/379] api/youtube: more refactoring, return audio even if there's no video --- api/src/processing/services/youtube.js | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 7517c351..b4583051 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -115,19 +115,7 @@ export default async function(o) { } else throw e; } - const quality = o.quality === "max" ? "9000" : o.quality; - - let info, isDubbed, - format = o.format || "h264"; - - const qual = (i) => { - if (!i.quality_label) { - return; - } - - return i.quality_label.split('p', 2)[0].split('s', 2)[0] - } - + let info; try { info = await yt.getBasicInfo(o.id, yt.session.logged_in ? 'ANDROID' : 'IOS'); } catch(e) { @@ -195,6 +183,8 @@ export default async function(o) { } } + let format = o.format || "h264"; + const filterByCodec = (formats) => formats.filter(e => e.mime_type.includes(codecList[format].videoCodec) @@ -213,14 +203,14 @@ export default async function(o) { const bestVideo = adaptive_formats.find(i => i.has_video && i.content_length); const hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); - if (!bestVideo || (!hasAudio && o.isAudioOnly)) { + if ((!bestVideo && !o.isAudioOnly) || (!hasAudio && o.isAudioOnly)) { return { error: "fetch.empty" }; } - const bestQuality = qual(bestVideo); const checkBestAudio = (i) => (i.has_audio && !i.has_video); let audio = adaptive_formats.find(i => checkBestAudio(i) && i.is_original); + let isDubbed; if (o.dubLang) { let dubbedAudio = adaptive_formats.find(i => @@ -271,7 +261,18 @@ export default async function(o) { bestAudio: format === "h264" ? "m4a" : "opus", } + const qual = (i) => { + if (!i.quality_label) { + return; + } + + return i.quality_label.split('p', 2)[0].split('s', 2)[0] + } + + const quality = o.quality === "max" ? "9000" : o.quality; + const bestQuality = qual(bestVideo); const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality; + const video = adaptive_formats.find(i => qual(i) === matchingQuality && i.has_video && !i.has_audio ); From 2ccc2106224b61915c7b4f85c0d5b2ce8aa81d18 Mon Sep 17 00:00:00 2001 From: jj Date: Sat, 26 Oct 2024 18:07:13 +0000 Subject: [PATCH 062/379] api/test: add test for audio download if no video found tests for bug fixed in fb7325f3b2a9ebcbe60061a10bf343800cff0a90 --- api/src/util/tests.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/src/util/tests.json b/api/src/util/tests.json index 402a2baf..c8a1782d 100644 --- a/api/src/util/tests.json +++ b/api/src/util/tests.json @@ -486,6 +486,17 @@ "code": 400, "status": "error" } + }, + { + "name": "broken audioOnly download", + "url": "https://www.youtube.com/watch?v=ink80Al5nbw", + "params": { + "downloadMode": "audio" + }, + "expected": { + "code": 200, + "status": "tunnel" + } } ], "vk": [ From d8b7a6b5591d484a593126c8d319d9e32caf4bfa Mon Sep 17 00:00:00 2001 From: jj Date: Sat, 26 Oct 2024 18:08:24 +0000 Subject: [PATCH 063/379] api/test: remove youtube vp9 test we fall back to h264 now, so this will always succeed --- api/src/util/tests.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/api/src/util/tests.json b/api/src/util/tests.json index c8a1782d..2d8d8708 100644 --- a/api/src/util/tests.json +++ b/api/src/util/tests.json @@ -437,17 +437,6 @@ "status": "tunnel" } }, - { - "name": "audio bitrate higher than video, no vp9 video in response (vp9)", - "url": "https://www.youtube.com/watch?v=t5nC_ucYBrc", - "params": { - "youtubeVideoCodec": "vp9" - }, - "expected": { - "code": 400, - "status": "error" - } - }, { "name": "short, defaults", "url": "https://www.youtube.com/shorts/r5FpeOJItbw", From a4e6b49d7fc2b375013981b3c37698805ba4dd9a Mon Sep 17 00:00:00 2001 From: jj Date: Sat, 26 Oct 2024 18:28:25 +0000 Subject: [PATCH 064/379] util/jwt: ensure uniform distribution of characters --- api/src/util/generate-jwt-secret.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/src/util/generate-jwt-secret.js b/api/src/util/generate-jwt-secret.js index 83f0aa5b..8db6e230 100644 --- a/api/src/util/generate-jwt-secret.js +++ b/api/src/util/generate-jwt-secret.js @@ -4,8 +4,17 @@ const makeSecureString = (length = 64) => { const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'; const out = []; - for (const byte of crypto.getRandomValues(new Uint8Array(length))) - out.push(alphabet[byte % alphabet.length]); + while (out.length < length) { + for (const byte of crypto.getRandomValues(new Uint8Array(length))) { + if (byte < alphabet.length) { + out.push(alphabet[byte]); + } + + if (out.length === length) { + break; + } + } + } return out.join(''); } From c463e3eabb8b8303590449def8d68b482ba1ea95 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 19:18:15 +0100 Subject: [PATCH 065/379] ci: run codeql on all branches --- .github/workflows/codeql.yml | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..47ea374e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,93 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: + - '**' + pull_request: + branches: [ "main", "7" ] + schedule: + - cron: '33 7 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From b22d0efbf1ad781767c124cebb1b869d1f1d6e03 Mon Sep 17 00:00:00 2001 From: KwiatekMiki Date: Sun, 27 Oct 2024 18:34:11 +0100 Subject: [PATCH 066/379] api/service-patterns: recognize older streamable links (#862) --- api/src/processing/service-patterns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 7f8982b5..9a0acd75 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -36,7 +36,7 @@ export const testers = { || pattern.shortLink?.length <= 16, "streamable": pattern => - pattern.id?.length === 6, + pattern.id?.length <= 6, "tiktok": pattern => pattern.postId?.length <= 21 || pattern.shortLink?.length <= 13, From 5ea23bee134de366a8e7ebb99b6413505c4a8128 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 17:52:04 +0000 Subject: [PATCH 067/379] api/console-text: refactor --- api/src/misc/console-text.js | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/api/src/misc/console-text.js b/api/src/misc/console-text.js index 6ce747d7..8df8fcc6 100644 --- a/api/src/misc/console-text.js +++ b/api/src/misc/console-text.js @@ -1,23 +1,36 @@ -function t(color, tt) { - return color + tt + "\x1b[0m" +const ANSI = { + RESET: "\x1b[0m", + BRIGHT: "\x1b[1m", + RED: "\x1b[31m", + GREEN: "\x1b[32m", + CYAN: "\x1b[36m", + YELLOW: "\x1b[93m" } -export function Bright(tt) { - return t("\x1b[1m", tt) +function wrap(color, text) { + if (!ANSI[color.toUpperCase()]) { + throw "invalid color"; + } + + return ANSI[color.toUpperCase()] + text + ANSI.RESET; } -export function Red(tt) { - return t("\x1b[31m", tt) +export function Bright(text) { + return wrap('bright', text); } -export function Green(tt) { - return t("\x1b[32m", tt) +export function Red(text) { + return wrap('red', text); } -export function Cyan(tt) { - return t("\x1b[36m", tt) +export function Green(text) { + return wrap('green', text); } -export function Yellow(tt) { - return t("\x1b[93m", tt) +export function Cyan(text) { + return wrap('cyan', text); +} + +export function Yellow(text) { + return wrap('yellow', text); } From af50852815ceaf7ca4f1b4a8dffca5d961a2f812 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 18:00:05 +0000 Subject: [PATCH 068/379] api/api-keys: log message to confirm successful file load --- api/src/security/api-keys.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/src/security/api-keys.js b/api/src/security/api-keys.js index 72063099..bbb00194 100644 --- a/api/src/security/api-keys.js +++ b/api/src/security/api-keys.js @@ -1,6 +1,6 @@ import { env } from "../config.js"; import { readFile } from "node:fs/promises"; -import { Yellow } from "../misc/console-text.js"; +import { Green, Yellow } from "../misc/console-text.js"; import ip from "ipaddr.js"; // this function is a modified variation of code @@ -134,9 +134,13 @@ const loadKeys = async (source) => { keys = formatKeys(updated); } -const wrapLoad = (url) => { +const wrapLoad = (url, initial = false) => { loadKeys(url) - .then(() => {}) + .then(() => { + if (initial) { + console.log(`${Green('[✓]')} api keys loaded successfully!`) + } + }) .catch((e) => { console.error(`${Yellow('[!]')} Failed loading API keys at ${new Date().toISOString()}.`); console.error('Error:', e); @@ -200,7 +204,7 @@ export const validateAuthorization = (req) => { } export const setup = (url) => { - wrapLoad(url); + wrapLoad(url, true); if (env.keyReloadInterval > 0) { setInterval(() => wrapLoad(url), env.keyReloadInterval * 1000); } From 5a5a65b3734554b47804ef5584dabbcab442bd93 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 18:10:57 +0000 Subject: [PATCH 069/379] api/cookies: trigger cookie load from api entrypoint --- api/src/core/api.js | 5 +++++ api/src/processing/cookie/manager.js | 10 ++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api/src/core/api.js b/api/src/core/api.js index b3123033..4811cd40 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -18,6 +18,7 @@ import { friendlyServiceName } from "../processing/service-alias.js"; import { verifyStream, getInternalStream } from "../stream/manage.js"; import { createResponse, normalizeRequest, getIP } from "../processing/request.js"; import * as APIKeys from "../security/api-keys.js"; +import * as Cookies from "../processing/cookie/manager.js"; const git = { branch: await getBranch(), @@ -348,6 +349,10 @@ export const runAPI = (express, app, __dirname) => { APIKeys.setup(env.apiKeyURL); } + if (env.cookiePath) { + Cookies.setup(env.cookiePath); + } + app.listen(env.apiPort, env.listenAddress, () => { console.log(`\n` + Bright(Cyan("cobalt ")) + Bright("API ^ω⁠^") + "\n" + diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 25bf9c90..3afb4bfd 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -1,26 +1,20 @@ import Cookie from './cookie.js'; import { readFile, writeFile } from 'fs/promises'; import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; -import { env } from '../../config.js'; const WRITE_INTERVAL = 60000, - cookiePath = env.cookiePath, COUNTER = Symbol('counter'); let cookies = {}, dirty = false, intervalId; -const setup = async () => { +export const setup = async (cookiePath) => { try { - if (!cookiePath) return; - cookies = await readFile(cookiePath, 'utf8'); cookies = JSON.parse(cookies); - intervalId = setInterval(writeChanges, WRITE_INTERVAL) + intervalId = setInterval(writeChanges, WRITE_INTERVAL); } catch { /* no cookies for you */ } } -setup(); - function writeChanges() { if (!dirty) return; dirty = false; From b434b0b45e25f4610e2188870a8ff9f4ee2dcaaa Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 18:12:01 +0000 Subject: [PATCH 070/379] api/cookies: log message to confirm successful file load --- api/src/processing/cookie/manager.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 3afb4bfd..060f8695 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -1,6 +1,7 @@ import Cookie from './cookie.js'; import { readFile, writeFile } from 'fs/promises'; import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; +import { Green, Yellow } from '../../misc/console-text.js'; const WRITE_INTERVAL = 60000, COUNTER = Symbol('counter'); @@ -12,7 +13,11 @@ export const setup = async (cookiePath) => { cookies = await readFile(cookiePath, 'utf8'); cookies = JSON.parse(cookies); intervalId = setInterval(writeChanges, WRITE_INTERVAL); - } catch { /* no cookies for you */ } + console.log(`${Green('[✓]')} cookies loaded successfully!`) + } catch(e) { + console.error(`${Yellow('[!]')} failed to load cookies.`); + console.error('error:', e); + } } function writeChanges() { From 7dc01210318936d44d0742b99dcc968b0b1fb200 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 27 Oct 2024 18:12:59 +0000 Subject: [PATCH 071/379] api: defer file loads until api is running --- api/src/core/api.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/src/core/api.js b/api/src/core/api.js index 4811cd40..9532be81 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -345,14 +345,6 @@ export const runAPI = (express, app, __dirname) => { setGlobalDispatcher(new ProxyAgent(env.externalProxy)) } - if (env.apiKeyURL) { - APIKeys.setup(env.apiKeyURL); - } - - if (env.cookiePath) { - Cookies.setup(env.cookiePath); - } - app.listen(env.apiPort, env.listenAddress, () => { console.log(`\n` + Bright(Cyan("cobalt ")) + Bright("API ^ω⁠^") + "\n" + @@ -367,6 +359,14 @@ export const runAPI = (express, app, __dirname) => { Bright("url: ") + Bright(Cyan(env.apiURL)) + "\n" + Bright("port: ") + env.apiPort + "\n" - ) + ); + + if (env.apiKeyURL) { + APIKeys.setup(env.apiKeyURL); + } + + if (env.cookiePath) { + Cookies.setup(env.cookiePath); + } }) } From 77988447553b909595e138efd8415e0cd649ea51 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 12:01:38 +0600 Subject: [PATCH 072/379] api/youtube: refactor, fix fallback, don't repeat same actions fallback to h264 is now done if there's no required media, not only if adaptive formats list is empty. best audio and best video are now picked only once. --- api/src/processing/services/youtube.js | 43 +++++++++++++------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index b4583051..c045dce0 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -184,6 +184,7 @@ export default async function(o) { } let format = o.format || "h264"; + let fallback = false; const filterByCodec = (formats) => formats.filter(e => @@ -195,25 +196,32 @@ export default async function(o) { let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); - if (adaptive_formats.length === 0 && ["vp9", "av1"].includes(format)) { + const checkBestVideo = (i) => (i.has_video && i.content_length); + const checkBestAudio = (i) => (i.has_audio && i.content_length && i.is_original); + const checkNoMedia = (video, audio) => (!video && !o.isAudioOnly) || (!audio && o.isAudioOnly); + + const earlyBestVideo = adaptive_formats.find(i => checkBestVideo(i)); + const earlyBestAudio = adaptive_formats.find(i => checkBestAudio(i)); + + // check if formats have all needed media and fall back to h264 if not + if (["vp9", "av1"].includes(format) && checkNoMedia(earlyBestVideo, earlyBestAudio)) { + fallback = true; format = "h264"; adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); } - const bestVideo = adaptive_formats.find(i => i.has_video && i.content_length); - const hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); + const bestVideo = !fallback ? earlyBestVideo : adaptive_formats.find(i => checkBestVideo(i)); + const bestAudio = !fallback ? earlyBestAudio : adaptive_formats.find(i => checkBestAudio(i)); - if ((!bestVideo && !o.isAudioOnly) || (!hasAudio && o.isAudioOnly)) { - return { error: "fetch.empty" }; + if (checkNoMedia(bestVideo, bestAudio)) { + return { error: "youtube.codec" }; } - const checkBestAudio = (i) => (i.has_audio && !i.has_video); - - let audio = adaptive_formats.find(i => checkBestAudio(i) && i.is_original); + let audio = bestAudio; let isDubbed; if (o.dubLang) { - let dubbedAudio = adaptive_formats.find(i => + const dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i.language === o.dubLang && i.audio_track ) @@ -223,10 +231,6 @@ export default async function(o) { } } - if (!audio) { - audio = adaptive_formats.find(i => checkBestAudio(i)); - } - const fileMetadata = { title: cleanString(basicInfo.title.trim()), artist: cleanString(basicInfo.author.replace("- Topic", "").trim()) @@ -262,19 +266,16 @@ export default async function(o) { } const qual = (i) => { - if (!i.quality_label) { - return; - } - - return i.quality_label.split('p', 2)[0].split('s', 2)[0] + if (!i.quality_label) return; + return i.quality_label.split('p', 2)[0].split('s', 2)[0]; } const quality = o.quality === "max" ? "9000" : o.quality; const bestQuality = qual(bestVideo); - const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality; + const useBestQuality = Number(quality) > Number(bestQuality); - const video = adaptive_formats.find(i => - qual(i) === matchingQuality && i.has_video && !i.has_audio + const video = useBestQuality ? bestVideo : adaptive_formats.find(i => + qual(i) === quality && checkBestVideo(i) ); if (video && audio) { From 7c516c04686adfee0cdcdfb7abbbdbd5d2a929b4 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 12:08:12 +0600 Subject: [PATCH 073/379] api/cookie/manager: pass `cookiePath` to `writeChanges()` also reordered functions to maintain the hierarchy --- api/src/processing/cookie/manager.js | 39 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 060f8695..02eecfda 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -1,26 +1,15 @@ import Cookie from './cookie.js'; + import { readFile, writeFile } from 'fs/promises'; -import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; import { Green, Yellow } from '../../misc/console-text.js'; +import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; const WRITE_INTERVAL = 60000, COUNTER = Symbol('counter'); let cookies = {}, dirty = false, intervalId; -export const setup = async (cookiePath) => { - try { - cookies = await readFile(cookiePath, 'utf8'); - cookies = JSON.parse(cookies); - intervalId = setInterval(writeChanges, WRITE_INTERVAL); - console.log(`${Green('[✓]')} cookies loaded successfully!`) - } catch(e) { - console.error(`${Yellow('[!]')} failed to load cookies.`); - console.error('error:', e); - } -} - -function writeChanges() { +function writeChanges(cookiePath) { if (!dirty) return; dirty = false; @@ -29,6 +18,18 @@ function writeChanges() { }) } +export const setup = async (cookiePath) => { + try { + cookies = await readFile(cookiePath, 'utf8'); + cookies = JSON.parse(cookies); + intervalId = setInterval(() => writeChanges(cookiePath), WRITE_INTERVAL); + console.log(`${Green('[✓]')} cookies loaded successfully!`) + } catch(e) { + console.error(`${Yellow('[!]')} failed to load cookies.`); + console.error('error:', e); + } +} + export function getCookie(service) { if (!cookies[service] || !cookies[service].length) return; @@ -46,6 +47,11 @@ export function getCookie(service) { return cookies[service][n] } +export function updateCookieValues(cookie, values) { + cookie.set(values); + if (Object.keys(values).length) dirty = true +} + export function updateCookie(cookie, headers) { if (!cookie) return; @@ -58,8 +64,3 @@ export function updateCookie(cookie, headers) { 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); - if (Object.keys(values).length) dirty = true -} From a46e04358aefcb3ff8ad861a95f536bc03b3d31a Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 15:14:36 +0600 Subject: [PATCH 074/379] api/match-action: rename `isM3U8` to `isHLS` and `u` to `url` --- api/src/processing/match-action.js | 18 +++++++++--------- api/src/processing/request.js | 4 ++-- api/src/processing/services/bluesky.js | 2 +- api/src/processing/services/dailymotion.js | 2 +- api/src/processing/services/rutube.js | 2 +- api/src/processing/services/vimeo.js | 2 +- api/src/stream/manage.js | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 31d12e7f..578a5218 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -9,7 +9,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab let action, responseType = "tunnel", defaultParams = { - u: r.urls, + url: r.urls, headers: r.headers, service: host, filename: r.filenameAttributes ? @@ -24,7 +24,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab else if (r.isGif && twitterGif) action = "gif"; else if (isAudioOnly) action = "audio"; else if (isAudioMuted) action = "muteVideo"; - else if (r.isM3U8) action = "m3u8"; + else if (r.isHLS) action = "hls"; else action = "video"; if (action === "picker" || action === "audio") { @@ -54,7 +54,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab params = { type: "gif" }; break; - case "m3u8": + case "hls": params = { type: Array.isArray(r.urls) ? "merge" : "remux" } @@ -62,12 +62,12 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "muteVideo": let muteType = "mute"; - if (Array.isArray(r.urls) && !r.isM3U8) { + if (Array.isArray(r.urls) && !r.isHLS) { muteType = "proxy"; } params = { type: muteType, - u: Array.isArray(r.urls) ? r.urls[0] : r.urls + url: Array.isArray(r.urls) ? r.urls[0] : r.urls } if (host === "reddit" && r.typeId === "redirect") { responseType = "redirect"; @@ -92,10 +92,10 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab } params = { picker: r.picker, - u: createStream({ + url: createStream({ service: "tiktok", type: audioStreamType, - u: r.urls, + url: r.urls, headers: r.headers, filename: r.audioFilename, isAudioOnly: true, @@ -184,14 +184,14 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab } } - if (r.isM3U8 || host === "vimeo") { + if (r.isHLS || host === "vimeo") { copy = false; processType = "audio"; } params = { type: processType, - u: Array.isArray(r.urls) ? r.urls[1] : r.urls, + url: Array.isArray(r.urls) ? r.urls[1] : r.urls, audioBitrate, audioCopy: copy, diff --git a/api/src/processing/request.js b/api/src/processing/request.js index 4287267c..d512bfe5 100644 --- a/api/src/processing/request.js +++ b/api/src/processing/request.js @@ -37,7 +37,7 @@ export function createResponse(responseType, responseData) { case "redirect": response = { - url: responseData?.u, + url: responseData?.url, filename: responseData?.filename } break; @@ -52,7 +52,7 @@ export function createResponse(responseType, responseData) { case "picker": response = { picker: responseData?.picker, - audio: responseData?.u, + audio: responseData?.url, audioFilename: responseData?.filename } break; diff --git a/api/src/processing/services/bluesky.js b/api/src/processing/services/bluesky.js index 5f5cbcec..d200296c 100644 --- a/api/src/processing/services/bluesky.js +++ b/api/src/processing/services/bluesky.js @@ -26,7 +26,7 @@ const extractVideo = async ({ media, filename }) => { urls: videoURL, filename: `${filename}.mp4`, audioFilename: `${filename}_audio`, - isM3U8: true, + isHLS: true, } } diff --git a/api/src/processing/services/dailymotion.js b/api/src/processing/services/dailymotion.js index a403a16b..a30a8bc7 100644 --- a/api/src/processing/services/dailymotion.js +++ b/api/src/processing/services/dailymotion.js @@ -92,7 +92,7 @@ export default async function({ id }) { return { urls: bestQuality.uri, - isM3U8: true, + isHLS: true, filenameAttributes: { service: 'dailymotion', id: media.xid, diff --git a/api/src/processing/services/rutube.js b/api/src/processing/services/rutube.js index 4305241a..67609ffc 100644 --- a/api/src/processing/services/rutube.js +++ b/api/src/processing/services/rutube.js @@ -65,7 +65,7 @@ export default async function(obj) { return { urls: matchingQuality.uri, - isM3U8: true, + isHLS: true, filenameAttributes: { service: "rutube", id: obj.id, diff --git a/api/src/processing/services/vimeo.js b/api/src/processing/services/vimeo.js index 23e84191..0268e1ec 100644 --- a/api/src/processing/services/vimeo.js +++ b/api/src/processing/services/vimeo.js @@ -122,7 +122,7 @@ const getHLS = async (configURL, obj) => { return { urls, - isM3U8: true, + isHLS: true, filenameAttributes: { resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`, qualityLabel: `${resolutionMatch[bestQuality.resolution.width]}p`, diff --git a/api/src/stream/manage.js b/api/src/stream/manage.js index e25f4434..557caa55 100644 --- a/api/src/stream/manage.js +++ b/api/src/stream/manage.js @@ -34,7 +34,7 @@ export function createStream(obj) { streamData = { exp: exp, type: obj.type, - urls: obj.u, + urls: obj.url, service: obj.service, filename: obj.filename, From 24ae08b1050fb78f3dd6776617e69027014c6122 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 15:15:41 +0600 Subject: [PATCH 075/379] api/stream: add `isHLS` to stream cache --- api/src/processing/match-action.js | 3 ++- api/src/stream/manage.js | 5 ++++- api/src/stream/types.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 578a5218..a502c6d2 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -56,7 +56,8 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "hls": params = { - type: Array.isArray(r.urls) ? "merge" : "remux" + type: Array.isArray(r.urls) ? "merge" : "remux", + isHLS: true, } break; diff --git a/api/src/stream/manage.js b/api/src/stream/manage.js index 557caa55..5f55b5fa 100644 --- a/api/src/stream/manage.js +++ b/api/src/stream/manage.js @@ -46,6 +46,8 @@ export function createStream(obj) { audioBitrate: obj.audioBitrate, audioCopy: !!obj.audioCopy, audioFormat: obj.audioFormat, + + isHLS: obj.isHLS || false, }; streamCache.set( @@ -100,7 +102,8 @@ export function createInternalStream(url, obj = {}) { service: obj.service, headers, controller, - dispatcher + dispatcher, + isHLS: obj.isHLS, }); let streamLink = new URL('/itunnel', `http://127.0.0.1:${env.apiPort}`); diff --git a/api/src/stream/types.js b/api/src/stream/types.js index 184af873..2a1b9677 100644 --- a/api/src/stream/types.js +++ b/api/src/stream/types.js @@ -101,7 +101,7 @@ const merge = (streamInfo, res) => { args = args.concat(ffmpegArgs[format]); - if (hlsExceptions.includes(streamInfo.service)) { + if (hlsExceptions.includes(streamInfo.service) && streamInfo.isHLS) { args.push('-bsf:a', 'aac_adtstoasc') } From c9eefc4d553b795e477977183e46850f9c153a74 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 15:17:54 +0600 Subject: [PATCH 076/379] api/youtube: add an option to use HLS streams - added `youtubeHLS` variable to api - added youtube HLS parsing & handling --- api/src/processing/match.js | 3 +- api/src/processing/schema.js | 2 + api/src/processing/service-config.js | 2 +- api/src/processing/services/youtube.js | 219 +++++++++++++++++++------ api/src/stream/internal.js | 2 +- 5 files changed, 171 insertions(+), 57 deletions(-) diff --git a/api/src/processing/match.js b/api/src/processing/match.js index b0022d08..8500c3b5 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -97,13 +97,14 @@ export default async function({ host, patternMatch, params }) { case "youtube": let fetchInfo = { + dispatcher, id: patternMatch.id.slice(0, 11), quality: params.videoQuality, format: params.youtubeVideoCodec, isAudioOnly, isAudioMuted, dubLang: params.youtubeDubLang, - dispatcher + youtubeHLS: params.youtubeHLS, } if (url.hostname === "music.youtube.com" || isAudioOnly) { diff --git a/api/src/processing/schema.js b/api/src/processing/schema.js index 172d480c..f7493325 100644 --- a/api/src/processing/schema.js +++ b/api/src/processing/schema.js @@ -42,6 +42,8 @@ export const apiSchema = z.object({ tiktokFullAudio: z.boolean().default(false), tiktokH265: z.boolean().default(false), twitterGif: z.boolean().default(true), + youtubeDubBrowserLang: z.boolean().default(false), + youtubeHLS: z.boolean().default(false), }) .strict(); diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index a2136ad0..fc437095 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -1,7 +1,7 @@ import UrlPattern from "url-pattern"; export const audioIgnore = ["vk", "ok", "loom"]; -export const hlsExceptions = ["dailymotion", "vimeo", "rutube", "bsky"]; +export const hlsExceptions = ["dailymotion", "vimeo", "rutube", "bsky", "youtube"]; export const services = { bilibili: { diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index c045dce0..af8d8c3d 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -1,3 +1,5 @@ +import HLS from "hls-parser"; + import { fetch } from "undici"; import { Innertube, Session } from "youtubei.js"; @@ -27,6 +29,19 @@ const codecList = { } } +const hlsCodecList = { + h264: { + videoCodec: "avc1", + audioCodec: "mp4a", + container: "mp4" + }, + vp9: { + videoCodec: "vp09", + audioCodec: "mp4a", + container: "mp4" + } +} + const transformSessionData = (cookie) => { if (!cookie) return; @@ -117,7 +132,7 @@ export default async function(o) { let info; try { - info = await yt.getBasicInfo(o.id, yt.session.logged_in ? 'ANDROID' : 'IOS'); + info = await yt.getBasicInfo(o.id, o.youtubeHLS ? 'IOS' : 'ANDROID'); } catch(e) { if (e?.info?.reason === "This video is private") { return { error: "content.video.private" }; @@ -183,51 +198,139 @@ export default async function(o) { } } - let format = o.format || "h264"; - let fallback = false; + const quality = o.quality === "max" ? 9000 : Number(o.quality); + const matchQuality = (resolution) => { + return resolution.height > resolution.width ? resolution.width : resolution.height; + } - const filterByCodec = (formats) => - formats.filter(e => - e.mime_type.includes(codecList[format].videoCodec) - || e.mime_type.includes(codecList[format].audioCodec) - ).sort((a, b) => - Number(b.bitrate) - Number(a.bitrate) + let video, audio, isDubbed, + format = o.format || "h264"; + + if (o.youtubeHLS) { + const hlsManifest = info.streaming_data.hls_manifest_url; + + if (!hlsManifest) { + return { error: "content.video.no_streams" }; + } + + const fetchedHlsManifest = await fetch(hlsManifest, { + dispatcher: o.dispatcher, + }).then(r => { + if (r.status === 200) { + return r.text(); + } else { + throw new Error("couldn't fetch the HLS playlist"); + } + }).catch(() => {}); + + if (!fetchedHlsManifest) { + return { error: "content.video.no_streams" }; + } + + const variants = HLS.parse(fetchedHlsManifest).variants.sort( + (a, b) => Number(b.bandwidth) - Number(a.bandwidth) ); - let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); + if (!variants || variants.length === 0) { + return { error: "content.video.no_streams" }; + } - const checkBestVideo = (i) => (i.has_video && i.content_length); - const checkBestAudio = (i) => (i.has_audio && i.content_length && i.is_original); - const checkNoMedia = (video, audio) => (!video && !o.isAudioOnly) || (!audio && o.isAudioOnly); + const matchHlsCodec = codecs => ( + codecs.includes(hlsCodecList[format].videoCodec) + ); - const earlyBestVideo = adaptive_formats.find(i => checkBestVideo(i)); - const earlyBestAudio = adaptive_formats.find(i => checkBestAudio(i)); + const best = variants.find(i => { + if (matchHlsCodec(i.codecs)) { + return i; + } + }); - // check if formats have all needed media and fall back to h264 if not - if (["vp9", "av1"].includes(format) && checkNoMedia(earlyBestVideo, earlyBestAudio)) { - fallback = true; - format = "h264"; - adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); - } + const preferred = variants.find((i) => { + if (matchHlsCodec(i.codecs) && matchQuality(i.resolution) === quality) { + return i; + } + }); - const bestVideo = !fallback ? earlyBestVideo : adaptive_formats.find(i => checkBestVideo(i)); - const bestAudio = !fallback ? earlyBestAudio : adaptive_formats.find(i => checkBestAudio(i)); + const selected = preferred || best; + const defaultAudio = selected.audio.find(i => i.isDefault); - if (checkNoMedia(bestVideo, bestAudio)) { - return { error: "youtube.codec" }; - } + audio = defaultAudio; - let audio = bestAudio; - let isDubbed; + if (o.dubLang) { + const dubbedAudio = selected.audio.find(i => + i.language === o.dubLang + ) - if (o.dubLang) { - const dubbedAudio = adaptive_formats.find(i => - checkBestAudio(i) && i.language === o.dubLang && i.audio_track - ) + if (dubbedAudio && !dubbedAudio.isDefault) { + audio = dubbedAudio; + } + } - if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) { - audio = dubbedAudio; - isDubbed = true; + selected.audio = []; + selected.subtitles = []; + video = selected; + } else { + let fallback = false; + + const filterByCodec = (formats) => + formats.filter(e => + e.mime_type.includes(codecList[format].videoCodec) + || e.mime_type.includes(codecList[format].audioCodec) + ).sort((a, b) => + Number(b.bitrate) - Number(a.bitrate) + ); + + let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); + + const checkBestVideo = (i) => (i.has_video && i.content_length); + const checkBestAudio = (i) => (i.has_audio && i.content_length && i.is_original); + const checkNoMedia = (video, audio) => (!video && !o.isAudioOnly) || (!audio && o.isAudioOnly); + + const earlyBestVideo = adaptive_formats.find(i => checkBestVideo(i)); + const earlyBestAudio = adaptive_formats.find(i => checkBestAudio(i)); + + // check if formats have all needed media and fall back to h264 if not + if (["vp9", "av1"].includes(format) && checkNoMedia(earlyBestVideo, earlyBestAudio)) { + fallback = true; + format = "h264"; + adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); + } + + const bestVideo = !fallback ? earlyBestVideo : adaptive_formats.find(i => checkBestVideo(i)); + const bestAudio = !fallback ? earlyBestAudio : adaptive_formats.find(i => checkBestAudio(i)); + + if (checkNoMedia(bestVideo, bestAudio)) { + return { error: "youtube.codec" }; + } + + audio = bestAudio; + + if (o.dubLang) { + const dubbedAudio = adaptive_formats.find(i => + checkBestAudio(i) && i.language === o.dubLang && i.audio_track + ) + + if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) { + audio = dubbedAudio; + isDubbed = true; + } + } + + if (!o.isAudioOnly) { + const qual = (i) => { + return matchQuality({ + width: i.width, + height: i.height, + }) + } + + const quality = o.quality === "max" ? "9000" : o.quality; + const bestQuality = qual(bestVideo); + const useBestQuality = Number(quality) > Number(bestQuality); + + video = useBestQuality ? bestVideo : adaptive_formats.find(i => + qual(i) === quality && checkBestVideo(i) + ); } } @@ -263,35 +366,43 @@ export default async function(o) { filenameAttributes, fileMetadata, bestAudio: format === "h264" ? "m4a" : "opus", + isHLS: o.youtubeHLS, } - const qual = (i) => { - if (!i.quality_label) return; - return i.quality_label.split('p', 2)[0].split('s', 2)[0]; - } - - const quality = o.quality === "max" ? "9000" : o.quality; - const bestQuality = qual(bestVideo); - const useBestQuality = Number(quality) > Number(bestQuality); - - const video = useBestQuality ? bestVideo : adaptive_formats.find(i => - qual(i) === quality && checkBestVideo(i) - ); - if (video && audio) { - filenameAttributes.qualityLabel = video.quality_label; - filenameAttributes.resolution = `${video.width}x${video.height}`; - filenameAttributes.extension = codecList[format].container; + let resolution; + + if (o.youtubeHLS) { + resolution = matchQuality(video.resolution); + filenameAttributes.resolution = `${video.resolution.width}x${video.resolution.height}`; + filenameAttributes.extension = hlsCodecList[format].container; + + video = video.uri; + audio = audio.uri; + } else { + resolution = matchQuality({ + width: video.width, + height: video.height, + }); + filenameAttributes.resolution = `${video.width}x${video.height}`; + filenameAttributes.extension = codecList[format].container; + + video = video.url; + audio = audio.url; + } + + filenameAttributes.qualityLabel = `${resolution}p`; filenameAttributes.youtubeFormat = format; return { type: "merge", urls: [ - video.url, - audio.url + video, + audio, ], filenameAttributes, - fileMetadata + fileMetadata, + isHLS: o.youtubeHLS, } } diff --git a/api/src/stream/internal.js b/api/src/stream/internal.js index 51552d4c..4235d722 100644 --- a/api/src/stream/internal.js +++ b/api/src/stream/internal.js @@ -114,7 +114,7 @@ async function handleGenericStream(streamInfo, res) { } export function internalStream(streamInfo, res) { - if (streamInfo.service === 'youtube') { + if (streamInfo.service === 'youtube' && !streamInfo.isHLS) { return handleYoutubeStream(streamInfo, res); } From 60b22cb5f7d1bbd2ab3b4da2f185997599fe8b57 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 28 Oct 2024 15:27:51 +0600 Subject: [PATCH 077/379] web: add support for youtube hls also increased api response timeout to 20 seconds --- web/i18n/en/settings.json | 4 ++++ web/src/lib/api/api.ts | 3 ++- web/src/lib/settings/defaults.ts | 1 + web/src/lib/types/settings.ts | 1 + web/src/routes/settings/video/+page.svelte | 16 ++++++++++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index ba2fc886..b583a043 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -32,6 +32,10 @@ "video.youtube.codec": "youtube video codec and container", "video.youtube.codec.description": "h264: best compatibility, average quality. max quality is 1080p. \nav1: best quality and efficiency. supports 8k & HDR. \nvp9: same quality as av1, but file is ~2x bigger. supports 4k & HDR.\n\nav1 and vp9 aren't as widely supported as h264. if av1 or vp9 isn't available, h264 is used instead.", + "video.youtube.hls": "youtube hls", + "video.youtube.hls.title": "use hls formats for videos", + "video.youtube.hls.description": "only h264 and vp9 codecs are supported in this mode, both are served in a mp4 container. files download faster and are less prone to errors. files may be less compatible with editing software or video players.", + "video.twitter.gif": "twitter/x", "video.twitter.gif.title": "convert looping videos to GIF", "video.twitter.gif.description": "GIF conversion is inefficient, converted file may be obnoxiously big and low quality.", diff --git a/web/src/lib/api/api.ts b/web/src/lib/api/api.ts index 19f27c94..bdbbe59b 100644 --- a/web/src/lib/api/api.ts +++ b/web/src/lib/api/api.ts @@ -26,6 +26,7 @@ const request = async (url: string) => { youtubeVideoCodec: getSetting("save", "youtubeVideoCodec"), videoQuality: getSetting("save", "videoQuality"), + youtubeHLS: getSetting("save", "youtubeHLS"), filenameStyle: getSetting("save", "filenameStyle"), disableMetadata: getSetting("save", "disableMetadata"), @@ -82,7 +83,7 @@ const request = async (url: string) => { const response: Optional = await fetch(api, { method: "POST", redirect: "manual", - signal: AbortSignal.timeout(10000), + signal: AbortSignal.timeout(20000), body: JSON.stringify(request), headers: { "Accept": "application/json", diff --git a/web/src/lib/settings/defaults.ts b/web/src/lib/settings/defaults.ts index c0b0c187..168ca54a 100644 --- a/web/src/lib/settings/defaults.ts +++ b/web/src/lib/settings/defaults.ts @@ -26,6 +26,7 @@ const defaultSettings: CobaltSettings = { videoQuality: "1080", youtubeVideoCodec: "h264", youtubeDubBrowserLang: false, + youtubeHLS: false, }, privacy: { alwaysProxy: false, diff --git a/web/src/lib/types/settings.ts b/web/src/lib/types/settings.ts index 3c9cef6e..25eb1017 100644 --- a/web/src/lib/types/settings.ts +++ b/web/src/lib/types/settings.ts @@ -48,6 +48,7 @@ type CobaltSettingsSave = { videoQuality: typeof videoQualityOptions[number], youtubeVideoCodec: typeof youtubeVideoCodecOptions[number], youtubeDubBrowserLang: boolean, + youtubeHLS: boolean, }; export type CurrentCobaltSettings = { diff --git a/web/src/routes/settings/video/+page.svelte b/web/src/routes/settings/video/+page.svelte index c43016cb..4760d9c0 100644 --- a/web/src/routes/settings/video/+page.svelte +++ b/web/src/routes/settings/video/+page.svelte @@ -1,4 +1,5 @@ -
-
+
+

- {$t("settings.language.preferred.title")} + {title}

- onChange(e)} {disabled}> + {#each Object.keys(items) as value} + {/each}
-
- {$t("settings.language.preferred.description")} -
+ + {#if description} +
+ {description} +
+ {/if}
From b015af7dde5c12611128e0a02c852a50536b11d1 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 15:24:50 +0600 Subject: [PATCH 208/379] web/remux: add bullet points explaining what remux is --- web/i18n/en/remux.json | 7 +- web/src/routes/remux/+page.svelte | 108 ++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/web/i18n/en/remux.json b/web/i18n/en/remux.json index b7a7f34a..d8b031c3 100644 --- a/web/i18n/en/remux.json +++ b/web/i18n/en/remux.json @@ -1,3 +1,8 @@ { - "description": "remuxing often fixes compatibility issues with old software. it's fast, lossless, and everything is processed on-device." + "bullet.purpose.title": "what does remux do?", + "bullet.purpose.description": "remux fixes any issues with the file container, such as missing time info. it helps increase compatibility with old software, such as vegas pro and windows media player.", + "bullet.explainer.title": "how does it work?", + "bullet.explainer.description": "remuxing takes existing codec data and copies it over to a new media container. it's lossless, media data doesn't get re-encoded.", + "bullet.privacy.title": "on-device processing", + "bullet.privacy.description": "cobalt remuxes files locally. files never leave your device, so processing is nearly instant." } diff --git a/web/src/routes/remux/+page.svelte b/web/src/routes/remux/+page.svelte index d0edaf4f..475c94bd 100644 --- a/web/src/routes/remux/+page.svelte +++ b/web/src/routes/remux/+page.svelte @@ -10,6 +10,11 @@ import Skeleton from "$components/misc/Skeleton.svelte"; import DropReceiver from "$components/misc/DropReceiver.svelte"; import FileReceiver from "$components/misc/FileReceiver.svelte"; + import BulletExplain from "$components/misc/BulletExplain.svelte"; + + import IconRepeat from "@tabler/icons-svelte/IconRepeat.svelte"; + import IconDevices from "@tabler/icons-svelte/IconDevices.svelte"; + import IconInfoCircle from "@tabler/icons-svelte/IconInfoCircle.svelte"; let draggedOver = false; let file: File | undefined; @@ -40,7 +45,7 @@ let processing = false; - const ff = new LibAVWrapper(progress => { + const ff = new LibAVWrapper((progress) => { if (progress.out_time_sec) { processedDuration = progress.out_time_sec; } @@ -62,7 +67,7 @@ speed = undefined; processing = true; - const file_info = await ff.probe(file).catch(e => { + const file_info = await ff.probe(file).catch((e) => { if (e?.message?.toLowerCase().includes("out of memory")) { console.error("uh oh! out of memory"); console.error(e); @@ -105,7 +110,7 @@ totalDuration = Number(file_info.format.duration); if (file instanceof File && !file.type) { - file = new File([ file ], file.name, { + file = new File([file], file.name, { type: mime.getType(file.name) ?? undefined, }); } @@ -144,8 +149,8 @@ return await downloadFile({ file: new File([render], filename, { - type: render.type - }) + type: render.type, + }), }); } finally { processing = false; @@ -198,7 +203,7 @@ {$t("tabs.remux")} ~ {$t("general.cobalt")} @@ -210,22 +215,41 @@ data-first-focus data-focus-ring-hidden > - -
- {$t("remux.description")} +
+ +
+ +
+ + + + +
@@ -263,13 +287,14 @@ #remux-open { display: flex; - flex-direction: column; + flex-direction: row; justify-content: center; align-items: center; - max-width: 450px; text-align: center; - gap: 24px; - transition: transform 0.2s, opacity 0.2s; + gap: 48px; + transition: + transform 0.2s, + opacity 0.2s; } #remux-processing { @@ -278,7 +303,9 @@ flex-direction: column; opacity: 0; transform: scale(0.9); - transition: transform 0.2s, opacity 0.2s; + transition: + transform 0.2s, + opacity 0.2s; pointer-events: none; } @@ -314,16 +341,29 @@ text-align: center; } - .remux-description { - font-size: 14px; - line-height: 1.5; + #remux-receiver { + max-width: 450px; + } + + #remux-bullets { + display: flex; + flex-direction: column; + gap: var(--padding); + max-width: 450px; + } + + @media screen and (max-width: 920px) { + #remux-open { + flex-direction: column; + gap: var(--padding); + } + + #remux-bullets { + padding: var(--padding); + } } @media screen and (max-width: 535px) { - .remux-description { - font-size: 12px; - } - .progress-bar { width: 350px; } From 6aade3cc7821e4b05fc82ee2958a02e85849abd8 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 15:26:37 +0600 Subject: [PATCH 209/379] web/BulletExplain: increase font size on desktop --- web/src/components/misc/BulletExplain.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/components/misc/BulletExplain.svelte b/web/src/components/misc/BulletExplain.svelte index 747a3b90..92840411 100644 --- a/web/src/components/misc/BulletExplain.svelte +++ b/web/src/components/misc/BulletExplain.svelte @@ -44,6 +44,7 @@ .bullet-description { padding: 0; line-height: 1.5; + font-size: 13.5px; } .bullet-icon { @@ -64,6 +65,10 @@ font-size: 15px; } + .bullet-description { + font-size: 13px; + } + .bullet-icon :global(svg) { width: 19px; height: 19px; From b036437871e21de5471f3719367ba974f8f1a7db Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 15:32:13 +0600 Subject: [PATCH 210/379] web/i18n/general: update embed description to be less corny --- web/i18n/en/general.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/general.json b/web/i18n/en/general.json index c3667f26..a50db46b 100644 --- a/web/i18n/en/general.json +++ b/web/i18n/en/general.json @@ -3,5 +3,5 @@ "meowbalt": "meowbalt", "beta": "beta", - "embed.description": "save what you love without ads, tracking, paywalls or other nonsense. cobalt is a truly open web app, built with love and care by imput." + "embed.description": "cobalt lets you save what you love without ads, tracking, paywalls or other nonsense. just paste the link and you're ready to rock!" } From 277a6caefad31c492dde2099d499bed52f5a24fe Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 15:44:32 +0600 Subject: [PATCH 211/379] web/ManageSettings: use `downloadFile` for exporting settings and also use 4 spaces for formatting the json file cuz 2 spaces is foul --- .../components/settings/ManageSettings.svelte | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/web/src/components/settings/ManageSettings.svelte b/web/src/components/settings/ManageSettings.svelte index 5c113a01..abc053bf 100644 --- a/web/src/components/settings/ManageSettings.svelte +++ b/web/src/components/settings/ManageSettings.svelte @@ -1,12 +1,9 @@ From 4a70f09017b2fb765b0cefbcade3cd3b16bf58e7 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 16:27:39 +0600 Subject: [PATCH 212/379] web/Omnibox: add community instance label now it's easier for the end user to differentiate if an instance is official or not --- web/i18n/en/save.json | 4 +++- web/src/components/save/Omnibox.svelte | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/web/i18n/en/save.json b/web/i18n/en/save.json index 79a65dfb..e6edc0de 100644 --- a/web/i18n/en/save.json +++ b/web/i18n/en/save.json @@ -19,5 +19,7 @@ "tutorial.step.3": "select the respective shortcut in the share sheet.", "tutorial.outro": "these shortcuts will work only from the cobalt app, sharing links from other apps will not work.", "tutorial.shortcut.photos": "to photos", - "tutorial.shortcut.files": "to files" + "tutorial.shortcut.files": "to files", + + "label.community_instance": "community instance" } diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index c366dd8f..15368df3 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -1,15 +1,16 @@ -{#if env.DEFAULT_API} - - - -{/if} - Date: Mon, 18 Nov 2024 16:42:59 +0600 Subject: [PATCH 215/379] web/Omnibox: fix main instance domain check oops --- web/src/components/save/Omnibox.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index 15368df3..4b93f98f 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -139,7 +139,7 @@ if you want to remove the community instance label, refer to the license first https://github.com/imputnet/cobalt/tree/main/web#license --> -{#if env.DEFAULT_API || $page.url.host !== "cobalt.tools" || !$page.url.host.endsWith(".cobalt.tools")} +{#if env.DEFAULT_API || (!$page.url.host.endsWith(".cobalt.tools") && $page.url.host !== "cobalt.tools")}
{$t("save.label.community_instance")}
From a0b621c5e7369a3ce4eea1cb84ff9beef74ba51f Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 16:59:59 +0600 Subject: [PATCH 216/379] web/remux: increase bullet gap on desktop --- web/src/routes/remux/+page.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/routes/remux/+page.svelte b/web/src/routes/remux/+page.svelte index 475c94bd..4038a3a4 100644 --- a/web/src/routes/remux/+page.svelte +++ b/web/src/routes/remux/+page.svelte @@ -348,7 +348,7 @@ #remux-bullets { display: flex; flex-direction: column; - gap: var(--padding); + gap: 18px; max-width: 450px; } @@ -360,6 +360,7 @@ #remux-bullets { padding: var(--padding); + gap: var(--padding); } } From e09e098b27a5c76277a64691071aff815c2a996e Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 17:02:22 +0600 Subject: [PATCH 217/379] web/remux: reduce bullet padding only on small screens --- web/src/routes/remux/+page.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/routes/remux/+page.svelte b/web/src/routes/remux/+page.svelte index 4038a3a4..282e19ac 100644 --- a/web/src/routes/remux/+page.svelte +++ b/web/src/routes/remux/+page.svelte @@ -360,7 +360,6 @@ #remux-bullets { padding: var(--padding); - gap: var(--padding); } } @@ -368,5 +367,9 @@ .progress-bar { width: 350px; } + + #remux-bullets { + gap: var(--padding); + } } From b38cb779527b9fd6f887d5ac469bb505a8c5ea95 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 21:05:47 +0600 Subject: [PATCH 218/379] web/turnstile: refresh turnstile if it expires in background also renamed `turnstileLoaded` to `turnstileSolved` for more clarity --- web/src/components/misc/Turnstile.svelte | 10 ++++++++-- web/src/components/save/Omnibox.svelte | 15 +++------------ web/src/lib/api/api.ts | 4 ++-- web/src/lib/api/turnstile.ts | 17 +++++++++++++++++ web/src/lib/state/turnstile.ts | 2 +- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/web/src/components/misc/Turnstile.svelte b/web/src/components/misc/Turnstile.svelte index f1c9b625..7cb194c5 100644 --- a/web/src/components/misc/Turnstile.svelte +++ b/web/src/components/misc/Turnstile.svelte @@ -2,7 +2,9 @@ import { onMount } from "svelte"; import { cachedInfo } from "$lib/api/server-info"; - import { turnstileLoaded, turnstileCreated } from "$lib/state/turnstile"; + import { turnstileSolved, turnstileCreated } from "$lib/state/turnstile"; + + import turnstile from "$lib/api/turnstile"; let turnstileElement: HTMLElement; let turnstileScript: HTMLElement; @@ -21,7 +23,7 @@ return true; }, callback: () => { - $turnstileLoaded = true; + $turnstileSolved = true; } }); } @@ -31,6 +33,10 @@ } else { turnstileScript.addEventListener("load", setup); } + + window.addEventListener("focus", () => { + turnstile.refreshIfExpired(); + }); }); diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index 4b93f98f..8b0b443c 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -12,7 +12,7 @@ import dialogs from "$lib/state/dialogs"; import { link } from "$lib/state/omnibox"; import { updateSetting } from "$lib/state/settings"; - import { turnstileLoaded } from "$lib/state/turnstile"; + import { turnstileSolved } from "$lib/state/turnstile"; import type { Optional } from "$lib/types/generic"; import type { DownloadModeOption } from "$lib/types/settings"; @@ -39,7 +39,8 @@ let isDisabled = false; let isLoading = false; - let isBotCheckOngoing = false; + $: isBotCheckOngoing = + !!$cachedInfo?.info?.cobalt?.turnstileSitekey && !$turnstileSolved; const validLink = (url: string) => { try { @@ -61,16 +62,6 @@ goto("/", { replaceState: true }); } - $: if ($cachedInfo?.info?.cobalt?.turnstileSitekey) { - if ($turnstileLoaded) { - isBotCheckOngoing = false; - } else { - isBotCheckOngoing = true; - } - } else { - isBotCheckOngoing = false; - } - const pasteClipboard = () => { if ($dialogs.length > 0 || isDisabled || isLoading) { return; diff --git a/web/src/lib/api/api.ts b/web/src/lib/api/api.ts index fb265f54..f69c3323 100644 --- a/web/src/lib/api/api.ts +++ b/web/src/lib/api/api.ts @@ -5,7 +5,7 @@ import lazySettingGetter from "$lib/settings/lazy-get"; import { getSession } from "$lib/api/session"; import { currentApiURL } from "$lib/api/api-url"; -import { turnstileLoaded } from "$lib/state/turnstile"; +import { turnstileSolved } from "$lib/state/turnstile"; import { cachedInfo, getServerInfo } from "$lib/api/server-info"; import type { Optional } from "$lib/types/generic"; @@ -49,7 +49,7 @@ const request = async (url: string) => { } as CobaltErrorResponse; } - if (getCachedInfo?.info?.cobalt?.turnstileSitekey && !get(turnstileLoaded)) { + if (getCachedInfo?.info?.cobalt?.turnstileSitekey && !get(turnstileSolved)) { return { status: "error", error: { diff --git a/web/src/lib/api/turnstile.ts b/web/src/lib/api/turnstile.ts index 33d65a7f..6a39ad7e 100644 --- a/web/src/lib/api/turnstile.ts +++ b/web/src/lib/api/turnstile.ts @@ -1,3 +1,5 @@ +import { turnstileSolved } from "$lib/state/turnstile"; + const getResponse = () => { const turnstileElement = document.getElementById("turnstile-widget"); @@ -12,13 +14,28 @@ const update = () => { const turnstileElement = document.getElementById("turnstile-widget"); if (turnstileElement) { + turnstileSolved.set(false); return window?.turnstile?.reset(turnstileElement); } return null; } +const refreshIfExpired = () => { + const turnstileElement = document.getElementById("turnstile-widget"); + + if (turnstileElement) { + const isExpired = window?.turnstile?.isExpired(turnstileElement); + if (isExpired) { + console.log("expired, refreshing the turnstile widget rn"); + return update(); + } + console.log("turnstile not expired, nothing to do"); + } +} + export default { getResponse, update, + refreshIfExpired, } diff --git a/web/src/lib/state/turnstile.ts b/web/src/lib/state/turnstile.ts index 5a165bc7..12231b11 100644 --- a/web/src/lib/state/turnstile.ts +++ b/web/src/lib/state/turnstile.ts @@ -1,4 +1,4 @@ import { writable } from "svelte/store"; -export const turnstileLoaded = writable(false); +export const turnstileSolved = writable(false); export const turnstileCreated = writable(false); From c67132d2cc95f53b5fedcba0e65fd6fe9582667d Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 21:06:19 +0600 Subject: [PATCH 219/379] web/Omnibox: add a cool animation to input icons --- web/src/components/save/Omnibox.svelte | 44 ++---------- web/src/components/save/OmniboxIcon.svelte | 78 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 40 deletions(-) create mode 100644 web/src/components/save/OmniboxIcon.svelte diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index 8b0b443c..a1e9873a 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -17,13 +17,11 @@ import type { Optional } from "$lib/types/generic"; import type { DownloadModeOption } from "$lib/types/settings"; - import IconLink from "@tabler/icons-svelte/IconLink.svelte"; - import IconLoader2 from "@tabler/icons-svelte/IconLoader2.svelte"; - import ClearButton from "$components/save/buttons/ClearButton.svelte"; import DownloadButton from "$components/save/buttons/DownloadButton.svelte"; import Switcher from "$components/buttons/Switcher.svelte"; + import OmniboxIcon from "$components/save/OmniboxIcon.svelte"; import ActionButton from "$components/buttons/ActionButton.svelte"; import SettingsButton from "$components/buttons/SettingsButton.svelte"; @@ -142,17 +140,7 @@ class:focused={isFocused} class:downloadable={validLink($link)} > - - + + import IconLink from "@tabler/icons-svelte/IconLink.svelte"; + import IconLoader2 from "@tabler/icons-svelte/IconLoader2.svelte"; + + export let loading: boolean; + + +
+
+ +
+ +
+ + From 6abccd9743dc7e936f0fcd2cec87f9df6eb8af11 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 18 Nov 2024 23:02:46 +0600 Subject: [PATCH 220/379] web/Turnstile: log to console on expired and timeout callback --- web/src/components/misc/Turnstile.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/src/components/misc/Turnstile.svelte b/web/src/components/misc/Turnstile.svelte index 7cb194c5..b1fc162c 100644 --- a/web/src/components/misc/Turnstile.svelte +++ b/web/src/components/misc/Turnstile.svelte @@ -22,6 +22,12 @@ console.log("error code from turnstile:", error); return true; }, + "expired-callback": () => { + console.log("turnstile expired. i am callback this is my message") + }, + "timeout-callback": () => { + console.log("turnstile timed out. i am callback this is my message") + }, callback: () => { $turnstileSolved = true; } From b31c126cecc78beca03e88bcf2e921dd718c004f Mon Sep 17 00:00:00 2001 From: jj Date: Mon, 18 Nov 2024 17:34:34 +0000 Subject: [PATCH 221/379] api/instagram: fix module not using graphql api --- api/src/processing/services/instagram.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/processing/services/instagram.js b/api/src/processing/services/instagram.js index 111d7603..d9a646aa 100644 --- a/api/src/processing/services/instagram.js +++ b/api/src/processing/services/instagram.js @@ -266,6 +266,7 @@ export default function(obj) { } async function getPost(id, alwaysProxy) { + const hasData = (data) => data && data.gql_data !== null; let data, result; try { const cookie = getCookie('instagram'); @@ -282,16 +283,16 @@ export default function(obj) { if (media_id && token) data = await requestMobileApi(media_id, { token }); // mobile api (no cookie, cookie) - if (media_id && !data) data = await requestMobileApi(media_id); - if (media_id && cookie && !data) data = await requestMobileApi(media_id, { cookie }); + if (media_id && !hasData(data)) data = await requestMobileApi(media_id); + if (media_id && cookie && !hasData(data)) data = await requestMobileApi(media_id, { cookie }); // html embed (no cookie, cookie) - if (!data) data = await requestHTML(id); - if (!data && cookie) data = await requestHTML(id, cookie); + if (!hasData(data)) data = await requestHTML(id); + if (!hasData(data) && cookie) data = await requestHTML(id, cookie); // web app graphql api (no cookie, cookie) - if (!data) data = await requestGQL(id); - if (!data && cookie) data = await requestGQL(id, cookie); + if (!hasData(data)) data = await requestGQL(id); + if (!hasData(data) && cookie) data = await requestGQL(id, cookie); } catch {} if (!data) return { error: "fetch.fail" }; From a3c807a9932c7ddf1edb7ebac2c28d4ef22b23ba Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 19 Nov 2024 00:20:27 +0600 Subject: [PATCH 222/379] web/turnstile: use own callback for refreshing the widget or at least try to, idk man, im so tired of cf turnstile --- web/src/components/misc/Turnstile.svelte | 12 ++++-------- web/src/lib/api/session.ts | 2 +- web/src/lib/api/turnstile.ts | 18 ++---------------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/web/src/components/misc/Turnstile.svelte b/web/src/components/misc/Turnstile.svelte index b1fc162c..6b714c49 100644 --- a/web/src/components/misc/Turnstile.svelte +++ b/web/src/components/misc/Turnstile.svelte @@ -18,15 +18,15 @@ const setup = () => { window.turnstile?.render(turnstileElement, { sitekey, + "refresh-expired": "never", + "error-callback": (error) => { console.log("error code from turnstile:", error); return true; }, "expired-callback": () => { - console.log("turnstile expired. i am callback this is my message") - }, - "timeout-callback": () => { - console.log("turnstile timed out. i am callback this is my message") + console.log("turnstile expired, refreshing neow"); + turnstile.reset(); }, callback: () => { $turnstileSolved = true; @@ -39,10 +39,6 @@ } else { turnstileScript.addEventListener("load", setup); } - - window.addEventListener("focus", () => { - turnstile.refreshIfExpired(); - }); }); diff --git a/web/src/lib/api/session.ts b/web/src/lib/api/session.ts index f627b6f2..5b3e542b 100644 --- a/web/src/lib/api/session.ts +++ b/web/src/lib/api/session.ts @@ -35,7 +35,7 @@ export const requestSession = async () => { } }); - turnstile.update(); + turnstile.reset(); return response; } diff --git a/web/src/lib/api/turnstile.ts b/web/src/lib/api/turnstile.ts index 6a39ad7e..b95ecfd1 100644 --- a/web/src/lib/api/turnstile.ts +++ b/web/src/lib/api/turnstile.ts @@ -10,7 +10,7 @@ const getResponse = () => { return null; } -const update = () => { +const reset = () => { const turnstileElement = document.getElementById("turnstile-widget"); if (turnstileElement) { @@ -21,21 +21,7 @@ const update = () => { return null; } -const refreshIfExpired = () => { - const turnstileElement = document.getElementById("turnstile-widget"); - - if (turnstileElement) { - const isExpired = window?.turnstile?.isExpired(turnstileElement); - if (isExpired) { - console.log("expired, refreshing the turnstile widget rn"); - return update(); - } - console.log("turnstile not expired, nothing to do"); - } -} - export default { getResponse, - update, - refreshIfExpired, + reset, } From ea73d09c8ff35b0ef14813a1143f84055f3774e0 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 19 Nov 2024 00:33:07 +0600 Subject: [PATCH 223/379] web/Turnstile: reduce retry interval to 800ms --- web/src/components/misc/Turnstile.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/misc/Turnstile.svelte b/web/src/components/misc/Turnstile.svelte index 6b714c49..adedb0ee 100644 --- a/web/src/components/misc/Turnstile.svelte +++ b/web/src/components/misc/Turnstile.svelte @@ -19,6 +19,7 @@ window.turnstile?.render(turnstileElement, { sitekey, "refresh-expired": "never", + "retry-interval": 800, "error-callback": (error) => { console.log("error code from turnstile:", error); From 7b9830c5af9ed731cf1d37ad693312eba58e4b29 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 19 Nov 2024 14:20:12 +0000 Subject: [PATCH 224/379] dockerfile: drop privileges to regular user --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1af6273a..7bfc3dac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,10 @@ RUN pnpm deploy --filter=@imput/cobalt-api --prod /prod/api FROM base AS api WORKDIR /app -COPY --from=build /prod/api /app -COPY --from=build /app/.git /app/.git +COPY --from=build --chown=node:node /prod/api /app +COPY --from=build --chown=node:node /app/.git /app/.git + +USER node EXPOSE 9000 CMD [ "node", "src/cobalt" ] From 540bbbdad7e3feb0e2b3ee0dbdf4f6e8f4ef9e61 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 14:14:37 +0600 Subject: [PATCH 225/379] web/SidebarTab: pass icon prop instead of using slot --- web/src/components/sidebar/Sidebar.svelte | 28 ++++++-------------- web/src/components/sidebar/SidebarTab.svelte | 19 +++++++------ 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/web/src/components/sidebar/Sidebar.svelte b/web/src/components/sidebar/Sidebar.svelte index ca3a963e..39407a19 100644 --- a/web/src/components/sidebar/Sidebar.svelte +++ b/web/src/components/sidebar/Sidebar.svelte @@ -19,8 +19,8 @@ let aboutLink = defaultNavPage("about"); $: screenWidth, - settingsLink = defaultNavPage("settings"), - aboutLink = defaultNavPage("about"); + (settingsLink = defaultNavPage("settings")), + (aboutLink = defaultNavPage("about")); @@ -29,26 +29,14 @@ diff --git a/web/src/components/sidebar/SidebarTab.svelte b/web/src/components/sidebar/SidebarTab.svelte index 9f3b51ef..2330039f 100644 --- a/web/src/components/sidebar/SidebarTab.svelte +++ b/web/src/components/sidebar/SidebarTab.svelte @@ -3,8 +3,10 @@ import { t } from "$lib/i18n/translations"; - export let tabName: string; - export let tabLink: string; + export let name: string; + export let path: string; + export let icon: ConstructorOfATypedSvelteComponent; + export let beta = false; const firstTabPage = ["save", "remux", "settings"]; @@ -12,14 +14,14 @@ let tab: HTMLElement; $: currentTab = $page.url.pathname.split("/")[1]; - $: baseTabPath = tabLink.split("/")[1]; + $: baseTabPath = path.split("/")[1]; $: isTabActive = currentTab === baseTabPath; const showTab = (e: HTMLElement) => { if (e) { e.scrollIntoView({ - inline: firstTabPage.includes(tabName) ? "end" : "start", + inline: firstTabPage.includes(name) ? "end" : "start", block: "nearest", behavior: "smooth", }); @@ -32,10 +34,10 @@ showTab(tab)} role="tab" @@ -44,8 +46,9 @@ {#if beta}
β
{/if} - - {$t(`tabs.${tabName}`)} + + + {$t(`tabs.${name}`)}
From 94e5aad6c0ee5f1556c7c6513e2d8fb0b904970c Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:33:09 +0600 Subject: [PATCH 230/379] web/Toggle: accommodate for rtl layouts --- web/src/components/misc/Toggle.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/components/misc/Toggle.svelte b/web/src/components/misc/Toggle.svelte index fcb0aa92..bb935b14 100644 --- a/web/src/components/misc/Toggle.svelte +++ b/web/src/components/misc/Toggle.svelte @@ -1,8 +1,8 @@ -
+
@@ -10,6 +10,7 @@ .toggle { --base-size: 22px; --ratio-factor: 0.9; + --enabled-pos: calc(100% * var(--ratio-factor)); display: flex; justify-content: start; @@ -23,6 +24,10 @@ transition: background 0.2s; } + .toggle:dir(rtl) { + --enabled-pos: calc(-100% * var(--ratio-factor)); + } + .toggle-switcher { height: var(--base-size); width: var(--base-size); @@ -37,6 +42,7 @@ } .toggle.enabled .toggle-switcher { - transform: translateX(calc(100% * var(--ratio-factor))); + transform: translateX(var(--enabled-pos)); + } } From 72c30a58aa86676e326d39210d8bc13c1b07c05f Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:33:27 +0600 Subject: [PATCH 231/379] web/Switcher: fix rounded corners in RTL layout --- web/src/components/buttons/Switcher.svelte | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/web/src/components/buttons/Switcher.svelte b/web/src/components/buttons/Switcher.svelte index 2ead0caf..0792c9e4 100644 --- a/web/src/components/buttons/Switcher.svelte +++ b/web/src/components/buttons/Switcher.svelte @@ -43,6 +43,20 @@ border-bottom-left-radius: 0; } + .switcher:not(.big):dir(rtl) :global(.button:first-child) { + border-top-right-radius: inherit; + border-bottom-right-radius: inherit; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .switcher:not(.big):dir(rtl) :global(.button:last-child) { + border-top-left-radius: inherit; + border-bottom-left-radius: inherit; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .switcher.big { background: var(--button); box-shadow: var(--button-box-shadow); From e7c2196a25a701eefa36fd1196b87a8bdf0613c4 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:33:51 +0600 Subject: [PATCH 232/379] web/DownloadButton: adapt for RTL layout --- .../components/save/buttons/DownloadButton.svelte | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/web/src/components/save/buttons/DownloadButton.svelte b/web/src/components/save/buttons/DownloadButton.svelte index 43a7412d..981bb0ad 100644 --- a/web/src/components/save/buttons/DownloadButton.svelte +++ b/web/src/components/save/buttons/DownloadButton.svelte @@ -183,6 +183,16 @@ border-bottom-right-radius: var(--border-radius); } + #download-button:dir(rtl) { + border-left: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + border-right: 1.5px var(--input-border) solid; + border-top-left-radius: var(--border-radius); + border-bottom-left-radius: var(--border-radius); + } + #download-button:focus-visible { box-shadow: 0 0 0 2px var(--blue) inset; } @@ -209,6 +219,11 @@ border-left: 2px var(--secondary) solid; } + :global(#input-container.focused) #download-button:dir(rtl) { + border-left: 0; + border-right: 2px var(--secondary) solid; + } + @media (hover: hover) { #download-button:hover { background: var(--button-hover-transparent); From 88ed5876ae0fa83aac106499722ccf37a1ed4881 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:34:10 +0600 Subject: [PATCH 233/379] web/Omnibox: adapt for RTL layout --- web/src/components/save/Omnibox.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index a1e9873a..108371ac 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -219,12 +219,13 @@ } #input-container { + --input-padding: 10px; display: flex; box-shadow: 0 0 0 1.5px var(--input-border) inset; border-radius: var(--border-radius); - padding: 0 10px; + padding: 0 var(--input-padding); align-items: center; - gap: 10px; + gap: var(--input-padding); font-size: 14px; flex: 1; } @@ -233,6 +234,11 @@ padding-right: 0; } + #input-container.downloadable:dir(rtl) { + padding-right: var(--input-padding); + padding-left: 0; + } + #input-container.focused { box-shadow: 0 0 0 1.5px var(--secondary) inset; outline: var(--secondary) 0.5px solid; @@ -250,7 +256,7 @@ display: flex; width: 100%; margin: 0; - padding: 10px 0; + padding: var(--input-padding) 0; height: 18px; align-items: center; From 45e639a7e1afab2ba5dd1c5da02463b937a86e33 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:34:23 +0600 Subject: [PATCH 234/379] web/Sidebar: fix padding in RTL layout --- web/src/components/sidebar/Sidebar.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/components/sidebar/Sidebar.svelte b/web/src/components/sidebar/Sidebar.svelte index 39407a19..1733cf09 100644 --- a/web/src/components/sidebar/Sidebar.svelte +++ b/web/src/components/sidebar/Sidebar.svelte @@ -107,6 +107,16 @@ #sidebar :global(.sidebar-inner-container:last-child) { padding-right: calc(var(--border-radius) * 2); } + + #sidebar :global(.sidebar-inner-container:first-child:dir(rtl)) { + padding-left: 0; + padding-right: calc(var(--border-radius) * 2); + } + + #sidebar :global(.sidebar-inner-container:last-child:dir(rtl)) { + padding-right: 0; + padding-left: calc(var(--border-radius) * 2); + } } /* add padding for notch / dynamic island in landscape */ From 620bd2424356cbbf6a5019f8c845bdff2a9f402d Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:34:37 +0600 Subject: [PATCH 235/379] web/PageNav: fix page padding in RTL layout --- web/src/components/subnav/PageNav.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/components/subnav/PageNav.svelte b/web/src/components/subnav/PageNav.svelte index 7fa46b0e..83e3f0a0 100644 --- a/web/src/components/subnav/PageNav.svelte +++ b/web/src/components/subnav/PageNav.svelte @@ -129,6 +129,11 @@ padding-left: var(--subnav-padding); } + .subnav-page:dir(rtl) { + padding-left: 0; + padding-right: var(--subnav-padding); + } + .subnav-page-content { display: flex; flex-direction: column; @@ -211,7 +216,8 @@ } @media screen and (max-width: 750px) { - .subnav-page { + .subnav-page, + .subnav-page:dir(rtl) { --subnav-nav-width: 100%; display: flex; flex-direction: column; From c9833a358b7e978a721cb83ce36afcb21c7720a7 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:34:59 +0600 Subject: [PATCH 236/379] web/layout: fix content rounded corners in RTL layout --- web/src/routes/+layout.svelte | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 03bebef3..8f3a4f17 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -278,21 +278,30 @@ display: flex; overflow: scroll; background-color: var(--primary); - border-top-left-radius: var(--border-radius); border-bottom-left-radius: var(--border-radius); } + #content:dir(rtl) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + } + @media screen and (max-width: 535px) { #cobalt { display: grid; grid-template-columns: unset; grid-template-rows: 1fr var(--sidebar-height-mobile); } - #content { + + #content, + #content:dir(rtl) { padding-top: env(safe-area-inset-top); order: -1; border-top-left-radius: 0; + border-top-right-radius: 0; border-bottom-left-radius: calc(var(--border-radius) * 2); border-bottom-right-radius: calc(var(--border-radius) * 2); } From c50cecae92500fbdc0549ff40109edaa3456d9d9 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:35:36 +0600 Subject: [PATCH 237/379] web/settings: replace advanced settings icon with a cooler one --- web/src/routes/settings/+layout.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/routes/settings/+layout.svelte b/web/src/routes/settings/+layout.svelte index 79bd7b93..a1f7bfb9 100644 --- a/web/src/routes/settings/+layout.svelte +++ b/web/src/routes/settings/+layout.svelte @@ -18,7 +18,7 @@ import IconBug from "@tabler/icons-svelte/IconBug.svelte"; import IconWorld from "@tabler/icons-svelte/IconWorld.svelte"; - import IconSettingsBolt from "@tabler/icons-svelte/IconSettingsBolt.svelte"; + import IconAdjustmentsStar from "@tabler/icons-svelte/IconAdjustmentsStar.svelte"; $: versionText = $version ? `v${$version.version}-${$version.commit.slice(0, 8)}` @@ -78,7 +78,7 @@ {#if $settings.advanced.debug} From b8c1c1fe5103c1f3a27c6bf0f1152fadde6e61a4 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 15:41:36 +0600 Subject: [PATCH 238/379] web/Toggle: remove accidentally committed bracket --- web/src/components/misc/Toggle.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/misc/Toggle.svelte b/web/src/components/misc/Toggle.svelte index bb935b14..2ab52c32 100644 --- a/web/src/components/misc/Toggle.svelte +++ b/web/src/components/misc/Toggle.svelte @@ -44,5 +44,4 @@ .toggle.enabled .toggle-switcher { transform: translateX(var(--enabled-pos)); } - } From 1374693c2f68a15e55b1d0a0afea7f114f383573 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 20 Nov 2024 16:06:48 +0600 Subject: [PATCH 239/379] web/Toggle: make the toggle stretchy --- web/src/components/misc/Toggle.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/components/misc/Toggle.svelte b/web/src/components/misc/Toggle.svelte index 2ab52c32..7101df95 100644 --- a/web/src/components/misc/Toggle.svelte +++ b/web/src/components/misc/Toggle.svelte @@ -34,7 +34,7 @@ background: var(--white); border-radius: 100px; transform: translateX(0%); - transition: transform 0.2s; + transition: transform 0.2s, width 0.2s; } .toggle.enabled { @@ -44,4 +44,8 @@ .toggle.enabled .toggle-switcher { transform: translateX(var(--enabled-pos)); } + + :global(.toggle-container:active .toggle:not(.enabled) .toggle-switcher) { + width: calc(var(--base-size) * 1.3); + } From f1f995515969ff5040a527e66af2c84ac91c60d3 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 00:32:08 +0600 Subject: [PATCH 240/379] web/i18n/error: rephrase a bunch of strings for more clarity and context i didn't expect to rewrite this much ngl --- web/i18n/en/error.json | 62 +++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 638caa4a..f350d729 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -1,55 +1,55 @@ { - "import.no_data": "there's nothing to load from the file. are you sure it's the right one?", - "import.invalid": "your file doesn't have valid cobalt settings to import. are you sure it's the right one?", + "import.no_data": "there are no settings to load from this file. are you sure it's the right one?", + "import.invalid": "this file doesn't have valid cobalt settings to import. are you sure it's the right one?", "import.unknown": "couldn't load data from the file. it may be corrupted or of wrong format. here's the error i got:\n\n{{ value }}", "remux.corrupted": "couldn't read the metadata from this file, it may be corrupted.", - "remux.out_of_resources": "cobalt ran out of resources and can't continue with on-device processing. this is related to limitations on your browser's side. try refreshing or reopening the app and trying again. some devices can only process tiny files.", + "remux.out_of_resources": "cobalt ran out of resources and can't continue with on-device processing. this is caused by your browser's limitations. refresh or reopen the app and try again!", - "tunnel.probe": "couldn't verify whether you can download this file. try again in a few seconds!", + "tunnel.probe": "couldn't test this tunnel. your browser or network configuration may be blocking access to one of cobalt servers. are you sure you don't have any weird browser extensions?", - "captcha_ongoing": "still checking if you're not a bot. wait for the spinner to disappear and try again.\n\nif it takes too long, please let us know! we use cloudflare turnstile for bot protection and sometimes it blocks people for no reason.", + "captcha_ongoing": "cloudflare turnstile is still checking if you're not a bot.\n\nif it takes too long, you can try: disabling weird browser extensions, changing networks, using a different browser, or checking your device for malware.", - "api.auth.jwt.missing": "couldn't confirm whether you're not a robot because the processing server didn't receive the human access token. try again in a few seconds or reload the page!", - "api.auth.jwt.invalid": "couldn't confirm whether you're not a robot because your human access token expired and wasn't renewed. try again in a few seconds or reload the page!", - "api.auth.turnstile.missing": "couldn't confirm whether you're not a robot because the processing server didn't receive the human access token. try again in a few seconds or reload the page!", - "api.auth.turnstile.invalid": "couldn't confirm whether you're not a robot because your human access token expired and wasn't renewed. try again in a few seconds or reload the page!", + "api.auth.jwt.missing": "couldn't authenticate with the processing instance because the access token is missing. try again in a few seconds or reload the page!", + "api.auth.jwt.invalid": "couldn't authenticate with the processing instance because the access token expired. try again in a few seconds or reload the page!", + "api.auth.turnstile.missing": "couldn't authenticate with the processing instance because the captcha solution is missing. try again in a few seconds or reload the page!", + "api.auth.turnstile.invalid": "couldn't authenticate with the processing instance because the captcha solution is invalid. try again in a few seconds or reload the page!", - "api.unreachable": "couldn't connect to the processing server. check your internet connection and try again.", - "api.timed_out": "the processing server took way too long to respond. it may be overwhelmed at the moment, try again in a few seconds!", - "api.rate_exceeded": "you're making way too many requests. try again in {{ limit }} seconds!", - "api.capacity": "cobalt is at capacity and can't process your request at the moment. try again in a few seconds. if it still doesn't work, let us know and we'll try to help!", + "api.unreachable": "couldn't connect to the processing instance. check your internet connection and try again!", + "api.timed_out": "the processing instance took too long to respond. it may be overwhelmed at the moment, try again in a few seconds!", + "api.rate_exceeded": "you're making too many requests. try again in {{ limit }} seconds.", + "api.capacity": "cobalt is at capacity and can't process your request at the moment. try again in a few seconds!", - "api.generic": "something went wrong and i couldn't get anything for you. try again in a few seconds, but if issue sticks, let us know and we'll try to help!", - "api.unknown_response": "couldn't parse the response from the server. this could be caused by a version mismatch. are you sure you're on the latest version of cobalt?", + "api.generic": "something went wrong and i couldn't get anything for you, try again in a few seconds. if the issue sticks, please report it!", + "api.unknown_response": "couldn't read the response from the processing instance. this could be caused by a version mismatch between cobalt instances.", "api.service.unsupported": "this service is not supported yet. have you pasted the right link?", - "api.service.disabled": "this service is supported by cobalt, but it's disabled on this instance. try a link from another service!", + "api.service.disabled": "this service is generally supported by cobalt, but it's disabled on this processing instance. try a link from another service!", "api.link.invalid": "your link is invalid or this service is not supported yet. have you pasted the right link?", "api.link.unsupported": "{{ service }} is supported, but i couldn't recognize your link. have you pasted the right one?", - "api.fetch.fail": "something went wrong when fetching info from {{ service }} and i couldn't find anything for you. are you sure your link works? if it does and you still see this error, let us know and we'll try to help!", - "api.fetch.critical": "the {{ service }} module returned an error that i don't recognize. try again in a few seconds, but if issue sticks, let us know!", + "api.fetch.fail": "something went wrong when fetching info from {{ service }} and i couldn't get anything for you. if this issue sticks, please report it!", + "api.fetch.critical": "the {{ service }} module returned an error that i don't recognize. try again in a few seconds, but if this issue sticks, please report it!", "api.fetch.empty": "couldn't find any media that i could download for you. are you sure you pasted the right link?", - "api.fetch.rate": "the cobalt processing server got rate limited by the {{ service }} api. try again in a few seconds!", - "api.fetch.short_link": "couldn't get link info from the short link. are you sure it works? if it does and you still get this error, let us know, and we'll try to help!", + "api.fetch.rate": "the processing instance got rate limited by {{ service }}. try again in a few seconds!", + "api.fetch.short_link": "couldn't get info from the short link. are you sure it works? if it does and you still get this error, please report the issue!", - "api.content.too_long": "the media you requested is too long. current duration limit is {{ limit }} minutes. try something shorter instead!", + "api.content.too_long": "media you requested is too long. the duration limit on this instance is {{ limit }} minutes. try something shorter instead!", - "api.content.video.unavailable": "i can't access this video. it may be restricted on {{ service }}'s side. have you pasted the right link?", - "api.content.video.live": "this video is currently live, so i can't download it yet. wait for the live stream to finish, and then try again!", - "api.content.video.private": "this video is private, so i cannot access it. change its visibility or try another one!", + "api.content.video.unavailable": "i can't access this video. it may be restricted on {{ service }}'s side. try another one!", + "api.content.video.live": "this video is currently live, so i can't download it yet. wait for the live stream to finish and try again!", + "api.content.video.private": "this video is private, so i can't access it. change its visibility or try another one!", "api.content.video.age": "this video is age-restricted, so i can't access it anonymously. try another one!", - "api.content.video.region": "this video is region locked, and the processing server is in a different location. try another one!", + "api.content.video.region": "this video is region locked, and the processing instance is in a different location. try another one!", - "api.content.post.unavailable": "couldn't find anything about this post. its visibility may be limited or it may not exist at all. make sure your link works and try again in a few seconds!", - "api.content.post.private": "this post is from a private account, so i can't access it. have you pasted the right link?", - "api.content.post.age": "this post is age-restricted, so i can't access it anonymously. have you pasted the right link?", + "api.content.post.unavailable": "couldn't find anything about this post. its visibility may be limited or it may not exist. make sure your link works and try again in a few seconds!", + "api.content.post.private": "this post is from a private account, so i can't access it. try another one!", + "api.content.post.age": "this post is age-restricted, so i can't access it anonymously. try another one!", - "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo. either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them.", - "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video.\n\ntry again in a few seconds, but if issue sticks, contact us for support.", - "api.youtube.login": "couldn't get this video because youtube labeled me as a bot. this is potentially caused by the processing instance not having any active account tokens. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", + "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo, either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them. try enabling the hls option in video settings!", + "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video. try again in a few seconds, but if this issue sticks, please report it!", + "api.youtube.login": "couldn't get this video because youtube asked the instance to log in. this is potentially caused by the processing instance not having any active account tokens. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", "api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", "api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!" } From 5b445d5c7e5d451780d7bb1afa907a050b024b29 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 15:37:42 +0600 Subject: [PATCH 241/379] api/youtube: catch even more innertube errors --- api/src/processing/services/youtube.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index e63a21b2..9e99d62f 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -143,13 +143,22 @@ export default async function(o) { try { info = await yt.getBasicInfo(o.id, useHLS ? 'IOS' : 'ANDROID'); } catch (e) { - if (e?.info?.reason === "This video is private") { - return { error: "content.video.private" }; - } else if (e?.message === "This video is unavailable") { - return { error: "content.video.unavailable" }; - } else { - return { error: "fetch.fail" }; + if (e?.info) { + const errorInfo = JSON.parse(e?.info); + + if (errorInfo?.reason === "This video is private") { + return { error: "content.video.private" }; + } + if (["INVALID_ARGUMENT", "UNAUTHENTICATED"].includes(errorInfo?.error?.status)) { + return { error: "youtube.api_error" }; + } } + + if (e?.message === "This video is unavailable") { + return { error: "content.video.unavailable" }; + } + + return { error: "fetch.fail" }; } if (!info) return { error: "fetch.fail" }; From 7fa387b12f80478e06b45bd91d0b2ae56445750f Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 15:38:33 +0600 Subject: [PATCH 242/379] web/i18n/error: add youtube api error and update the login error --- web/i18n/en/error.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index f350d729..24434cbb 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -49,7 +49,8 @@ "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo, either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them. try enabling the hls option in video settings!", "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video. try again in a few seconds, but if this issue sticks, please report it!", - "api.youtube.login": "couldn't get this video because youtube asked the instance to log in. this is potentially caused by the processing instance not having any active account tokens. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", + "api.youtube.login": "couldn't get this video because youtube asked the instance to log in. this is potentially caused by the processing instance not having any active account tokens or youtube updating something about their api. try again in a few seconds, but if it still doesn't work, please report this issue!", "api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", - "api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!" + "api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!", + "api.youtube.api_error": "youtube updated something about its api and i couldn't get any info about this video. try again in a few seconds, but if this issue sticks, please report it!" } From ff9e248e4f6d255ad85d01fc9c2d647705e47559 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 15:42:47 +0600 Subject: [PATCH 243/379] api/util/test: add twitter to finnicky list they seemingly blocked ips of github workers --- api/src/util/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/util/test.js b/api/src/util/test.js index 256a92fe..2ba555ed 100644 --- a/api/src/util/test.js +++ b/api/src/util/test.js @@ -13,7 +13,7 @@ const getTests = (service) => loadJSON(getTestPath(service)); // services that are known to frequently fail due to external // factors (e.g. rate limiting) -const finnicky = new Set(['bilibili', 'instagram', 'facebook', 'youtube', 'vk']); +const finnicky = new Set(['bilibili', 'instagram', 'facebook', 'youtube', 'vk', 'twitter']); const runTestsFor = async (service) => { const tests = getTests(service); From 5b60065c9f8af638a7290ed03542326b6eb0fb7b Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 16:57:34 +0600 Subject: [PATCH 244/379] web/about/terms: update the abuse email --- web/i18n/en/about/terms.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/about/terms.md b/web/i18n/en/about/terms.md index aa32fd65..634e7502 100644 --- a/web/i18n/en/about/terms.md +++ b/web/i18n/en/about/terms.md @@ -49,7 +49,7 @@ fair use and credits benefit everyone. /> we have no way of detecting abusive behavior automatically because cobalt is 100% anonymous. -however, you can report such activities to us and we will do our best to comply manually: **safety@imput.net** +however, you can report such activities to us via email and we'll do our best to comply manually: abuse[at]imput.net **this email is not intended for user support, you will not get a response if your concern is not related to abuse.** From baebeed488b94be5ec93eaeadb7b8e4d03b691ea Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 19:08:24 +0600 Subject: [PATCH 245/379] web/settings/v4: add api key settings, remove override settings --- web/src/lib/settings/defaults.ts | 6 +++--- web/src/lib/types/settings.ts | 14 ++++++++------ web/src/lib/types/settings/v3.ts | 2 +- web/src/lib/types/settings/v4.ts | 9 +++++++++ 4 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 web/src/lib/types/settings/v4.ts diff --git a/web/src/lib/settings/defaults.ts b/web/src/lib/settings/defaults.ts index e629a306..a4448aaa 100644 --- a/web/src/lib/settings/defaults.ts +++ b/web/src/lib/settings/defaults.ts @@ -2,7 +2,7 @@ import { defaultLocale } from "$lib/i18n/translations"; import type { CobaltSettings } from "$lib/types/settings"; const defaultSettings: CobaltSettings = { - schemaVersion: 3, + schemaVersion: 4, advanced: { debug: false, }, @@ -33,10 +33,10 @@ const defaultSettings: CobaltSettings = { disableAnalytics: false, }, processing: { - allowDefaultOverride: false, customInstanceURL: "", + customApiKey: "", enableCustomInstances: false, - seenOverrideWarning: false, + enableCustomApiKey: false, seenCustomWarning: false, } } diff --git a/web/src/lib/types/settings.ts b/web/src/lib/types/settings.ts index fd9f31a5..93958e88 100644 --- a/web/src/lib/types/settings.ts +++ b/web/src/lib/types/settings.ts @@ -1,13 +1,15 @@ import type { RecursivePartial } from "$lib/types/generic"; -import type { CobaltSettingsV2 } from "./settings/v2"; -import type { CobaltSettingsV3 } from "./settings/v3"; +import type { CobaltSettingsV2 } from "$lib/types/settings/v2"; +import type { CobaltSettingsV3 } from "$lib/types/settings/v3"; +import type { CobaltSettingsV4 } from "$lib/types/settings/v4"; -export * from "./settings/v2"; -export * from "./settings/v3"; +export * from "$lib/types/settings/v2"; +export * from "$lib/types/settings/v3"; +export * from "$lib/types/settings/v4"; -export type CobaltSettings = CobaltSettingsV3; +export type CobaltSettings = CobaltSettingsV4; -export type AnyCobaltSettings = CobaltSettingsV2 | CobaltSettings; +export type AnyCobaltSettings = CobaltSettingsV3 | CobaltSettingsV2 | CobaltSettings; export type PartialSettings = RecursivePartial; diff --git a/web/src/lib/types/settings/v3.ts b/web/src/lib/types/settings/v3.ts index fb376d8e..7d02f2da 100644 --- a/web/src/lib/types/settings/v3.ts +++ b/web/src/lib/types/settings/v3.ts @@ -1,5 +1,5 @@ import type { YoutubeLang } from "$lib/settings/youtube-lang"; -import { type CobaltSettingsV2 } from "./v2"; +import { type CobaltSettingsV2 } from "$lib/types/settings/v2"; export type CobaltSettingsV3 = Omit & { schemaVersion: 3, diff --git a/web/src/lib/types/settings/v4.ts b/web/src/lib/types/settings/v4.ts new file mode 100644 index 00000000..cf27a0f2 --- /dev/null +++ b/web/src/lib/types/settings/v4.ts @@ -0,0 +1,9 @@ +import { type CobaltSettingsV3 } from "$lib/types/settings/v3"; + +export type CobaltSettingsV4 = Omit & { + schemaVersion: 4, + processing: Omit & { + customApiKey: string; + enableCustomApiKey: boolean; + }; +}; From 8415d0e4f36446bf9e5812ae75b3f346282faac8 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 19:08:41 +0600 Subject: [PATCH 246/379] web/i18n/error: update invalid jwt token error --- web/i18n/en/error.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 24434cbb..6cb04100 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -11,7 +11,7 @@ "captcha_ongoing": "cloudflare turnstile is still checking if you're not a bot.\n\nif it takes too long, you can try: disabling weird browser extensions, changing networks, using a different browser, or checking your device for malware.", "api.auth.jwt.missing": "couldn't authenticate with the processing instance because the access token is missing. try again in a few seconds or reload the page!", - "api.auth.jwt.invalid": "couldn't authenticate with the processing instance because the access token expired. try again in a few seconds or reload the page!", + "api.auth.jwt.invalid": "couldn't authenticate with the processing instance because the access token is invalid. try again in a few seconds or reload the page!", "api.auth.turnstile.missing": "couldn't authenticate with the processing instance because the captcha solution is missing. try again in a few seconds or reload the page!", "api.auth.turnstile.invalid": "couldn't authenticate with the processing instance because the captcha solution is invalid. try again in a few seconds or reload the page!", From 7c7cefe89bc1daea03ae85c413c84a57e2d18f8b Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 19:11:19 +0600 Subject: [PATCH 247/379] web/settings: add a reusable `SettingsInput` component --- .../settings/CustomInstanceInput.svelte | 167 --------------- .../components/settings/SettingsInput.svelte | 193 ++++++++++++++++++ .../routes/settings/instances/+page.svelte | 9 +- 3 files changed, 199 insertions(+), 170 deletions(-) delete mode 100644 web/src/components/settings/CustomInstanceInput.svelte create mode 100644 web/src/components/settings/SettingsInput.svelte diff --git a/web/src/components/settings/CustomInstanceInput.svelte b/web/src/components/settings/CustomInstanceInput.svelte deleted file mode 100644 index 7ebdb3cf..00000000 --- a/web/src/components/settings/CustomInstanceInput.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - -
-
- { - checkUrl(); - inputFocused = true; - }} - on:input={() => (inputFocused = true)} - on:focus={() => (inputFocused = true)} - on:blur={() => (inputFocused = false)} - spellcheck="false" - autocomplete="off" - autocapitalize="off" - maxlength="64" - placeholder={$t("settings.processing.custom.placeholder")} - /> -
-
- - - -
-
- - diff --git a/web/src/components/settings/SettingsInput.svelte b/web/src/components/settings/SettingsInput.svelte new file mode 100644 index 00000000..3fda62b4 --- /dev/null +++ b/web/src/components/settings/SettingsInput.svelte @@ -0,0 +1,193 @@ + + +
+
+ checkInput()} + on:input={() => (inputFocused = true)} + on:focus={() => (inputFocused = true)} + on:blur={() => (inputFocused = false)} + spellcheck="false" + autocomplete="off" + autocapitalize="off" + maxlength="64" + {placeholder} + /> +
+ +
+ + + +
+
+ + diff --git a/web/src/routes/settings/instances/+page.svelte b/web/src/routes/settings/instances/+page.svelte index 31b7934f..324a9af6 100644 --- a/web/src/routes/settings/instances/+page.svelte +++ b/web/src/routes/settings/instances/+page.svelte @@ -3,15 +3,14 @@ import { t } from "$lib/i18n/translations"; + import SettingsInput from "$components/settings/SettingsInput.svelte"; import SettingsToggle from "$components/buttons/SettingsToggle.svelte"; import SettingsCategory from "$components/settings/SettingsCategory.svelte"; - import CustomInstanceInput from "$components/settings/CustomInstanceInput.svelte";
{#if $settings.processing.enableCustomInstances} - + {/if}
From 601597eb151eabfe7d87edf54f3144f2cc14df80 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 23 Nov 2024 19:13:23 +0600 Subject: [PATCH 248/379] web: add support for custom api keys & improve turnstile states --- web/i18n/en/settings.json | 5 +- web/src/components/save/Omnibox.svelte | 7 +-- web/src/lib/api/api.ts | 60 ++++++++++++------- web/src/lib/state/turnstile.ts | 15 ++++- web/src/routes/+layout.svelte | 28 +++++---- .../routes/settings/instances/+page.svelte | 26 ++++++++ 6 files changed, 101 insertions(+), 40 deletions(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index b18b375a..b4665ee9 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -114,9 +114,10 @@ "advanced.data": "data management", "processing.community": "community instances", - "processing.enable_custom.title": "use a custom processing server", "processing.enable_custom.description": "cobalt will use a custom processing server if you choose to. even though cobalt has some security measures in place, we are not responsible for any damages done via a community instance, as we have no control over them.\n\nplease be mindful of what instances you use and make sure they're hosted by people you trust.", - "processing.custom.placeholder": "custom instance domain" + "processing.access_key": "instance access key", + "processing.access_key.title": "use an instance access key", + "processing.access_key.description": "cobalt will use this key to make requests to the api instance." } diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index 108371ac..8da051d4 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -7,12 +7,11 @@ import { SvelteComponent, tick } from "svelte"; import { t } from "$lib/i18n/translations"; - import { cachedInfo } from "$lib/api/server-info"; import dialogs from "$lib/state/dialogs"; import { link } from "$lib/state/omnibox"; import { updateSetting } from "$lib/state/settings"; - import { turnstileSolved } from "$lib/state/turnstile"; + import { turnstileEnabled, turnstileSolved } from "$lib/state/turnstile"; import type { Optional } from "$lib/types/generic"; import type { DownloadModeOption } from "$lib/types/settings"; @@ -37,8 +36,8 @@ let isDisabled = false; let isLoading = false; - $: isBotCheckOngoing = - !!$cachedInfo?.info?.cobalt?.turnstileSitekey && !$turnstileSolved; + + $: isBotCheckOngoing = $turnstileEnabled && !$turnstileSolved; const validLink = (url: string) => { try { diff --git a/web/src/lib/api/api.ts b/web/src/lib/api/api.ts index f69c3323..0f2d5b9f 100644 --- a/web/src/lib/api/api.ts +++ b/web/src/lib/api/api.ts @@ -5,12 +5,45 @@ import lazySettingGetter from "$lib/settings/lazy-get"; import { getSession } from "$lib/api/session"; import { currentApiURL } from "$lib/api/api-url"; -import { turnstileSolved } from "$lib/state/turnstile"; +import { turnstileEnabled, turnstileSolved } from "$lib/state/turnstile"; import { cachedInfo, getServerInfo } from "$lib/api/server-info"; import type { Optional } from "$lib/types/generic"; import type { CobaltAPIResponse, CobaltErrorResponse } from "$lib/types/api"; +const getAuthorization = async () => { + const processing = get(settings).processing; + + if (get(turnstileEnabled)) { + if (!get(turnstileSolved)) { + return { + status: "error", + error: { + code: "error.captcha_ongoing" + } + } as CobaltErrorResponse; + } + + const session = await getSession(); + + if (session) { + if ("error" in session) { + if (session.error.code !== "error.api.auth.not_configured") { + return session; + } + } else { + return `Bearer ${session.token}`; + } + } + } + + if (processing.enableCustomApiKey && processing.customApiKey.length > 0) { + return `Api-Key ${processing.customApiKey}`; + } + + return false; +} + const request = async (url: string) => { const getSetting = lazySettingGetter(get(settings)); @@ -49,31 +82,14 @@ const request = async (url: string) => { } as CobaltErrorResponse; } - if (getCachedInfo?.info?.cobalt?.turnstileSitekey && !get(turnstileSolved)) { - return { - status: "error", - error: { - code: "error.captcha_ongoing" - } - } as CobaltErrorResponse; - } - const api = currentApiURL(); - - const session = getCachedInfo?.info?.cobalt?.turnstileSitekey - ? await getSession() : undefined; + const authorization = await getAuthorization(); let extraHeaders = {} - if (session) { - if ("error" in session) { - if (session.error.code !== "error.api.auth.not_configured") { - return session; - } - } else { - extraHeaders = { - "Authorization": `Bearer ${session.token}`, - }; + if (authorization) { + extraHeaders = { + "Authorization": authorization } } diff --git a/web/src/lib/state/turnstile.ts b/web/src/lib/state/turnstile.ts index 12231b11..fffd90da 100644 --- a/web/src/lib/state/turnstile.ts +++ b/web/src/lib/state/turnstile.ts @@ -1,4 +1,17 @@ -import { writable } from "svelte/store"; +import settings from "$lib/state/settings"; +import { cachedInfo } from "$lib/api/server-info"; +import { derived, writable } from "svelte/store"; export const turnstileSolved = writable(false); export const turnstileCreated = writable(false); + +export const turnstileEnabled = derived( + [settings, cachedInfo], + ([$settings, $cachedInfo]) => { + return !!$cachedInfo?.info?.cobalt?.turnstileSitekey && + !( + $settings.processing.enableCustomApiKey && + $settings.processing.customApiKey.length > 0 + ) + } +) diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 8f3a4f17..1a878aff 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -7,18 +7,18 @@ import { updated } from "$app/stores"; import { browser } from "$app/environment"; import { afterNavigate } from "$app/navigation"; - import { getServerInfo, cachedInfo } from "$lib/api/server-info"; + import { getServerInfo } from "$lib/api/server-info"; import "$lib/polyfills"; import env from "$lib/env"; - import settings from "$lib/state/settings"; import locale from "$lib/i18n/locale"; + import settings from "$lib/state/settings"; import { t } from "$lib/i18n/translations"; import { device, app } from "$lib/device"; - import { turnstileCreated } from "$lib/state/turnstile"; import currentTheme, { statusBarColors } from "$lib/state/theme"; + import { turnstileCreated, turnstileEnabled } from "$lib/state/turnstile"; import Sidebar from "$components/sidebar/Sidebar.svelte"; import Turnstile from "$components/misc/Turnstile.svelte"; @@ -28,13 +28,12 @@ $: reduceMotion = $settings.appearance.reduceMotion || device.prefers.reducedMotion; + $: reduceTransparency = $settings.appearance.reduceTransparency || device.prefers.reducedTransparency; - $: spawnTurnstile = !!$cachedInfo?.info?.cobalt?.turnstileSitekey; - - afterNavigate(async() => { + afterNavigate(async () => { const to_focus: HTMLElement | null = document.querySelector("[data-first-focus]"); to_focus?.focus(); @@ -46,11 +45,14 @@ - - + + {#if env.HOST} - + {/if} {#if device.is.mobile} @@ -67,7 +69,11 @@ {/if} -
+
- {#if (spawnTurnstile && $page.url.pathname === "/") || $turnstileCreated} + {#if ($turnstileEnabled && $page.url.pathname === "/") || $turnstileCreated} {/if} diff --git a/web/src/routes/settings/instances/+page.svelte b/web/src/routes/settings/instances/+page.svelte index 324a9af6..6b749e21 100644 --- a/web/src/routes/settings/instances/+page.svelte +++ b/web/src/routes/settings/instances/+page.svelte @@ -31,6 +31,32 @@
+{#if $settings.processing.enableCustomInstances} + +
+ + {#if $settings.processing.enableCustomApiKey} + + {/if} +
+
+ {$t("settings.processing.access_key.description")} +
+
+{/if} + From da5cd3e3246be009b6145b3d6d17f2b5aa4fc39b Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 14:30:02 +0600 Subject: [PATCH 262/379] web/DonateBanner: optimize for rtl layouts --- web/src/components/donate/DonateBanner.svelte | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/web/src/components/donate/DonateBanner.svelte b/web/src/components/donate/DonateBanner.svelte index 0b2f1fb1..27482e25 100644 --- a/web/src/components/donate/DonateBanner.svelte +++ b/web/src/components/donate/DonateBanner.svelte @@ -100,6 +100,10 @@ bottom: 0; } + #banner-right:dir(rtl) { + position: relative; + } + #imput-logo { display: flex; } @@ -121,6 +125,11 @@ max-width: 55%; } + #banner-left:dir(rtl) { + padding-right: 47px; + padding-left: 0px; + } + #banner-title { font-family: serif; font-size: 48px; @@ -202,11 +211,6 @@ display: none; } - #banner-left { - max-width: 100%; - padding: 55px; - } - #banner-background { mask-image: linear-gradient( 180deg, @@ -219,7 +223,9 @@ justify-content: center; } - #banner-left { + #banner-left, + #banner-left:dir(rtl) { + max-width: 100%; padding: 45px 12px; gap: 14px; align-items: center; @@ -238,7 +244,8 @@ } @media screen and (max-width: 550px) { - #banner-left { + #banner-left, + #banner-left:dir(rtl) { padding: 32px 12px; gap: 12px; } From 6a430545d28d1e99756f9e01e71f0666c1739d4c Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 14:55:10 +0600 Subject: [PATCH 263/379] api/utils/cleanString: add more forbidden chars --- api/src/misc/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/misc/utils.js b/api/src/misc/utils.js index c584d5d8..85d250ca 100644 --- a/api/src/misc/utils.js +++ b/api/src/misc/utils.js @@ -1,4 +1,4 @@ -const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; +const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', ':', '`', '$', '"', "@", '=', '?', '|', '*']; export function metadataManager(obj) { const keys = Object.keys(obj); @@ -21,7 +21,7 @@ export function metadataManager(obj) { export function cleanString(string) { for (const i in forbiddenCharsString) { - string = string.replaceAll("/", "_") + string = string.replaceAll("/", "_").replaceAll("\\", "_") .replaceAll(forbiddenCharsString[i], '') } return string; From 407c27ed863a243dc3ba4b57130c9d49c1a00752 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 14:55:46 +0600 Subject: [PATCH 264/379] api/utils: rename metadata converter function --- api/src/misc/utils.js | 2 +- api/src/stream/types.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/misc/utils.js b/api/src/misc/utils.js index 85d250ca..cf4fefe6 100644 --- a/api/src/misc/utils.js +++ b/api/src/misc/utils.js @@ -1,6 +1,6 @@ const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', ':', '`', '$', '"', "@", '=', '?', '|', '*']; -export function metadataManager(obj) { +export function convertMetadataToFFmpeg(obj) { const keys = Object.keys(obj); const tags = [ "album", diff --git a/api/src/stream/types.js b/api/src/stream/types.js index 6fbe314e..d1e56bf2 100644 --- a/api/src/stream/types.js +++ b/api/src/stream/types.js @@ -4,8 +4,8 @@ import { spawn } from "child_process"; import { create as contentDisposition } from "content-disposition-header"; import { env } from "../config.js"; -import { metadataManager } from "../misc/utils.js"; import { destroyInternalStream } from "./manage.js"; +import { convertMetadataToFFmpeg } from "../misc/utils.js"; import { hlsExceptions } from "../processing/service-config.js"; import { getHeaders, closeRequest, closeResponse, pipe } from "./shared.js"; @@ -110,7 +110,7 @@ const merge = (streamInfo, res) => { } if (streamInfo.metadata) { - args = args.concat(metadataManager(streamInfo.metadata)) + args = args.concat(convertMetadataToFFmpeg(streamInfo.metadata)) } args.push('-f', format, 'pipe:3'); @@ -242,7 +242,7 @@ const convertAudio = (streamInfo, res) => { } if (streamInfo.metadata) { - args = args.concat(metadataManager(streamInfo.metadata)) + args = args.concat(convertMetadataToFFmpeg(streamInfo.metadata)) } args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); From 67707381168e60d600b268885f1ddbabd89f0250 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:12:21 +0600 Subject: [PATCH 265/379] api/create-filename: build & sanitize filenames in one place --- api/src/misc/utils.js | 10 ---------- api/src/processing/create-filename.js | 12 +++++++++++- api/src/processing/services/ok.js | 5 ++--- api/src/processing/services/rutube.js | 6 ++---- api/src/processing/services/soundcloud.js | 5 ++--- api/src/processing/services/twitch.js | 5 ++--- api/src/processing/services/vimeo.js | 6 ++---- api/src/processing/services/vk.js | 5 ++--- api/src/processing/services/youtube.js | 5 ++--- 9 files changed, 25 insertions(+), 34 deletions(-) diff --git a/api/src/misc/utils.js b/api/src/misc/utils.js index cf4fefe6..271df1c5 100644 --- a/api/src/misc/utils.js +++ b/api/src/misc/utils.js @@ -1,5 +1,3 @@ -const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', ':', '`', '$', '"', "@", '=', '?', '|', '*']; - export function convertMetadataToFFmpeg(obj) { const keys = Object.keys(obj); const tags = [ @@ -19,14 +17,6 @@ export function convertMetadataToFFmpeg(obj) { return commands; } -export function cleanString(string) { - for (const i in forbiddenCharsString) { - string = string.replaceAll("/", "_").replaceAll("\\", "_") - .replaceAll(forbiddenCharsString[i], '') - } - return string; -} - export function getRedirectingURL(url) { return fetch(url, { redirect: 'manual' }).then((r) => { if ([301, 302, 303].includes(r.status) && r.headers.has('location')) diff --git a/api/src/processing/create-filename.js b/api/src/processing/create-filename.js index 216b15a4..cc985d51 100644 --- a/api/src/processing/create-filename.js +++ b/api/src/processing/create-filename.js @@ -1,3 +1,13 @@ +const illegalCharacters = ['}', '{', '%', '>', '<', '^', ';', ':', '`', '$', '"', "@", '=', '?', '|', '*']; + +const sanitizeString = (string) => { + for (const i in illegalCharacters) { + string = string.replaceAll("/", "_").replaceAll("\\", "_") + .replaceAll(illegalCharacters[i], '') + } + return string; +} + export default (f, style, isAudioOnly, isAudioMuted) => { let filename = ''; @@ -5,7 +15,7 @@ export default (f, style, isAudioOnly, isAudioMuted) => { let classicTags = [...infoBase]; let basicTags = []; - const title = `${f.title} - ${f.author}`; + const title = `${sanitizeString(f.title)} - ${sanitizeString(f.author)}`; if (f.resolution) { classicTags.push(f.resolution); diff --git a/api/src/processing/services/ok.js b/api/src/processing/services/ok.js index 2fb6082d..10fb785b 100644 --- a/api/src/processing/services/ok.js +++ b/api/src/processing/services/ok.js @@ -1,5 +1,4 @@ import { genericUserAgent, env } from "../../config.js"; -import { cleanString } from "../../misc/utils.js"; const resolutions = { "ultra": "2160", @@ -44,8 +43,8 @@ export default async function(o) { 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 || videoData.compilationTitle).trim()), + title: videoData.movie.title.trim(), + author: (videoData.author?.name || videoData.compilationTitle).trim(), } if (bestVideo) return { diff --git a/api/src/processing/services/rutube.js b/api/src/processing/services/rutube.js index 67609ffc..ed8c58da 100644 --- a/api/src/processing/services/rutube.js +++ b/api/src/processing/services/rutube.js @@ -1,7 +1,5 @@ import HLS from "hls-parser"; - import { env } from "../../config.js"; -import { cleanString } from "../../misc/utils.js"; async function requestJSON(url) { try { @@ -59,8 +57,8 @@ export default async function(obj) { }); const fileMetadata = { - title: cleanString(play.title.trim()), - artist: cleanString(play.author.name.trim()), + title: play.title.trim(), + artist: play.author.name.trim(), } return { diff --git a/api/src/processing/services/soundcloud.js b/api/src/processing/services/soundcloud.js index 394f7dfe..d78cedda 100644 --- a/api/src/processing/services/soundcloud.js +++ b/api/src/processing/services/soundcloud.js @@ -1,5 +1,4 @@ import { env } from "../../config.js"; -import { cleanString } from "../../misc/utils.js"; const cachedID = { version: '', @@ -91,8 +90,8 @@ export default async function(obj) { if (!file) return { error: "fetch.empty" }; let fileMetadata = { - title: cleanString(json.title.trim()), - artist: cleanString(json.user.username.trim()), + title: json.title.trim(), + artist: json.user.username.trim(), } return { diff --git a/api/src/processing/services/twitch.js b/api/src/processing/services/twitch.js index ac85fbcf..4b9d4551 100644 --- a/api/src/processing/services/twitch.js +++ b/api/src/processing/services/twitch.js @@ -1,5 +1,4 @@ import { env } from "../../config.js"; -import { cleanString } from '../../misc/utils.js'; const gqlURL = "https://gql.twitch.tv/gql"; const clientIdHead = { "client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko" }; @@ -73,13 +72,13 @@ export default async function (obj) { token: req_token[0].data.clip.playbackAccessToken.value })}`, fileMetadata: { - title: cleanString(clipMetadata.title.trim()), + title: clipMetadata.title.trim(), artist: `Twitch Clip by @${clipMetadata.broadcaster.login}, clipped by @${clipMetadata.curator.login}`, }, filenameAttributes: { service: "twitch", id: clipMetadata.id, - title: cleanString(clipMetadata.title.trim()), + title: clipMetadata.title.trim(), author: `${clipMetadata.broadcaster.login}, clipped by ${clipMetadata.curator.login}`, qualityLabel: `${format.quality}p`, extension: 'mp4' diff --git a/api/src/processing/services/vimeo.js b/api/src/processing/services/vimeo.js index 0268e1ec..45a52db7 100644 --- a/api/src/processing/services/vimeo.js +++ b/api/src/processing/services/vimeo.js @@ -1,7 +1,5 @@ import HLS from "hls-parser"; - import { env } from "../../config.js"; -import { cleanString, merge } from '../../misc/utils.js'; const resolutionMatch = { "3840": 2160, @@ -152,8 +150,8 @@ export default async function(obj) { } const fileMetadata = { - title: cleanString(info.name), - artist: cleanString(info.user.name), + title: info.name, + artist: info.user.name, }; return merge( diff --git a/api/src/processing/services/vk.js b/api/src/processing/services/vk.js index e3c18e47..6f5f2ea2 100644 --- a/api/src/processing/services/vk.js +++ b/api/src/processing/services/vk.js @@ -1,4 +1,3 @@ -import { cleanString } from "../../misc/utils.js"; import { genericUserAgent, env } from "../../config.js"; const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"]; @@ -43,8 +42,8 @@ export default async function(o) { url = js.player.params[0][`url${quality}`]; let fileMetadata = { - title: cleanString(js.player.params[0].md_title.trim()), - author: cleanString(js.player.params[0].md_author.trim()), + title: js.player.params[0].md_title.trim(), + author: js.player.params[0].md_author.trim(), } if (url) return { diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 9e99d62f..3af6ba94 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -4,7 +4,6 @@ import { fetch } from "undici"; import { Innertube, Session } from "youtubei.js"; import { env } from "../../config.js"; -import { cleanString } from "../../misc/utils.js"; import { getCookie, updateCookieValues } from "../cookie/manager.js"; const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms @@ -403,8 +402,8 @@ export default async function(o) { } const fileMetadata = { - title: cleanString(basicInfo.title.trim()), - artist: cleanString(basicInfo.author.replace("- Topic", "").trim()) + title: basicInfo.title.trim(), + artist: basicInfo.author.replace("- Topic", "").trim() } if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) { From 1cbffc2d755e6c24b439ac63e7587d992949f8b8 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:13:22 +0600 Subject: [PATCH 266/379] api/stream/types: convert metadata in one place also sanitize values & throw an error if tag isn't supported --- api/src/misc/utils.js | 19 ------------------- api/src/stream/types.js | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/api/src/misc/utils.js b/api/src/misc/utils.js index 271df1c5..fd497d18 100644 --- a/api/src/misc/utils.js +++ b/api/src/misc/utils.js @@ -1,22 +1,3 @@ -export function convertMetadataToFFmpeg(obj) { - const keys = Object.keys(obj); - const tags = [ - "album", - "copyright", - "title", - "artist", - "track", - "date" - ] - let commands = [] - - for (const i in keys) { - if (tags.includes(keys[i])) - commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) - } - return commands; -} - export function getRedirectingURL(url) { return fetch(url, { redirect: 'manual' }).then((r) => { if ([301, 302, 303].includes(r.status) && r.headers.has('location')) diff --git a/api/src/stream/types.js b/api/src/stream/types.js index d1e56bf2..98c3b04e 100644 --- a/api/src/stream/types.js +++ b/api/src/stream/types.js @@ -5,7 +5,6 @@ import { create as contentDisposition } from "content-disposition-header"; import { env } from "../config.js"; import { destroyInternalStream } from "./manage.js"; -import { convertMetadataToFFmpeg } from "../misc/utils.js"; import { hlsExceptions } from "../processing/service-config.js"; import { getHeaders, closeRequest, closeResponse, pipe } from "./shared.js"; @@ -16,6 +15,29 @@ const ffmpegArgs = { gif: ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"] } +const metadataTags = [ + "album", + "copyright", + "title", + "artist", + "track", + "date", +]; + +const convertMetadataToFFmpeg = (metadata) => { + let args = []; + + for (const [ name, value ] of Object.entries(metadata)) { + if (metadataTags.includes(name)) { + args.push('-metadata', `${name}=${value.replace(/[\u0000-\u0009]/g, "")}`); + } else { + throw `${name} metadata tag is not supported.`; + } + } + + return args; +} + const toRawHeaders = (headers) => { return Object.entries(headers) .map(([key, value]) => `${key}: ${value}\r\n`) From eb52ab2be8581ad87d213258ad8588df242fa1d2 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:19:56 +0600 Subject: [PATCH 267/379] api/vimeo: return accidentally remove merge function --- api/src/processing/services/vimeo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/processing/services/vimeo.js b/api/src/processing/services/vimeo.js index 45a52db7..8d704771 100644 --- a/api/src/processing/services/vimeo.js +++ b/api/src/processing/services/vimeo.js @@ -1,5 +1,6 @@ import HLS from "hls-parser"; import { env } from "../../config.js"; +import { merge } from '../../misc/utils.js'; const resolutionMatch = { "3840": 2160, From 43c3294230248d02667198e74b05b80c5891dd0d Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:35:07 +0600 Subject: [PATCH 268/379] api/soundcloud: catch region locked and paid songs and show an error --- api/src/processing/services/soundcloud.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api/src/processing/services/soundcloud.js b/api/src/processing/services/soundcloud.js index d78cedda..ad535479 100644 --- a/api/src/processing/services/soundcloud.js +++ b/api/src/processing/services/soundcloud.js @@ -62,7 +62,17 @@ export default async function(obj) { if (!json) return { error: "fetch.fail" }; - if (!json.media.transcodings) return { error: "fetch.empty" }; + if (json?.policy === "BLOCK") { + return { error: "content.region" }; + } + + if (json?.policy === "SNIP") { + return { error: "content.paid" }; + } + + if (!json?.media?.transcodings || !json?.media?.transcodings.length === 0) { + return { error: "fetch.empty" }; + } let bestAudio = "opus", selectedStream = json.media.transcodings.find(v => v.preset === "opus_0_0"), @@ -74,6 +84,10 @@ export default async function(obj) { bestAudio = "mp3" } + if (!selectedStream) { + return { error: "fetch.empty" }; + } + let fileUrlBase = selectedStream.url; let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; From 9b0e4ab0bdb5c23158eb9e4be7e1349ebe38ad2d Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:35:32 +0600 Subject: [PATCH 269/379] api/tests/soundcloud: add tests for region locked and paid songs --- api/src/util/tests/soundcloud.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api/src/util/tests/soundcloud.json b/api/src/util/tests/soundcloud.json index ea705cb5..04ed8632 100644 --- a/api/src/util/tests/soundcloud.json +++ b/api/src/util/tests/soundcloud.json @@ -83,5 +83,24 @@ "code": 200, "status": "tunnel" } + }, + { + "name": "go+ song, should fail", + "url": "https://soundcloud.com/dualipa/illusion-1", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } + }, + { + "name": "region locked song, should fail", + "canFail": true, + "url": "https://soundcloud.com/gotye/somebody-2024-feat-kimbra", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } } ] \ No newline at end of file From 2ed52a161e7c18983c92502244879deda197e897 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:35:57 +0600 Subject: [PATCH 270/379] web/i18n/error: add general content region & paid errors --- web/i18n/en/error.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 369a4daf..679915f5 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -46,15 +46,18 @@ "api.content.too_long": "media you requested is too long. the duration limit on this instance is {{ limit }} minutes. try something shorter instead!", - "api.content.video.unavailable": "i can't access this video. it may be restricted on {{ service }}'s side. try another one!", + "api.content.video.unavailable": "i can't access this video. it may be restricted on {{ service }}'s side. try a different link!", "api.content.video.live": "this video is currently live, so i can't download it yet. wait for the live stream to finish and try again!", "api.content.video.private": "this video is private, so i can't access it. change its visibility or try another one!", - "api.content.video.age": "this video is age-restricted, so i can't access it anonymously. try another one!", - "api.content.video.region": "this video is region locked, and the processing instance is in a different location. try another one!", + "api.content.video.age": "this video is age-restricted, so i can't access it anonymously. try a different link!", + "api.content.video.region": "this video is region locked, and the processing instance is in a different location. try a different link!", + + "api.content.region": "this content is region locked, and the processing instance is in a different location. try a different link!", + "api.content.paid": "this content requires purchase. cobalt can't download paid content. try a different link!", "api.content.post.unavailable": "couldn't find anything about this post. its visibility may be limited or it may not exist. make sure your link works and try again in a few seconds!", - "api.content.post.private": "this post is from a private account, so i can't access it. try another one!", - "api.content.post.age": "this post is age-restricted, so i can't access it anonymously. try another one!", + "api.content.post.private": "this post is from a private account, so i can't access it.", + "api.content.post.age": "this post is age-restricted, so i can't access it anonymously.", "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo, either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them. try enabling the hls option in video settings!", "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video. try again in a few seconds, but if this issue sticks, please report it!", From 6039eae6a3a84c4f542fd065c5a64a294efa82fa Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:43:50 +0600 Subject: [PATCH 271/379] api/rutube: catch a region lock error closes #930 --- api/src/processing/services/rutube.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/processing/services/rutube.js b/api/src/processing/services/rutube.js index ed8c58da..5b502452 100644 --- a/api/src/processing/services/rutube.js +++ b/api/src/processing/services/rutube.js @@ -33,6 +33,10 @@ export default async function(obj) { const play = await requestJSON(requestURL); if (!play) return { error: "fetch.fail" }; + if (play.detail?.type === "blocking_rule") { + return { error: "content.video.region" }; + } + if (play.detail || !play.video_balancer) return { error: "fetch.empty" }; if (play.live_streams?.hls) return { error: "content.video.live" }; From cdd349cfb69de1026de00adb9ca1b909277bb5be Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 18:44:07 +0600 Subject: [PATCH 272/379] api/tests/rutube: add a region locked video test --- api/src/util/tests/rutube.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/util/tests/rutube.json b/api/src/util/tests/rutube.json index 967c52fb..2eaf69bf 100644 --- a/api/src/util/tests/rutube.json +++ b/api/src/util/tests/rutube.json @@ -86,5 +86,15 @@ "code": 200, "status": "tunnel" } + }, + { + "name": "region locked video, should fail", + "canFail": true, + "url": "https://rutube.ru/video/e7ac82708cc22bd068a3bf6a7004d1b1/", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } } ] \ No newline at end of file From 8a24dbb42d5c8bf31439ee1a15508bbbd4e86300 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:02:10 +0600 Subject: [PATCH 273/379] api/match-action: fix audio in tiktok picker it didn't have an audio format in the filename, so it either failed or downloaded without an extension. closes #870 --- api/src/processing/match-action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 5820214e..262b3acf 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -98,7 +98,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab type: audioStreamType, url: r.urls, headers: r.headers, - filename: r.audioFilename, + filename: `${r.audioFilename}.${audioFormat}`, isAudioOnly: true, audioFormat, }) From 2433681d8bfe64bf6af69b77030ecf546859f241 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:08:40 +0600 Subject: [PATCH 274/379] api/package: bump version to 10.4.1 --- api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index 20fc4a74..9eec8e9d 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.4", + "version": "10.4.1", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From 1559ed13af93b0ca1dbb4d0b2fe19190f288fec2 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:08:52 +0600 Subject: [PATCH 275/379] web/package: bump version to 10.4.1 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index e5f150df..4c6e49ef 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@imput/cobalt-web", - "version": "10.4", + "version": "10.4.1", "type": "module", "private": true, "scripts": { From a1fa79f2f56d3c48020744fd062e4656f21c8d11 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:26:44 +0600 Subject: [PATCH 276/379] api/tikok: catch an age restriction error --- api/src/processing/services/tiktok.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/src/processing/services/tiktok.js b/api/src/processing/services/tiktok.js index d5e5c6e4..976350b0 100644 --- a/api/src/processing/services/tiktok.js +++ b/api/src/processing/services/tiktok.js @@ -51,13 +51,22 @@ export default async function(obj) { return { error: "fetch.fail" }; } + if (detail.isContentClassified) { + return { error: "content.post.age" }; + } + + if (!detail.author) { + return { error: "fetch.empty" }; + } + let video, videoFilename, audioFilename, audio, images, - filenameBase = `tiktok_${detail.author.uniqueId}_${postId}`, + filenameBase = `tiktok_${detail.author?.uniqueId}_${postId}`, bestAudio; // will get defaulted to m4a later on in match-action images = detail.imagePost?.images; - let playAddr = detail.video.playAddr; + let playAddr = detail.video?.playAddr; + if (obj.h265) { const h265PlayAddr = detail?.video?.bitrateInfo?.find(b => b.CodecType.includes("h265"))?.PlayAddr.UrlList[0] playAddr = h265PlayAddr || playAddr From e2f01234188c67567cdfd5bb4bc0e84139d057e6 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:26:59 +0600 Subject: [PATCH 277/379] api/tests/tiktok: add an age restricted video test --- api/src/util/tests/tiktok.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/src/util/tests/tiktok.json b/api/src/util/tests/tiktok.json index d23c0acc..c8dbce8c 100644 --- a/api/src/util/tests/tiktok.json +++ b/api/src/util/tests/tiktok.json @@ -34,5 +34,14 @@ "code": 400, "status": "error" } + }, + { + "name": "age restricted video", + "url": "https://www.tiktok.com/@.kyle.films/video/7415757181145877793", + "params": {}, + "expected": { + "code": 400, + "status": "error" + } } ] \ No newline at end of file From 47804f462ccc81e03dda96f1772587b812273946 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 24 Nov 2024 19:29:53 +0600 Subject: [PATCH 278/379] web/i18n/error: update private & age post errors --- web/i18n/en/error.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 679915f5..bd512532 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -56,8 +56,8 @@ "api.content.paid": "this content requires purchase. cobalt can't download paid content. try a different link!", "api.content.post.unavailable": "couldn't find anything about this post. its visibility may be limited or it may not exist. make sure your link works and try again in a few seconds!", - "api.content.post.private": "this post is from a private account, so i can't access it.", - "api.content.post.age": "this post is age-restricted, so i can't access it anonymously.", + "api.content.post.private": "couldn't get anything about this post because it's from a private account. try a different link!", + "api.content.post.age": "this post is age-restricted and isn't available without logging in. try a different link!", "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo, either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them. try enabling the hls option in video settings!", "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video. try again in a few seconds, but if this issue sticks, please report it!", From e93aa54e2fd4dcae945a5fb8ae5bfa862ca91f11 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Nov 2024 12:22:28 +0600 Subject: [PATCH 279/379] web/SavingDialog: fix weird focus border in chromium browsers --- web/src/components/dialog/SavingDialog.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/components/dialog/SavingDialog.svelte b/web/src/components/dialog/SavingDialog.svelte index ec719aed..03127353 100644 --- a/web/src/components/dialog/SavingDialog.svelte +++ b/web/src/components/dialog/SavingDialog.svelte @@ -144,6 +144,10 @@ gap: var(--padding); } + .dialog-inner-container:focus-visible { + box-shadow: none; + } + .dialog-inner-container { overflow-y: scroll; gap: 8px; From 5be87895760ca150634d3bdf6c479537877909b5 Mon Sep 17 00:00:00 2001 From: wukko Date: Mon, 25 Nov 2024 12:24:09 +0600 Subject: [PATCH 280/379] web/PageNavTab: flip the chevron in rtl layout --- web/src/components/subnav/PageNavTab.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/components/subnav/PageNavTab.svelte b/web/src/components/subnav/PageNavTab.svelte index b828edb6..6ec9a2de 100644 --- a/web/src/components/subnav/PageNavTab.svelte +++ b/web/src/components/subnav/PageNavTab.svelte @@ -83,6 +83,10 @@ width: 18px; } + .subnav-tab-chevron:dir(rtl) { + transform: scale(-1, 1); + } + @media (hover: hover) { .subnav-tab:hover { background: var(--button-hover-transparent); From d4bcb1ba617ad0f099b23e03ce05e4d41895ba9f Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 26 Nov 2024 18:21:44 +0600 Subject: [PATCH 281/379] api/service-config: add new domains for vk --- api/src/processing/service-config.js | 1 + api/src/processing/url.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 7e9dfaa4..0744474c 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -159,6 +159,7 @@ export const services = { "clips:duplicate?z=clip:userId_:videoId" ], subdomains: ["m"], + altDomains: ["vkvideo.ru", "vk.ru"], }, youtube: { patterns: [ diff --git a/api/src/processing/url.js b/api/src/processing/url.js index 899005c0..64517099 100644 --- a/api/src/processing/url.js +++ b/api/src/processing/url.js @@ -42,7 +42,7 @@ function aliasURL(url) { case "fixvx": case "x": if (services.twitter.altDomains.includes(url.hostname)) { - url.hostname = 'twitter.com' + url.hostname = 'twitter.com'; } break; @@ -85,6 +85,13 @@ function aliasURL(url) { url.hostname = 'instagram.com'; } break; + + case "vk": + case "vkvideo": + if (services.vk.altDomains.includes(url.hostname)) { + url.hostname = 'vk.com'; + } + break; } return url From d7ae13213e7f3d9c011a80b76964ab86c456cbd4 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 26 Nov 2024 18:34:13 +0600 Subject: [PATCH 282/379] web/i18n/settings: rename debug to nerd mode and also update description for it --- web/i18n/en/settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index dc876b8e..db9b54b4 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -5,7 +5,7 @@ "page.audio": "audio", "page.download": "downloading", "page.advanced": "advanced", - "page.debug": "debug information", + "page.debug": "info for nerds", "page.instances": "instances", "section.general": "general", @@ -108,8 +108,8 @@ "privacy.tunnel.description": "cobalt will hide your ip address, browser info, and bypass local network restrictions. when enabled, files will also have readable filenames that otherwise would be gibberish.", "advanced.debug": "debug", - "advanced.debug.title": "enable debug features", - "advanced.debug.description": "gives you access to a page with various info that can be useful for debugging.", + "advanced.debug.title": "enable features for nerds", + "advanced.debug.description": "gives you easy access to app info that can be useful for debugging. enabling this does not affect functionality of cobalt in any way.", "advanced.data": "data management", From 31d65c9fb72558f13325e07ea84ccac551fe3424 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 13:44:51 +0000 Subject: [PATCH 283/379] api/cookie: validate service names for cookies --- api/src/processing/cookie/manager.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 6b647db5..bb36b9da 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -7,13 +7,23 @@ import * as cluster from '../../misc/cluster.js'; import { isCluster } from '../../config.js'; const WRITE_INTERVAL = 60000; +const VALID_SERVICES = new Set([ + 'instagram', + 'instagram_bearer', + 'reddit', + 'twitter', + 'youtube_oauth' +]); + +const invalidCookies = {}; let cookies = {}, dirty = false, intervalId; function writeChanges(cookiePath) { if (!dirty) return; dirty = false; - writeFile(cookiePath, JSON.stringify(cookies, null, 4)).catch(() => { + const cookieData = JSON.stringify({ ...cookies, ...invalidCookies }, null, 4); + writeFile(cookiePath, cookieData).catch(() => { clearInterval(intervalId) }) } @@ -22,6 +32,14 @@ const setupMain = async (cookiePath) => { try { cookies = await readFile(cookiePath, 'utf8'); cookies = JSON.parse(cookies); + for (const serviceName in cookies) { + if (!VALID_SERVICES.has(serviceName)) { + console.warn(`${Yellow('[!]')} ignoring unknown service in cookie file: ${serviceName}`); + invalidCookies[serviceName] = cookies[serviceName]; + delete cookies[serviceName]; + } + } + intervalId = setInterval(() => writeChanges(cookiePath), WRITE_INTERVAL); cluster.broadcast({ cookies }); @@ -68,6 +86,11 @@ export const setup = async (cookiePath) => { } export function getCookie(service) { + if (!VALID_SERVICES.has(service)) { + throw `${service} not in allowed services list for cookies.` + + ' if adding a new cookie type, include it there.'; + } + if (!cookies[service] || !cookies[service].length) return; const idx = Math.floor(Math.random() * cookies[service].length); From 3d95361c094c820d44de60b8163c4b2537607771 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 13:51:49 +0000 Subject: [PATCH 284/379] api/cookie: validate cookie file format --- api/src/processing/cookie/manager.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index bb36b9da..a738611e 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -35,9 +35,14 @@ const setupMain = async (cookiePath) => { for (const serviceName in cookies) { if (!VALID_SERVICES.has(serviceName)) { console.warn(`${Yellow('[!]')} ignoring unknown service in cookie file: ${serviceName}`); - invalidCookies[serviceName] = cookies[serviceName]; - delete cookies[serviceName]; - } + } else if (!Array.isArray(cookies[serviceName])) { + console.warn(`${Yellow('[!]')} ${serviceName} in cookies file is not an array, ignoring it`); + } else if (cookies[serviceName].some(c => typeof c !== 'string')) { + console.warn(`${Yellow('[!]')} cookies file contains non-string value for ${serviceName}`); + } else continue; + + invalidCookies[serviceName] = cookies[serviceName]; + delete cookies[serviceName]; } intervalId = setInterval(() => writeChanges(cookiePath), WRITE_INTERVAL); From 00ac0252356518c1f6e8025ef52897a21dce89ad Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 13:52:20 +0000 Subject: [PATCH 285/379] api/cookie: warn if writing updated cookies fails --- api/src/processing/cookie/manager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index a738611e..1b8981c4 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -23,8 +23,10 @@ function writeChanges(cookiePath) { dirty = false; const cookieData = JSON.stringify({ ...cookies, ...invalidCookies }, null, 4); - writeFile(cookiePath, cookieData).catch(() => { - clearInterval(intervalId) + writeFile(cookiePath, cookieData).catch((e) => { + console.warn(`${Yellow('[!]')} failed writing updated cookies to storage`); + console.warn(e); + clearInterval(intervalId); }) } From 20074a50913898a61e9bd54a582409719237fd08 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 13:55:18 +0000 Subject: [PATCH 286/379] api/cookie: rephrase non-string warning --- api/src/processing/cookie/manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 1b8981c4..82f73a5f 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -40,7 +40,7 @@ const setupMain = async (cookiePath) => { } else if (!Array.isArray(cookies[serviceName])) { console.warn(`${Yellow('[!]')} ${serviceName} in cookies file is not an array, ignoring it`); } else if (cookies[serviceName].some(c => typeof c !== 'string')) { - console.warn(`${Yellow('[!]')} cookies file contains non-string value for ${serviceName}`); + console.warn(`${Yellow('[!]')} some cookie for ${serviceName} contains non-string value in cookies file`); } else continue; invalidCookies[serviceName] = cookies[serviceName]; From a4cb6ada7940800df5c571053b6b51ad346bc592 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 14:01:36 +0000 Subject: [PATCH 287/379] api/cookie: split initial load into separate function --- api/src/processing/cookie/manager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 82f73a5f..946ab70a 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -62,13 +62,19 @@ const setupWorker = async () => { cookies = (await cluster.waitFor('cookies')).cookies; } -export const setup = async (cookiePath) => { +export const loadFromFile = async (path) => { if (cluster.isPrimary) { - await setupMain(cookiePath); + await setupMain(path); } else if (cluster.isWorker) { await setupWorker(); } + dirty = false; +} + +export const setup = async (path) => { + await loadFromFile(path); + if (isCluster) { const messageHandler = (message) => { if ('cookieUpdate' in message) { From fbacb944954787bc10dff12c20477a278199e78e Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 14:02:16 +0000 Subject: [PATCH 288/379] api/cookie: do not recreate interval if it already exists --- api/src/processing/cookie/manager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 946ab70a..fb5ca1b6 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -27,6 +27,7 @@ function writeChanges(cookiePath) { console.warn(`${Yellow('[!]')} failed writing updated cookies to storage`); console.warn(e); clearInterval(intervalId); + intervalId = null; }) } @@ -47,7 +48,9 @@ const setupMain = async (cookiePath) => { delete cookies[serviceName]; } - intervalId = setInterval(() => writeChanges(cookiePath), WRITE_INTERVAL); + if (!intervalId) { + intervalId = setInterval(() => writeChanges(cookiePath), WRITE_INTERVAL); + } cluster.broadcast({ cookies }); From 58edad553ed7a8dfc2fe6e24106dc71961d84f52 Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 14:04:39 +0000 Subject: [PATCH 289/379] api/cookie: replace name exception with console log much easier to debug when writing a service --- api/src/processing/cookie/manager.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index fb5ca1b6..4363aec9 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -1,7 +1,7 @@ import Cookie from './cookie.js'; import { readFile, writeFile } from 'fs/promises'; -import { Green, Yellow } from '../../misc/console-text.js'; +import { Red, Green, Yellow } from '../../misc/console-text.js'; import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser'; import * as cluster from '../../misc/cluster.js'; import { isCluster } from '../../config.js'; @@ -103,8 +103,9 @@ export const setup = async (path) => { export function getCookie(service) { if (!VALID_SERVICES.has(service)) { - throw `${service} not in allowed services list for cookies.` - + ' if adding a new cookie type, include it there.'; + console.error(`${Red('[!]')} ${service} not in allowed services list for cookies.` + + ' if adding a new cookie type, include it there.'); + return; } if (!cookies[service] || !cookies[service].length) return; From 55c97f77b8df165b169835c1bc66b9727d8d0e7a Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 26 Nov 2024 14:24:54 +0000 Subject: [PATCH 290/379] api/cookie: reformat console.error in getCookie --- api/src/processing/cookie/manager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 4363aec9..c2b37801 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -103,8 +103,10 @@ export const setup = async (path) => { export function getCookie(service) { if (!VALID_SERVICES.has(service)) { - console.error(`${Red('[!]')} ${service} not in allowed services list for cookies.` - + ' if adding a new cookie type, include it there.'); + console.error( + `${Red('[!]')} ${service} not in allowed services list for cookies.` + + ' if adding a new cookie type, include it there.' + ); return; } From eee9beef91fcec29c6f697bd4fb192cdf2e21c70 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 15:47:30 +0600 Subject: [PATCH 291/379] api/create-filename: don't require author for pretty title --- api/src/processing/create-filename.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/processing/create-filename.js b/api/src/processing/create-filename.js index cc985d51..911b5603 100644 --- a/api/src/processing/create-filename.js +++ b/api/src/processing/create-filename.js @@ -15,7 +15,11 @@ export default (f, style, isAudioOnly, isAudioMuted) => { let classicTags = [...infoBase]; let basicTags = []; - const title = `${sanitizeString(f.title)} - ${sanitizeString(f.author)}`; + let title = sanitizeString(f.title); + + if (f.author) { + title += ` - ${sanitizeString(f.author)}`; + } if (f.resolution) { classicTags.push(f.resolution); From 50344eda17a435e4c0abab16a661eab84414d867 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 15:48:18 +0600 Subject: [PATCH 292/379] api/match-action: proper error code for unsupported audio extraction --- api/src/processing/match-action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 262b3acf..8d2c1e38 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -160,7 +160,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "audio": if (audioIgnore.includes(host) || (host === "reddit" && r.typeId === "redirect")) { return createResponse("error", { - code: "error.api.fetch.empty" + code: "error.api.service.audio_not_supported" }) } From 5ffc0c61614a176229a971d77c0ed27ab50c17ec Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 15:49:15 +0600 Subject: [PATCH 293/379] web/i18n/error: add string for api.service.audio_not_supported --- web/i18n/en/error.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index bd512532..75023338 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -34,6 +34,7 @@ "api.service.unsupported": "this service is not supported yet. have you pasted the right link?", "api.service.disabled": "this service is generally supported by cobalt, but it's disabled on this processing instance. try a link from another service!", + "api.service.audio_not_supported": "this service doesn't support audio extraction. try a link from another service!", "api.link.invalid": "your link is invalid or this service is not supported yet. have you pasted the right link?", "api.link.unsupported": "{{ service }} is supported, but i couldn't recognize your link. have you pasted the right one?", From f696335278c0828b55ec9e1b9403b3021199cfb2 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 16:01:26 +0600 Subject: [PATCH 294/379] api/vk: use proper api, add support for more links, refactor also added support for video access keys --- api/src/processing/match.js | 3 +- api/src/processing/service-config.js | 11 +- api/src/processing/service-patterns.js | 3 +- api/src/processing/services/vk.js | 154 +++++++++++++++++++------ api/src/stream/shared.js | 4 + 5 files changed, 134 insertions(+), 41 deletions(-) diff --git a/api/src/processing/match.js b/api/src/processing/match.js index fe587f09..57f04b36 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -77,8 +77,9 @@ export default async function({ host, patternMatch, params }) { case "vk": r = await vk({ - userId: patternMatch.userId, + ownerId: patternMatch.ownerId, videoId: patternMatch.videoId, + accessKey: patternMatch.accessKey, quality: params.videoQuality }); break; diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 0744474c..81afaf39 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -154,9 +154,14 @@ export const services = { }, vk: { patterns: [ - "video:userId_:videoId", - "clip:userId_:videoId", - "clips:duplicate?z=clip:userId_:videoId" + "video:ownerId_:videoId", + "clip:ownerId_:videoId", + "clips:duplicate?z=clip:ownerId_:videoId", + "videos:duplicate?z=video:ownerId_:videoId", + "video:ownerId_:videoId_:accessKey", + "clip:ownerId_:videoId_:accessKey", + "clips:duplicate?z=clip:ownerId_:videoId_:accessKey", + "videos:duplicate?z=video:ownerId_:videoId_:accessKey" ], subdomains: ["m"], altDomains: ["vkvideo.ru", "vk.ru"], diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 0c3d63d4..e8c46639 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -56,7 +56,8 @@ export const testers = { && (!pattern.password || pattern.password.length < 16), "vk": pattern => - pattern.userId?.length <= 10 && pattern.videoId?.length <= 10, + (pattern.ownerId?.length <= 10 && pattern.videoId?.length <= 10) || + (pattern.ownerId?.length <= 10 && pattern.videoId?.length <= 10 && pattern.videoId?.accessKey <= 18), "youtube": pattern => pattern.id?.length <= 11, diff --git a/api/src/processing/services/vk.js b/api/src/processing/services/vk.js index 6f5f2ea2..0ef61c58 100644 --- a/api/src/processing/services/vk.js +++ b/api/src/processing/services/vk.js @@ -1,62 +1,144 @@ -import { genericUserAgent, env } from "../../config.js"; +import { env } from "../../config.js"; -const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"]; +const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240", "144"]; -export default async function(o) { - let html, url, quality = o.quality === "max" ? 2160 : o.quality; +const oauthUrl = "https://oauth.vk.com/oauth/get_anonym_token"; +const apiUrl = "https://api.vk.com/method"; - html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, { - headers: { - "user-agent": genericUserAgent - } - }) - .then(r => r.arrayBuffer()) - .catch(() => {}); +const clientId = "51552953"; +const clientSecret = "qgr0yWwXCrsxA1jnRtRX"; - if (!html) return { error: "fetch.fail" }; +// used in stream/shared.js for accessing media files +export const vkClientAgent = "com.vk.vkvideo.prod/822 (iPhone, iOS 16.7.7, iPhone10,4, Scale/2.0) SAK/1.119"; - // decode cyrillic from windows-1251 because vk still uses apis from prehistoric times - let decoder = new TextDecoder('windows-1251'); - html = decoder.decode(html); +const cachedToken = { + token: "", + expiry: 0, + device_id: "", +}; - if (!html.includes(`{"lang":`)) return { error: "fetch.empty" }; - - let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); - - if (Number(js.mvData.is_active_live) !== 0) { - return { error: "content.video.live" }; +const getToken = async () => { + if (cachedToken.expiry - 10 > Math.floor(new Date().getTime() / 1000)) { + return cachedToken.token; } - if (js.mvData.duration > env.durationLimit) { + const randomDeviceId = crypto.randomUUID().toUpperCase(); + + const anonymOauth = new URL(oauthUrl); + anonymOauth.searchParams.set("client_id", clientId); + anonymOauth.searchParams.set("client_secret", clientSecret); + anonymOauth.searchParams.set("device_id", randomDeviceId); + + const oauthResponse = await fetch(anonymOauth.toString(), { + headers: { + "user-agent": vkClientAgent, + } + }).then(r => { + if (r.status === 200) { + return r.json(); + } + }); + + if (!oauthResponse) return; + + if (oauthResponse?.token && oauthResponse?.expired_at && typeof oauthResponse?.expired_at === "number") { + cachedToken.token = oauthResponse.token; + cachedToken.expiry = oauthResponse.expired_at; + cachedToken.device_id = randomDeviceId; + } + + if (!cachedToken.token) return; + + return cachedToken.token; +} + +const getVideo = async (ownerId, videoId, accessKey) => { + const video = await fetch(`${apiUrl}/video.get`, { + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded; charset=utf-8", + "user-agent": vkClientAgent, + }, + body: new URLSearchParams({ + anonymous_token: cachedToken.token, + device_id: cachedToken.device_id, + lang: "en", + v: "5.244", + videos: `${ownerId}_${videoId}${accessKey ? `_${accessKey}` : ''}` + }).toString() + }) + .then(r => { + if (r.status === 200) { + return r.json(); + } + }); + + return video; +} + +export default async function ({ ownerId, videoId, accessKey, quality }) { + const token = await getToken(); + if (!token) return { error: "fetch.fail" }; + + const videoGet = await getVideo(ownerId, videoId, accessKey); + + if (!videoGet || !videoGet.response || videoGet.response.items.length !== 1) { + return { error: "fetch.empty" }; + } + + const video = videoGet.response.items[0]; + + if (video.restriction) { + const title = video.restriction.title; + if (title.endsWith("country") || title.endsWith("region.")) { + return { error: "content.video.region" }; + } + if (title === "Processing video") { + return { error: "fetch.empty" }; + } + return { error: "content.video.unavailable" }; + } + + if (!video.files || !video.duration) { + return { error: "fetch.fail" }; + } + + if (video.duration > env.durationLimit) { return { error: "content.too_long" }; } + const userQuality = quality === "max" ? 2160 : quality; + let pickedQuality; + for (let i in resolutions) { - if (js.player.params[0][`url${resolutions[i]}`]) { - quality = resolutions[i]; + if (video.files[`mp4_${resolutions[i]}`]) { + pickedQuality = resolutions[i]; break } } - if (Number(quality) > Number(o.quality)) quality = o.quality; - url = js.player.params[0][`url${quality}`]; - - let fileMetadata = { - title: js.player.params[0].md_title.trim(), - author: js.player.params[0].md_author.trim(), + if (Number(pickedQuality) > Number(userQuality)) { + pickedQuality = userQuality; } - if (url) return { + const url = video.files[`mp4_${pickedQuality}`]; + + if (!url) return { error: "fetch.fail" }; + + const fileMetadata = { + title: video.title.trim(), + } + + return { urls: url, + fileMetadata, filenameAttributes: { service: "vk", - id: `${o.userId}_${o.videoId}`, + id: `${ownerId}_${videoId}${accessKey ? `_${accessKey}` : ''}`, title: fileMetadata.title, - author: fileMetadata.author, - resolution: `${quality}p`, - qualityLabel: `${quality}p`, + resolution: `${pickedQuality}p`, + qualityLabel: `${pickedQuality}p`, extension: "mp4" } } - return { error: "fetch.empty" } } diff --git a/api/src/stream/shared.js b/api/src/stream/shared.js index 91e1ac2f..65af03f0 100644 --- a/api/src/stream/shared.js +++ b/api/src/stream/shared.js @@ -1,4 +1,5 @@ import { genericUserAgent } from "../config.js"; +import { vkClientAgent } from "../processing/services/vk.js"; const defaultHeaders = { 'user-agent': genericUserAgent @@ -13,6 +14,9 @@ const serviceHeaders = { origin: 'https://www.youtube.com', referer: 'https://www.youtube.com', DNT: '?1' + }, + vk: { + 'user-agent': vkClientAgent } } From 4700682ccb8fdae753bfe96db6014118c09c89e5 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 17:32:10 +0600 Subject: [PATCH 295/379] api/vk: refactor quality picking --- api/src/processing/services/vk.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/api/src/processing/services/vk.js b/api/src/processing/services/vk.js index 0ef61c58..33224d69 100644 --- a/api/src/processing/services/vk.js +++ b/api/src/processing/services/vk.js @@ -107,20 +107,16 @@ export default async function ({ ownerId, videoId, accessKey, quality }) { return { error: "content.too_long" }; } - const userQuality = quality === "max" ? 2160 : quality; + const userQuality = quality === "max" ? resolutions[0] : quality; let pickedQuality; - for (let i in resolutions) { - if (video.files[`mp4_${resolutions[i]}`]) { - pickedQuality = resolutions[i]; + for (const resolution of resolutions) { + if (video.files[`mp4_${resolution}`] && +resolution <= +userQuality) { + pickedQuality = resolution; break } } - if (Number(pickedQuality) > Number(userQuality)) { - pickedQuality = userQuality; - } - const url = video.files[`mp4_${pickedQuality}`]; if (!url) return { error: "fetch.fail" }; From 15a0ba30c702dbe92bb8b02ced0f41c3dbccd7db Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 17:32:41 +0600 Subject: [PATCH 296/379] api/tests/vk: add new domain test --- api/src/util/tests/vk.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/src/util/tests/vk.json b/api/src/util/tests/vk.json index 1dc3ca95..71720af5 100644 --- a/api/src/util/tests/vk.json +++ b/api/src/util/tests/vk.json @@ -40,7 +40,7 @@ } }, { - "name": "4k video", + "name": "big 4k video", "url": "https://vk.com/video-1112285_456248465", "params": { "videoQuality": "max" @@ -50,6 +50,17 @@ "status": "tunnel" } }, + { + "name": "short 4k video, 480p, vkvideo.ru domain", + "url": "https://vkvideo.ru/video-26006257_456245538", + "params": { + "videoQuality": "480" + }, + "expected": { + "code": 200, + "status": "tunnel" + } + }, { "name": "ancient video (fallback to 240p)", "url": "https://vk.com/video-1959_28496479", From 3126acc08efb3a8b59210dc5c822c89133a4c93a Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 17:53:25 +0600 Subject: [PATCH 297/379] web/package: bump version to 10.4.2 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 4c6e49ef..c6962d99 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@imput/cobalt-web", - "version": "10.4.1", + "version": "10.4.2", "type": "module", "private": true, "scripts": { From 0e5914f66cb6653fbf86197dc36d9b59fa737696 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 28 Nov 2024 17:53:35 +0600 Subject: [PATCH 298/379] api/package: bump version 10.4.2 --- api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index 9eec8e9d..c6380f09 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.4.1", + "version": "10.4.2", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From 569c232b47f05294fc842b1f784c39b1f58ac2f2 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 29 Nov 2024 12:29:44 +0600 Subject: [PATCH 299/379] web/i18n/settings: update description of "reduce transparency" toggle --- web/i18n/en/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index db9b54b4..c450b4b9 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -88,7 +88,7 @@ "accessibility": "accessibility", "accessibility.transparency.title": "reduce visual transparency", - "accessibility.transparency.description": "reduces transparency of surfaces and disables blur effects. may also improve ui performance on low-end devices.", + "accessibility.transparency.description": "reduces transparency of surfaces and disables blur effects. may also improve ui performance on low performance devices.", "accessibility.motion.title": "reduce motion", "accessibility.motion.description": "disables animations and transitions whenever possible.", From 6ca377ded648e3b4a1734db8507b2097391772f0 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 4 Dec 2024 12:28:05 +0600 Subject: [PATCH 300/379] api/tiktok: catch unavailable post error --- api/src/processing/services/tiktok.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/api/src/processing/services/tiktok.js b/api/src/processing/services/tiktok.js index 976350b0..6978e071 100644 --- a/api/src/processing/services/tiktok.js +++ b/api/src/processing/services/tiktok.js @@ -44,9 +44,19 @@ export default async function(obj) { try { const json = html .split('')[0] - const data = JSON.parse(json) - detail = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"] + .split('')[0]; + + const data = JSON.parse(json); + const videoDetail = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]; + + if (!videoDetail) throw "no video detail found"; + + // status_deleted or etc + if (videoDetail.statusMsg) { + return { error: "content.post.unavailable"}; + } + + detail = videoDetail?.itemInfo?.itemStruct; } catch { return { error: "fetch.fail" }; } From 6c39edbc1034be247b5c8ebef71c47846a6b8713 Mon Sep 17 00:00:00 2001 From: jj Date: Wed, 4 Dec 2024 18:17:07 +0000 Subject: [PATCH 301/379] api/stream: use dispatcher if passed to istream --- api/src/stream/manage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/stream/manage.js b/api/src/stream/manage.js index de4a2cf6..79b5c1db 100644 --- a/api/src/stream/manage.js +++ b/api/src/stream/manage.js @@ -75,7 +75,7 @@ export function getInternalStream(id) { export function createInternalStream(url, obj = {}) { assert(typeof url === 'string'); - let dispatcher; + let dispatcher = obj.dispatcher; if (obj.requestIP) { dispatcher = freebind?.dispatcherFromIP(obj.requestIP, { strict: false }) } From 6f0a8196ff1552fc3f6feb20bd40d26ee4d8ba24 Mon Sep 17 00:00:00 2001 From: jj Date: Wed, 4 Dec 2024 18:19:07 +0000 Subject: [PATCH 302/379] api/istream: remove icy-metadata header if sent by client --- api/src/stream/internal.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/stream/internal.js b/api/src/stream/internal.js index 751ecfa6..7d8bf4c9 100644 --- a/api/src/stream/internal.js +++ b/api/src/stream/internal.js @@ -123,6 +123,10 @@ async function handleGenericStream(streamInfo, res) { } export function internalStream(streamInfo, res) { + if (streamInfo.headers) { + streamInfo.headers.delete('icy-metadata'); + } + if (streamInfo.service === 'youtube' && !streamInfo.isHLS) { return handleYoutubeStream(streamInfo, res); } From e1b84e747211f334eae38283960a20a2bf48662c Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 5 Dec 2024 00:27:53 +0600 Subject: [PATCH 303/379] api/package: bump version to 10.4.3 --- api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index c6380f09..0498fe23 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.4.2", + "version": "10.4.3", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From 4b8b0a0e9ec2842874eaa89534e31f3d861817e5 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 10 Dec 2024 17:30:32 +0600 Subject: [PATCH 304/379] api/youtube: don't retrieve the player as cobalt doesn't use it we don't decipher anything lol --- api/src/processing/services/youtube.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index 3af6ba94..c0178b35 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -68,7 +68,8 @@ const cloneInnertube = async (customFetch) => { const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date(); if (!innertube || shouldRefreshPlayer) { innertube = await Innertube.create({ - fetch: customFetch + fetch: customFetch, + retrieve_player: false, }); lastRefreshedAt = +new Date(); } From e041e376c7283eedeeb7c5316d5ee2d0e19a8742 Mon Sep 17 00:00:00 2001 From: wukko Date: Tue, 10 Dec 2024 19:55:43 +0600 Subject: [PATCH 305/379] api & web: bump dependencies --- api/package.json | 6 +- pnpm-lock.yaml | 599 +++++++++++++++++++++++++---------------------- web/package.json | 10 +- 3 files changed, 333 insertions(+), 282 deletions(-) diff --git a/api/package.json b/api/package.json index 0498fe23..7766a10b 100644 --- a/api/package.json +++ b/api/package.json @@ -31,17 +31,17 @@ "cors": "^2.8.5", "dotenv": "^16.0.1", "esbuild": "^0.14.51", - "express": "^4.21.1", + "express": "^4.21.2", "express-rate-limit": "^7.4.1", "ffmpeg-static": "^5.1.0", "hls-parser": "^0.10.7", "ipaddr.js": "2.2.0", - "nanoid": "^4.0.2", + "nanoid": "^5.0.9", "node-cache": "^5.1.2", "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^11.0.1", + "youtubei.js": "^12.1.0", "zod": "^3.23.8" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a96ed57..56b9d285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,11 +32,11 @@ importers: specifier: ^0.14.51 version: 0.14.54 express: - specifier: ^4.21.1 - version: 4.21.1 + specifier: ^4.21.2 + version: 4.21.2 express-rate-limit: specifier: ^7.4.1 - version: 7.4.1(express@4.21.1) + version: 7.4.1(express@4.21.2) ffmpeg-static: specifier: ^5.1.0 version: 5.2.0 @@ -47,8 +47,8 @@ importers: specifier: 2.2.0 version: 2.2.0 nanoid: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^5.0.9 + version: 5.0.9 node-cache: specifier: ^5.1.2 version: 5.1.2 @@ -62,8 +62,8 @@ importers: specifier: 1.0.3 version: 1.0.3 youtubei.js: - specifier: ^11.0.1 - version: 11.0.1 + specifier: ^12.1.0 + version: 12.1.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -73,7 +73,7 @@ importers: version: 0.2.2 rate-limit-redis: specifier: ^4.2.0 - version: 4.2.0(express-rate-limit@7.4.1(express@4.21.1)) + version: 4.2.0(express-rate-limit@7.4.1(express@4.21.2)) redis: specifier: ^4.7.0 version: 4.7.0 @@ -113,11 +113,11 @@ importers: specifier: workspace:^ version: link:../packages/version-info '@sveltejs/adapter-static': - specifier: ^3.0.2 - version: 3.0.2(@sveltejs/kit@2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14))) + specifier: ^3.0.6 + version: 3.0.6(@sveltejs/kit@2.9.1(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14))) '@sveltejs/kit': - specifier: ^2.0.0 - version: 2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) + specifier: ^2.9.1 + version: 2.9.1(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) '@sveltejs/vite-plugin-svelte': specifier: ^3.0.0 version: 3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) @@ -143,11 +143,11 @@ importers: specifier: ^16.0.1 version: 16.4.5 eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: ^9.16.0 + version: 9.16.0 glob: - specifier: ^10.4.5 - version: 10.4.5 + specifier: ^11.0.0 + version: 11.0.0 mdsvex: specifier: ^0.11.2 version: 0.11.2(svelte@4.2.19) @@ -179,8 +179,8 @@ importers: specifier: ^5.4.5 version: 5.5.4 typescript-eslint: - specifier: ^8.8.0 - version: 8.8.0(eslint@8.57.0)(typescript@5.5.4) + specifier: ^8.18.0 + version: 8.18.0(eslint@9.16.0)(typescript@5.5.4) vite: specifier: ^5.3.6 version: 5.4.8(@types/node@20.14.14) @@ -498,22 +498,38 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.16.0': + resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.8.0': resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -527,18 +543,25 @@ packages: '@fontsource/redaction-10@5.0.2': resolution: {integrity: sha512-PODxYvb06YrNxdUBGcygiMibpgcZihzmvkmlX/TQAA2F7BUU/anfSKQi/VnLdJ/8LIK81/bUY+i7L/GP27FkVw==} - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} '@imput/libav.js-remux-cli@5.5.6': resolution: {integrity: sha512-XdAab90EZKf6ULtD/x9Y2bnlmNJodXSO6w8aWrn97+N2IRuOS8zv3tAFPRC69SWKa8Utjeu5YTYuTolnX3QprQ==} @@ -696,19 +719,19 @@ packages: cpu: [x64] os: [win32] - '@sveltejs/adapter-static@3.0.2': - resolution: {integrity: sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==} + '@sveltejs/adapter-static@3.0.6': + resolution: {integrity: sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==} peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.5.19': - resolution: {integrity: sha512-r/lah3nnYEZX1btlvpSy+Exkt1aWhmOP5pnCt+BBro+tZrh2Zci+26Xnm1fCBLLMeM5q7gHvWiS8c/UtrWjdvQ==} + '@sveltejs/kit@2.9.1': + resolution: {integrity: sha512-D+yH3DTvvkjXdl3Xv7akKmolrArDZRtsFv3nlxJPjlIKsZEpkkInnomKJuAql2TrNGJ2dJMGBO1YYgVn2ILmag==} engines: {node: '>=18.13'} hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 + vite: ^5.0.3 || ^6.0.0 '@sveltejs/vite-plugin-svelte-inspector@2.1.0': resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==} @@ -774,65 +797,52 @@ packages: '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - '@typescript-eslint/eslint-plugin@8.8.0': - resolution: {integrity: sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==} + '@typescript-eslint/eslint-plugin@8.18.0': + resolution: {integrity: sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.8.0': - resolution: {integrity: sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==} + '@typescript-eslint/parser@8.18.0': + resolution: {integrity: sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.8.0': - resolution: {integrity: sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==} + '@typescript-eslint/scope-manager@8.18.0': + resolution: {integrity: sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.8.0': - resolution: {integrity: sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@8.8.0': - resolution: {integrity: sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.8.0': - resolution: {integrity: sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@8.8.0': - resolution: {integrity: sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==} + '@typescript-eslint/type-utils@8.18.0': + resolution: {integrity: sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.8.0': - resolution: {integrity: sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==} + '@typescript-eslint/types@8.18.0': + resolution: {integrity: sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@typescript-eslint/typescript-estree@8.18.0': + resolution: {integrity: sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.18.0': + resolution: {integrity: sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.18.0': + resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-basic-ssl@1.1.0': resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==} @@ -854,6 +864,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -1024,6 +1039,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -1072,12 +1091,8 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - devalue@5.0.0: - resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} @@ -1260,26 +1275,34 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.16.0: + resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - esm-env@1.0.0: - resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + esm-env@1.2.1: + resolution: {integrity: sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} @@ -1314,8 +1337,8 @@ packages: peerDependencies: express: 4 || 5 || ^5.0.0-beta.1 - express@4.21.1: - resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} fast-deep-equal@3.1.3: @@ -1346,9 +1369,9 @@ packages: resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==} engines: {node: '>=16'} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -1362,9 +1385,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} @@ -1419,13 +1442,18 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globalyzer@0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} @@ -1537,10 +1565,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} @@ -1554,8 +1578,12 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jintr@3.0.2: - resolution: {integrity: sha512-5g2EBudeJFOopjAX4exAv5OCCW1DgUISfoioCsm1h9Q9HJ41LmnZ6J52PCsqBlQihsmp0VDuxreAVzM7yk5nFA==} + jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + + jintr@3.1.0: + resolution: {integrity: sha512-azhCHApkRfBH8INpiUCwKBYaNCdB5G+x3NApsI2MxQXSlgFAx7rap3YwE3JAkN08GO8f3ilZsGB0Yvc+412ntQ==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -1612,6 +1640,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} @@ -1671,6 +1703,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1714,9 +1750,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@4.0.2: - resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} - engines: {node: ^14 || ^16 || >=18} + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} hasBin: true natural-compare@1.4.0: @@ -1799,8 +1835,12 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.10: - resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -1922,11 +1962,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rollup@4.24.0: resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1990,9 +2025,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} sorcery@0.11.1: resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} @@ -2152,9 +2187,6 @@ packages: syscall-napi@0.0.6: resolution: {integrity: sha512-qHbwjyFXAAekKUXxl70lhDiBYJ3e7XM7kQwu7LV3F0pHMenKox+VcZPZkRkhdmL/wNJD3NmrMGnL7161kdecUQ==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2230,10 +2262,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -2241,14 +2269,12 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.8.0: - resolution: {integrity: sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==} + typescript-eslint@8.18.0: + resolution: {integrity: sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} @@ -2362,8 +2388,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - youtubei.js@11.0.1: - resolution: {integrity: sha512-ZsbOd+5XF2Ofi3FrLMfYd+f9g9H8xswlouFhjhOqbwT68dMJtX6CRGsHNj5VTFCR/+L/865x1lnUlllB2dDDTA==} + youtubei.js@12.1.0: + resolution: {integrity: sha512-42SUw7zPpx4b7+XBm9QW+//T2/tixBRoEjJAbfOLmGCiyGZuvfW+oq/IngnXo2Keu+yD7YVAfYFc/NaFjFDxGg==} zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -2534,19 +2560,31 @@ snapshots: '@esbuild/win32-x64@0.23.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.16.0)': dependencies: - eslint: 8.57.0 + eslint: 9.16.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} + '@eslint-community/regexpp@4.12.1': {} - '@eslint/eslintrc@2.1.4': + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.3.6 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 debug: 4.3.6 - espree: 9.6.1 - globals: 13.24.0 + espree: 10.3.0 + globals: 14.0.0 ignore: 5.3.1 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -2555,10 +2593,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@9.16.0': {} '@eslint/js@9.8.0': {} + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.4': + dependencies: + levn: 0.4.1 + '@fastify/busboy@2.1.1': {} '@fontsource-variable/noto-sans-mono@5.0.20': {} @@ -2567,17 +2611,18 @@ snapshots: '@fontsource/redaction-10@5.0.2': {} - '@humanwhocodes/config-array@0.11.14': + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} '@imput/libav.js-remux-cli@5.5.6': {} @@ -2708,24 +2753,24 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': optional: true - '@sveltejs/adapter-static@3.0.2(@sveltejs/kit@2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))': + '@sveltejs/adapter-static@3.0.6(@sveltejs/kit@2.9.1(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))': dependencies: - '@sveltejs/kit': 2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) + '@sveltejs/kit': 2.9.1(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) - '@sveltejs/kit@2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14))': + '@sveltejs/kit@2.9.1(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)))(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14))': dependencies: '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.4.8(@types/node@20.14.14)) '@types/cookie': 0.6.0 cookie: 0.6.0 - devalue: 5.0.0 - esm-env: 1.0.0 + devalue: 5.1.1 + esm-env: 1.2.1 import-meta-resolve: 4.1.0 kleur: 4.1.5 magic-string: 0.30.11 mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.6.0 - sirv: 2.0.4 + sirv: 3.0.0 svelte: 4.2.19 tiny-glob: 0.2.9 vite: 5.4.8(@types/node@20.14.14) @@ -2797,88 +2842,82 @@ snapshots: '@types/unist@2.0.10': {} - '@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.5.4))(eslint@9.16.0)(typescript@5.5.4)': dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.8.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.8.0 - '@typescript-eslint/type-utils': 8.8.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.8.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.8.0 - eslint: 8.57.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.18.0(eslint@9.16.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/type-utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.18.0 + eslint: 9.16.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.8.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.8.0 - '@typescript-eslint/types': 8.8.0 - '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.8.0 + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.18.0 debug: 4.3.6 - eslint: 8.57.0 - optionalDependencies: + eslint: 9.16.0 typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.8.0': + '@typescript-eslint/scope-manager@8.18.0': dependencies: - '@typescript-eslint/types': 8.8.0 - '@typescript-eslint/visitor-keys': 8.8.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 - '@typescript-eslint/type-utils@8.8.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.18.0(eslint@9.16.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.8.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4) debug: 4.3.6 + eslint: 9.16.0 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.8.0': {} + '@typescript-eslint/types@8.18.0': {} - '@typescript-eslint/typescript-estree@8.8.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.18.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.8.0 - '@typescript-eslint/visitor-keys': 8.8.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 debug: 4.3.6 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.8.0(eslint@8.57.0)(typescript@5.5.4)': + '@typescript-eslint/utils@8.18.0(eslint@9.16.0)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@typescript-eslint/scope-manager': 8.8.0 - '@typescript-eslint/types': 8.8.0 - '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.5.4) - eslint: 8.57.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.16.0) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4) + eslint: 9.16.0 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.8.0': + '@typescript-eslint/visitor-keys@8.18.0': dependencies: - '@typescript-eslint/types': 8.8.0 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.2.0': {} + '@typescript-eslint/types': 8.18.0 + eslint-visitor-keys: 4.2.0 '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.8(@types/node@20.14.14))': dependencies: @@ -2889,12 +2928,14 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 + acorn: 8.14.0 acorn@8.12.1: {} + acorn@8.14.0: {} + agent-base@6.0.2: dependencies: debug: 4.3.6 @@ -3070,6 +3111,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css-tree@2.3.1: dependencies: mdn-data: 2.0.30 @@ -3101,11 +3148,7 @@ snapshots: detect-indent@6.1.0: {} - devalue@5.0.0: {} - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 + devalue@5.1.1: {} dotenv@16.4.5: {} @@ -3272,63 +3315,61 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-scope@7.2.2: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.0: + eslint-visitor-keys@4.2.0: {} + + eslint@9.16.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.16.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.16.0 + '@eslint/plugin-kit': 0.2.4 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.6 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 transitivePeerDependencies: - supports-color - esm-env@1.0.0: {} + esm-env@1.2.1: {} - espree@9.6.1: + espree@10.3.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 3.4.3 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esquery@1.6.0: dependencies: @@ -3360,11 +3401,11 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - express-rate-limit@7.4.1(express@4.21.1): + express-rate-limit@7.4.1(express@4.21.2): dependencies: - express: 4.21.1 + express: 4.21.2 - express@4.21.1: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 @@ -3385,7 +3426,7 @@ snapshots: methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.10 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 qs: 6.13.0 range-parser: 1.2.1 @@ -3431,9 +3472,9 @@ snapshots: transitivePeerDependencies: - supports-color - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 fill-range@7.1.1: dependencies: @@ -3456,11 +3497,10 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.1 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.3.1: {} @@ -3517,6 +3557,15 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@11.0.0: + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -3526,9 +3575,7 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globalyzer@0.1.0: {} @@ -3622,8 +3669,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-reference@3.0.2: dependencies: '@types/estree': 1.0.5 @@ -3638,7 +3683,11 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jintr@3.0.2: + jackspeak@4.0.2: + dependencies: + '@isaacs/cliui': 8.0.2 + + jintr@3.1.0: dependencies: acorn: 8.12.1 @@ -3683,6 +3732,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.0.2: {} + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3726,6 +3777,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -3760,7 +3815,7 @@ snapshots: nanoid@3.3.7: {} - nanoid@4.0.2: {} + nanoid@5.0.9: {} natural-compare@1.4.0: {} @@ -3830,7 +3885,12 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.10: {} + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} periscopic@3.1.0: dependencies: @@ -3885,9 +3945,9 @@ snapshots: range-parser@1.2.1: {} - rate-limit-redis@4.2.0(express-rate-limit@7.4.1(express@4.21.1)): + rate-limit-redis@4.2.0(express-rate-limit@7.4.1(express@4.21.2)): dependencies: - express-rate-limit: 7.4.1(express@4.21.1) + express-rate-limit: 7.4.1(express@4.21.2) optional: true raw-body@2.5.2: @@ -3927,10 +3987,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rollup@4.24.0: dependencies: '@types/estree': 1.0.6 @@ -4031,7 +4087,7 @@ snapshots: signal-exit@4.1.0: {} - sirv@2.0.4: + sirv@3.0.0: dependencies: '@polka/url': 1.0.0-next.25 mrmime: 2.0.0 @@ -4169,8 +4225,6 @@ snapshots: syscall-napi@0.0.6: optional: true - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -4246,8 +4300,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -4255,15 +4307,14 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.8.0(eslint@8.57.0)(typescript@5.5.4): + typescript-eslint@8.18.0(eslint@9.16.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/parser': 8.8.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.8.0(eslint@8.57.0)(typescript@5.5.4) - optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.5.4))(eslint@9.16.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.18.0(eslint@9.16.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4) + eslint: 9.16.0 typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color typescript@5.5.4: {} @@ -4343,10 +4394,10 @@ snapshots: yocto-queue@0.1.0: {} - youtubei.js@11.0.1: + youtubei.js@12.1.0: dependencies: '@bufbuild/protobuf': 2.1.0 - jintr: 3.0.2 + jintr: 3.1.0 tslib: 2.6.3 undici: 5.28.4 diff --git a/web/package.json b/web/package.json index c6962d99..538242a3 100644 --- a/web/package.json +++ b/web/package.json @@ -30,8 +30,8 @@ "@fontsource/redaction-10": "^5.0.2", "@imput/libav.js-remux-cli": "^5.5.6", "@imput/version-info": "workspace:^", - "@sveltejs/adapter-static": "^3.0.2", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/adapter-static": "^3.0.6", + "@sveltejs/kit": "^2.9.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tabler/icons-svelte": "3.6.0", "@types/eslint__js": "^8.42.3", @@ -40,8 +40,8 @@ "@vitejs/plugin-basic-ssl": "^1.1.0", "compare-versions": "^6.1.0", "dotenv": "^16.0.1", - "eslint": "^8.57.0", - "glob": "^10.4.5", + "eslint": "^9.16.0", + "glob": "^11.0.0", "mdsvex": "^0.11.2", "mime": "^4.0.4", "svelte": "^4.2.19", @@ -52,7 +52,7 @@ "tslib": "^2.4.1", "turnstile-types": "^1.2.2", "typescript": "^5.4.5", - "typescript-eslint": "^8.8.0", + "typescript-eslint": "^8.18.0", "vite": "^5.3.6" } } From f1916cef6e32b41f7ef51b4c7377ac39ba33b5fb Mon Sep 17 00:00:00 2001 From: jj Date: Tue, 10 Dec 2024 16:14:15 +0000 Subject: [PATCH 306/379] web: add automatic sitemap generation --- pnpm-lock.yaml | 86 ++++++++++++++++++++++++++++++++++++++++++++++ web/package.json | 1 + web/vite.config.ts | 19 +++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56b9d285..2ef40243 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ importers: svelte-preprocess: specifier: ^6.0.2 version: 6.0.2(postcss@8.4.47)(svelte@4.2.19)(typescript@5.5.4) + svelte-sitemap: + specifier: 2.6.0 + version: 2.6.0 sveltekit-i18n: specifier: ^2.4.2 version: 2.4.2(svelte@4.2.19) @@ -603,6 +606,22 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oozcitak/dom@1.15.10': + resolution: {integrity: sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==} + engines: {node: '>=8.0'} + + '@oozcitak/infra@1.0.8': + resolution: {integrity: sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==} + engines: {node: '>=6.0'} + + '@oozcitak/url@1.0.4': + resolution: {integrity: sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==} + engines: {node: '>=8.0'} + + '@oozcitak/util@8.3.8': + resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} + engines: {node: '>=8.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -899,6 +918,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1304,6 +1326,11 @@ packages: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -1589,6 +1616,10 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -2045,6 +2076,9 @@ packages: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -2175,6 +2209,11 @@ packages: typescript: optional: true + svelte-sitemap@2.6.0: + resolution: {integrity: sha512-WcwsuIeo8iJFG9a5cgvXwXEGoyjk6Zowb6JmL5BbwfnFXMzakGa1+mQjthw5Ni3UV/gGbE0PgJvc7Ygir3LmFg==} + engines: {node: '>= 14.17.0'} + hasBin: true + svelte@4.2.19: resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==} engines: {node: '>=16'} @@ -2381,6 +2420,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xmlbuilder2@3.1.1: + resolution: {integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==} + engines: {node: '>=12.0'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -2668,6 +2711,23 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@oozcitak/dom@1.15.10': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/url': 1.0.4 + '@oozcitak/util': 8.3.8 + + '@oozcitak/infra@1.0.8': + dependencies: + '@oozcitak/util': 8.3.8 + + '@oozcitak/url@1.0.4': + dependencies: + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + + '@oozcitak/util@8.3.8': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -2966,6 +3026,10 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.0: @@ -3371,6 +3435,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.0) eslint-visitor-keys: 4.2.0 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -3693,6 +3759,11 @@ snapshots: joycon@3.1.1: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -4108,6 +4179,8 @@ snapshots: dependencies: whatwg-url: 7.1.0 + sprintf-js@1.0.3: {} + statuses@2.0.1: {} string-width@4.2.3: @@ -4199,6 +4272,12 @@ snapshots: postcss: 8.4.47 typescript: 5.5.4 + svelte-sitemap@2.6.0: + dependencies: + fast-glob: 3.3.2 + minimist: 1.2.8 + xmlbuilder2: 3.1.1 + svelte@4.2.19: dependencies: '@ampproject/remapping': 2.3.0 @@ -4389,6 +4468,13 @@ snapshots: wrappy@1.0.2: {} + xmlbuilder2@3.1.1: + dependencies: + '@oozcitak/dom': 1.15.10 + '@oozcitak/infra': 1.0.8 + '@oozcitak/util': 8.3.8 + js-yaml: 3.14.1 + yallist@4.0.0: optional: true diff --git a/web/package.json b/web/package.json index 538242a3..1c3d67cb 100644 --- a/web/package.json +++ b/web/package.json @@ -47,6 +47,7 @@ "svelte": "^4.2.19", "svelte-check": "^3.6.0", "svelte-preprocess": "^6.0.2", + "svelte-sitemap": "2.6.0", "sveltekit-i18n": "^2.4.2", "ts-deepmerge": "^7.0.1", "tslib": "^2.4.1", diff --git a/web/vite.config.ts b/web/vite.config.ts index 5faa8c0f..f762fbbc 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -3,6 +3,7 @@ import { sveltekit } from "@sveltejs/kit/vite"; import basicSSL from "@vitejs/plugin-basic-ssl"; import { glob } from "glob"; import mime from "mime"; +import { createSitemap } from 'svelte-sitemap/src/index' import { cp, readdir, mkdir } from "node:fs/promises"; import { createReadStream } from "node:fs"; @@ -60,12 +61,28 @@ const enableCOEP: PluginOption = { } }; +const generateSitemap: PluginOption = { + name: "generate-sitemap", + async writeBundle(bundle) { + if (!process.env.WEB_HOST || !bundle.dir?.endsWith('server')) { + return; + } + + await createSitemap(`https://${process.env.WEB_HOST}`, { + changeFreq: 'monthly', + outDir: '.svelte-kit/output/prerendered/pages', + resetTime: true + }); + } +} + export default defineConfig({ plugins: [ basicSSL(), sveltekit(), enableCOEP, - exposeLibAV + exposeLibAV, + generateSitemap ], build: { rollupOptions: { From 112866096cfa565b47bac48283a851f290cc92d1 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 12 Dec 2024 23:00:49 +0600 Subject: [PATCH 307/379] api/url: return a diff error when youtube is disabled on main instance --- api/src/processing/url.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/processing/url.js b/api/src/processing/url.js index 64517099..8f0e7dc2 100644 --- a/api/src/processing/url.js +++ b/api/src/processing/url.js @@ -181,6 +181,11 @@ export function extract(url) { } if (!env.enabledServices.has(host)) { + // show a different message when youtube is disabled on official instances + // as it only happens when shit hits the fan + if (new URL(env.apiURL).hostname.endsWith(".imput.net") && host === "youtube") { + return { error: "youtube.temporary_disabled" }; + } return { error: "service.disabled" }; } From 994ce84483acba17e8802ed4c7b0fd3258ed2326 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 12 Dec 2024 23:01:05 +0600 Subject: [PATCH 308/379] web/error: add the error for temporarily disabled youtube --- web/i18n/en/error.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 75023338..6b83ceb9 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -65,5 +65,6 @@ "api.youtube.login": "couldn't get this video because youtube asked the instance to log in. this is potentially caused by the processing instance not having any active account tokens or youtube updating something about their api. try again in a few seconds, but if it still doesn't work, please report this issue!", "api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", "api.youtube.no_hls_streams": "couldn't find any matching HLS streams for this video. try downloading it without HLS!", - "api.youtube.api_error": "youtube updated something about its api and i couldn't get any info about this video. try again in a few seconds, but if this issue sticks, please report it!" + "api.youtube.api_error": "youtube updated something about its api and i couldn't get any info about this video. try again in a few seconds, but if this issue sticks, please report it!", + "api.youtube.temporary_disabled": "youtube downloading is temporarily disabled due to restrictions from youtube's side. we're already looking for ways to go around them.\n\nwe apologize for the inconvenience and are doing our best to restore this functionality. check cobalt's socials or github for timely updates!" } From 5eb411bb8319749789a8c7d4c122ec3e258d68c2 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 12 Dec 2024 23:01:32 +0600 Subject: [PATCH 309/379] web/package: bump version to 10.4.4 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 1c3d67cb..a98fe47e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@imput/cobalt-web", - "version": "10.4.2", + "version": "10.4.4", "type": "module", "private": true, "scripts": { From 5973d70053a6d5642ca2eb242e61813478ec0b6b Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 12 Dec 2024 23:03:00 +0600 Subject: [PATCH 310/379] api/package: bump version to 10.4.4 & update youtube.js --- api/package.json | 4 ++-- pnpm-lock.yaml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/package.json b/api/package.json index 7766a10b..21b76fc7 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.4.3", + "version": "10.4.4", "author": "imput", "exports": "./src/cobalt.js", "type": "module", @@ -41,7 +41,7 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^12.1.0", + "youtubei.js": "^12.2.0", "zod": "^3.23.8" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ef40243..4224fba3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,8 +62,8 @@ importers: specifier: 1.0.3 version: 1.0.3 youtubei.js: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^12.2.0 + version: 12.2.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -1609,8 +1609,8 @@ packages: resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} engines: {node: 20 || >=22} - jintr@3.1.0: - resolution: {integrity: sha512-azhCHApkRfBH8INpiUCwKBYaNCdB5G+x3NApsI2MxQXSlgFAx7rap3YwE3JAkN08GO8f3ilZsGB0Yvc+412ntQ==} + jintr@3.2.0: + resolution: {integrity: sha512-psD1yf05kMKDNsUdW1l5YhO59pHScQ6OIHHb8W5SKSM2dCOFPsqolmIuSHgVA8+3Dc47NJR181CXZ4alCAPTkA==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -2431,8 +2431,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - youtubei.js@12.1.0: - resolution: {integrity: sha512-42SUw7zPpx4b7+XBm9QW+//T2/tixBRoEjJAbfOLmGCiyGZuvfW+oq/IngnXo2Keu+yD7YVAfYFc/NaFjFDxGg==} + youtubei.js@12.2.0: + resolution: {integrity: sha512-G+50qrbJCToMYhu8jbaHiS3Vf+RRul+CcDbz3hEGwHkGPh+zLiWwD6SS+YhYF+2/op4ZU5zDYQJrGqJ+wKh7Gw==} zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -3753,9 +3753,9 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - jintr@3.1.0: + jintr@3.2.0: dependencies: - acorn: 8.12.1 + acorn: 8.14.0 joycon@3.1.1: {} @@ -4480,10 +4480,10 @@ snapshots: yocto-queue@0.1.0: {} - youtubei.js@12.1.0: + youtubei.js@12.2.0: dependencies: '@bufbuild/protobuf': 2.1.0 - jintr: 3.1.0 + jintr: 3.2.0 tslib: 2.6.3 undici: 5.28.4 From 3dafdd825a8d98983103f0275cf4199bbd6af01b Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 13 Dec 2024 16:01:16 +0600 Subject: [PATCH 311/379] api/types/proxy: use default dispatcher instead of a global one this function never gets anything but internal streams, so global proxy (`API_EXTERNAL_PROXY`) is only causing issues here. this commit fixes an issue of cobalt attempting to proxy internal streams, and failing spectacularly. --- api/src/stream/types.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/src/stream/types.js b/api/src/stream/types.js index 98c3b04e..0a4e2d47 100644 --- a/api/src/stream/types.js +++ b/api/src/stream/types.js @@ -1,4 +1,4 @@ -import { request } from "undici"; +import { Agent, request } from "undici"; import ffmpeg from "ffmpeg-static"; import { spawn } from "child_process"; import { create as contentDisposition } from "content-disposition-header"; @@ -60,6 +60,8 @@ const getCommand = (args) => { return [ffmpeg, args] } +const defaultAgent = new Agent(); + const proxy = async (streamInfo, res) => { const abortController = new AbortController(); const shutdown = () => ( @@ -78,7 +80,8 @@ const proxy = async (streamInfo, res) => { Range: streamInfo.range }, signal: abortController.signal, - maxRedirections: 16 + maxRedirections: 16, + dispatcher: defaultAgent, }); res.status(statusCode); From 86a67dee834c176b2aec40dd48fec56d69597f29 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 13 Dec 2024 16:03:32 +0600 Subject: [PATCH 312/379] api/package: bump version to 10.4.5 --- api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index 21b76fc7..02e09e46 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.4.4", + "version": "10.4.5", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From b44410e93bfcc5dee84dc7c5731f1da8bdeeabf5 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 14 Dec 2024 12:30:04 +0600 Subject: [PATCH 313/379] web/SupportedServices: springy expand animation --- web/src/components/save/SupportedServices.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/save/SupportedServices.svelte b/web/src/components/save/SupportedServices.svelte index 32129f20..28e97859 100644 --- a/web/src/components/save/SupportedServices.svelte +++ b/web/src/components/save/SupportedServices.svelte @@ -90,8 +90,8 @@ transform-origin: top center; transition: - transform 0.2s cubic-bezier(0.53, 0.05, 0.23, 0.99), - opacity 0.2s cubic-bezier(0.53, 0.05, 0.23, 0.99); + transform 0.3s cubic-bezier(0.53, 0.05, 0.23, 1.15), + opacity 0.25s cubic-bezier(0.53, 0.05, 0.23, 0.99); } #services-popover.expanded { From 89f197375ce46d4167f31d31b90227a94bd65700 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 14 Dec 2024 12:42:38 +0600 Subject: [PATCH 314/379] web/SupportedServices: better glow in dark mode --- web/src/components/save/SupportedServices.svelte | 2 +- web/src/routes/+layout.svelte | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/components/save/SupportedServices.svelte b/web/src/components/save/SupportedServices.svelte index 28e97859..0b51e577 100644 --- a/web/src/components/save/SupportedServices.svelte +++ b/web/src/components/save/SupportedServices.svelte @@ -78,7 +78,7 @@ background: var(--button); box-shadow: var(--button-box-shadow), - 0 0 10px 10px var(--button-stroke); + 0 0 10px 10px var(--popover-glow); position: relative; padding: 12px; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 1a878aff..062c2536 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -123,6 +123,8 @@ --button-elevated-hover: #dadada; --button-elevated-shimmer: #ededed; + --popover-glow: var(--button-stroke); + --popup-bg: #f1f1f1; --popup-stroke: rgba(0, 0, 0, 0.08); @@ -198,6 +200,8 @@ --button-elevated: #282828; --button-elevated-hover: #323232; + --popover-glow: rgba(135, 135, 135, 0.15); + --popup-bg: #191919; --popup-stroke: rgba(255, 255, 255, 0.08); From 35d991730135ad4fdc7b989520e14c5b808f1413 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 14 Dec 2024 12:51:00 +0600 Subject: [PATCH 315/379] web/SupportedServices: render popover only when needed & also focus it for screen readers --- .../components/save/SupportedServices.svelte | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/web/src/components/save/SupportedServices.svelte b/web/src/components/save/SupportedServices.svelte index 0b51e577..d3077a2a 100644 --- a/web/src/components/save/SupportedServices.svelte +++ b/web/src/components/save/SupportedServices.svelte @@ -8,8 +8,11 @@ let services: string[] = []; + let popover: HTMLDivElement; + $: expanded = false; $: loaded = false; + $: renderPopover = false; const loadInfo = async () => { await getServerInfo(); @@ -19,18 +22,28 @@ services = $cachedInfo.info.cobalt.services; } }; - -
- -
-
- {#if loaded} - {#each services as service} -
{service}
- {/each} - {:else} - {#each { length: 17 } as _} - - {/each} - {/if} + {#if renderPopover} +
+
+ {#if loaded} + {#each services as service} +
{service}
+ {/each} + {:else} + {#each { length: 17 } as _} + + {/each} + {/if} +
+
+ {$t("save.services.disclaimer")} +
-
- {$t("save.services.disclaimer")} -
-
+ {/if}