Merge pull request #17 from hansputera/dev

v2.0.0
This commit is contained in:
ハンニフ
2025-08-24 01:10:30 +08:00
committed by GitHub
102 changed files with 5907 additions and 2637 deletions

View File

@@ -1,2 +0,0 @@
node_modules
public

View File

@@ -1,19 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"google"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

2
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,2 @@
* @hansputera
packages/core/src/musicalyDownProvider.ts @OhYoonHee

12
.gitignore vendored
View File

@@ -1,4 +1,14 @@
.vercel
node_modules/
.env.local
.env
package-lock.json
.turbo
.yarn/cache
.yarn/sdks
.yarn/plugins
.yarn/patches
.yarn/*.gz
*.rdb
# Local Netlify folder
.netlify

View File

@@ -1,4 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint
npm run lint
npm run format

942
.yarn/releases/yarn-4.9.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

7
.yarnrc.yml Normal file
View File

@@ -0,0 +1,7 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.3.cjs

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:current-alpine3.15
RUN apk update
# Prepare
RUN mkdir -p /home/tiktok-dl
COPY . /home/tiktok-dl
WORKDIR /home/tiktok-dl
# Build
RUN yarn install
RUN yarn build
# Expose port
EXPOSE 3000
CMD ["yarn", "start"]

View File

@@ -3,16 +3,18 @@
Video TikTok Downloader using 🧰 NodeJS with Watermark and Non-Watermark!
## 〽️ Technology/Dependencies
- [Turbo Repo](https://turborepo.org)
- [Got](https://npmjs.com/got)
- [Ow](https://npmjs.com/ow)
- [VM2](https://npmjs.com/vm2)
- [Next.js](https://nextjs.org)
- [WindiCSS](https://windicss.org)
## Develop 👷
## Development 👷
- Make sure you have [yarn](https://yarnpkg.com) installed, and you are using the [yarn berry](https://yarnpkg.com/getting-started/migration#step-by-step) version.
- Clone/fork into a directory you want.
- Install all dependencies correctly (`yarn install` or `npm install`)
- Install vercel cli too with `npm install vercel -g` or `yarn global add vercel`
- Ran `vercel dev` for development (default port is: `3000`)
> Use `vercel --prod` for production use 😎
- Install all dependencies correctly (`yarn install`)
- Ran `yarn dev` (default web port: `3000`)
# Configuration 🔑
@@ -26,7 +28,10 @@ Video TikTok Downloader using 🧰 NodeJS with Watermark and Non-Watermark!
3. Add environment variable with `REDIS_URL` as the name, and fill the value using your redis url.
## ☁️ Endpoints
Check [this](https://docs.tiktok-dl.tslab.site) out.
Check [this](https://docs.tdl.besecure.eu.org) out.
## :bug: Bugs Report
Feel free to open an issue if you have a problem or find a bug. That would be very helpful!
## 🔥 Credits + Source
@@ -36,13 +41,10 @@ Check [this](https://docs.tiktok-dl.tslab.site) out.
- [SaveFrom](https://id.savefrom.net)
- [TTDownloader](https://ttdownloader.com)
- [Musically Down](https://musicaldown.com)
- [DLTik](https://dltik.com/)
- [TTSave](https://ttsave.app)
- [DDDTik](https://dddtik.com)
- [TikDown](https://tikdown.org)
- [DownTik](https://downtik.net)
- [LoveTik](https://lovetik.com)
- [Tokup](https://tokup.app)
- [TikTokDownloaderOne](https://tiktokdownloader.one)
- [GetVidTik](https://getvidtik.com)
🧗‍♀️ Contribution(s) are welcome!

View File

@@ -1,50 +0,0 @@
import type {VercelRequest, VercelResponse} from '@vercel/node';
import ow from 'ow';
import {getProvider, Providers} from '../lib/providers';
import {BaseProvider} from '../lib/providers/baseProvider';
import {rotateProvider} from '../lib/rotator';
import {ratelimitMiddleware} from '../middleware/ratelimit';
const providersType = Providers.map((p) => p.resourceName());
export default async (req: VercelRequest, res: VercelResponse) => {
try {
ow(req.method === 'POST' ? req.body : req.query, ow.object.partialShape({
'url': ow.string.url.validate((v) => ({
'validator': /^http(s?)(:\/\/)([a-z]+\.)*tiktok\.com\/(.*)?\/(.*)?$/gi
.test(v),
'message': 'Expected (.*).tiktok.com',
})),
'type': ow.optional.string.validate((v) => ({
'validator': typeof v === 'string' &&
providersType.includes(v.toLowerCase()),
'message': 'Invalid Provider, available provider is: ' +
Providers.map((x) => x.resourceName()).join(', '),
})),
'nocache': req.method === 'POST' ?
ow.optional.boolean : ow.optional.string,
'rotateOnError': req.method === 'POST' ?
ow.optional.boolean : ow.optional.string,
}));
const provider = getProvider((req.query && req.query.type || req.body && req.body.type) ?? 'random');
if (!provider) {
return res.status(400).json({
'error': 'Invalid provider',
'providers': providersType,
});
}
const result = await rotateProvider(
provider as BaseProvider, req.query.url || req.body.url,
req.method === 'POST' ?
req.body.url : req.query.url, req.method === 'POST' ?
req.body.rotateOnError :
!!req.query.rotateOnError);
await ratelimitMiddleware(req);
return res.status(200).json(result);
} catch (e) {
return res.status(400).json({
'error': (e as Error).message,
});
}
};

View File

@@ -1,11 +0,0 @@
import type {VercelRequest, VercelResponse} from '@vercel/node';
export default async (_: VercelRequest, res: VercelResponse) => {
res.json({
'index': 'Hello world!',
'endpoints': {
'ping': '/api/ping',
'download': '/api/download',
},
});
};

View File

@@ -1,16 +0,0 @@
import type {VercelRequest, VercelResponse} from '@vercel/node';
import {fetch} from '../lib';
export default async (_: VercelRequest, res: VercelResponse) => {
const start = Date.now();
try {
const response = await fetch('./');
res.status(200).json({
status: response.statusCode,
took: Date.now() - start,
data: response.statusMessage || 'Nothing.',
});
} catch (e) {
res.status(500).json({status: null, data: null, took: null});
}
};

View File

@@ -1,12 +0,0 @@
import type {VercelRequest, VercelResponse} from '@vercel/node';
import {Providers} from '../lib';
export default async (_: VercelRequest, res: VercelResponse) => {
const providers = Providers.map((p) => ({
'name': p.resourceName(),
'url': p.client.defaults.options.prefixUrl,
'maintenance': p.maintenance,
}));
return res.send(providers);
};

View File

@@ -1,9 +0,0 @@
import {VercelRequest, VercelResponse} from '@vercel/node';
import {matchLink} from '../lib/providers/util';
import {client} from '../lib/redis';
export default async (_: VercelRequest, res: VercelResponse) => {
const keys = await client.keys('*');
res.status(200).json(keys.filter((x) => matchLink(x)));
};

6
apps/web/.eslintrc.js Normal file
View File

@@ -0,0 +1,6 @@
const eslintConfigs = require('tiktok-dl-config/eslint.typescript');
module.exports = {
...eslintConfigs,
extends: eslintConfigs.extends.concat(['plugin:@next/next/recommended']),
};

2
apps/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.next
*.rdb

3
apps/web/.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
*.json
.next

1
apps/web/.prettierrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('tiktok-dl-config/prettier');

View File

@@ -0,0 +1,20 @@
import React from 'react';
/**
* Body Component.
* @param {{children: JSX.Element}} param0 BodyComponent props.
* @return {JSX.Element}
*/
export const BodyComponent = ({
children,
}: {
children: JSX.Element;
}): JSX.Element => {
return (
<React.Fragment>
<section className="min-h-screen bg-light-200">
<div className="md:container md:mx-auto">{children}</div>
</section>
</React.Fragment>
);
};

View File

@@ -0,0 +1,15 @@
import React from 'react';
export const Footer = () => (
<React.Fragment>
<p className="text-lg">
&copy; {new Date().getFullYear()}{' '}
<a
target="_blank"
href="https://github.com/hansputera/tiktok-dl.git"
>
TikTok-DL Project
</a>
</p>
</React.Fragment>
);

View File

@@ -0,0 +1,187 @@
import React from 'react';
import useSWR, {Fetcher} from 'swr';
import {ExtractedInfo} from 'tiktok-dl-core';
import {getTikTokURL} from '../lib/url';
import {VideoComponent} from './Video';
// // ERRORS ///
/**
* @class InvalidUrlError
*/
class InvalidUrlError extends Error {
/**
* @param {string} msg error message.
*/
constructor(msg?: string) {
super(msg);
this.name = 'INVALID_URL';
}
}
export type ExtractedInfoWithProvider = ExtractedInfo & {
provider: string;
_url: string;
};
interface StateData {
submitted: boolean;
error?: string | Error;
url: string;
}
const fetcher: Fetcher<ExtractedInfoWithProvider, string> = (...args) =>
fetch(...args).then((r) => r.json());
/**
* FormInput Component.
* @return {JSX.Element}
*/
export const FormInputComponent = (): JSX.Element => {
const [state, setState] = React.useState<StateData>({
submitted: false,
error: undefined,
url: '',
});
const {data, mutate} = useSWR(
(!state.error || !(state.error as string).length) &&
/^http(s?)(:\/\/)([a-z]+\.)*tiktok\.com\/(.+)$/gi.test(state.url)
? [
'/api/download',
{
method: 'POST',
body: JSON.stringify({url: state.url}),
},
]
: null,
fetcher,
{
loadingTimeout: 5_000,
refreshInterval: 60_000,
revalidateOnMount: false,
onSuccess: () =>
setState({
...state,
submitted: false,
}),
},
);
React.useEffect(() => {
if (
!/^http(s?)(:\/\/)([a-z]+\.)*tiktok\.com\/(.+)$/gi.test(
state.url,
) &&
state.url.length
) {
setState({
...state,
error: new InvalidUrlError('Invalid TikTok URL'),
});
} else {
// submit event trigger.
if (state.submitted && !state.error) {
mutate();
}
try {
const u = getTikTokURL(state.url);
if (!u) {
setState({
...state,
error: new InvalidUrlError('Invalid TikTok URL'),
});
return;
}
setState({
...state,
url: u,
});
} catch {
setState({
...state,
error: new InvalidUrlError('Invalid TikTok URL'),
});
}
setState({
...state,
error: undefined,
});
}
}, [state.submitted, state.url]);
return (
<React.Fragment>
<section className="inline-block">
<h1 className="text-lg leading-relaxed">
Fill TikTok's Video URL below:
</h1>
<p className="text-red-400 font-sans font-semibold">
{state.error instanceof Error
? state.error.name.concat(
': '.concat(state.error.message),
)
: state.error
? state.error
: ''}
</p>
<form
className="flex flex-col md:flex-row"
onSubmit={(event) => {
event.preventDefault();
if (!state.url.length) {
setState({
...state,
error: 'Please fill the URL!',
});
return;
}
!state.error &&
setState({
...state,
submitted: true,
});
}}
>
<div>
<input
type="url"
onChange={(event) =>
setState({
...state,
url: event.target.value,
})
}
value={state.url}
placeholder="e.g: "
className="p-3 border border-gray-300 font-sans h-auto w-auto outline-solid-blue-500"
/>
</div>
<div>
<button
className="p-3 lg:ml-2 mt-1 bg-sky-400 uppercase text-white shadow-sm"
disabled={state.submitted}
>
download
</button>
</div>
</form>
<section className="mt-3 mb-3">
{state.submitted && !data && (
<p className={'text-base font-sans text-blue-500'}>
Wait a minute
</p>
)}
{data && data.video && data.video.urls.length && (
<VideoComponent data={data} />
)}
</section>
</section>
</React.Fragment>
);
};
export default FormInputComponent;

View File

@@ -0,0 +1,36 @@
/**
* Next Head Component.
* @param {{title: string, children: JSX.Element }} param0 NextHeadComponent args.
* @return {JSX.Element}
*/
export const NextHeadComponent = ({
title,
children,
}: {
title?: string;
children?: JSX.Element;
}): JSX.Element => {
return (
<>
<title>{title ?? 'TikTok Downloader'}</title>
<meta
name="title"
content="TikTok Downloader | Non&Watermark Support"
/>
<meta
name="description"
content="An Open-Source Project where it could download TikTok's Video without annoying ads!"
/>
<meta
name="keywords"
content="tiktok-downloader, tiktokdl, tiktok, download video tiktok, tiktok no watermark"
/>
<meta
name="author"
content="Hanif Dwy Putra S <github.com/hansputera>"
/>
<meta name="robots" content="index, follow" />
{children}
</>
);
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import type {ExtractedInfoWithProvider} from './FormInput';
export const VideoComponent = ({data}: {data: ExtractedInfoWithProvider}) => {
const copyUrl = (url: string) => {
navigator.clipboard.writeText(url);
if (typeof window !== 'undefined') {
window.alert('URL Copied');
}
};
return (
<React.Fragment>
This video is downloaded from{' '}
<span className="font-semibold">{data.provider}</span>.
{data.caption && <pre>{data.caption}</pre>}
<div className="md:grid md:grid-cols-3 md:gap-4">
<video
controls={true}
autoPlay={false}
className="rounded-md h-64 w-80"
>
<source src={data.video?.urls[0]} />
</video>
<div className="flex flex-row font-sans basis-8 mt-2">
{data.video?.urls.map((url, index) => (
<button
key={index.toString()}
className="mr-1 bg-teal-400 md:p-2 p-1 rounded-md shadow"
onClick={() => copyUrl(url)}
>
LINK {index + 1}
</button>
))}
</div>
</div>
</React.Fragment>
);
};

View File

@@ -1,16 +1,16 @@
export const rateLimitConfig = {
'enable': true, // you can set it to 'false'
/**
enable: true, // you can set it to 'false'
/**
* How much rate limit count per x second(s)
* -is allowed? Default: 60 requests
*/
'maxRatelimitPerXSeconds': 60,
/**
maxRatelimitPerXSeconds: 60,
/**
* Every x second(s), the ratelimit data will removed.
* So, their ratelimit data will removed.
* PS: Data stored on redis.
*/
'ratelimitTime': 60,
ratelimitTime: 120,
};
/**
@@ -18,3 +18,9 @@ export const rateLimitConfig = {
* Default: 1 hour
*/
export const providerCache = 3600;
/**
* How much rotate retries is allowed?
* Default: 5x
*/
export const maxRotateCount = 3;

View File

@@ -0,0 +1,23 @@
/**
* @class HttpError
*/
export class HttpError extends Error {
public statusCode = 400;
/**
* @constructor
* @param {string} message HttpError message,
*/
constructor(message: string) {
super(message);
}
/**
* Set http status code.
* @param {number} code HttpError status code.
* @return {HttpError}
*/
setCode(code: number): HttpError {
this.statusCode = code;
return this;
}
}

1
apps/web/errors/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './httpError';

3
apps/web/lib/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './redis';
export * from './rotator';
export * from './url';

5
apps/web/lib/redis.ts Normal file
View File

@@ -0,0 +1,5 @@
import Redis from 'ioredis';
export const client = new Redis(
process.env.REDIS_URL ?? 'redis://:@localhost:6379',
);

90
apps/web/lib/rotator.ts Normal file
View File

@@ -0,0 +1,90 @@
import {maxRotateCount, providerCache} from '../config';
import {BaseProvider, ExtractedInfo, getRandomProvider} from 'tiktok-dl-core';
import {client as redisClient} from './redis';
/**
* Rotate provider.
* @param {BaseProvider} provider Provider instance
* @param {string} url Video TikTok URL
* @param {boolean?} skipOnError Rotate when error
* @param {Record<string,string>} params Advanced provider fetch options
* @param {number} retryCount Retry count.
* @return {Promise<ExtractedInfo>}
*/
export const rotateProvider = async (
provider: BaseProvider,
url: string,
skipOnError: boolean = true,
params?: Record<string, string>,
retryCount: number = 0,
): Promise<ExtractedInfo & {provider: string}> => {
if (retryCount >= maxRotateCount) {
return {
error: 'MAX_ROTATE_ALLOWED',
provider: retryCount.toString().concat(' providers'),
};
}
if (process.env.NODE_ENV === 'development') {
await redisClient.del(url);
}
// console.log(provider.resourceName());
if (provider.maintenance) {
return await rotateProvider(getRandomProvider(), url, skipOnError);
}
const cachedData = await redisClient.get(url);
if (!cachedData) {
try {
const data = await provider.fetch(url, params ?? {});
if (data.error) {
if (!skipOnError) {
return {
error: data.error,
provider: provider.resourceName(),
};
}
retryCount++;
// switching to other provider
return await rotateProvider(
getRandomProvider(),
url,
skipOnError,
params,
retryCount,
);
} else if (data.video && !data.video.urls.length) {
retryCount++;
return await rotateProvider(
getRandomProvider(),
url,
skipOnError,
params,
retryCount,
);
} else {
redisClient.set(
url,
JSON.stringify({
...data,
provider: provider.resourceName(),
}),
'EX',
providerCache,
);
return {...data, provider: provider.resourceName()};
}
} catch (e) {
if (skipOnError) {
return await rotateProvider(getRandomProvider(), url);
} else {
return {
error: (e as Error).message,
provider: provider.resourceName(),
};
}
}
} else {
return JSON.parse(cachedData);
}
};

17
apps/web/lib/url.ts Normal file
View File

@@ -0,0 +1,17 @@
/** Get TikTok Video URL.
* @param {string} url Video
* @return {string}
*/
export function getTikTokURL(url: string): string | undefined {
try {
if (/^http(s?)(:\/\/)([a-z]+\.)*tiktok\.com\/(.+)$/gi.test(url)) {
const u = new URL(url);
u.search = ''; // cleanup params
return u.href;
} else {
return undefined;
}
} catch {
return undefined;
}
}

View File

@@ -0,0 +1,31 @@
import {NextApiRequest, NextApiResponse} from 'next';
type Middleware = (
request: NextApiRequest,
response: NextApiResponse,
) => Promise<undefined>;
export const applyRoute = (
route: (req: NextApiRequest, res: NextApiResponse) => Promise<void>,
middlewares: Middleware[],
) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
const middleware:
| {
message: string;
status: number;
}
| undefined = await Promise.all(middlewares.map((m) => m(req, res)))
.catch((e) => ({
message: e.message,
status: 500,
}))
.then(() => undefined);
if (middleware)
return res.status(middleware.status).json({
message: middleware.message,
statusCode: middleware.status,
});
else return route(req, res);
};
};

View File

@@ -0,0 +1,33 @@
import type {NextApiRequest, NextApiResponse} from 'next';
import {rateLimitConfig} from '../config';
import {HttpError} from '../errors';
import {client} from '../lib';
export const ratelimitMiddleware = async (
req: NextApiRequest,
_res: NextApiResponse,
) => {
const ip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'];
if (!rateLimitConfig.enable || process.env.NODE_ENV === 'development')
return undefined;
else if (!ip)
throw new HttpError("Couldn't find your real ip address!").setCode(401);
const result = await client.get('rate-'.concat(ip.toString()));
if (result) {
if (parseInt(result) > rateLimitConfig.maxRatelimitPerXSeconds) {
throw new HttpError(
'Please try again, you are getting ratelimit!',
).setCode(429);
}
client.incr('rate-'.concat(ip.toString()));
return undefined;
} else {
client.set(
'rate-'.concat(ip.toString()),
'1',
'EX',
rateLimitConfig.ratelimitTime,
);
return undefined;
}
};

5
apps/web/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

20
apps/web/next.config.js Normal file
View File

@@ -0,0 +1,20 @@
const webpack = require('webpack');
const withTM = require('next-transpile-modules')(['tiktok-dl-core']);
const WindiCSSWebpackPlugin = require('windicss-webpack-plugin');
const {parsed: cusEnv} = require('dotenv').config({
path: require('path').resolve(__dirname, '..', '..', '.env'),
});
module.exports = withTM({
reactStrictMode: true,
experimental: {esmExternals: true},
webpack(config) {
// adding windicss plugin
config.plugins.push(new WindiCSSWebpackPlugin());
if (typeof cusEnv !== 'undefined') {
config.plugins.push(new webpack.EnvironmentPlugin(cusEnv));
}
return config;
},
});

35
apps/web/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "tiktok-dl-web",
"description": "TikTok-DL Project Website",
"version": "1.4.1",
"license": "MIT",
"dependencies": {
"dotenv": "16.6.1",
"ioredis": "5.7.0",
"next": "12.2.4",
"ow": "0.28.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"swr": "1.3.0",
"tiktok-dl-config": "*",
"tiktok-dl-core": "*"
},
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"format": "prettier . --write",
"start": "next start --port process.env.PORT"
},
"devDependencies": {
"@next/eslint-plugin-next": "12.2.4",
"@types/node": "17.0.35",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"next-transpile-modules": "9.0.0",
"prettier": "2.7.1",
"tiktok-dl-config": "*",
"typescript": "^5.9.2",
"windicss-webpack-plugin": "1.7.5"
}
}

11
apps/web/pages/_app.tsx Normal file
View File

@@ -0,0 +1,11 @@
import 'windi.css';
import type {AppProps} from 'next/app';
/**
* TikTokApp
* @param {AppProps} arg0 App properties.
* @return {JSX.Element}
*/
export default function TikTokApp({Component, pageProps}: AppProps) {
return <Component {...pageProps} />;
}

View File

@@ -0,0 +1,28 @@
import Document, {Head, Html, Main, NextScript} from 'next/document';
import {BodyComponent} from '../components/Body';
import {NextHeadComponent} from '../components/Header';
/**
* @class TikTokDocument
*/
export default class TikTokDocument extends Document {
/**
* Render TikTokDocument
* @return {JSX.Element}
*/
render(): JSX.Element {
return (
<Html lang="en">
<Head>
<NextHeadComponent />
</Head>
<body>
<BodyComponent>
<Main />
</BodyComponent>
<NextScript />
</body>
</Html>
);
}
}

103
apps/web/pages/about.tsx Normal file
View File

@@ -0,0 +1,103 @@
import useSWR from 'swr';
import Head from 'next/head';
export default () => {
const {data, error} = useSWR(
'https://api.github.com/repos/hansputera/tiktok-dl',
(...args) => fetch(...args).then((r) => r.json()),
);
return (
<>
<Head>
<title>About</title>
</Head>
<section className="p-5">
<h1 className="text-center text-2xl">About TikTok-DL</h1>
<div className="mt-3">
{error ? (
<p className="text-red-500">{error}</p>
) : (
data && (
<>
<p className="text-center font-sans">
{data.description}
</p>
<ul className="mb-3 mt-3">
<li>
This project is based on{' '}
<span className="font-semibold">
{data.license.name}
</span>
</li>
<li>
Currently, we have{' '}
{data.stargazers_count.toLocaleString()}{' '}
stars, {data.forks.toLocaleString()}{' '}
forks, and {data.open_issues} opened
issues.
</li>
<li>
Also, this project is created at{' '}
<span className="font-semibold">
{new Date(
data.created_at,
).toLocaleDateString('en-US', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'Asia/Jakarta',
})}
</span>{' '}
by{' '}
<span className="font-medium">
{data.owner.login}
</span>
</li>
<li>
Last update:{' '}
<span className="font-semibold">
{new Date(
data.updated_at,
).toLocaleDateString('en-US', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'Asia/Jakarta',
})}
</span>
</li>
</ul>{' '}
If you want see the source code,{' '}
<a
href={data.html_url}
className="font-semibold text-blue-500"
target="_blank"
>
click here
</a>
. And feel free to open an{' '}
<a
href={data.html_url.concat(
'/issues/new/choose',
)}
className="text-blue-500"
target="_blank"
>
issue
</a>{' '}
if you have a problem or find a bug. Thank you!
</>
)
)}
</div>
</section>
</>
);
};

View File

@@ -0,0 +1,87 @@
import type {NextApiRequest, NextApiResponse} from 'next';
import ow from 'ow';
import {getProvider, Providers, BaseProvider} from 'tiktok-dl-core';
import {getTikTokURL} from '../../lib';
import {rotateProvider} from '../../lib/rotator';
import {applyRoute} from '../../middleware/apply';
import {ratelimitMiddleware} from '../../middleware/ratelimit';
export default applyRoute(
async (req: NextApiRequest, res: NextApiResponse) => {
try {
if (req.method === 'POST' && typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
const providersType = Providers.map((p) => p.resourceName());
ow(
req.body || req.query,
ow.object.partialShape({
url: ow.string.url.validate((v) => ({
validator: !!getTikTokURL(v),
message: 'Expected (.*).tiktok.com',
})),
type: ow.optional.string.validate((v) => ({
validator:
typeof v === 'string' &&
providersType.includes(v.toLowerCase()),
message:
'Invalid Provider, available provider is: ' +
Providers.map((x) => x.resourceName()).join(', '),
})),
rotateOnError:
req.method === 'POST'
? ow.optional.boolean
: ow.optional.string,
}),
);
let provider = getProvider(
(req.query.type || req.body.type) ?? 'random',
);
if (!provider) {
return res.status(400).json({
error: 'Invalid provider',
providers: providersType,
});
}
const params = provider.getParams();
if (
params &&
provider.resourceName() ===
(req.query.type?.toString() || req.body.type)?.toLowerCase()
) {
ow(req.query || req.body, ow.object.partialShape(params));
} else if (params) {
provider = getProvider('random');
}
const url = getTikTokURL(req.query.url || req.body.url);
const result = await rotateProvider(
provider as BaseProvider,
url!,
req.method === 'POST'
? req.body.rotateOnError
: !!req.query.rotateOnError,
params
? Object.fromEntries(
Object.keys(params!).map((p) => [
p,
(req.query[p] as string) ?? req.body[p],
]),
)
: {},
);
return res.status(200).json(result);
} catch (e) {
return res
.status((e as Error).name === 'ArgumentError' ? 400 : 500)
.json({
error: (e as Error).name + '|' + (e as Error).message,
});
}
},
[ratelimitMiddleware],
);

View File

@@ -0,0 +1,5 @@
import type {NextApiRequest, NextApiResponse} from 'next';
export default async (_: NextApiRequest, res: NextApiResponse) => {
return res.redirect('https://github.com/hansputera/tiktok-dl.git');
};

View File

@@ -0,0 +1,24 @@
import type {NextApiRequest, NextApiResponse} from 'next';
import {Providers} from 'tiktok-dl-core';
import {ratelimitMiddleware} from '../../middleware/ratelimit';
import type {Shape} from 'ow';
import {applyRoute} from '../../middleware/apply';
export default applyRoute(
async (req: NextApiRequest, res: NextApiResponse) => {
const providers = Providers.map((p) => ({
name: p.resourceName(),
url: p.client?.defaults.options.prefixUrl,
maintenance: p.maintenance,
params: p.getParams()
? Object.keys(p.getParams()!).map((x) => ({
name: x,
type: (p.getParams()![x] as Shape).type,
}))
: {},
}));
return res.send(providers);
},
[ratelimitMiddleware],
);

View File

@@ -0,0 +1,14 @@
import type {NextApiRequest, NextApiResponse} from 'next';
import {client} from '../../lib';
import {ratelimitMiddleware} from '../../middleware/ratelimit';
import {matchLink} from 'tiktok-dl-core';
import {applyRoute} from '../../middleware/apply';
export default applyRoute(
async (req: NextApiRequest, res: NextApiResponse) => {
await ratelimitMiddleware(req, res);
const keys = await client.keys('*');
return res.status(200).json(keys.filter((x) => matchLink(x)));
},
[ratelimitMiddleware],
);

25
apps/web/pages/index.tsx Normal file
View File

@@ -0,0 +1,25 @@
import dynamic from 'next/dynamic';
import {Footer} from '../components/Footer';
const FormInputComponentDynamic = dynamic(
() => import('../components/FormInput'),
{
ssr: false,
},
);
export default () => {
return (
<section className="p-5">
<h1 className="align-middle text-4xl font-sans font-medium">
TikTok-DL{' '}
<span className="font-normal md:break-words text-2xl">
Download TikTok Video without watermark and free ads.
</span>
</h1>
<FormInputComponentDynamic />
<Footer />
</section>
);
};

37
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"allowJs": true,
"noEmit": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"outDir": "dist",
"target": "ESNext",
"module": "ESNext",
"removeComments": true,
"esModuleInterop": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"incremental": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}

8
apps/web/windi.config.js Normal file
View File

@@ -0,0 +1,8 @@
import {defineConfig} from 'windicss/helpers';
export default defineConfig({
extract: {
include: ['**/*.{jsx,tsx,css}'],
exclude: ['node_modules', '.git', '.next'],
},
});

19
docker-compose.yaml Normal file
View File

@@ -0,0 +1,19 @@
services:
app:
build: .
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis_db:6379
networks:
- app-network
depends_on:
- redis_db
image: "hansputera/tiktok-dl:latest"
redis_db:
image: "redis:alpine"
networks:
- app-network
networks:
app-network:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +0,0 @@
import got, {ExtendOptions} from 'got';
export const fetch = got.extend({
prefixUrl: 'https://www.tiktok.com',
dnsCache: true,
});
export const getFetch = (baseUrl: string, options?: ExtendOptions) =>
got.extend({
prefixUrl: baseUrl,
dnsCache: true,
...options,
});

View File

@@ -1,3 +0,0 @@
export * from './fetch';
export * from './providers';

View File

@@ -1,83 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
/**
* @class DLTikProvider
*/
export class DLTikProvider extends BaseProvider {
/**
* @return {string}
*/
public resourceName(): string {
return 'dltik';
}
public client = getFetch('https://dltik.com');
public maintenance = {
'reason': 'My prediction is that DLTik needs an active session to use.',
};
/**
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// getting verification token
const response = await this.client('./#url=' +
encodeURIComponent(url));
const token = (
response.body.match(/type="hidden" value="([^""]+)"/) as string[]
)[1];
const dlResponse = await this.client.post('./', {
'form': {
'm': 'getlink',
'url': `https://m.tiktok.com/v/${
(/predownload\('([0-9]+)'\)/gi.exec(response.body) as string[])[1]
}.html`,
'__RequestVerificationToken': token,
},
'headers': {
'Origin': this.client.defaults.options.prefixUrl,
'Referer': response.url,
'Cookie': response.headers['set-cookie']?.toString(),
'Content-Type': 'application/x-www-form-urlencoded',
'x-requested-with': 'XMLHttpRequest',
},
});
return this.extract(dlResponse.body);
}
/**
*
* @param {string} html - Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const json = JSON.parse(html);
if (!json.status) {
return {
'error': json.message,
};
} else {
// if (json.data.videoId === '7013188037203070234') {
// return {
// 'error': 'Invalid url',
// };
// }
return {
'video': {
'id': json.data.videoId,
'urls': [json.data.watermarkVideoUrl, json.data.destinationUrl],
'thumb': json.data.dynamicCover,
},
'music': {
'url': json.data.musicUrl,
},
'caption': json.data.desc,
};
}
}
};

View File

@@ -1,52 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '..';
import {matchLink} from './util';
/**
* @class DDDTikProvider
*/
export class DDDTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'dddtik';
}
public client = getFetch('https://dddtik.com');
public maintenance = undefined;
/**
* @param {string} url Tiktok video url
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.post('./down.php', {
'form': {
'url': url,
},
});
return this.extract(response.body);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchLink(html) as string[];
urls.pop();
const t = urls[1];
return {
'video': {
'urls': urls.filter((u) => u !== t),
'thumb': t,
},
};
}
}

View File

@@ -1,74 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '..';
import {matchCustomDownload} from './util';
/**
* @class DownTikProvider
*/
export class DownTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'downtik';
}
public client = getFetch('https://downtik.net');
public maintenance = undefined;
/**
* @param {string} url
*
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client('./');
const token = (
response.body.match(/id="token" value="([^""]+)"/) as string[]
)[1];
const responseAction = await this.client.post(
'./action.php', {
'form': {
'url': url,
'token': token,
},
'headers': {
'cookie': response.headers['set-cookie']?.toString(),
'Referer': 'https://downtik.net/',
'Origin': 'https://downtik.net',
},
},
);
if (JSON.parse(responseAction.body).error) {
return {
'error': JSON.parse(responseAction.body).message,
};
};
return this.extract(JSON.parse(responseAction.body).data);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchCustomDownload('downtik', html);
return {
'music': {
'url': urls.pop() as string,
},
'video': {
'thumb': urls?.shift(),
'urls': urls as string[],
},
};
}
}

View File

@@ -1,91 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '../fetch';
/**
* @class DownloadOne
*/
export class DownloadOne extends BaseProvider {
/**
* Get provider name
* @return {string}
*/
public resourceName(): string {
return 'ttdownloaderone';
}
public client = getFetch('http://tiktokdownloader.one');
public maintenance = undefined;
/**
* Fetch ttdownloader.one
* @param {string} url Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(
url: string,
): Promise<ExtractedInfo> {
// getting the token
const response = await this.client('./');
const token = (/name="_token_" content="(.*)"/gi
.exec(response.body) as string[])[1];
const dlResponse = await this.client(
'./api/v1/fetch?url=' + url, {
'headers': {
'TOKEN': token,
'Referer': 'http://tiktokdownloader.one/',
'Origin': 'http://tiktokdownloader.one',
'Accept': 'application/json, text/plain, */*',
},
},
);
if (dlResponse.statusCode !== 200) {
return {
'error': 'Probably the video doesn\'t exist',
};
}
return this.extract(dlResponse.body);
}
/**
* Extract page from ttdownloader.one site
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const json = JSON.parse(html);
return {
'video': {
'urls': [
json.url,
json.url_nwm,
],
'thumb': json.cover,
'id': json.video_id,
},
'music': {
'url': json.music.url,
'title': json.music.title,
'cover': json.music.cover,
'author': json.music.author,
},
'author': {
'id': json.user.name,
'username': json.user.username,
'thumb': json.user.cover,
},
'caption': json.caption,
'updatedAt': json.updatedAt ?? '-',
'uploadedAt': json.uploaded_at,
'commentsCount': json.stats.comment,
'sharesCount': json.stats.shares,
'likesCount': json.stats.likes,
'playsCount': json.stats.play,
};
}
}

View File

@@ -1,42 +0,0 @@
import type {BaseProvider} from './baseProvider';
import {MusicalyDown} from './musicalyDownProvider';
import {SnaptikProvider} from './snaptikProvider';
import {TikmateProvider} from './tikmateProvider';
import {TTDownloader} from './ttDownloaderProvider';
import {TTSave} from './ttSaveProvider';
import {DLTikProvider} from './DLTikProvider';
import {SaveFromProvider} from './saveFromProvider';
import {SaveTikProvider} from './saveTikProvider';
import {TikDownProvider} from './tikDownProvider';
import {DownTikProvider} from './downTikProvider';
import {LoveTikProvider} from './loveTikProvider';
import {DDDTikProvider} from './dddTikProvider';
import {TokupProvider} from './tokupProvider';
import {DownloadOne} from './downloaderOneProvider';
export const Providers: BaseProvider[] = [
new SnaptikProvider(),
new TikmateProvider(),
new MusicalyDown(),
new TTDownloader(),
new TTSave(), // won't work because we coudn't receive the cookie.
new DLTikProvider(),
new SaveFromProvider(),
new SaveTikProvider(),
new TikDownProvider(),
new DownTikProvider(), // SaveTik Mirror
new LoveTikProvider(),
new DDDTikProvider(),
new TokupProvider(), // ttsave alternative
new DownloadOne(),
];
export const getRandomProvider = () => Providers[
Math.floor(Math.random() * Providers.length)
];
export const getProvider = (name: string) => name.toLowerCase() !== 'random' ?
Providers.find(
(p) => p.resourceName() === name.toLowerCase(),
) : getRandomProvider();

View File

@@ -1,67 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '..';
/**
* @class LoveTikProvider
*/
export class LoveTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'lovetik';
}
public client = getFetch('https://lovetik.com');
public maintenance = undefined;
/**
* @param {string} url Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.post(
'./api/ajax/search', {
'form': {
'query': url,
},
'headers': {
'Origin': 'https://lovetik.com/',
'Referer': 'https://lovetik.com/',
},
},
);
return this.extract(response.body);
}
/**
* @param {string} jsonString
* @return {ExtractedInfo}
*/
extract(jsonString: string): ExtractedInfo {
const json = JSON.parse(jsonString);
if (json.mess) {
return {
'error': json.mess,
};
}
return {
'music': {
'url': json.links.pop().a,
},
'video': {
'thumb': json.cover,
'urls': json.links.map((l: Record<string, unknown>) => l.a),
},
'author': {
'username': json.author.replace(/(<([^>]+)>)/ig, ''),
},
};
}
}

View File

@@ -1,72 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
/**
* @class MusicalyDown
*/
export class MusicalyDown extends BaseProvider {
public client = getFetch('https://musicaldown.com/id');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'musicalydown';
}
public maintenance = undefined;
/**
*
* @param {string} url - Video Tiktok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
const res = await this.client('./', {
'headers': {
'Accept': '*/*',
'Referer': this.client.defaults.options.prefixUrl,
'Origin': this.client.defaults.options.prefixUrl,
},
});
const tokens = (
res.body.match(
/input name="([^""]+)" type="hidden" value="([^""]+)"/) as string[]
);
const response = await this.client.post('./download', {
form: {
[(
res.body.match(/input name="([^"]+)/) as string[]
)[1]]: url,
[tokens[1]]: tokens[2],
'verify': 1,
},
headers: {
'Cookie': res.headers['set-cookie']?.toString(),
'Accept': '*/*',
'Referer': this.client.defaults.options.prefixUrl,
'Origin': this.client.defaults.options.prefixUrl,
},
});
return this.extract(response.body);
}
/**
*
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
public extract(html: string): ExtractedInfo {
const matchUrls = (html
.match(/<a.*?target="_blank".*?href="(.*?)".*?<\/a>/gi) as string[]);
const urls = matchUrls.map((url) =>
/<a.*?target="_blank".*?href="(.*?)".*?<\/a>/gi.exec(url)?.[1] as string);
return {
'video': {
'urls': urls,
'thumb': /img class="responsive-img" src="(.*?)"/gi.exec(html)?.[1],
},
};
}
}

View File

@@ -1,69 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {deObfuscateSaveFromScript} from './util';
/**
* @class saveFromProvider
*/
export class SaveFromProvider extends BaseProvider {
/**
*
* @return {string}
*/
public resourceName(): string {
return 'savefrom';
}
public client = getFetch('https://worker-as.sf-tools.com');
public maintenance = undefined;
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.post('./savefrom.php', {
'form': {
'sf_url': url,
'sf_submit': '',
'new': '2',
'lang': 'id',
'country': 'id',
'os': 'Ubuntu',
'browser': 'Firefox',
'channel': 'Downloader',
'sf-nomad': '1',
},
'headers': {
'Origin': 'https://id.savefrom.net',
'Referer': 'https://id.savefrom.net',
},
});
return this.extract(response.body);
}
/**
*
* @param {string} html - HTML Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const deobfuscated = deObfuscateSaveFromScript(html);
const json = JSON.parse(
(deobfuscated.match(/\({(.*)}\)/) as string[])[0]
.replace(/(\(|\))/g, ''),
);
return {
'video': {
'thumb': json.thumb,
'id': json.id,
'urls': json.url.map((x: { url: string; }) => x.url),
'duration': json.meta.duration,
'title': json.meta.title,
},
};
}
}

View File

@@ -1,73 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '..';
import {matchCustomDownload} from './util';
/**
* @class SaveTikProvider
*/
export class SaveTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'savetik';
}
public client = getFetch('https://savetik.net');
public maintenance = undefined;
/**
* @param {string} url Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client('./');
const token = (
response.body.match(/id="token" value="([^""]+)"/) as string[]
)[1];
const responseAction = await this.client.post(
'./action.php', {
'form': {
'url': url,
'token': token,
},
'headers': {
'cookie': response.headers['set-cookie']?.toString(),
'Referer': 'https://savetik.net/',
'Origin': 'https://savetik.net',
},
},
);
if (JSON.parse(responseAction.body).error) {
return {
'error': JSON.parse(responseAction.body).message,
};
};
return this.extract(JSON.parse(responseAction.body).data);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchCustomDownload('savetik', html);
return {
'music': {
'url': urls?.pop() as string,
},
'video': {
'thumb': urls?.shift(),
'urls': urls as string[],
},
};
}
}

View File

@@ -1,52 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {deObfuscate, matchLink} from './util';
/**
* @class SnaptikProvider
*/
export class SnaptikProvider extends BaseProvider {
public client = getFetch('https://snaptik.app/en');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'snaptik';
}
public maintenance = undefined;
/**
*
* @param {string} url - TikTok Video URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client('./abc.php', {
searchParams: {
'url': url,
},
});
return this.extract(response.body);
}
/**
* Extract information from raw html
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const results = matchLink(deObfuscate(html));
if (!results || !results.length) throw new Error('Broken');
return {
'video': {
'thumb': results?.shift(),
'urls': [...new Set(results)],
},
};
};
};

View File

@@ -1,69 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '..';
import {matchLink} from './util';
/**
* @class TikDownProvider
*/
export class TikDownProvider extends BaseProvider {
/**
* Get resource name.
*
* @return {string}
*/
public resourceName(): string {
return 'tikdown';
}
public client = getFetch('https://tikdown.org');
public maintenance = undefined;
/**
* @param {string} url
*
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client('./');
const token = (
response.body.match(
/name="_token" value="([^""]+)"/) as string[]
)[1];
const responseAjax = await this.client.post(
'./getAjax', {
'form': {
'url': url,
'_token': token,
},
'headers': {
'x-csrf-token': token,
'cookie': response.headers['set-cookie']?.toString(),
},
},
);
if (!JSON.parse(responseAjax.body).status) {
return {
'error': 'Something was wrong',
};
}
return this.extract(JSON.parse(responseAjax.body).html);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchLink(html) as string[];
return {
'video': {
'thumb': urls.shift(),
'urls': urls,
},
};
}
}

View File

@@ -1,66 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {deObfuscate, matchCustomDownload} from './util';
/**
* @class TikmateProvider
*/
export class TikmateProvider extends BaseProvider {
public client = getFetch('https://tikmate.online');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'tikmate';
}
public maintenance = undefined;
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// we need to get the token
const response = await this.client('./');
const matchs = (
response.body.match(/id="token" value="(.*)?"/) as string[]);
const cookies = response.headers['cookie'];
const abcResponse = await this.client.post('./abc.php', {
form: matchs ? {
'url': url,
'token': matchs[1],
} : {
'url': url,
},
headers: {
'Origin': this.client.defaults.options.prefixUrl,
'Referer': this.client.defaults.options.prefixUrl + '/',
'Cookie': cookies,
},
});
return this.extract(abcResponse.body);
}
/**
* Extract information from raw html
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const matchs = matchCustomDownload('tikmate', deObfuscate(html));
return {
'video': {
'thumb': matchs.shift(),
'urls': matchs,
},
};
}
};

View File

@@ -1,98 +0,0 @@
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {getFetch} from '../fetch';
/**
* @class TokupProvider
*/
export class TokupProvider extends BaseProvider {
/**
* Get provider name
* @return {string}
*/
public resourceName(): string {
return 'tokup';
}
public client = getFetch('https://tokup.app');
public maintenance = {
reason: 'Tokup site returned \'Oops! Something went wrong!\'',
};
/**
* Fetch tokup
* @param {string} url - TikTok Video URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(
url: string,
): Promise<ExtractedInfo> {
const response = await this.client.post(
'./', {
'form': {
'url': url,
},
'headers': {
'Origin': this.client.defaults.options.prefixUrl,
'Referer': this.client.defaults.options.prefixUrl,
},
'timeout': 3000,
},
);
if (response.statusCode !== 200 ||
/video not found\b/gi.test(
response.body,
)) {
return {
'error': 'Video Not Found',
};
} else if (/oops/gi.test(response.body)) {
return {
'error': 'Tokup Error',
};
} else {
return this.extract(
response.body,
);
}
}
/**
* Extract tokup html elements
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
console.log(html);
const authorProfile = (/http(s)?(:\/\/(.*)\.tiktokcdn\.com\/(.*))/gi.exec(
html,
) as string[])[0];
const nums = (html.match(/<td>[0-9]+<\/td>/g) as string[]).map(
(n) => n.replace(/<(\/)?[a-zA-Z0-9]+>/gi, ''));
const url = [...new Set(
(html.match(
// eslint-disable-next-line max-len
/http(s)?(:\/\/tikmate\.app\/download\/[A-Za-z0-9\-\_]+\/[0-9]+\.mp4+)/gi) as string[]),
)][0];
return {
'video': {
'urls': [url, url + '?hd=1'],
},
'author': {
'username': (/target="_blank"\>(.*)\</.exec(
html,
) as string[])[1],
'thumb': authorProfile
.substring(0, authorProfile.length-1),
},
'uploadedAt': (html.match(
/<p>(.+)<\/p>/,
) as string[])[1],
'likesCount': nums[0] as unknown as number,
'commentsCount': nums[1] as unknown as number,
'sharesCount': nums[2] as unknown as number,
};
}
}

View File

@@ -1,60 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {matchLink} from './util';
/**
* @class TTDownloader
*/
export class TTDownloader extends BaseProvider {
/**
* @return {string}
*/
public resourceName(): string {
return 'ttdownloader';
}
public client = getFetch('https://ttdownloader.com');
public maintenance = undefined;
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// getting token and cookies
const firstResponse = await this.client('./');
const token = (firstResponse.body
.match(/name="token" value="(.*)?"/) as string[])[1];
const videoResponse = await this.client.post('./req', {
form: {
'token': token,
'format': '',
'url': url,
},
headers: {
'Origin': 'https://ttdownloader.com',
'Referer': 'https://ttdownloader.com',
'Cookie': firstResponse.headers['set-cookie']?.toString(),
},
});
return this.extract(videoResponse.body);
}
/**
*
* @param {string} html - HTML Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchLink(html);
urls?.pop(); // remove 'https://snaptik.fans'
return {
'video': {
'urls': urls as string[],
},
};
}
};

View File

@@ -1,75 +0,0 @@
import {getFetch} from '..';
import {BaseProvider, ExtractedInfo} from './baseProvider';
import {keyGeneratorTTSave, matchLink} from './util';
/**
* @class TTSave
*/
export class TTSave extends BaseProvider {
/**
* @return {string}
*/
public resourceName(): string {
return 'ttsave';
}
public client = getFetch('https://ttsave.app');
public maintenance = {
reason: 'TTSave doesn\'t returned cookie to manipulate the session',
};
/**
*
* @param {string} url - TikTok Video URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// getting token
const response = await this.client('./');
const token = (
response.body.match(/(m|doDownload)?\(e,"(.*)"\)}/) as string[]
).filter((x) => x.length).pop() as string;
const key = await keyGeneratorTTSave(token);
const dlResponse = await this.client.post('./download.php', {
'json': {
'id': url,
'token': token,
'key': key,
},
'headers': {
'Origin': this.client.defaults.options.prefixUrl,
'Referer': this.client.defaults.options.prefixUrl,
'Cookie': response.headers['set-cookie']?.toString(), // no cookies :(
...response.headers,
},
});
return this.extract(dlResponse.body);
}
/**
*
* @param {string} html - HTML Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const tiktokCDNs = (matchLink(html) as string[]).filter(
(x) => /http(s)?:\/\/(.*)?.tiktokcdn.com/gi.test(x),
);
const videoCDNs = tiktokCDNs.filter((x) => !/jpeg/gi.test(x));
return {
'video': {
'thumb': tiktokCDNs.find((x) => /jpeg/gi.test(x)),
'urls': videoCDNs.filter((x) => !/music/gi.test(x)),
},
'music': {
'url': videoCDNs.find((x) => /music/gi.test(x)) as string,
},
};
}
};

View File

@@ -1,73 +0,0 @@
import {getProvider} from '..';
import type {BaseProvider} from '../baseProvider';
import {NodeVM} from 'vm2';
export const deObfuscate = (html: string): string => {
if (/error/gi.test(html)) {
throw new Error(html.split('\'')
.find((x) => /(((url)? error)|could)/gi.test(x)),
);
} else {
// only match script tag
const obfuscatedScripts = html
.match(/<script[\s\S]*?>[\s\S]*?<\/script>/gi);
if (!obfuscatedScripts?.length) {
throw new Error(
'Cannot download the video!',
);
} else {
const transformed = obfuscatedScripts[0]
.replace(/<(\/)?script( type=".+")?>/g, '').trim().replace('eval', '')
.replace(/\(function(.)?\(h/gi, 'module.exports = (function (h');
const deObfuscated = new NodeVM({
'compiler': 'javascript',
'console': 'inherit',
'require': {
'external': true,
'root': './',
},
}).run(transformed, 'deobfuscate.js');
return deObfuscated;
}
}
};
export const matchLink = (raw: string): string[] | null => {
// eslint-disable-next-line max-len
return raw.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi);
};
export const matchCustomDownload =
(provider: string, raw: string): string[] => {
const links = matchLink(raw) as string[];
const urls = raw.match(/\/download.php\?token=(.*?)"/gi)
?.map((url) => (getProvider(provider) as BaseProvider).client.
defaults.options.prefixUrl.slice(0, -1)+
url.slice(0, -3));
return [links[0]].concat(urls as string[]);
};
export const deObfuscateSaveFromScript = (scriptContent: string): string => {
const safeScript = 'let result;' +
scriptContent.replace(/\/\*js\-response\*\//gi, '')
.replace(/eval\(a\)/gi, 'return a')
.replace(/\[\]\["filter"\]\["constructor"\]\(b\)\.call\(a\);/gi, `
if (b.includes('showResult')) {
result = b;
return;
} else []['filter']['constructor'](b).call(a);`) +
'module.exports = result;';
const vm = new NodeVM({
'compiler': 'javascript',
'console': 'inherit',
'require': {
'external': true,
'root': './',
},
});
const result = vm.run(safeScript, 'savefrom.js');
return result;
};

View File

@@ -1,12 +0,0 @@
/**
* Generate key for ttsave.app
*
* @param {string} token - Generated token by TTSave
* @return {string | undefined}
*/
export const keyGeneratorTTSave = async (token: string): Promise<string> => {
const expectedLen = (token.length / 3);
return token.split('').reverse().join('')
.slice(-(expectedLen));
};

View File

@@ -1,4 +0,0 @@
import Redis from 'ioredis';
export const client = new Redis(process.env.REDIS_URL);

View File

@@ -1,54 +0,0 @@
import {getRandomProvider} from '.';
import {providerCache} from '../config';
import {BaseProvider, ExtractedInfo} from './providers/baseProvider';
import {client as redisClient} from './redis';
/**
* Rotate provider.
* @param {BaseProvider} provider Provider instance
* @param {string} url Video TikTok URL
* @param {boolean?} noCache NoCache option
* @param {boolean?} skipOnError Rotate when error
* @return {Promise<ExtractedInfo>}
*/
export const rotateProvider = async (
provider: BaseProvider, url: string,
noCache: boolean = false, skipOnError: boolean = true):
Promise<ExtractedInfo & { provider: string; }> => {
// await redisClient.del(url);
// console.log(provider.resourceName());
if (provider.maintenance) {
return await rotateProvider(getRandomProvider(), url, noCache, skipOnError);
}
const cachedData = await redisClient.get(url);
if (!cachedData) {
try {
const data = await provider.fetch(url);
if (data.error) {
// switching to other provider
return await rotateProvider(getRandomProvider(), url);
} else if (data.video && !data.video.urls.length) {
return await rotateProvider(getRandomProvider(), url);
} else {
if (!noCache) {
redisClient.set(url,
JSON.stringify(
{...data, provider: provider.resourceName()}), 'ex',
providerCache);
}
return {...data, provider: provider.resourceName()};
}
} catch (e) {
if (skipOnError) {
return await rotateProvider(getRandomProvider(), url);
} else {
return {
error: (e as Error).message,
provider: provider.resourceName(),
};
}
}
} else {
return JSON.parse(cachedData);
}
};

View File

@@ -1,27 +0,0 @@
import {VercelRequest} from '@vercel/node';
import {rateLimitConfig} from '../config';
import {client} from '../lib/redis';
export const ratelimitMiddleware = async (req: VercelRequest) => {
const ip = req.headers['x-real-ip'];
return await new Promise((resolve, reject) => {
if (!rateLimitConfig.enable) return resolve(undefined);
else if (!ip) {
return reject(
new Error('Can\'t find your real ip address!'));
}
client.get('rate-' + ip, (_, result) => {
if (result) {
if (parseInt(result) > rateLimitConfig.maxRatelimitPerXSeconds) {
return reject(
new Error('Please try again, you are getting ratelimit!'));
}
client.incr('rate-' + ip);
return resolve(undefined);
} else {
client.set('rate-' + ip, '1', 'ex', rateLimitConfig.ratelimitTime);
return resolve(undefined);
}
});
});
};

View File

@@ -1,27 +1,28 @@
{
"name": "tktk",
"version": "1.0.3",
"version": "2.0.0",
"main": "index.js",
"private": true,
"license": "MIT",
"dependencies": {
"got": "^11.8.2",
"ioredis": "^4.28.0",
"ow": "^0.28.1",
"vm2": "^3.9.5"
},
"devDependencies": {
"@types/ioredis": "^4.28.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"@vercel/node": "^1.12.1",
"eslint": "^8.2.0",
"eslint-config-google": "^0.14.0",
"husky": "^7.0.4",
"typescript": "^4.4.4"
"husky": "8.0.3",
"turbo": "1.13.4"
},
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "echo build",
"lint": "eslint \"+(lib|api|middleware)/**/*.ts\" --fix",
"prepare": "husky install"
}
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint --parallel",
"format": "turbo run format --parallel",
"prepare": "husky install",
"start": "turbo run start"
},
"packageManager": "yarn@4.9.3",
"engines": {
"node": ">=14.x"
},
"type": "module"
}

12
packages/config/eslint.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
extends: ['google', 'prettier'],
settings: {
next: {
rootDir: [
'apps/web/',
'packages/core/',
'packages/config/'
]
}
}
}

View File

@@ -0,0 +1,4 @@
module.exports = {
...require('./eslint.js'),
parser: '@typescript-eslint/parser',
};

View File

@@ -0,0 +1,22 @@
{
"name": "tiktok-dl-config",
"version": "1.4.1",
"description": "TikTok-DL Project Configuration",
"files": [
"eslint.js",
"eslint.typescript.js",
"useragents.js",
"useragents.d.ts",
"prettier.js"
],
"dependencies": {
"eslint-config-google": "0.14.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.33.0",
"@typescript-eslint/parser": "5.33.0",
"eslint": "8.21.0",
"eslint-config-prettier": "8.5.0",
"prettier": "2.7.1"
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
semi: true,
singleQuote: true,
tabWidth: 4,
trailingComma: 'all',
bracketSpacing: false,
// useTabs: true,
};

5
packages/config/useragents.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare const _default: {
random: () => string;
}
export = _default;

View File

@@ -0,0 +1,513 @@
const uas = [
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 (+https://whatis.contentkingapp.com)",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; LG-H631 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/38.0.2125.102 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.193 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (en-us) AppleWebKit/534.14 (KHTML, like Gecko; Google Wireless Transcoder) Chrome/9.0.597 Safari/534.14",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/58.0.3029.83 Mobile/14A346 Safari/E7FBAF",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/79.0.3945.73 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (X11; CrOS x86_64 13597.94.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.24 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36 HubSpot Webcrawler - web-crawlers@hubspot.com",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.80 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/92.0.4515.90 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (X11; CrOS x86_64 13597.105.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.208 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.163 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/E7FBAF",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; Lenovo-A6020l36 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 11; SM-A102U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.2; vivo y35 Build/N2G48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.158 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.152 Safari/537.22",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Lenovo-A6020l36 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.3; MediaPad 7 Youth 2 Build/HuaweiMediaPad) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/E7FBAF",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko; Google Web Preview) Chrome/27.0.1453 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36",
"Mozilla/5.0 (Linux; Android 9; SM-A102U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.136 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36"
];
module.exports = {
/**
* Get random UA.
* @return {string}
*/
random: () => {
return uas[Math.floor(Math.random() * uas.length)];
},
uas,
}

View File

@@ -0,0 +1 @@
module.exports = require('tiktok-dl-config/eslint.typescript');

View File

@@ -0,0 +1 @@
*.json

View File

@@ -0,0 +1 @@
module.exports = require('tiktok-dl-config/prettier');

8
packages/core/fetch.ts Normal file
View File

@@ -0,0 +1,8 @@
import got, {ExtendOptions} from 'got';
export const getFetch = (baseUrl: string, options?: ExtendOptions) =>
got.extend({
prefixUrl: baseUrl,
dnsCache: true,
...options,
});

3
packages/core/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './src';
export * from './fetch';
export * from './src/utils';

View File

@@ -0,0 +1,21 @@
{
"name": "tiktok-dl-core",
"version": "2.0.0",
"main": "./index.ts",
"license": "MIT",
"description": "TikTok-DL Project Core",
"dependencies": {
"got": "^12.6.1",
"ow": "0.28.1",
"vm2": "^3.9.19"
},
"scripts": {
"lint": "eslint \"+(src)/**/*.ts\" --fix",
"format": "prettier . --write"
},
"devDependencies": {
"@typescript-eslint/parser": "5.33.0",
"eslint": "8.21.0",
"prettier": "2.7.1"
}
}

View File

@@ -1,4 +1,5 @@
import {Got} from 'got';
import type {Shape} from 'ow';
export interface ExtractedInfo {
error?: string;
@@ -15,11 +16,14 @@ export interface ExtractedInfo {
author?: string;
id?: string;
cover?: string;
album?: string;
duration?: number;
};
author?: {
username?: string;
thumb?: string;
id?: string;
nick?: string;
};
caption?: string;
playsCount?: number;
@@ -28,19 +32,23 @@ export interface ExtractedInfo {
likesCount?: number;
uploadedAt?: string;
updatedAt?: string;
};
}
export interface MaintenanceProvider {
reason: string;
};
}
/**
* @class BaseProvider
*/
export abstract class BaseProvider {
abstract client: Got;
abstract client?: Got;
abstract getParams(): Shape | undefined;
abstract maintenance?: MaintenanceProvider;
abstract resourceName(): string;
abstract fetch(url: string): Promise<ExtractedInfo>;
abstract fetch(
url: string,
params?: Record<string, string>,
): Promise<ExtractedInfo>;
abstract extract(html: string): ExtractedInfo;
};
}

View File

@@ -0,0 +1,108 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
import {matchCustomDownload, matchLink, runObfuscatedScript} from './utils';
import type {Shape} from 'ow';
/**
* @class DownTikProvider
*/
export class DownTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'downtik';
}
public client = getFetch('https://downtik.io');
public maintenance = undefined;
/**
* @param {string} url
*
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client('./', {
searchParams: new URLSearchParams({
lang: 'en',
}),
});
const token = (
response.body.match(/id="token" value="([^""]+)"/) as string[]
)[1];
const responseAction = await this.client.post('./action.php', {
form: {
url: url,
token: token,
},
searchParams: new URLSearchParams({
lang: 'en',
}),
headers: {
cookie: response.headers['set-cookie']?.toString(),
Referer: 'https://downtik.io/',
Origin: 'https://downtik.io',
},
});
try {
if (JSON.parse(responseAction.body).error) {
return {
error: JSON.parse(responseAction.body).message,
};
}
} catch {
// if JSON.parse fail
return this.extract(responseAction.body);
}
return this.extract(responseAction.body);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
let urls = matchCustomDownload('downtik', html);
if (!urls?.length) {
urls = matchLink(runObfuscatedScript(html)) as string[];
if (!urls?.length)
return {
error: "Couldn't match any links!",
};
return {
video: {
thumb: urls?.shift(),
urls: urls as string[],
},
};
} else {
return {
music: {
url: urls.pop() as string,
},
video: {
thumb: urls?.shift(),
urls: urls as string[],
},
};
}
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,41 @@
import type {BaseProvider, ExtractedInfo} from './base';
import {MusicalyDown} from './musicalyDownProvider';
import {SnaptikProvider} from './snaptikProvider';
import {TikmateProvider} from './tikmateProvider';
import {TTDownloader} from './ttDownloaderProvider';
import {SaveFromProvider} from './saveFromProvider';
import {SaveTikProvider} from './saveTikProvider';
import {TikDownProvider} from './tikDownProvider';
import {DownTikProvider} from './downTikProvider';
// import {LoveTikProvider} from './loveTikProvider';
// import {DDDTikProvider} from './dddTikProvider';
// import {DownloadOne} from './downloaderOneProvider';
import {NativeProvider} from './nativeProvider';
// import {GetVidTikProvider} from './getVidTikProvider';
export const Providers: BaseProvider[] = [
new SnaptikProvider(),
new TikmateProvider(),
new MusicalyDown(),
new TTDownloader(),
new SaveFromProvider(),
new SaveTikProvider(),
new TikDownProvider(),
new DownTikProvider(), // SaveTik Mirror
// new LoveTikProvider(),
// new DDDTikProvider(),
// new DownloadOne(),
new NativeProvider(),
// new GetVidTikProvider(),
];
export const getRandomProvider = () =>
Providers[Math.floor(Math.random() * Providers.length)];
export const getProvider = (name: string) =>
name.toLowerCase() !== 'random'
? Providers.find((p) => p.resourceName() === name.toLowerCase())
: getRandomProvider();
export {BaseProvider, ExtractedInfo};

View File

@@ -0,0 +1,83 @@
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base';
import type {Shape} from 'ow';
import { matchLink } from './utils';
/**
* @class MusicalyDown
*/
export class MusicalyDown extends BaseProvider {
public client = getFetch('https://musicaldown.com/id');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'musicalydown';
}
public maintenance = undefined;
/**
*
* @param {string} url - Video Tiktok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
const res = await this.client('./', {
headers: {
Accept: '*/*',
Referer: this.client.defaults.options.prefixUrl.toString(),
Origin: this.client.defaults.options.prefixUrl.toString(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
},
});
const tokens = res.body.match(
/input name="([^""]+)" type="hidden" value="([^""]+)"/,
) as string[];
const response = await this.client.post('./download', {
form: {
[(res.body.match(/input name="([^"]+)/) as string[])[1]]: url,
[tokens[1]]: tokens[2],
verify: 1,
},
headers: {
Cookie: res.headers['set-cookie']?.toString(),
Accept: '*/*',
Referer: this.client.defaults.options.prefixUrl.toString(),
Origin: this.client.defaults.options.prefixUrl.toString(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
},
});
return this.extract(response.body);
}
/**
*
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
public extract(html: string): ExtractedInfo {
const urls = matchLink(html);
const matchedUrls = urls?.filter(url => /muscdn/gi.test(url)) ?? [];
return {
video: {
urls: matchedUrls.filter(murl => !murl.includes('images')),
thumb: matchedUrls.find(murl => murl.includes('images')),
},
};
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,106 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
import {matchTikTokData} from './utils';
import ow, {Shape} from 'ow';
/**
* @class NativeProvider
*/
export class NativeProvider extends BaseProvider {
/**
* Get resource name.
* @return {string}
*/
public resourceName(): string {
return 'native';
}
public maintenance = undefined;
public client = undefined;
/**
* @param {string} url Tiktok video url
* @param {Record<string, string>} params Advanced options.
* @return {Promise<ExtractedInfo>}
*/
async fetch(
url: string,
params: Record<string, string>,
): Promise<ExtractedInfo> {
const urlInstance = new URL(url);
const response = await getFetch(urlInstance.origin).get(
`.${urlInstance.pathname}`,
{
headers: {
Referer: urlInstance.href,
Origin: urlInstance.origin,
'User-Agent': params['user-agent'],
},
timeout: {
socket: 10000,
},
},
);
return this.extract(response.body);
}
/**
* @param {string} html Raw HTML Data
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const matches = matchTikTokData(html);
if (matches.length) {
const json = Object.values(
JSON.parse(matches).ItemModule,
)[0] as any;
return {
video: {
id: json.id,
urls: [json.video.playAddr, json.video.downloadAddr],
thumb: json.video.cover,
duration: json.video.duration,
},
music: {
url: json.music.playUrl,
title: json.music.title,
author: json.music.authorName,
id: json.music.id,
cover: json.music.coverLarge,
album: json.music.album,
duration: json.music.duration,
},
author: {
username: json.author,
id: json.authorId,
thumb: json.avatarThumb,
nick: json.nickname,
},
caption: json.desc,
playsCount: json.stats.playCount,
sharesCount: json.stats.shareCount,
commentsCount: json.stats.commentCount,
likesCount: json.stats.diggCount,
uploadedAt: json.createTime,
};
} else {
return {
error: 'Something was wrong!',
};
}
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return {
'user-agent': ow.string.not.empty
.minLength(5)
.message('Invalid user-agent'),
};
}
}

View File

@@ -0,0 +1,85 @@
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
import {deObfuscateSaveFromScript} from './utils';
import type {Shape} from 'ow';
/**
* @class saveFromProvider
*/
export class SaveFromProvider extends BaseProvider {
/**
*
* @return {string}
*/
public resourceName(): string {
return 'savefrom';
}
public client = getFetch('https://worker.savefrom.net');
public maintenance: MaintenanceProvider = {
reason: 'Need advance investigate to Reverse Engineering the response scripts.'
}
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.post('./savefrom.php', {
form: {
sf_url: url,
sf_submit: '',
new: '2',
lang: 'id',
country: 'id',
os: 'Ubuntu',
browser: 'Firefox',
channel: 'Downloader',
'sf-nomad': '1',
url,
ts: Date.now(),
},
headers: {
Origin: 'https://id.savefrom.net',
Referer: 'https://id.savefrom.net',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
},
});
return this.extract(response.body);
}
/**
*
* @param {string} html - HTML Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const deobfuscated = deObfuscateSaveFromScript(html);
const json = JSON.parse(
(deobfuscated.match(/\({(.*)}\)/) as string[])[0].replace(
/(\(|\))/g,
'',
),
);
return {
video: {
thumb: json.thumb,
id: json.id,
urls: json.url.map((x: {url: string}) => x.url),
duration: json.meta.duration,
title: json.meta.title,
},
};
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,106 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
// import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
import type {Shape} from 'ow';
/**
* @class SaveTikProvider
*/
export class SaveTikProvider extends BaseProvider {
/**
* Get resource name
*
* @return {string}
*/
public resourceName(): string {
return 'savetik';
}
public client = getFetch('https://savetik.net');
public maintenance = undefined;
/**
* @param {string} url Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.get('./api/action', {
searchParams: new URLSearchParams({
url,
}),
throwHttpErrors: false,
});
if (response.statusCode === 400)
{
return {
error: 'Video not found',
};
}
return this.extract(response.body);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
// const results = runObfuscatedReplaceEvalScript(html);
// const matchUrls = matchLink(results) ?? [];
// const urls = matchUrls.filter(url => /(tiktokcdn|rapidcdn)/gi.test(url));
// return {
// music: {
// url: urls?.pop() as string,
// },
// video: {
// thumb: urls?.shift(),
// urls: urls as string[],
// },
// };
try {
const json = JSON.parse(html);
if (json.error) {
return {
error: json.error,
};
}
return {
video: {
urls: [
json.hdDownloadUrl,
json.downloadUrl,
],
title: json.postinfo.media_title,
duration: json.duration.toString(),
},
author: {
username: json.postinfo.unique_id,
id: json.postinfo.uid,
nick: json.postinfo.username,
thumb: json.postinfo.avatar_url,
},
sharesCount: json.stats.shareCount,
playsCount: json.stats.playCount,
commentsCount: json.stats.commentCount,
}
} catch {
return {
error: 'Video not found',
}
}
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,75 @@
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
import type {Shape} from 'ow';
/**
* @class SnaptikProvider
*/
export class SnaptikProvider extends BaseProvider {
public client = getFetch('https://snaptik.app/en');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'snaptik';
}
public maintenance?: MaintenanceProvider | undefined = undefined
/**
*
* @param {string} url - TikTok Video URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// get token
const responseToken = await this.client('./', {
headers: {},
});
const token = (
responseToken.body.match(
/name="token" value="([^""]+)"/,
) as string[]
)[1];
const response = await this.client('./abc2.php', {
searchParams: {
url: url,
token,
lang: 'ID2',
},
headers: {
Cookie: responseToken.headers['set-cookie']?.toString(),
},
});
return this.extract(response.body);
}
/**
* Extract information from raw html
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const scriptsResult = runObfuscatedReplaceEvalScript(html);
const results = matchLink(scriptsResult);
if (!results || !results.length) throw new Error('Broken');
return {
video: {
thumb: results?.shift(),
urls: [...new Set(results.slice(0, results.length - 1))],
},
};
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,78 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
import type {Shape} from 'ow';
/**
* @class TikDownProvider
*/
export class TikDownProvider extends BaseProvider {
/**
* Get resource name.
*
* @return {string}
*/
public resourceName(): string {
return 'tikdown';
}
public client = getFetch('https://tikdown.org');
public maintenance = undefined;
/**
* @param {string} url
*
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client.post('./', {
form: {
'tiktok-url': url,
},
});
const body = response.body;
if (/please double/gi.test(body))
{
return {
error: 'Video not found',
};
}
const indexLink = body.match(/\.\/index\.php\?url=[^'"]+/gi)?.at(0);
if (!indexLink) {
return {
error: 'Couldnt find URL',
};
}
const responseVideo = await this.client.get(indexLink);
if (!responseVideo.body.length) {
return {
error: 'Couldnt find downloaded URL',
}
}
return this.extract(responseVideo.body);
}
/**
* @param {string} html
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
return {
video: {
urls: [new URL(`./${html}`, this.client.defaults.options.prefixUrl.toString()).href],
}
}
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,90 @@
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base';
import {deObfuscate, matchLink} from './utils';
import type {Shape} from 'ow';
/**
* @class TikmateProvider
*/
export class TikmateProvider extends BaseProvider {
public client = getFetch('https://tikmate.io');
/**
*
* @return {string}
*/
public resourceName(): string {
return 'tikmate';
}
public maintenance = undefined;
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// we need to get the token
const response = await this.client('./', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
}
});
const matchs = response.body.match(
/(name|id)="(\_)?token" value="([^""]+)"/,
) as string[];
const cookies =
response.headers['cookie'] ||
response.headers['set-cookie']?.toString();
const abcResponse = await this.client.post('./abc.php', {
form: matchs.at(-1)
? {
url: url,
token: matchs.at(-1),
}
: {
url: url,
},
headers: {
Origin: this.client.defaults.options.prefixUrl.toString(),
Referer: this.client.defaults.options.prefixUrl.toString(),
Cookie: cookies,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
},
});
return this.extract(abcResponse.body);
}
/**
* Extract information from raw html
* @param {string} html - Raw HTML
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const matchs = matchLink(deObfuscate(html));
if (!matchs)
return {
error: "Couldn't match any links!",
};
return {
video: {
thumb: matchs.shift(),
urls: matchs,
},
};
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,70 @@
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base';
import {matchLink} from './utils';
import type {Shape} from 'ow';
/**
* @class TTDownloader
*/
export class TTDownloader extends BaseProvider {
/**
* @return {string}
*/
public resourceName(): string {
return 'ttdownloader';
}
public client = getFetch('https://ttdownloader.com');
public maintenance = undefined;
/**
*
* @param {string} url - Video TikTok URL
* @return {Promise<ExtractedInfo>}
*/
public async fetch(url: string): Promise<ExtractedInfo> {
// getting token and cookies
const firstResponse = await this.client('./');
const token = (
firstResponse.body.match(/name="token" value="(.*)?"/) as string[]
)[1];
const videoResponse = await this.client.post('./search', {
form: {
token: token,
format: '',
url: url,
},
headers: {
Origin: 'https://ttdownloader.com',
Referer: 'https://ttdownloader.com',
Cookie: firstResponse.headers['set-cookie']?.toString(),
},
});
return this.extract(videoResponse.body);
}
/**
*
* @param {string} html - HTML Raw
* @return {ExtractedInfo}
*/
extract(html: string): ExtractedInfo {
const urls = matchLink(html);
urls?.pop(); // remove 'https://snaptik.fans'
return {
video: {
urls: (urls as string[]) ?? [],
},
};
}
/**
* Get ow.Shape params.
* @return {Shape | undefined}
*/
public getParams(): Shape | undefined {
return undefined;
}
}

View File

@@ -0,0 +1,103 @@
import {getProvider} from '..';
import type {BaseProvider} from '../base';
import {NodeVM} from 'vm2';
export const matchTikTokData = (html: string): string => {
const data = html.match(
// eslint-disable-next-line max-len
/window\['SIGI_STATE'\]=(.*)<\/script><script id="__LOADABLE_REQUIRED_CHUNKS__" type="application\/json">/,
);
if (data) {
return data[1].replace(/;window.+/gi, '');
} else {
return '';
}
};
export const runObfuscatedReplaceEvalScript = (jsCode: string): string => {
return runObfuscatedScript(jsCode.replace('eval', 'module.exports = '));
}
export const runObfuscatedScript = (jsCode: string): string => {
const transformed = jsCode
.trim()
.replace('eval', '')
.replace(/\(function(.)?\(h/gi, 'module.exports = (function (h');
const deObfuscated = new NodeVM({
compiler: 'javascript',
console: 'inherit',
require: {
external: true,
root: './',
},
}).run(transformed, 'deobfuscate.js');
return deObfuscated;
};
export const deObfuscate = (html: string): string => {
if (/error/gi.test(html)) {
throw new Error(
html.split("'").find((x) => /(((url)? error)|could)/gi.test(x)),
);
} else {
// only match script tag.
const obfuscatedScripts = html.match(
/<script[\s\S]*?>[\s\S]*?<\/script>/gi,
);
if (!obfuscatedScripts?.length) {
throw new Error('Cannot download the video!');
} else {
return runObfuscatedScript(
obfuscatedScripts
.find((x) => x.length)!
.replace(/<(\/)?script( type=".+")?>/g, '')
.trim(),
);
}
}
};
export const matchLink = (raw: string): string[] | null => {
return raw.match(
// eslint-disable-next-line max-len
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi,
);
};
export const matchCustomDownload = (
provider: string,
raw: string,
): string[] => {
const links = matchLink(raw) as string[];
const urls = raw
.match(/\/download.php\?token=(.*?)"/gi)
?.map(
(url) =>
(getProvider(provider) as BaseProvider)
.client!.defaults.options.prefixUrl.toString()
.slice(0, -1) + url.slice(0, -3),
);
if (!urls?.length) return [];
return [links[0]].concat(urls as string[]);
};
export const deObfuscateSaveFromScript = (scriptContent: string): string => {
const safeScript =
'let result;' +
scriptContent
.replace(/\/\*js\-response\*\//gi, '');
const vm = new NodeVM({
compiler: 'javascript',
console: 'inherit',
require: {
external: true,
root: './',
},
});
const result = vm.run(safeScript, 'savefrom.js');
return result;
};

View File

@@ -1,2 +1 @@
export * from './extractor';
export * from './generator';

View File

@@ -7,14 +7,14 @@
"strictFunctionTypes": true,
"strictNullChecks": true,
"outDir": "dist",
"target": "ES2016",
"module": "CommonJS",
"target": "ESNext",
"module": "ESNext",
"removeComments": true,
"esModuleInterop": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true
"moduleResolution": "node"
},
"exclude": ["node_modules", "dist"]
}
}

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TikTok Download</title>
</head>
<body>
<pre>Go to:</pre>
<a href="/api">/api</a>
</body>
</html>

View File

@@ -1 +0,0 @@
hello there!

10
renovate.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"assignees": ["hansputera"],
"username": "hansputera",
"labels": ["dependencies"],
"reviewers": ["hansputera"]
}

Some files were not shown because too many files have changed in this diff Show More