Compare commits
18 Commits
main
...
api-client
Author | SHA1 | Date | |
---|---|---|---|
|
c68ebbe949 | ||
|
1cd57fc7ff | ||
|
95f0fbdb5e | ||
|
5d7cd861f3 | ||
|
6d4477a962 | ||
|
64ac458941 | ||
|
d3e278660d | ||
|
d201deb60a | ||
|
00d5754ea8 | ||
|
ff57a6a448 | ||
|
1d30ac0139 | ||
|
a80c7b7a5a | ||
|
afe9917169 | ||
|
4755787b69 | ||
|
c8ccb32421 | ||
|
6cc1227288 | ||
|
ab8650030b | ||
|
abb6a26ee0 |
1
packages/api-client/.gitignore
vendored
Normal file
1
packages/api-client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist
|
43
packages/api-client/i18n/error.json
Normal file
43
packages/api-client/i18n/error.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"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.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.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.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.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.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.content.too_long": "the media you requested is too long. current duration limit 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 livestream 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.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.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.youtube.codec": "youtube didn't return anything with your preferred codec & resolution. try another set of settings!",
|
||||
"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.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!"
|
||||
}
|
@ -3,13 +3,34 @@
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "imput <meow@imput.net>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript": "^5.4.5"
|
||||
"turnstile-types": "^1.2.2",
|
||||
"typescript": "^5.4.5",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"tsup": {
|
||||
"dts": true,
|
||||
"bundle": false,
|
||||
"treeshake": true,
|
||||
"target": "node18",
|
||||
"format": ["esm", "cjs"],
|
||||
"entry": ["src/**/*.ts"]
|
||||
}
|
||||
}
|
||||
|
3
packages/api-client/src/index.ts
Normal file
3
packages/api-client/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./turnstile-api";
|
||||
export * from "./unauthenticated-api";
|
||||
export * from "./types/interface";
|
53
packages/api-client/src/internal/base-api.ts
Normal file
53
packages/api-client/src/internal/base-api.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { CobaltResponseType, type CobaltResponse } from "../types/response";
|
||||
import { CobaltReachabilityError } from "../types/errors";
|
||||
import type { CobaltRequest } from "../types/request";
|
||||
import { CobaltAPIClient } from "../types/interface";
|
||||
|
||||
export default class CobaltAPI implements CobaltAPIClient {
|
||||
#baseURL: string | undefined;
|
||||
|
||||
getBaseURL() {
|
||||
return this.#baseURL;
|
||||
}
|
||||
|
||||
setBaseURL(baseURL: string) {
|
||||
const url = new URL(baseURL);
|
||||
|
||||
if (baseURL !== url.origin && baseURL !== `${url.origin}/`) {
|
||||
throw new Error('Invalid cobalt instance URL');
|
||||
}
|
||||
|
||||
return this.#baseURL = url.origin;
|
||||
}
|
||||
|
||||
async request(data: CobaltRequest, headers?: Record<string, string>) {
|
||||
const baseURL = this.getBaseURL();
|
||||
if (!baseURL) throw "baseURL is undefined";
|
||||
|
||||
const response: CobaltResponse = await fetch(baseURL, {
|
||||
method: 'POST',
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(10000),
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.catch((e) => {
|
||||
const timedOut = e?.message?.includes("timed out");
|
||||
return {
|
||||
status: CobaltResponseType.Error,
|
||||
error: {
|
||||
code: timedOut
|
||||
? CobaltReachabilityError.TimedOut
|
||||
: CobaltReachabilityError.Unreachable
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
72
packages/api-client/src/internal/session.ts
Normal file
72
packages/api-client/src/internal/session.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { CobaltSession, CobaltResponseType, CobaltSessionResponse } from "../types/response";
|
||||
import { CobaltReachabilityError } from "../types/errors";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
import { CobaltAPIClient } from "../types/interface";
|
||||
|
||||
const currentTime = () => Math.floor(new Date().getTime() / 1000);
|
||||
const EXPIRY_THRESHOLD_SECONDS = 2;
|
||||
|
||||
export default class CobaltSessionHandler {
|
||||
#client: CobaltAPIClient;
|
||||
#turnstile: TurnstileObject;
|
||||
#session: CobaltSession | undefined;
|
||||
|
||||
constructor(client: CobaltAPIClient, turnstile: TurnstileObject) {
|
||||
this.#client = client;
|
||||
this.#turnstile = turnstile;
|
||||
}
|
||||
|
||||
async #requestSession(): Promise<CobaltSessionResponse> {
|
||||
const baseURL = this.#client.getBaseURL();
|
||||
if (!baseURL) throw "baseURL is undefined";
|
||||
|
||||
const endpoint = new URL('/session', baseURL);
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(10000),
|
||||
headers: {
|
||||
'cf-turnstile-response': this.#turnstile.getResponse('#turnstile-widget')
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.catch((e) => {
|
||||
const timedOut = e?.message?.includes("timed out");
|
||||
return {
|
||||
status: CobaltResponseType.Error,
|
||||
error: {
|
||||
code: timedOut
|
||||
? CobaltReachabilityError.TimedOut
|
||||
: CobaltReachabilityError.Unreachable
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if ('token' in response && 'exp' in response) {
|
||||
this.#session = {
|
||||
token: response.token,
|
||||
exp: currentTime() + response.exp
|
||||
}
|
||||
}
|
||||
|
||||
this.#turnstile.reset('#turnstile-widget');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
hasSession() {
|
||||
return this.#session && this.#session.exp - EXPIRY_THRESHOLD_SECONDS > currentTime();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#session = undefined;
|
||||
}
|
||||
|
||||
async getSession(): Promise<CobaltSessionResponse> {
|
||||
if (this.hasSession()) {
|
||||
return this.#session!;
|
||||
}
|
||||
|
||||
return this.#requestSession();
|
||||
}
|
||||
};
|
47
packages/api-client/src/turnstile-api.ts
Normal file
47
packages/api-client/src/turnstile-api.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import CobaltSessionHandler from "./internal/session";
|
||||
import CobaltAPI from "./internal/base-api";
|
||||
import { CobaltRequest } from "./types/request";
|
||||
import { CobaltAuthError } from "./types/errors";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
export class TurnstileCobaltAPI extends CobaltAPI {
|
||||
#session: CobaltSessionHandler;
|
||||
#instanceHasTurnstile = true;
|
||||
|
||||
constructor(turnstile: TurnstileObject) {
|
||||
super();
|
||||
this.#session = new CobaltSessionHandler(this, turnstile);
|
||||
}
|
||||
|
||||
needsSession() {
|
||||
return this.#instanceHasTurnstile && !this.#session.hasSession();
|
||||
}
|
||||
|
||||
setBaseURL(baseURL: string): string {
|
||||
if (this.#session && super.getBaseURL() !== super.setBaseURL(baseURL)) {
|
||||
this.#session.reset();
|
||||
}
|
||||
|
||||
return super.getBaseURL()!;
|
||||
}
|
||||
|
||||
async request(data: CobaltRequest) {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (this.#instanceHasTurnstile) {
|
||||
const sessionOrError = await this.#session.getSession();
|
||||
|
||||
if ("error" in sessionOrError) {
|
||||
if (sessionOrError.error.code !== CobaltAuthError.NotConfigured) {
|
||||
return sessionOrError;
|
||||
} else {
|
||||
this.#instanceHasTurnstile = false;
|
||||
}
|
||||
} else {
|
||||
headers['Authorization'] = `Bearer ${sessionOrError.token}`;
|
||||
}
|
||||
}
|
||||
|
||||
return super.request(data, headers);
|
||||
}
|
||||
}
|
67
packages/api-client/src/types/errors.ts
Normal file
67
packages/api-client/src/types/errors.ts
Normal file
@ -0,0 +1,67 @@
|
||||
export enum CobaltAuthError {
|
||||
NotConfigured = 'error.api.auth.not_configured',
|
||||
JWTMissing = 'error.api.auth.jwt.missing',
|
||||
JWTInvalid = 'error.api.auth.jwt.invalid',
|
||||
TurnstileMissing = 'error.api.auth.turnstile.missing',
|
||||
TurnstileInvalid = 'api.auth.turnstile.invalid'
|
||||
};
|
||||
|
||||
export enum CobaltReachabilityError {
|
||||
Unreachable = 'error.api.unreachable',
|
||||
TimedOut = 'error.api.timed_out',
|
||||
RateExceeded = 'error.api.rate_exceeded',
|
||||
AtCapacity = 'error.api.capacity'
|
||||
};
|
||||
|
||||
export enum CobaltGenericError {
|
||||
Generic = 'error.api.generic',
|
||||
UnknownResponse = 'error.api.unknown_response'
|
||||
};
|
||||
|
||||
export enum CobaltServiceError {
|
||||
Unsupported = 'error.api.service.unsupported',
|
||||
Disabled = 'error.api.service.disabled'
|
||||
};
|
||||
|
||||
export enum CobaltLinkError {
|
||||
Invalid = 'error.api.link.invalid',
|
||||
FormatUnsupported = 'error.api.link.unsupported'
|
||||
};
|
||||
|
||||
export enum CobaltProcessingError {
|
||||
Fail = 'error.api.fetch.fail',
|
||||
Critical = 'error.api.fetch.critical',
|
||||
Empty = 'error.api.fetch.empty',
|
||||
RateLimited = 'error.api.fetch.rate',
|
||||
ShortLink = 'error.api.fetch.short_link'
|
||||
};
|
||||
|
||||
export enum CobaltContentError {
|
||||
TooLong = 'error.api.content.too_long',
|
||||
|
||||
VideoUnavailable = 'error.api.content.video.unavailable',
|
||||
VideoIsLive = 'error.api.content.video.live',
|
||||
VideoIsPrivate = 'error.api.content.video.private',
|
||||
VideoIsAgeRestricted = 'error.api.content.video.age',
|
||||
VideoIsRegionRestricted = 'error.api.content.video.region',
|
||||
|
||||
PostUnavailable = 'error.api.content.post.unavailable',
|
||||
PostIsPrivate = 'error.api.content.post.private',
|
||||
PostIsAgeRestricted = 'error.api.content.post.age',
|
||||
};
|
||||
|
||||
export enum CobaltYouTubeError {
|
||||
MissingCodec = 'error.api.youtube.codec',
|
||||
CannotDecipher = 'error.api.youtube.decipher',
|
||||
MissingLogin = 'error.api.youtube.login',
|
||||
TokenExpired = 'error.api.youtube.token_expired'
|
||||
}
|
||||
|
||||
export type CobaltAPIErrorCode = CobaltAuthError
|
||||
| CobaltReachabilityError
|
||||
| CobaltGenericError
|
||||
| CobaltServiceError
|
||||
| CobaltLinkError
|
||||
| CobaltProcessingError
|
||||
| CobaltContentError
|
||||
| CobaltYouTubeError;
|
8
packages/api-client/src/types/interface.ts
Normal file
8
packages/api-client/src/types/interface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { CobaltRequest } from "./request";
|
||||
import { CobaltResponse } from "./response";
|
||||
|
||||
export interface CobaltAPIClient {
|
||||
request(data: CobaltRequest): Promise<CobaltResponse>;
|
||||
getBaseURL(): string | undefined;
|
||||
setBaseURL(baseURL: string): string;
|
||||
}
|
46
packages/api-client/src/types/request.ts
Normal file
46
packages/api-client/src/types/request.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// FIXME: this is duplicated from api/src/processing/schema.js
|
||||
// (minus defaults) until the api is converted to TS
|
||||
const apiSchema = z.object({
|
||||
url: z.string()
|
||||
.min(1),
|
||||
|
||||
audioBitrate: z.enum(
|
||||
["320", "256", "128", "96", "64", "8"]
|
||||
).optional(),
|
||||
|
||||
audioFormat: z.enum(
|
||||
["best", "mp3", "ogg", "wav", "opus"]
|
||||
).optional(),
|
||||
|
||||
downloadMode: z.enum(
|
||||
["auto", "audio", "mute"]
|
||||
).optional(),
|
||||
|
||||
filenameStyle: z.enum(
|
||||
["classic", "pretty", "basic", "nerdy"]
|
||||
).optional(),
|
||||
|
||||
youtubeVideoCodec: z.enum(
|
||||
["h264", "av1", "vp9"]
|
||||
).optional(),
|
||||
|
||||
videoQuality: z.enum(
|
||||
["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"]
|
||||
).optional(),
|
||||
|
||||
youtubeDubLang: z.string()
|
||||
.length(2)
|
||||
.optional(),
|
||||
|
||||
alwaysProxy: z.boolean().optional(),
|
||||
disableMetadata: z.boolean().optional(),
|
||||
tiktokFullAudio: z.boolean().optional(),
|
||||
tiktokH265: z.boolean().optional(),
|
||||
twitterGif: z.boolean().optional(),
|
||||
youtubeDubBrowserLang: z.boolean().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CobaltRequest = z.infer<typeof apiSchema>;
|
71
packages/api-client/src/types/response.ts
Normal file
71
packages/api-client/src/types/response.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { CobaltAPIErrorCode } from "./errors";
|
||||
|
||||
export enum CobaltResponseType {
|
||||
Error = 'error',
|
||||
Picker = 'picker',
|
||||
Redirect = 'redirect',
|
||||
Tunnel = 'tunnel',
|
||||
}
|
||||
|
||||
export type CobaltErrorResponse = {
|
||||
status: CobaltResponseType.Error,
|
||||
error: {
|
||||
code: CobaltAPIErrorCode,
|
||||
context?: {
|
||||
service?: string,
|
||||
limit?: number,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
type CobaltPartialURLResponse = {
|
||||
url: string,
|
||||
filename: string,
|
||||
}
|
||||
|
||||
type CobaltPickerResponse = {
|
||||
status: CobaltResponseType.Picker
|
||||
picker: {
|
||||
type: 'photo' | 'video' | 'gif',
|
||||
url: string,
|
||||
thumb?: string,
|
||||
}[];
|
||||
audio?: string,
|
||||
audioFilename?: string,
|
||||
};
|
||||
|
||||
type CobaltRedirectResponse = {
|
||||
status: CobaltResponseType.Redirect,
|
||||
} & CobaltPartialURLResponse;
|
||||
|
||||
type CobaltTunnelResponse = {
|
||||
status: CobaltResponseType.Tunnel,
|
||||
} & CobaltPartialURLResponse;
|
||||
|
||||
export type CobaltSession = {
|
||||
token: string,
|
||||
exp: number,
|
||||
}
|
||||
|
||||
export type CobaltServerInfo = {
|
||||
cobalt: {
|
||||
version: string,
|
||||
url: string,
|
||||
startTime: string,
|
||||
durationLimit: number,
|
||||
services: string[]
|
||||
},
|
||||
git: {
|
||||
branch: string,
|
||||
commit: string,
|
||||
remote: string,
|
||||
}
|
||||
}
|
||||
|
||||
export type CobaltSessionResponse = CobaltSession | CobaltErrorResponse;
|
||||
export type CobaltServerInfoResponse = CobaltServerInfo | CobaltErrorResponse;
|
||||
|
||||
export type CobaltResponse = CobaltErrorResponse
|
||||
| CobaltPickerResponse
|
||||
| CobaltRedirectResponse
|
||||
| CobaltTunnelResponse;
|
9
packages/api-client/src/unauthenticated-api.ts
Normal file
9
packages/api-client/src/unauthenticated-api.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import CobaltAPI from "./internal/base-api";
|
||||
import { CobaltRequest } from "./types/request";
|
||||
import { CobaltAPIClient } from "./types/interface";
|
||||
|
||||
export class UnauthenticatedCobaltAPI extends CobaltAPI implements CobaltAPIClient {
|
||||
async request(data: CobaltRequest) {
|
||||
return super.request(data, {});
|
||||
}
|
||||
}
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -77,9 +77,15 @@ importers:
|
||||
tsup:
|
||||
specifier: ^8.2.4
|
||||
version: 8.2.4(postcss@8.4.40)(typescript@5.5.4)
|
||||
turnstile-types:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.5.4
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
|
||||
packages/version-info: {}
|
||||
|
||||
@ -119,6 +125,9 @@ importers:
|
||||
'@fontsource/redaction-10':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@imput/cobalt-client':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/api-client
|
||||
'@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.18)(vite@5.3.5(@types/node@20.14.14)))(svelte@4.2.18)(vite@5.3.5(@types/node@20.14.14)))
|
||||
|
@ -6,47 +6,5 @@
|
||||
"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.",
|
||||
|
||||
"tunnel.probe": "couldn't verify whether you can download this file. try again in a few seconds!",
|
||||
|
||||
"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.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.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.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.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.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.content.too_long": "the media you requested is too long. current duration limit 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 livestream 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.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.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.youtube.codec": "youtube didn't return anything with your preferred codec & resolution. try another set of settings!",
|
||||
"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.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!"
|
||||
"tunnel.probe": "couldn't verify whether you can download this file. try again in a few seconds!"
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.5.0",
|
||||
"@fontsource/redaction-10": "^5.0.2",
|
||||
"@imput/cobalt-client": "workspace:^",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
import { link } from "$lib/state/omnibox";
|
||||
import { updateSetting } from "$lib/state/settings";
|
||||
import { turnstileLoaded } from "$lib/state/turnstile";
|
||||
import { turnstileCreated } from "$lib/state/turnstile";
|
||||
|
||||
import type { Optional } from "$lib/types/generic";
|
||||
import type { DownloadModeOption } from "$lib/types/settings";
|
||||
@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
$: if (env.TURNSTILE_KEY) {
|
||||
if ($turnstileLoaded) {
|
||||
if ($turnstileCreated) {
|
||||
isDisabled = false;
|
||||
} else {
|
||||
isDisabled = true;
|
||||
|
@ -1,12 +1,23 @@
|
||||
<script lang="ts">
|
||||
import "@fontsource-variable/noto-sans-mono";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import settings from "$lib/state/settings";
|
||||
import API from "$lib/api/api";
|
||||
import APIUrl from "$lib/state/api-url";
|
||||
import lazySettingGetter from "$lib/settings/lazy-get";
|
||||
import { apiOverrideWarning } from "$lib/api/safety-warning";
|
||||
|
||||
import env from "$lib/env";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { createDialog } from "$lib/dialogs";
|
||||
import { downloadFile } from "$lib/download";
|
||||
|
||||
import type { DialogInfo } from "$lib/types/dialog";
|
||||
import { downloadFile } from "$lib/download";
|
||||
import {
|
||||
UnauthenticatedCobaltAPI,
|
||||
TurnstileCobaltAPI,
|
||||
type CobaltAPIClient
|
||||
} from "@imput/cobalt-client";
|
||||
|
||||
export let url: string;
|
||||
export let disabled = false;
|
||||
@ -58,10 +69,47 @@
|
||||
}
|
||||
};
|
||||
|
||||
let client: CobaltAPIClient;
|
||||
|
||||
$: client?.setBaseURL($APIUrl);
|
||||
|
||||
onMount(() => {
|
||||
if (env.TURNSTILE_KEY) {
|
||||
client = new TurnstileCobaltAPI(window.turnstile);
|
||||
} else {
|
||||
client = new UnauthenticatedCobaltAPI();
|
||||
}
|
||||
client.setBaseURL($APIUrl);
|
||||
});
|
||||
|
||||
export const download = async (link: string) => {
|
||||
changeDownloadButton("think");
|
||||
|
||||
const response = await API.request(link);
|
||||
const getSetting = lazySettingGetter($settings);
|
||||
|
||||
const request = {
|
||||
url: link,
|
||||
|
||||
downloadMode: getSetting("save", "downloadMode"),
|
||||
audioBitrate: getSetting("save", "audioBitrate"),
|
||||
audioFormat: getSetting("save", "audioFormat"),
|
||||
tiktokFullAudio: getSetting("save", "tiktokFullAudio"),
|
||||
youtubeDubBrowserLang: getSetting("save", "youtubeDubBrowserLang"),
|
||||
|
||||
youtubeVideoCodec: getSetting("save", "youtubeVideoCodec"),
|
||||
videoQuality: getSetting("save", "videoQuality"),
|
||||
|
||||
filenameStyle: getSetting("save", "filenameStyle"),
|
||||
disableMetadata: getSetting("save", "disableMetadata"),
|
||||
|
||||
twitterGif: getSetting("save", "twitterGif"),
|
||||
tiktokH265: getSetting("save", "tiktokH265"),
|
||||
|
||||
alwaysProxy: getSetting("privacy", "alwaysProxy"),
|
||||
}
|
||||
|
||||
await apiOverrideWarning();
|
||||
const response = await client.request(request);
|
||||
|
||||
if (!response) {
|
||||
changeDownloadButton("error");
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import env, { apiURL } from "$lib/env";
|
||||
import settings from "$lib/state/settings";
|
||||
|
||||
export const currentApiURL = () => {
|
||||
const processingSettings = get(settings).processing;
|
||||
const customInstanceURL = processingSettings.customInstanceURL;
|
||||
|
||||
if (processingSettings.enableCustomInstances && customInstanceURL.length > 0) {
|
||||
return new URL(customInstanceURL).origin;
|
||||
}
|
||||
|
||||
if (env.DEFAULT_API && processingSettings.allowDefaultOverride) {
|
||||
return new URL(env.DEFAULT_API).origin;
|
||||
}
|
||||
|
||||
return new URL(apiURL).origin;
|
||||
}
|
@ -1,85 +1,3 @@
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import settings from "$lib/state/settings";
|
||||
import { getSession } from "$lib/api/session";
|
||||
import { currentApiURL } from "$lib/api/api-url";
|
||||
import { apiOverrideWarning } from "$lib/api/safety-warning";
|
||||
import type { Optional } from "$lib/types/generic";
|
||||
import type { CobaltAPIResponse, CobaltErrorResponse } from "$lib/types/api";
|
||||
import lazySettingGetter from "$lib/settings/lazy-get";
|
||||
|
||||
const request = async (url: string) => {
|
||||
const getSetting = lazySettingGetter(get(settings));
|
||||
|
||||
const request = {
|
||||
url,
|
||||
|
||||
downloadMode: getSetting("save", "downloadMode"),
|
||||
audioBitrate: getSetting("save", "audioBitrate"),
|
||||
audioFormat: getSetting("save", "audioFormat"),
|
||||
tiktokFullAudio: getSetting("save", "tiktokFullAudio"),
|
||||
youtubeDubBrowserLang: getSetting("save", "youtubeDubBrowserLang"),
|
||||
|
||||
youtubeVideoCodec: getSetting("save", "youtubeVideoCodec"),
|
||||
videoQuality: getSetting("save", "videoQuality"),
|
||||
|
||||
filenameStyle: getSetting("save", "filenameStyle"),
|
||||
disableMetadata: getSetting("save", "disableMetadata"),
|
||||
|
||||
twitterGif: getSetting("save", "twitterGif"),
|
||||
tiktokH265: getSetting("save", "tiktokH265"),
|
||||
|
||||
alwaysProxy: getSetting("privacy", "alwaysProxy"),
|
||||
}
|
||||
|
||||
await apiOverrideWarning();
|
||||
|
||||
const usingCustomInstance = getSetting("processing", "enableCustomInstances")
|
||||
&& getSetting("processing", "customInstanceURL");
|
||||
const api = currentApiURL();
|
||||
// FIXME: rewrite this to allow custom instances to specify their own turnstile tokens
|
||||
const session = usingCustomInstance ? undefined : await getSession();
|
||||
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const response: Optional<CobaltAPIResponse> = await fetch(api, {
|
||||
method: "POST",
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(10000),
|
||||
body: JSON.stringify(request),
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...extraHeaders,
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.catch((e) => {
|
||||
if (e?.message?.includes("timed out")) {
|
||||
return {
|
||||
status: "error",
|
||||
error: {
|
||||
code: "error.api.timed_out"
|
||||
}
|
||||
} as CobaltErrorResponse;
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const probeCobaltTunnel = async (url: string) => {
|
||||
const request = await fetch(`${url}&p=1`).catch(() => {});
|
||||
if (request?.status === 200) {
|
||||
@ -89,6 +7,5 @@ const probeCobaltTunnel = async (url: string) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
request,
|
||||
probeCobaltTunnel,
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { currentApiURL } from "$lib/api/api-url";
|
||||
import APIUrl from "$lib/state/api-url";
|
||||
|
||||
import type { CobaltServerInfoResponse, CobaltErrorResponse, CobaltServerInfo } from "$lib/types/api";
|
||||
|
||||
@ -11,7 +11,7 @@ export type CobaltServerInfoCache = {
|
||||
export const cachedInfo = writable<CobaltServerInfoCache | undefined>();
|
||||
|
||||
const request = async () => {
|
||||
const apiEndpoint = `${currentApiURL()}/`;
|
||||
const apiEndpoint = `${get(APIUrl)}/`;
|
||||
|
||||
const response: CobaltServerInfoResponse = await fetch(apiEndpoint, {
|
||||
redirect: "manual",
|
||||
@ -35,7 +35,7 @@ const request = async () => {
|
||||
export const getServerInfo = async () => {
|
||||
const cache = get(cachedInfo);
|
||||
|
||||
if (cache && cache.origin === currentApiURL()) {
|
||||
if (cache && cache.origin === get(APIUrl)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export const getServerInfo = async () => {
|
||||
if (!("status" in freshInfo)) {
|
||||
cachedInfo.set({
|
||||
info: freshInfo,
|
||||
origin: currentApiURL(),
|
||||
origin: get(APIUrl),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
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<CobaltSession | undefined>();
|
||||
|
||||
export const requestSession = async() => {
|
||||
const apiEndpoint = `${currentApiURL()}/session`;
|
||||
|
||||
let requestHeaders = {};
|
||||
|
||||
const turnstileResponse = turnstile.getResponse();
|
||||
if (turnstileResponse) {
|
||||
requestHeaders = {
|
||||
"cf-turnstile-response": turnstileResponse
|
||||
};
|
||||
}
|
||||
|
||||
const response: CobaltSessionResponse = await fetch(apiEndpoint, {
|
||||
method: "POST",
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(10000),
|
||||
headers: requestHeaders,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.catch((e) => {
|
||||
if (e?.message?.includes("timed out")) {
|
||||
return {
|
||||
status: "error",
|
||||
error: {
|
||||
code: "error.api.timed_out"
|
||||
}
|
||||
} as CobaltErrorResponse
|
||||
}
|
||||
});
|
||||
|
||||
turnstile.update();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const newSession = await requestSession();
|
||||
|
||||
if (!newSession) return {
|
||||
status: "error",
|
||||
error: {
|
||||
code: "error.api.unreachable"
|
||||
}
|
||||
} as CobaltErrorResponse
|
||||
|
||||
if (!("status" in newSession)) {
|
||||
newSession.exp = currentTime() + newSession.exp;
|
||||
cachedSession.set(newSession);
|
||||
}
|
||||
return newSession;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
const getResponse = () => {
|
||||
const turnstileElement = document.getElementById("turnstile-widget");
|
||||
|
||||
if (turnstileElement) {
|
||||
return window?.turnstile?.getResponse(turnstileElement);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const turnstileElement = document.getElementById("turnstile-widget");
|
||||
|
||||
if (turnstileElement) {
|
||||
return window?.turnstile?.reset(turnstileElement);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default {
|
||||
getResponse,
|
||||
update,
|
||||
}
|
18
web/src/lib/state/api-url.ts
Normal file
18
web/src/lib/state/api-url.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { derived } from "svelte/store";
|
||||
|
||||
import env, { apiURL } from "$lib/env";
|
||||
import settings from "$lib/state/settings";
|
||||
|
||||
export default derived(
|
||||
settings,
|
||||
$settings => {
|
||||
const { processing } = $settings;
|
||||
|
||||
if (processing.enableCustomInstances && processing.customInstanceURL)
|
||||
return new URL(processing.customInstanceURL).origin;
|
||||
else if (env.DEFAULT_API && processing.allowDefaultOverride)
|
||||
return new URL(env.DEFAULT_API).origin;
|
||||
else
|
||||
return new URL(apiURL).origin;
|
||||
}
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user