mirror of
https://github.com/hansputera/tiktok-dl.git
synced 2026-04-05 19:51:57 +02:00
BREAK: init new apps/web to latest nextjs and switch from ow to zod
Signed-off-by: Hanif Dwy Putra S <hanifdwyputrasembiring@gmail.com>
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
const eslintConfigs = require('tiktok-dl-config/eslint.typescript');
|
||||
|
||||
module.exports = {
|
||||
...eslintConfigs,
|
||||
extends: eslintConfigs.extends.concat(['plugin:@next/next/recommended']),
|
||||
};
|
||||
43
apps/web/.gitignore
vendored
43
apps/web/.gitignore
vendored
@@ -1,2 +1,41 @@
|
||||
.next
|
||||
*.rdb
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
*.json
|
||||
.next
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('tiktok-dl-config/prettier');
|
||||
21
apps/web/components.json
Normal file
21
apps/web/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Footer = () => (
|
||||
<React.Fragment>
|
||||
<p className="text-lg">
|
||||
© {new Date().getFullYear()}{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/hansputera/tiktok-dl.git"
|
||||
>
|
||||
TikTok-DL Project
|
||||
</a>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -1,187 +0,0 @@
|
||||
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;
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* @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 +0,0 @@
|
||||
export * from './httpError';
|
||||
25
apps/web/eslint.config.mjs
Normal file
25
apps/web/eslint.config.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './redis';
|
||||
export * from './rotator';
|
||||
export * from './url';
|
||||
@@ -1,31 +0,0 @@
|
||||
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);
|
||||
};
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
3
apps/web/next-env.d.ts
vendored
3
apps/web/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
},
|
||||
});
|
||||
8
apps/web/next.config.ts
Normal file
8
apps/web/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['tiktok-dl-core'],
|
||||
serverExternalPackages: ['vm2'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,35 +1,39 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ioredis": "^5.7.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"next": "15.5.2",
|
||||
"ow": "^2.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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} />;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
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],
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
import type {NextApiRequest, NextApiResponse} from 'next';
|
||||
|
||||
export default async (_: NextApiRequest, res: NextApiResponse) => {
|
||||
return res.redirect('https://github.com/hansputera/tiktok-dl.git');
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
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],
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
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],
|
||||
);
|
||||
@@ -1,25 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
5
apps/web/postcss.config.mjs
Normal file
5
apps/web/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
57
apps/web/src/app/api/download/route.ts
Normal file
57
apps/web/src/app/api/download/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { downloadValidator } from "@/app/validators/download.validator";
|
||||
import { rotateProvider } from "@/services/rotator";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getProvider } from "tiktok-dl-core";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const json = await request.json();
|
||||
const safeData = await downloadValidator.safeParseAsync(json);
|
||||
|
||||
if (safeData.error || !safeData.success) {
|
||||
return Response.json({
|
||||
errors: safeData.error,
|
||||
message: 'Validation errors',
|
||||
}, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const provider = getProvider(safeData.data.type);
|
||||
if (!provider) {
|
||||
return Response.json({
|
||||
message: 'Provider not found',
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
const params = provider?.getParams();
|
||||
if (params) {
|
||||
const safeDataParams = await params.safeParseAsync(safeData.data.params);
|
||||
if (safeDataParams.error || !safeDataParams.success) {
|
||||
return Response.json({
|
||||
errors: safeDataParams.error,
|
||||
message: 'Validation errors in params!',
|
||||
}, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await rotateProvider(provider, safeData.data.url, safeData.data.rotateOnError, safeData.data.params);
|
||||
return Response.json({
|
||||
data: result,
|
||||
});
|
||||
} catch (e) {
|
||||
return Response.json({
|
||||
message: (e as Error).message,
|
||||
}, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
message: 'Currently we moved to POST method only.',
|
||||
});
|
||||
}
|
||||
22
apps/web/src/app/api/providers/route.ts
Normal file
22
apps/web/src/app/api/providers/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Shape } from "ow";
|
||||
import { BaseProvider, Providers } from "tiktok-dl-core";
|
||||
|
||||
const transformProvider = (provider: BaseProvider) => {
|
||||
const params = provider.getParams();
|
||||
|
||||
return {
|
||||
name: provider.resourceName(),
|
||||
url: provider.client?.defaults.options.prefixUrl,
|
||||
maintenance: provider.maintenance,
|
||||
params: params ? Object.keys(params).map(key => ({
|
||||
name: key,
|
||||
type: (params?.[key] as Shape | undefined)?.type,
|
||||
})) : {},
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
data: Providers.map(transformProvider),
|
||||
});
|
||||
}
|
||||
9
apps/web/src/app/api/stored-links/route.ts
Normal file
9
apps/web/src/app/api/stored-links/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { redisClient } from "@/lib/redis";
|
||||
import { matchLink } from "tiktok-dl-core";
|
||||
|
||||
export async function GET() {
|
||||
const keys = await redisClient.keys('*');
|
||||
return Response.json({
|
||||
data: keys.filter(key => matchLink(key)),
|
||||
});
|
||||
}
|
||||
74
apps/web/src/app/globals.css
Normal file
74
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,74 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(94.27% 0.0268 242.57);
|
||||
--secondary-background: oklch(100% 0 0);
|
||||
--foreground: oklch(0% 0 0);
|
||||
--main-foreground: oklch(0% 0 0);
|
||||
--main: oklch(66.9% 0.18368 248.8066);
|
||||
--border: oklch(0% 0 0);
|
||||
--ring: oklch(0% 0 0);
|
||||
--overlay: oklch(0% 0 0 / 0.8);
|
||||
--shadow: 4px 4px 0px 0px var(--border);
|
||||
--chart-1: #0099FF;
|
||||
--chart-2: #FF4D50;
|
||||
--chart-3: #FACC00;
|
||||
--chart-4: #05E17A;
|
||||
--chart-5: #7A83FF;
|
||||
--chart-active-dot: #000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(27.08% 0.0336 240.69);
|
||||
--secondary-background: oklch(23.93% 0 0);
|
||||
--foreground: oklch(92.49% 0 0);
|
||||
--main-foreground: oklch(0% 0 0);
|
||||
--main: oklch(61.9% 0.16907 248.5982);
|
||||
--border: oklch(0% 0 0);
|
||||
--ring: oklch(100% 0 0);
|
||||
--shadow: 4px 4px 0px 0px var(--border);
|
||||
--chart-1: #008AE5;
|
||||
--chart-2: #FF6669;
|
||||
--chart-3: #E0B700;
|
||||
--chart-4: #04C86D;
|
||||
--chart-5: #7A83FF;
|
||||
--chart-active-dot: #fff;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-main: var(--main);
|
||||
--color-background: var(--background);
|
||||
--color-secondary-background: var(--secondary-background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-main-foreground: var(--main-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-overlay: var(--overlay);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
--spacing-boxShadowX: 4px;
|
||||
--spacing-boxShadowY: 4px;
|
||||
--spacing-reverseBoxShadowX: -4px;
|
||||
--spacing-reverseBoxShadowY: -4px;
|
||||
--radius-base: 10px;
|
||||
--shadow-shadow: var(--shadow);
|
||||
--font-weight-base: 500;
|
||||
--font-weight-heading: 800;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply text-foreground font-base bg-background;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6{
|
||||
@apply font-heading;
|
||||
}
|
||||
}
|
||||
26
apps/web/src/app/layout.tsx
Normal file
26
apps/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "TikTok-DL",
|
||||
description: "An Open-Source Project where it could download TikTok's Video without annoying ads!",
|
||||
keywords: 'tiktok-downloader, tiktokdl, tiktok, download video tiktok, tiktok no watermark',
|
||||
authors: {
|
||||
name: 'Hanif Dwy Putra S.',
|
||||
url: 'https://hanifu.id',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body suppressHydrationWarning>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
102
apps/web/src/app/page.tsx
Normal file
102
apps/web/src/app/page.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getTikTokURL } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
|
||||
export default function Home() {
|
||||
const formSchema = z.object({
|
||||
url: z.url().refine((val) => getTikTokURL(val), {
|
||||
error: 'Invalid VT Tiktok URL',
|
||||
}),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
url: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center lg:justify-center">
|
||||
<div className="w-full max-w-6xl mx-auto px-4">
|
||||
{/* Mobile Layout */}
|
||||
<div className="lg:hidden">
|
||||
<h1 className="text-4xl font-sans text-black mb-6">
|
||||
Download TikTok Videos!
|
||||
</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField control={form.control} name="url" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">VT URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tiktok video URL (e.g. https://vt.tiktok.com/XXXXXX)" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please provide the tiktok video URL to download
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
|
||||
<Button type="submit">
|
||||
Download
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* Large Screen Hero Layout */}
|
||||
<div className="hidden lg:flex lg:items-center lg:gap-12">
|
||||
{/* Hero Title */}
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-6xl font-sans text-black leading-tight">
|
||||
Download<br />
|
||||
TikTok<br />
|
||||
Videos!
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Form Section */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField control={form.control} name="url" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">VT URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Tiktok video URL (e.g. https://vt.tiktok.com/XXXXXX)"
|
||||
className="text-lg py-3"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-base">
|
||||
Please provide the tiktok video URL to download
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
|
||||
<Button type="submit" size="lg" className="w-full">
|
||||
Download
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
apps/web/src/app/validators/download.validator.ts
Normal file
13
apps/web/src/app/validators/download.validator.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getTikTokURL } from "@/lib/utils";
|
||||
import { Providers } from "tiktok-dl-core";
|
||||
import z from "zod";
|
||||
|
||||
export const downloadValidator = z.object({
|
||||
url: z.url().refine((url) => getTikTokURL(url), {
|
||||
error: 'Invalid VT URL',
|
||||
}),
|
||||
type: z.enum(Providers.map(provider => provider.resourceName()).concat('random')),
|
||||
rotateOnError: z.boolean().default(true),
|
||||
nocache: z.boolean().default(false),
|
||||
params: z.object().optional(),
|
||||
});
|
||||
56
apps/web/src/components/ui/button.tsx
Normal file
56
apps/web/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-base text-sm font-base ring-offset-white transition-all gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"text-main-foreground bg-main border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
|
||||
noShadow: "text-main-foreground bg-main border-2 border-border",
|
||||
neutral:
|
||||
"bg-secondary-background text-foreground border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
|
||||
reverse:
|
||||
"text-main-foreground bg-main border-2 border-border hover:translate-x-reverseBoxShadowX hover:translate-y-reverseBoxShadowY hover:shadow-shadow",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 px-3",
|
||||
lg: "h-11 px-8",
|
||||
icon: "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
167
apps/web/src/components/ui/form.tsx
Normal file
167
apps/web/src/components/ui/form.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"use client"
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
)
|
||||
|
||||
function FormField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("font-heading", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm font-base text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-base text-red-500", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
19
apps/web/src/components/ui/input.tsx
Normal file
19
apps/web/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-base border-2 border-border bg-secondary-background selection:bg-main selection:text-main-foreground px-3 py-2 text-sm font-base text-foreground file:border-0 file:bg-transparent file:text-sm file:font-heading placeholder:text-foreground/50 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
25
apps/web/src/components/ui/label.tsx
Normal file
25
apps/web/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"text-sm font-heading leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -23,4 +23,4 @@ export const providerCache = 3600;
|
||||
* How much rotate retries is allowed?
|
||||
* Default: 5x
|
||||
*/
|
||||
export const maxRotateCount = 3;
|
||||
export const maxRotateCount = 3;
|
||||
@@ -1,5 +1,5 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
export const client = new Redis(
|
||||
export const redisClient = new Redis(
|
||||
process.env.REDIS_URL ?? 'redis://:@localhost:6379',
|
||||
);
|
||||
@@ -1,3 +1,10 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/** Get TikTok Video URL.
|
||||
* @param {string} url Video
|
||||
* @return {string}
|
||||
@@ -14,4 +21,4 @@ export function getTikTokURL(url: string): string | undefined {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {maxRotateCount, providerCache} from '../config';
|
||||
import {maxRotateCount, providerCache} from '@/config/config';
|
||||
import {BaseProvider, ExtractedInfo, getRandomProvider} from 'tiktok-dl-core';
|
||||
import {client as redisClient} from './redis';
|
||||
import {redisClient} from '@/lib/redis';
|
||||
|
||||
/**
|
||||
* Rotate provider.
|
||||
@@ -87,4 +87,4 @@ export const rotateProvider = async (
|
||||
} else {
|
||||
return JSON.parse(cachedData);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,37 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"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",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import {defineConfig} from 'windicss/helpers';
|
||||
|
||||
export default defineConfig({
|
||||
extract: {
|
||||
include: ['**/*.{jsx,tsx,css}'],
|
||||
exclude: ['node_modules', '.git', '.next'],
|
||||
},
|
||||
});
|
||||
@@ -6,8 +6,8 @@
|
||||
"description": "TikTok-DL Project Core",
|
||||
"dependencies": {
|
||||
"got": "^12.6.1",
|
||||
"ow": "0.28.1",
|
||||
"vm2": "^3.9.19"
|
||||
"vm2": "^3.9.19",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint \"+(src)/**/*.ts\" --fix",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Got} from 'got';
|
||||
import type {Shape} from 'ow';
|
||||
import { ZodObject } from 'zod';
|
||||
|
||||
export interface ExtractedInfo {
|
||||
error?: string;
|
||||
@@ -43,7 +43,7 @@ export interface MaintenanceProvider {
|
||||
*/
|
||||
export abstract class BaseProvider {
|
||||
abstract client?: Got;
|
||||
abstract getParams(): Shape | undefined;
|
||||
abstract getParams(): ZodObject | undefined;
|
||||
abstract maintenance?: MaintenanceProvider;
|
||||
abstract resourceName(): string;
|
||||
abstract fetch(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {getFetch} from '../fetch';
|
||||
import {matchCustomDownload, matchLink, runObfuscatedScript} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
import { ZodObject } from 'zod';
|
||||
|
||||
/**
|
||||
* @class DownTikProvider
|
||||
@@ -99,10 +99,10 @@ export class DownTikProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params.
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {getFetch} from '../fetch';
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import type {Shape} from 'ow';
|
||||
import { matchLink } from './utils';
|
||||
|
||||
/**
|
||||
@@ -73,11 +72,7 @@ export class MusicalyDown extends BaseProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {getFetch} from '../fetch';
|
||||
import {matchTikTokData} from './utils';
|
||||
import ow, {Shape} from 'ow';
|
||||
import z from 'zod';
|
||||
|
||||
/**
|
||||
* @class NativeProvider
|
||||
@@ -92,15 +92,9 @@ export class NativeProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
return {
|
||||
'user-agent': ow.string.not.empty
|
||||
.minLength(5)
|
||||
.message('Invalid user-agent'),
|
||||
};
|
||||
public getParams() {
|
||||
return z.object({
|
||||
'user-agent': z.string().min(5),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {getFetch} from '../fetch';
|
||||
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
|
||||
import {deObfuscateSaveFromScript} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
|
||||
/**
|
||||
* @class saveFromProvider
|
||||
@@ -75,11 +74,7 @@ export class SaveFromProvider extends BaseProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {getFetch} from '../fetch';
|
||||
import { ZodObject } from 'zod';
|
||||
// import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
|
||||
/**
|
||||
* @class SaveTikProvider
|
||||
@@ -97,10 +97,10 @@ export class SaveTikProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ZodObject } from 'zod';
|
||||
import {getFetch} from '../fetch';
|
||||
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
|
||||
import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
|
||||
/**
|
||||
* @class SnaptikProvider
|
||||
@@ -66,10 +66,10 @@ export class SnaptikProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {getFetch} from '../fetch';
|
||||
import type {Shape} from 'ow';
|
||||
import { ZodObject } from 'zod';
|
||||
|
||||
/**
|
||||
* @class TikDownProvider
|
||||
@@ -69,10 +69,10 @@ export class TikDownProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ZodObject } from 'zod';
|
||||
import {getFetch} from '../fetch';
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {deObfuscate, matchLink} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
|
||||
/**
|
||||
* @class TikmateProvider
|
||||
@@ -81,10 +81,10 @@ export class TikmateProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ZodObject } from 'zod';
|
||||
import {getFetch} from '../fetch';
|
||||
import {BaseProvider, ExtractedInfo} from './base';
|
||||
import {matchLink} from './utils';
|
||||
import type {Shape} from 'ow';
|
||||
|
||||
/**
|
||||
* @class TTDownloader
|
||||
@@ -61,10 +61,10 @@ export class TTDownloader extends BaseProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ow.Shape params.
|
||||
* @return {Shape | undefined}
|
||||
* Get zod params
|
||||
* @return {ZodObject | undefined}
|
||||
*/
|
||||
public getParams(): Shape | undefined {
|
||||
public getParams(): ZodObject | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user