Compare commits

...

18 Commits

Author SHA1 Message Date
dumbmoron
c68ebbe949
web/DownloadButton: use api-client 2024-09-14 19:14:47 +00:00
dumbmoron
1cd57fc7ff
web/Omnibox: mark url input as ready once turnstile is loaded 2024-09-14 19:14:47 +00:00
dumbmoron
95f0fbdb5e
api-client/turnstile: don't retry making session if it's not needed 2024-09-14 19:14:47 +00:00
dumbmoron
5d7cd861f3
web/api: remove stuff that was moved to api-client 2024-09-14 19:14:47 +00:00
dumbmoron
6d4477a962
web/api-url: replace getter with state 2024-09-14 19:14:47 +00:00
dumbmoron
64ac458941
api-client: make base URL reconfigurable 2024-09-14 19:14:47 +00:00
dumbmoron
d3e278660d
api-client/session: pull URL from client 2024-09-14 19:14:47 +00:00
dumbmoron
d201deb60a
api-client/package: configure tsup output and exports 2024-09-14 19:14:47 +00:00
dumbmoron
00d5754ea8
api-client: simplify interface, take turnstile object on init 2024-09-14 19:14:47 +00:00
dumbmoron
ff57a6a448
api-client/turnstile: expose information about client needing session 2024-09-14 19:14:47 +00:00
dumbmoron
1d30ac0139
api-client: implement individual api clients 2024-09-14 19:14:47 +00:00
dumbmoron
a80c7b7a5a
api-client: rename CobaltAPIResponse type to CobaltResponse 2024-09-14 19:14:47 +00:00
dumbmoron
afe9917169
api-client: move base api and session to internal subfolder 2024-09-14 19:14:47 +00:00
dumbmoron
4755787b69
api-client: initial api request 2024-09-14 19:14:47 +00:00
dumbmoron
c8ccb32421
api-client: copy request schema from api 2024-09-14 19:14:47 +00:00
dumbmoron
6cc1227288
api-client: migrate session handler and response types 2024-09-14 19:14:47 +00:00
dumbmoron
ab8650030b
api-client: set up error enums/types 2024-09-14 19:14:47 +00:00
dumbmoron
abb6a26ee0
i18n: move api errors to api-client package 2024-09-14 19:14:47 +00:00
23 changed files with 529 additions and 246 deletions

1
packages/api-client/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View 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!"
}

View File

@ -3,13 +3,34 @@
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": {}, "scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"keywords": [], "keywords": [],
"author": "imput <meow@imput.net>", "author": "imput <meow@imput.net>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"prettier": "3.3.3", "prettier": "3.3.3",
"tsup": "^8.2.4", "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"]
} }
} }

View File

@ -0,0 +1,3 @@
export * from "./turnstile-api";
export * from "./unauthenticated-api";
export * from "./types/interface";

View 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;
}
};

View 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();
}
};

View 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);
}
}

View 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;

View 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;
}

View 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>;

View 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;

View 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
View File

@ -77,9 +77,15 @@ importers:
tsup: tsup:
specifier: ^8.2.4 specifier: ^8.2.4
version: 8.2.4(postcss@8.4.40)(typescript@5.5.4) version: 8.2.4(postcss@8.4.40)(typescript@5.5.4)
turnstile-types:
specifier: ^1.2.2
version: 1.2.2
typescript: typescript:
specifier: ^5.4.5 specifier: ^5.4.5
version: 5.5.4 version: 5.5.4
zod:
specifier: ^3.23.8
version: 3.23.8
packages/version-info: {} packages/version-info: {}
@ -119,6 +125,9 @@ importers:
'@fontsource/redaction-10': '@fontsource/redaction-10':
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2
'@imput/cobalt-client':
specifier: workspace:^
version: link:../packages/api-client
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.2 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))) 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)))

View File

@ -6,47 +6,5 @@
"remux.corrupted": "couldn't read the metadata from this file, it may be corrupted.", "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 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!", "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!"
} }

View File

@ -26,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.5.0", "@eslint/js": "^9.5.0",
"@fontsource/redaction-10": "^5.0.2", "@fontsource/redaction-10": "^5.0.2",
"@imput/cobalt-client": "workspace:^",
"@sveltejs/adapter-static": "^3.0.2", "@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",

View File

@ -11,7 +11,7 @@
import { link } from "$lib/state/omnibox"; import { link } from "$lib/state/omnibox";
import { updateSetting } from "$lib/state/settings"; 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 { Optional } from "$lib/types/generic";
import type { DownloadModeOption } from "$lib/types/settings"; import type { DownloadModeOption } from "$lib/types/settings";
@ -58,7 +58,7 @@
} }
$: if (env.TURNSTILE_KEY) { $: if (env.TURNSTILE_KEY) {
if ($turnstileLoaded) { if ($turnstileCreated) {
isDisabled = false; isDisabled = false;
} else { } else {
isDisabled = true; isDisabled = true;

View File

@ -1,12 +1,23 @@
<script lang="ts"> <script lang="ts">
import "@fontsource-variable/noto-sans-mono"; import "@fontsource-variable/noto-sans-mono";
import { onMount } from "svelte";
import settings from "$lib/state/settings";
import API from "$lib/api/api"; 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 { t } from "$lib/i18n/translations";
import { createDialog } from "$lib/dialogs"; import { createDialog } from "$lib/dialogs";
import { downloadFile } from "$lib/download";
import type { DialogInfo } from "$lib/types/dialog"; 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 url: string;
export let disabled = false; 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) => { export const download = async (link: string) => {
changeDownloadButton("think"); 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) { if (!response) {
changeDownloadButton("error"); changeDownloadButton("error");

View File

@ -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;
}

View File

@ -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 probeCobaltTunnel = async (url: string) => {
const request = await fetch(`${url}&p=1`).catch(() => {}); const request = await fetch(`${url}&p=1`).catch(() => {});
if (request?.status === 200) { if (request?.status === 200) {
@ -89,6 +7,5 @@ const probeCobaltTunnel = async (url: string) => {
} }
export default { export default {
request,
probeCobaltTunnel, probeCobaltTunnel,
} }

View File

@ -1,5 +1,5 @@
import { get, writable } from "svelte/store"; 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"; import type { CobaltServerInfoResponse, CobaltErrorResponse, CobaltServerInfo } from "$lib/types/api";
@ -11,7 +11,7 @@ export type CobaltServerInfoCache = {
export const cachedInfo = writable<CobaltServerInfoCache | undefined>(); export const cachedInfo = writable<CobaltServerInfoCache | undefined>();
const request = async () => { const request = async () => {
const apiEndpoint = `${currentApiURL()}/`; const apiEndpoint = `${get(APIUrl)}/`;
const response: CobaltServerInfoResponse = await fetch(apiEndpoint, { const response: CobaltServerInfoResponse = await fetch(apiEndpoint, {
redirect: "manual", redirect: "manual",
@ -35,7 +35,7 @@ const request = async () => {
export const getServerInfo = async () => { export const getServerInfo = async () => {
const cache = get(cachedInfo); const cache = get(cachedInfo);
if (cache && cache.origin === currentApiURL()) { if (cache && cache.origin === get(APIUrl)) {
return true return true
} }
@ -48,7 +48,7 @@ export const getServerInfo = async () => {
if (!("status" in freshInfo)) { if (!("status" in freshInfo)) {
cachedInfo.set({ cachedInfo.set({
info: freshInfo, info: freshInfo,
origin: currentApiURL(), origin: get(APIUrl),
}); });
return true; return true;
} }

View File

@ -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;
}

View File

@ -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,
}

View 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;
}
);