├── backend ├── app │ ├── config │ │ ├── __init__.py │ │ ├── prod.py │ │ └── dev.py │ ├── schemas │ │ ├── __init__.py │ │ ├── test.py │ │ └── tiktok.py │ ├── utils │ │ ├── __init__.py │ │ ├── proxy │ │ │ ├── __init__.py │ │ │ ├── validators.py │ │ │ └── http.py │ │ ├── tiktok │ │ │ ├── __init__.py │ │ │ ├── validators.py │ │ │ ├── http.py │ │ │ └── formatters.py │ │ ├── yt_dlp.py │ │ ├── models.py │ │ ├── logger.py │ │ ├── cache.py │ │ └── api.py │ ├── routes │ │ ├── __init__.py │ │ └── api │ │ │ ├── tiktok.py │ │ │ ├── proxy.py │ │ │ ├── __init__.py │ │ │ └── tests.py │ ├── extensions │ │ ├── cache.py │ │ ├── __init__.py │ │ ├── cors.py │ │ └── limiter.py │ └── __init__.py ├── tests │ ├── flask │ │ ├── __init__.py │ │ ├── test_cors.py │ │ ├── test_cache.py │ │ ├── test_limiter.py │ │ ├── test_route_tiktok.py │ │ └── test_api.py │ ├── proxy │ │ ├── __init__.py │ │ └── test_proxy_validators.py │ ├── tiktok │ │ ├── __init__.py │ │ └── test_tiktok_validators.py │ └── __init__.py ├── requirements.txt ├── .gitignore ├── server.py ├── .env.example ├── Dockerfile └── README.md ├── frontend ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── images │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ └── android-chrome-512x512.png │ └── site.webmanifest ├── .eslintrc.json ├── .prettierignore ├── src │ ├── lib │ │ ├── exceptions │ │ │ ├── index.ts │ │ │ ├── client.ts │ │ │ └── server.ts │ │ ├── tiktok │ │ │ ├── index.ts │ │ │ └── validators.ts │ │ └── utils.ts │ ├── styles │ │ └── globals.css │ ├── types │ │ ├── tiktok.ts │ │ └── index.ts │ ├── app │ │ ├── page.tsx │ │ └── layout.tsx │ ├── configs │ │ ├── api.ts │ │ ├── site.ts │ │ └── seo.ts │ └── components │ │ ├── Navbar.tsx │ │ ├── AlertError.tsx │ │ ├── ui │ │ └── DownloadButton.tsx │ │ ├── Footer.tsx │ │ ├── Icons.tsx │ │ └── tiktok │ │ └── TiktokForm.tsx ├── next.config.js ├── postcss.config.js ├── .prettierrc ├── Dockerfile ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts ├── package.json └── README.md ├── .gitignore ├── .dockerignore ├── nginx └── nginx.conf ├── LICENSE.md ├── docker-compose.yml └── README.md /backend/app/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/flask/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/tiktok/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /backend/app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import api_bp 2 | 3 | 4 | __all__ = [api_bp, ] 5 | -------------------------------------------------------------------------------- /backend/app/utils/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | from .http import * 2 | from .validators import * 3 | -------------------------------------------------------------------------------- /frontend/src/lib/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./server"; 2 | export * from "./client"; 3 | -------------------------------------------------------------------------------- /frontend/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/backend/requirements.txt -------------------------------------------------------------------------------- /backend/app/extensions/cache.py: -------------------------------------------------------------------------------- 1 | # Flask modules 2 | from flask_caching import Cache 3 | 4 | cache = Cache() 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/app/utils/tiktok/__init__.py: -------------------------------------------------------------------------------- 1 | from .http import * 2 | from .validators import * 3 | from .formatters import * 4 | 5 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache___ 2 | **/.pytest_cache 3 | **/venv 4 | **/.idea 5 | **/.env* 6 | **/logs 7 | !.env.example 8 | 9 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /backend/app/schemas/test.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class TestModel(BaseModel): 5 | title: str 6 | content: str 7 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Set 'False' to test with .env 4 | # Set 'True' to test with .env.dev 5 | os.environ['FLASK_DEBUG'] = 'True' 6 | -------------------------------------------------------------------------------- /frontend/public/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riad-azz/tiktok-saver/HEAD/frontend/public/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /backend/app/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from .cors import cors 2 | from .cache import cache 3 | from .limiter import limiter 4 | 5 | __all__ = [cors, cache, limiter] 6 | -------------------------------------------------------------------------------- /backend/server.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app(debug=False) 4 | 5 | if __name__ == "__main__": 6 | app.run("0.0.0.0", port=5000, debug=True) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.vscode 2 | **/node_modules 3 | **/venv 4 | **/__pycache__ 5 | **/.next 6 | **/.idea 7 | **/.env 8 | **/prod.cookies.txt 9 | **/env* 10 | !**/.env.example 11 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "plugins": ["prettier-plugin-tailwindcss"] 7 | } 8 | -------------------------------------------------------------------------------- /backend/app/extensions/cors.py: -------------------------------------------------------------------------------- 1 | from flask_cors import CORS 2 | 3 | CORS_ORIGINS = "*" 4 | 5 | cors = CORS( 6 | methods=[ 7 | "GET", 8 | ], 9 | origins=CORS_ORIGINS, 10 | ) 11 | -------------------------------------------------------------------------------- /frontend/src/types/tiktok.ts: -------------------------------------------------------------------------------- 1 | export type VideoInfo = { 2 | filename: string; 3 | is_watermarked: boolean; 4 | duration: number; 5 | description: string; 6 | video_link: string; 7 | thumbnail: string; 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/lib/exceptions/client.ts: -------------------------------------------------------------------------------- 1 | export class ClientException extends Error { 2 | /** 3 | * @param message 4 | */ 5 | constructor(message = "Something went wrong, please try again.") { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import TiktokForm from "@/components/tiktok/TiktokForm"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /backend/app/schemas/tiktok.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class VideoInfo(BaseModel): 5 | filename: str 6 | is_watermarked: bool 7 | duration: int 8 | description: str 9 | video_link: str 10 | thumbnail: str 11 | -------------------------------------------------------------------------------- /backend/app/utils/proxy/validators.py: -------------------------------------------------------------------------------- 1 | def is_allowed_proxy_domain(url: str): 2 | allowed_domains = [ 3 | "https://api16-normal-c-useast1a.tiktokv.com/", 4 | "https://v77.tiktokcdn.com/", 5 | "https://v16m.tiktokcdn.com/", 6 | ] 7 | 8 | return any(url.startswith(x) for x in allowed_domains) 9 | -------------------------------------------------------------------------------- /frontend/src/configs/api.ts: -------------------------------------------------------------------------------- 1 | let baseApiURL: string; 2 | if (process.env.NODE_ENV === "production") { 3 | baseApiURL = `/api`; 4 | } else { 5 | baseApiURL = "http://127.0.0.1:5000/api"; 6 | } 7 | 8 | export const proxyApiURL = `${baseApiURL}/proxy/download`; 9 | export const tiktokApiURL = `${baseApiURL}/tiktok/info`; 10 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Flask Variables 2 | SECRET_KEY="YOUR-SECRET-KEY" 3 | # Flask Ratelimit 4 | RATELIMIT_ENABLED="True" 5 | RATELIMIT_STORAGE_URI="memory://" # or redis://localhost:6379/0 6 | # Flask Cache 7 | CACHE_ENABLED="True" 8 | CACHE_TYPE="SimpleCache" # or RedisCache 9 | CACHE_STORAGE_URL="redis://@localhost:6379/1" 10 | -------------------------------------------------------------------------------- /backend/app/utils/tiktok/validators.py: -------------------------------------------------------------------------------- 1 | def is_valid_tiktok_domain(url: str): 2 | allowed_domains = [ 3 | "https://m.tiktok.com/", 4 | "https://www.tiktok.com/", 5 | "https://tiktok.com/", 6 | "https://vm.tiktok.com/" 7 | ] 8 | 9 | return any(url.startswith(x) and len(url) > len(x) for x in allowed_domains) 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | **/venv 3 | **/tests 4 | **/__pycache__ 5 | 6 | # Nextjs 7 | **/.next 8 | **/dist 9 | **/node_modules 10 | **/npm-debug.log 11 | 12 | # IDE + Git 13 | **/.git 14 | **/.vscode 15 | **/.idea 16 | 17 | # .env files 18 | **/.env.* 19 | **/.env* 20 | **/.env 21 | 22 | # Other 23 | **/DEVNOTES.md 24 | **/README.md 25 | **/LICENSE.MD 26 | -------------------------------------------------------------------------------- /frontend/src/configs/site.ts: -------------------------------------------------------------------------------- 1 | import { SiteConfig } from "@/types"; 2 | 3 | export const siteConfig: SiteConfig = { 4 | name: "Tiktok Saver", 5 | description: "Tiktok Saver is......", 6 | url: "http://localhost:3000/", 7 | ogImageUrl: "http://localhost:3000/og.png", 8 | links: { 9 | twitter: "https://twitter.com/riadazz", 10 | github: "https://github.com/riad-azz/tiktok-saver", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM node:20-alpine 3 | 4 | # Set working directory 5 | WORKDIR /next-app 6 | 7 | # Copy package.json and package-lock.json 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the Next.js application 14 | COPY . . 15 | 16 | # Build the Next.js app 17 | RUN npm run build 18 | 19 | EXPOSE 3000 20 | 21 | # Start the Next.js app 22 | # CMD ["npm", "start"] 23 | -------------------------------------------------------------------------------- /backend/app/utils/yt_dlp.py: -------------------------------------------------------------------------------- 1 | # To use these options you must check : https://github.com/yt-dlp/yt-dlp 2 | USE_COOKIES = False 3 | COOKIES_PATH = "cookies.txt" 4 | COOKIES_BROWSER = "chrome" 5 | 6 | 7 | def get_command(url: str) -> str: 8 | if USE_COOKIES: 9 | command = f"yt-dlp {url} --cookies {COOKIES_PATH} --no-download --dump-json -q --cookies-from-browser {COOKIES_BROWSER}" 10 | else: 11 | command = f"yt-dlp {url} --no-download --dump-json -q" 12 | 13 | return command 14 | -------------------------------------------------------------------------------- /frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = { 2 | name: string; 3 | description: string; 4 | url: string; 5 | ogImageUrl: string; 6 | links: { 7 | twitter: string; 8 | github: string; 9 | }; 10 | }; 11 | 12 | export type SuccessResponse = { 13 | status: "success"; 14 | message?: string; 15 | data: T; 16 | }; 17 | 18 | export type ErrorResponse = { 19 | status: "error"; 20 | message: string; 21 | }; 22 | 23 | export type APIResponse = SuccessResponse | ErrorResponse; 24 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image for Python Flask 2 | FROM python:3.11-bullseye 3 | 4 | ENV PIP_DISABLE_PIP_VERSION_CHECK 1 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory 9 | WORKDIR /flask-app 10 | 11 | # Copy the requirements file 12 | COPY ./requirements.txt . 13 | 14 | # Install the dependencies 15 | RUN pip install -r requirements.txt 16 | 17 | # Copy the Flask app file 18 | COPY . . 19 | 20 | EXPOSE 5000 21 | 22 | # Run the Flask app 23 | # CMD ["gunicorn", "--bind", "0.0.0.0:5000", "server:app"] 24 | -------------------------------------------------------------------------------- /backend/tests/flask/test_cors.py: -------------------------------------------------------------------------------- 1 | # Other modules 2 | import os 3 | import pytest 4 | 5 | # Local modules 6 | from app import create_app 7 | 8 | 9 | @pytest.fixture 10 | def app(): 11 | DEBUG = os.environ.get("DEBUG", "False") == "True" 12 | app = create_app(debug=DEBUG) 13 | 14 | return app 15 | 16 | 17 | def test_cors_enabled(app): 18 | with app.test_client() as client: 19 | response = client.get("/api/tests/success") 20 | assert response.status_code == 200 21 | assert response.headers["Access-Control-Allow-Origin"] is not None 22 | -------------------------------------------------------------------------------- /backend/tests/tiktok/test_tiktok_validators.py: -------------------------------------------------------------------------------- 1 | # Local modules 2 | from app.utils.tiktok import is_valid_tiktok_domain 3 | 4 | 5 | def test_valid_tiktok_url(): 6 | urls_list = [ 7 | "https://m.tiktok.com/random/extra/stuff?v=55", 8 | "https://tiktok.com/dummy/data/?v=544", 9 | "https://www.tiktok.com/very/long/url/here/testing?potato=5", 10 | "https://vm.tiktok.com/@user/v/7541654654" 11 | ] 12 | for url in urls_list: 13 | assert is_valid_tiktok_domain(url) is True 14 | 15 | 16 | def test_invalid_tiktok_url(): 17 | url = "https://www.example.com" 18 | valid_url = is_valid_tiktok_domain(url) 19 | assert valid_url is False 20 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /backend/app/utils/tiktok/http.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from app.utils.yt_dlp import get_command 4 | 5 | 6 | def get_video_info(post_url: str) -> dict: 7 | command = get_command(post_url) 8 | try: 9 | output = subprocess.check_output(command, shell=True) 10 | json_data = output.decode("utf-8").strip() 11 | return json.loads(json_data) 12 | except subprocess.CalledProcessError: 13 | print(f"Unable to extract video url for : {post_url}") 14 | except json.JSONDecodeError as e: 15 | print(f"Error while decoding {post_url} video JSON: {e}") 16 | except Exception as e: 17 | print(f"An error occurred while fetching {post_url} video Json: {e}") 18 | 19 | raise Exception(f"Failed to fetch tiktok info for : {post_url}") 20 | -------------------------------------------------------------------------------- /backend/tests/flask/test_cache.py: -------------------------------------------------------------------------------- 1 | # Other modules 2 | import pytest 3 | 4 | # Local modules 5 | from app import create_app 6 | from app.utils.cache import get_cached_response 7 | 8 | 9 | @pytest.fixture 10 | def app(): 11 | app = create_app() 12 | 13 | return app 14 | 15 | 16 | def test_api_cache(app): 17 | request_url = "/api/tests/cached" 18 | with app.test_client() as client: 19 | response = client.get(request_url) 20 | assert response.status_code == 200 21 | assert response.json["status"] == "success" 22 | assert response.json["data"]["title"] == "riad-azz" 23 | assert response.json["data"]["content"] == "Cached API response" 24 | 25 | request = response.request 26 | is_cached = get_cached_response(request) 27 | 28 | assert bool(is_cached) is True -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tiktok Saver", 3 | "short_name": "Tiktok Saver", 4 | "icons": [ 5 | { 6 | "src": "/images/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "lang": "en-us", 17 | "dir": "ltr", 18 | "start_url": "/", 19 | "background_color": "#ffffff", 20 | "theme_color": "#ffffff", 21 | "display": "standalone", 22 | "orientation": "landscape-primary", 23 | "description": "Tiktok Saver is...", 24 | "categories": [ 25 | "Entertainment", 26 | "Social", 27 | "Video", 28 | "Saver", 29 | "Downloader", 30 | "Tiktok", 31 | "Media" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | gzip on; 9 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 10 | 11 | 12 | keepalive_timeout 65; 13 | server { 14 | listen 80; 15 | server_name example.com; 16 | 17 | location /api/ { 18 | # Reverse proxy configuration 19 | proxy_pass http://backend:5000/api/; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | } 23 | 24 | location / { 25 | # Reverse proxy configuration 26 | proxy_pass http://frontend:3000; 27 | proxy_set_header Host $host; 28 | proxy_set_header X-Real-IP $remote_addr; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/tests/proxy/test_proxy_validators.py: -------------------------------------------------------------------------------- 1 | # Local modules 2 | from app.utils.proxy import is_allowed_proxy_domain 3 | 4 | 5 | def test_valid_proxy_domain(): 6 | urls_list = [ 7 | "https://api16-normal-c-useast1a.tiktokv.com/", 8 | "https://v77.tiktokcdn.com/", 9 | "https://v16m.tiktokcdn.com/", 10 | "https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/play/?video_id=v12044gd0000cile1rbc77u9s4smhvrg&" 11 | "line=0&is_play_url=1&source=PackSourceEnum_FEED&file_id=023e564f131e40b59a6f7bc85099e7ed&" 12 | "item_id=7253860622557908266&signv3=video_id;file_id;item_id.09e6e43bafd52daea9d780c5caba0141"] 13 | for url in urls_list: 14 | assert is_allowed_proxy_domain(url) is True 15 | 16 | 17 | def test_invalid_proxy_domain(): 18 | url = "https://www.example.com" 19 | valid_url = is_allowed_proxy_domain(url) 20 | assert valid_url is False 21 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter as MainFont } from "next/font/google"; 2 | import { mainMetadata } from "@/configs/seo"; 3 | 4 | import "@/styles/globals.css"; 5 | import Footer from "@/components/Footer"; 6 | import Navbar from "@/components/Navbar"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | const mainFont = MainFont({ 10 | subsets: ["latin"], 11 | }); 12 | 13 | export const metadata = mainMetadata; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 29 | 30 | {children} 31 |