mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-18 20:12:12 +00:00
refactor!: cleanup platform support (#306)
* refactor!: cleanup platform support * chore: lint * fix: web platform * feat: provide UniversalCache Provide UniversalCache as a wrapper around Platform.shim.Cache. * fix: invalid import * refactor: remove isolated-vm support * fix: type info * refactor: cleanup exports * fix: mark jintr as external dependency In the bundled CJS node build, mark jintr as external. * chore: add additional exports web exports provide a way to select web implementation manually without relying on the bundler to select it correctly from the "exports" field web points to src/platform/web.js web.bundle points to bundle/browser.js web.bundle.browser points to bundle/browser.min.js agnostic exports provide users of the library to provide their own platform implementation without first importing the default one. agnostic points to src/platform/lib.ts * fix: toDash on web * revert: eval is synchronous * fix: use serializeDOM in FormatUtils * ci: automate releases with `release-please` * chore: clean up workflow files * ci: fix NPM publish action --------- Co-authored-by: LuanRT <luan.lrt4@gmail.com>
This commit is contained in:
@@ -1,238 +1,21 @@
|
||||
import { getRuntime } from './Utils';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
import { Platform } from './Utils.js';
|
||||
|
||||
// Browser Cache is based off:
|
||||
// https://github.com/elias551/simple-kvs/blob/master/src/index.ts
|
||||
|
||||
export default class UniversalCache {
|
||||
#persistent_directory: string;
|
||||
#persistent: boolean;
|
||||
|
||||
constructor(persistent = false, persistent_directory?: string) {
|
||||
this.#persistent_directory = persistent_directory || UniversalCache.default_persistent_directory;
|
||||
this.#persistent = persistent;
|
||||
export default class UniversalCache implements ICache {
|
||||
#cache: ICache;
|
||||
constructor(persistent: boolean, persistent_directory?: string) {
|
||||
this.#cache = new Platform.shim.Cache(persistent, persistent_directory);
|
||||
}
|
||||
|
||||
static get temp_directory() {
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
return `${Deno.env.get('TMPDIR') || Deno.env.get('TMP') || Deno.env.get('TEMP') || '/tmp'}/youtubei.js`;
|
||||
case 'node':
|
||||
return `${Reflect.get(module, 'require')('os').tmpdir()}/youtubei.js`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static get default_persistent_directory() {
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
return `${Deno.cwd()}/.cache/youtubei.js`;
|
||||
case 'node':
|
||||
return Reflect.get(module, 'require')('path').resolve(__dirname, '..', '..', '.cache', 'youtubei.js');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
get cache_dir() {
|
||||
return this.#persistent ? this.#persistent_directory : UniversalCache.temp_directory;
|
||||
return this.#cache.cache_dir;
|
||||
}
|
||||
|
||||
async #createCache() {
|
||||
const dir = this.cache_dir;
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
try {
|
||||
const cwd = await Deno.stat(dir);
|
||||
if (!cwd.isDirectory)
|
||||
throw new Error('An unexpected file was found in place of the cache directory');
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotFound)
|
||||
await Deno.mkdir(dir, { recursive: true });
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'node':
|
||||
const fs = Reflect.get(module, 'require')('fs/promises');
|
||||
try {
|
||||
const cwd = await fs.stat(dir);
|
||||
if (!cwd.isDirectory())
|
||||
throw new Error('An unexpected file was found in place of the cache directory');
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'ENOENT')
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
break;
|
||||
}
|
||||
get(key: string) {
|
||||
return this.#cache.get(key);
|
||||
}
|
||||
|
||||
#getBrowserDB() {
|
||||
const indexedDB: IDBFactory = Reflect.get(globalThis, 'indexedDB') || Reflect.get(globalThis, 'webkitIndexedDB') || Reflect.get(globalThis, 'mozIndexedDB') || Reflect.get(globalThis, 'msIndexedDB');
|
||||
|
||||
if (!indexedDB) return console.log('IndexedDB is not supported. No cache will be used.');
|
||||
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open('youtubei.js', 1);
|
||||
request.onsuccess = function () {
|
||||
resolve(this.result);
|
||||
};
|
||||
|
||||
request.onerror = function (event) {
|
||||
reject('indexedDB request error');
|
||||
console.error(event);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = function () {
|
||||
const store = this.result.createObjectStore('kv-store', {
|
||||
keyPath: 'k'
|
||||
});
|
||||
|
||||
store.transaction.oncomplete = function () {
|
||||
resolve(this.db);
|
||||
};
|
||||
};
|
||||
});
|
||||
set(key: string, value: ArrayBuffer) {
|
||||
return this.#cache.set(key, value);
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
await this.#createCache();
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
{
|
||||
const file = `${this.cache_dir}/${key}`;
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
try {
|
||||
const stat = await Deno.stat(file);
|
||||
if (stat.isFile) {
|
||||
const data: Uint8Array = await Deno.readFile(file);
|
||||
return data.buffer;
|
||||
}
|
||||
throw new Error('An unexpected file was found in place of the cache key');
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotFound)
|
||||
return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
case 'node':
|
||||
{
|
||||
const fs = Reflect.get(module, 'require')('fs/promises');
|
||||
const file = Reflect.get(module, 'require')('path').resolve(this.cache_dir, key);
|
||||
try {
|
||||
const stat = await fs.stat(file);
|
||||
if (stat.isFile()) {
|
||||
const data: Buffer = await fs.readFile(file);
|
||||
return data.buffer;
|
||||
}
|
||||
throw new Error('An unexpected file was found in place of the cache key');
|
||||
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'ENOENT')
|
||||
return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
case 'browser':
|
||||
{
|
||||
const db = await this.#getBrowserDB();
|
||||
if (!db) return;
|
||||
|
||||
return new Promise<ArrayBuffer | undefined>((resolve, reject) => {
|
||||
const request = db.transaction('kv-store', 'readonly').objectStore('kv-store').get(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = function () {
|
||||
const result: Uint8Array | undefined = this.result?.v;
|
||||
resolve(result ? result.buffer : undefined);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: ArrayBuffer) {
|
||||
await this.#createCache();
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
{
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
const file = `${this.cache_dir}/${key}`;
|
||||
await Deno.writeFile(file, new Uint8Array(value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'node':
|
||||
{
|
||||
const fs = Reflect.get(module, 'require')('fs/promises');
|
||||
const file = Reflect.get(module, 'require')('path').resolve(this.cache_dir, key);
|
||||
await fs.writeFile(file, new Uint8Array(value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'browser':
|
||||
{
|
||||
const db = await this.#getBrowserDB();
|
||||
if (!db) return;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = db.transaction('kv-store', 'readwrite').objectStore('kv-store').put({ k: key, v: value });
|
||||
request.onerror = reject;
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
await this.#createCache();
|
||||
switch (getRuntime()) {
|
||||
case 'deno':
|
||||
{
|
||||
const file = `${this.cache_dir}/${key}`;
|
||||
const Deno: any = Reflect.get(globalThis, 'Deno');
|
||||
try {
|
||||
await Deno.remove(file);
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotFound) return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'node':
|
||||
{
|
||||
const fs = Reflect.get(module, 'require')('fs/promises');
|
||||
const file = Reflect.get(module, 'require')('path').resolve(this.cache_dir, key);
|
||||
try {
|
||||
await fs.unlink(file);
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'ENOENT') return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'browser':
|
||||
{
|
||||
const db = await this.#getBrowserDB();
|
||||
if (!db) return;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = db.transaction('kv-store', 'readwrite').objectStore('kv-store').delete(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
remove(key: string) {
|
||||
return this.#cache.remove(key);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
import Player from '../core/Player';
|
||||
import Actions from '../core/Actions';
|
||||
import Player from '../core/Player.js';
|
||||
import Actions from '../core/Actions.js';
|
||||
|
||||
import type Format from '../parser/classes/misc/Format';
|
||||
import type AudioOnlyPlayability from '../parser/classes/AudioOnlyPlayability';
|
||||
import type { YTNode } from '../parser/helpers';
|
||||
import type Format from '../parser/classes/misc/Format.js';
|
||||
import type AudioOnlyPlayability from '../parser/classes/AudioOnlyPlayability.js';
|
||||
import type { YTNode } from '../parser/helpers.js';
|
||||
|
||||
import { DOMParser } from 'linkedom';
|
||||
import type { Element } from 'linkedom/types/interface/element';
|
||||
import type { Node } from 'linkedom/types/interface/node';
|
||||
import type { XMLDocument } from 'linkedom/types/xml/document';
|
||||
|
||||
import { Constants } from '.';
|
||||
import { getStringBetweenStrings, InnertubeError, streamToIterable } from './Utils';
|
||||
import { Constants } from './index.js';
|
||||
import { getStringBetweenStrings, InnertubeError, Platform, streamToIterable } from './Utils.js';
|
||||
|
||||
export type URLTransformer = (url: URL) => URL;
|
||||
export type FormatFilter = (format: Format) => boolean;
|
||||
@@ -111,7 +106,7 @@ class FormatUtils {
|
||||
|
||||
let cancel: AbortController;
|
||||
|
||||
const readable_stream = new ReadableStream<Uint8Array>({
|
||||
const readable_stream = new Platform.shim.ReadableStream<Uint8Array>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
start() { },
|
||||
pull: async (controller) => {
|
||||
@@ -275,10 +270,11 @@ class FormatUtils {
|
||||
|
||||
const length = adaptive_formats[0].approx_duration_ms / 1000;
|
||||
|
||||
const document = new DOMParser().parseFromString('', 'text/xml');
|
||||
const document = new Platform.shim.DOMParser().parseFromString('<?xml version="1.0" encoding="utf-8"?><MPD />', 'application/xml');
|
||||
const mpd = document.querySelector('MPD') as HTMLElement;
|
||||
const period = document.createElement('Period');
|
||||
|
||||
document.appendChild(this.#el(document, 'MPD', {
|
||||
mpd.replaceWith(this.#el(document, 'MPD', {
|
||||
xmlns: 'urn:mpeg:dash:schema:mpd:2011',
|
||||
minBufferTime: 'PT1.500S',
|
||||
profiles: 'urn:mpeg:dash:profile:isoff-main:2011',
|
||||
@@ -292,13 +288,13 @@ class FormatUtils {
|
||||
|
||||
this.#generateAdaptationSet(document, period, adaptive_formats, url_transformer, cpn, player);
|
||||
|
||||
return `${document}`;
|
||||
return Platform.shim.serializeDOM(document);
|
||||
}
|
||||
|
||||
static #el(document: XMLDocument, tag: string, attrs: Record<string, string | undefined>, children: Node[] = []) {
|
||||
const el = document.createElement(tag);
|
||||
for (const [ key, value ] of Object.entries(attrs)) {
|
||||
el.setAttribute(key, value);
|
||||
value && el.setAttribute(key, value);
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'undefined') continue;
|
||||
@@ -435,7 +431,7 @@ class FormatUtils {
|
||||
]));
|
||||
}
|
||||
|
||||
static #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player) {
|
||||
static async #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player) {
|
||||
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
|
||||
if (!format.index_range || !format.init_range)
|
||||
throw new InnertubeError('Index and init ranges not available', { format });
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import Session, { Context } from '../core/Session';
|
||||
import Constants from './Constants';
|
||||
import { generateSidAuth, getRandomUserAgent, getStringBetweenStrings, InnertubeError, isServer } from './Utils';
|
||||
|
||||
export type FetchFunction = typeof fetch;
|
||||
import Session, { Context } from '../core/Session.js';
|
||||
import { FetchFunction } from '../types/PlatformShim.js';
|
||||
import Constants from './Constants.js';
|
||||
import {
|
||||
Platform,
|
||||
generateSidAuth,
|
||||
getRandomUserAgent,
|
||||
getStringBetweenStrings,
|
||||
InnertubeError
|
||||
} from './Utils.js';
|
||||
|
||||
export interface HTTPClientInit {
|
||||
baseURL?: string;
|
||||
@@ -16,7 +21,7 @@ export default class HTTPClient {
|
||||
constructor(session: Session, cookie?: string, fetch?: FetchFunction) {
|
||||
this.#session = session;
|
||||
this.#cookie = cookie;
|
||||
this.#fetch = fetch || globalThis.fetch;
|
||||
this.#fetch = fetch || Platform.shim.fetch;
|
||||
}
|
||||
|
||||
get fetch_function(): FetchFunction {
|
||||
@@ -40,12 +45,12 @@ export default class HTTPClient {
|
||||
|
||||
const headers =
|
||||
init?.headers ||
|
||||
(input instanceof Request ? input.headers : new Headers()) ||
|
||||
new Headers();
|
||||
(input instanceof Platform.shim.Request ? input.headers : new Platform.shim.Headers()) ||
|
||||
new Platform.shim.Headers();
|
||||
|
||||
const body = init?.body || (input instanceof Request ? input.body : undefined);
|
||||
const body = init?.body || (input instanceof Platform.shim.Request ? input.body : undefined);
|
||||
|
||||
const request_headers = new Headers(headers);
|
||||
const request_headers = new Platform.shim.Headers(headers);
|
||||
|
||||
request_headers.set('Accept', '*/*');
|
||||
request_headers.set('Accept-Language', '*');
|
||||
@@ -53,7 +58,7 @@ export default class HTTPClient {
|
||||
request_headers.set('x-origin', request_url.origin);
|
||||
request_headers.set('x-youtube-client-version', this.#session.context.client.clientVersion || '');
|
||||
|
||||
if (isServer()) {
|
||||
if (Platform.shim.server) {
|
||||
request_headers.set('User-Agent', getRandomUserAgent('desktop'));
|
||||
request_headers.set('origin', request_url.origin);
|
||||
}
|
||||
@@ -115,13 +120,13 @@ export default class HTTPClient {
|
||||
}
|
||||
}
|
||||
|
||||
const request = new Request(request_url, input instanceof Request ? input : init);
|
||||
const request = new Platform.shim.Request(request_url, input instanceof Platform.shim.Request ? input : init);
|
||||
|
||||
const response = await this.#fetch(request, {
|
||||
body: request_body,
|
||||
headers: request_headers,
|
||||
credentials: 'include',
|
||||
redirect: input instanceof Request ? input.redirect : init?.redirect || 'follow'
|
||||
redirect: input instanceof Platform.shim.Request ? input.redirect : init?.redirect || 'follow'
|
||||
});
|
||||
|
||||
// Check if 2xx
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import package_json from '../../package.json';
|
||||
import { Memo } from '../parser/helpers';
|
||||
import { FetchFunction } from './HTTPClient';
|
||||
import userAgents from './user-agents.json';
|
||||
import { Memo } from '../parser/helpers.js';
|
||||
import PlatformShim, { FetchFunction } from '../types/PlatformShim.js';
|
||||
import userAgents from './user-agents.js';
|
||||
|
||||
export class Platform {
|
||||
static #shim: PlatformShim | undefined;
|
||||
static load(platform: PlatformShim): void {
|
||||
Platform.#shim = platform;
|
||||
}
|
||||
static get shim(): PlatformShim {
|
||||
if (!Platform.#shim) {
|
||||
throw new Error('Platform is not loaded');
|
||||
}
|
||||
return Platform.#shim;
|
||||
}
|
||||
}
|
||||
export class InnertubeError extends Error {
|
||||
date: Date;
|
||||
version: string;
|
||||
@@ -16,7 +27,7 @@ export class InnertubeError extends Error {
|
||||
}
|
||||
|
||||
this.date = new Date();
|
||||
this.version = package_json.version;
|
||||
this.version = Platform.shim.info.version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,40 +81,8 @@ export function getRandomUserAgent(type: DeviceCategory): string {
|
||||
return available_agents[random_index];
|
||||
}
|
||||
|
||||
export async function sha1Hash(str: string): Promise<string> {
|
||||
const SubtleCrypto = getRuntime() === 'node' ? (Reflect.get(module, 'require')('crypto').webcrypto as unknown as Crypto).subtle : window.crypto.subtle;
|
||||
const byteToHex = [
|
||||
'00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f',
|
||||
'10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f',
|
||||
'20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f',
|
||||
'30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f',
|
||||
'40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f',
|
||||
'50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f',
|
||||
'60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f',
|
||||
'70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f',
|
||||
'80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f',
|
||||
'90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f',
|
||||
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af',
|
||||
'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf',
|
||||
'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf',
|
||||
'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df',
|
||||
'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef',
|
||||
'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff'
|
||||
];
|
||||
|
||||
function hex(arrayBuffer: ArrayBuffer): string {
|
||||
const buff = new Uint8Array(arrayBuffer);
|
||||
const hexOctets = [];
|
||||
for (let i = 0; i < buff.length; ++i)
|
||||
hexOctets.push(byteToHex[buff[i]]);
|
||||
return hexOctets.join('');
|
||||
}
|
||||
|
||||
return hex(await SubtleCrypto.digest('SHA-1', new TextEncoder().encode(str)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an authentication token from a cookies' sid.
|
||||
* Generates an authentication token from a cookies' sid..js
|
||||
* @param sid - Sid extracted from cookies
|
||||
*/
|
||||
export async function generateSidAuth(sid: string): Promise<string> {
|
||||
@@ -111,7 +90,7 @@ export async function generateSidAuth(sid: string): Promise<string> {
|
||||
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000);
|
||||
const input = [ timestamp, sid, youtube ].join(' ');
|
||||
const gen_hash = await sha1Hash(input);
|
||||
const gen_hash = await Platform.shim.sha1Hash(input);
|
||||
|
||||
return [ 'SAPISIDHASH', [ timestamp, gen_hash ].join('_') ].join(' ');
|
||||
}
|
||||
@@ -177,36 +156,6 @@ export function hasKeys<T extends object, R extends (keyof T)[]>(params: T, ...k
|
||||
return true;
|
||||
}
|
||||
|
||||
export function uuidv4(): string {
|
||||
if (getRuntime() === 'node') {
|
||||
return Reflect.get(module, 'require')('crypto').webcrypto.randomUUID();
|
||||
}
|
||||
|
||||
if (globalThis.crypto?.randomUUID()) {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
// See https://stackoverflow.com/a/2117523
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (cc) => {
|
||||
const c = parseInt(cc);
|
||||
return (c ^ window.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export type Runtime = 'node' | 'deno' | 'browser';
|
||||
|
||||
export function getRuntime(): Runtime {
|
||||
if ((typeof process !== 'undefined') && (process?.versions?.node))
|
||||
return 'node';
|
||||
if (Reflect.has(globalThis, 'Deno'))
|
||||
return 'deno';
|
||||
return 'browser';
|
||||
}
|
||||
|
||||
export function isServer(): boolean {
|
||||
return [ 'node', 'deno' ].includes(getRuntime());
|
||||
}
|
||||
|
||||
export async function* streamToIterable(stream: ReadableStream<Uint8Array>) {
|
||||
const reader = stream.getReader();
|
||||
|
||||
@@ -262,7 +211,7 @@ export const debugFetch: FetchFunction = (input, init) => {
|
||||
' body:\n${body_contents}`
|
||||
);
|
||||
|
||||
return globalThis.fetch(input, init);
|
||||
return Platform.shim.fetch(input, init);
|
||||
};
|
||||
|
||||
export function u8ToBase64(u8: Uint8Array): string {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
export * as Utils from './Utils';
|
||||
export * as Constants from './Constants';
|
||||
export { default as UniversalCache } from './Cache';
|
||||
export { default as EventEmitter } from './EventEmitterLike';
|
||||
export { default as HTTPClient } from './HTTPClient';
|
||||
export * from './HTTPClient';
|
||||
export { default as UniversalCache } from './Cache.js';
|
||||
|
||||
export * as Constants from './Constants.js';
|
||||
|
||||
export { default as EventEmitter } from './EventEmitterLike.js';
|
||||
|
||||
export { default as HTTPClient } from './HTTPClient.js';
|
||||
export * from './HTTPClient.js';
|
||||
|
||||
export { Platform } from './Utils.js';
|
||||
export * as Utils from './Utils.js';
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"desktop": [
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
|
||||
],
|
||||
"mobile": [
|
||||
"Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 11; SM-G781B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; arm_64; Android 12; RMX3081) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.148 YaBrowser/22.7.3.82.00 SA/3 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 11; GM1900) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0675.117 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 11; 21061119BI) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 10; HarmonyOS; TEL-AN10; HMSCore 6.6.0.312) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 HuaweiBrowser/12.1.1.321 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; U; Android 8.0.0; zh-cn; Mi Note 2 Build/OPR1.170623.032) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.128 Mobile Safari/537.36 XiaoMi/MiuiBrowser/10.1.1",
|
||||
"Mozilla/5.0 (Linux; Android 12; IN2013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 9; moto e6s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 11; ONEPLUS A6013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 12; SM-G986B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Prime) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"
|
||||
]
|
||||
}
|
||||
60
src/utils/user-agents.ts
Normal file
60
src/utils/user-agents.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/* Generated file do not edit */
|
||||
export default {
|
||||
"desktop": [
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
|
||||
],
|
||||
"mobile": [
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 12; SM-G990U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/108.1 Mobile/15E148 Safari/605.1.15",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77 ChannelId(73) NebulaSDK/1.8.100112 Nebula PSDType(1) AlipayDefined(nt:4G,ws:320|504|2.0) AliApp(AP/10.1.30.300) AlipayClient/10.1.30.300 Alipay Language/zh-Hans",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-N981U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-A515F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 12; M2010J19SG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/109.0.5414.83 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 11; M2102J20SG) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1"
|
||||
]
|
||||
} as { desktop: string[], mobile: string[] };
|
||||
Reference in New Issue
Block a user