├── .github └── dependabot.yml ├── .gitignore ├── Dockerfile ├── README.md ├── cache.py ├── config.py ├── package-lock.json ├── package.json ├── requirements.txt ├── serverless.yml ├── slideshowBuilder └── __init__.py ├── templates ├── base.html ├── message.html └── video.html ├── vxtiktok-cf-worker-proxy ├── .editorconfig ├── .gitignore ├── .prettierrc ├── package-lock.json ├── package.json ├── src │ └── index.js ├── vitest.config.js └── wrangler.toml ├── vxtiktok.ini ├── vxtiktok.py ├── vxtiktok.service └── wsgi.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv/ 3 | node_modules 4 | tmp/ 5 | *.conf 6 | .serverless/ 7 | .vscode/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine AS build 2 | RUN apk add build-base python3-dev linux-headers pcre-dev jpeg-dev zlib-dev 3 | RUN pip install --upgrade pip 4 | RUN pip install uwsgi 5 | 6 | FROM python:3.10-alpine AS deps 7 | WORKDIR /vxtiktok 8 | COPY requirements.txt requirements.txt 9 | COPY --from=build /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages 10 | RUN pip install -r requirements.txt 11 | 12 | FROM python:3.10-alpine AS runner 13 | EXPOSE 9000 14 | RUN apk add pcre-dev jpeg-dev zlib-dev 15 | WORKDIR /vxtiktok 16 | CMD ["uwsgi", "vxtiktok.ini"] 17 | COPY --from=build /usr/local/bin/uwsgi /usr/local/bin/uwsgi 18 | COPY --from=deps /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages 19 | COPY . . 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vxtiktok 2 | Basic Website that serves fixed TikTok video embeds to various platforms (i.e Discord) by using yt-dlp to grab TikTok video information. 3 | ## How to use the hosted version 4 | 5 | Just replace tiktok.com with vxtiktok.com on the link to the TikTok post! `https://www.tiktok.com/@username/video/1234567890123456789` -> `https://vxtiktok.com/@username/video/1234567890123456789` 6 | 7 | ## Other stuff 8 | 9 | We handle various TikTok URL formats, including short mobile URLs and subdomain variants. The application will attempt to resolve these to the full TikTok URL before processing. Be sure to only replace "tiktok" in the url (i.e `vm.tiktok.com` -> `vm.vxtiktok.com`) 10 | 11 | For TikTok posts containing multiple images, a slideshow video will be generated* (This can be done locally or via an external API, depending on the configuration.) 12 | 13 | \* As of the time of writing, due to TikTok API changes this may not be working 14 | 15 | **Note**: If you enjoy this service, please consider donating to help cover server costs. https://ko-fi.com/dylanpdx 16 | 17 | ## Limitations 18 | - The application relies on TikTok's internal api; Changes made by them might break things 19 | - Some features may not work for all types of TikTok content, especially non-video formats. 20 | -------------------------------------------------------------------------------- /cache.py: -------------------------------------------------------------------------------- 1 | from codecs import getdecoder 2 | import config 3 | import pymongo 4 | import boto3 5 | from datetime import date,datetime, timedelta 6 | 7 | client=None 8 | db=None 9 | DYNAMO_CACHE_TBL=None 10 | if config.currentConfig["CACHE"]["cacheMethod"] == "mongodb": 11 | client = pymongo.MongoClient(config.currentConfig["CACHE"]["databaseurl"], connect=False) 12 | table = config.currentConfig["CACHE"]["databasetable"] 13 | db = client[table] 14 | elif config.currentConfig["CACHE"]["cacheMethod"]=="dynamodb": 15 | DYNAMO_CACHE_TBL=config.currentConfig["CACHE"]["databasetable"] 16 | client = boto3.resource('dynamodb') 17 | 18 | def getDefaultTTL(): 19 | return datetime.today().replace(microsecond=0) + timedelta(days=1) 20 | 21 | def addToCache(url,vidInfo): 22 | try: 23 | if config.currentConfig["CACHE"]["cacheMethod"] == "none": 24 | pass 25 | elif config.currentConfig["CACHE"]["cacheMethod"] == "mongodb": 26 | ttl=getDefaultTTL() 27 | db.linkCache.insert_one({"url":url,"info":vidInfo,"ttl":ttl}) 28 | elif config.currentConfig["CACHE"]["cacheMethod"] == "dynamodb": 29 | ttl=getDefaultTTL() 30 | ttl = int(ttl.strftime('%s')) 31 | table = client.Table(DYNAMO_CACHE_TBL) 32 | table.put_item(Item={"url":url,"info":vidInfo,"ttl":ttl}) 33 | except Exception as e: 34 | print("addToCache for URL "+url+" failed: "+str(e)) 35 | 36 | def getFromCache(url): 37 | try: 38 | if config.currentConfig["CACHE"]["cacheMethod"] == "none": 39 | return None 40 | elif config.currentConfig["CACHE"]["cacheMethod"] == "mongodb": 41 | obj = db.linkCache.find_one({'url': url}) 42 | if obj == None: 43 | return None 44 | return obj["info"] 45 | elif config.currentConfig["CACHE"]["cacheMethod"] == "dynamodb": 46 | obj = client.Table(DYNAMO_CACHE_TBL).get_item(Key={'url': url}) 47 | if 'Item' not in obj: 48 | return None 49 | return obj["Item"]["info"] 50 | except Exception as e: 51 | print("getFromCache for URL "+url+" failed: "+str(e)) 52 | return None -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | 4 | currentConfig = configparser.ConfigParser() 5 | 6 | ## default values 7 | 8 | currentConfig["MAIN"]={ 9 | "appName": "vxTiktok", 10 | "embedColor": "#EE1D52", 11 | "repoURL":"https://github.com/dylanpdx/vxtiktok", 12 | "domainName":"vxtiktok.com", 13 | "slideshowRenderer":"local", 14 | } 15 | 16 | currentConfig["CACHE"]={ 17 | "cacheMethod":"none", 18 | "databaseURL":"{mongodb URL}", 19 | "databaseTable":"vxTiktok", 20 | "cacheTTL":86400, 21 | } 22 | 23 | if 'RUNNING_SERVERLESS' in os.environ and os.environ['RUNNING_SERVERLESS'] == '1': 24 | currentConfig["MAIN"]={ 25 | "appName": os.environ['APP_NAME'], 26 | "embedColor": "#EE1D52", 27 | "repoURL":os.environ['REPO_URL'], 28 | "domainName":os.environ['DOMAINNAME'], 29 | "slideshowRenderer":os.environ['SLIDESHOW_RENDERER'], 30 | } 31 | currentConfig["CACHE"]={ 32 | "cacheMethod":os.environ['CACHE_METHOD'], 33 | "databaseURL":os.environ['DATABASE_URL'], 34 | "databaseTable":os.environ['CACHE_TABLE'], 35 | "cacheTTL":int(os.environ['CACHE_TTL']), 36 | } 37 | else: 38 | if os.path.exists("vxTiktok.conf"): 39 | # as per python docs, "the most recently added configuration has the highest priority" 40 | # "conflicting keys are taken from the more recent configuration while the previously existing keys are retained" 41 | currentConfig.read("vxTiktok.conf") 42 | 43 | with open("vxTiktok.conf", "w") as configfile: 44 | currentConfig.write(configfile) # write current config to file -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vxtiktok", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dylanpdx/vxtiktok.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/dylanpdx/vxtiktok/issues" 18 | }, 19 | "homepage": "https://github.com/dylanpdx/vxtiktok#readme", 20 | "dependencies": { 21 | "serverless-plugin-common-excludes": "^4.0.0", 22 | "serverless-plugin-include-dependencies": "^5.0.0", 23 | "serverless-python-requirements": "^5.4.0", 24 | "serverless-wsgi": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | Flask-Cors==4.0.0 3 | yt-dlp==2024.08.06 4 | pymongo==4.5.0 5 | boto3==1.28.37 6 | requests==2.32.3 7 | idna<4,>=2.5 8 | urllib3>=1.21.1,<2.1 9 | certifi>=2017.4.17 10 | charset-normalizer>=2,<4 11 | Werkzeug==2.3.7 -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: vxTiktok 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.8 6 | stage: dev 7 | iamRoleStatements: 8 | - Effect: Allow 9 | Action: 10 | - dynamodb:Query 11 | - dynamodb:Scan 12 | - dynamodb:GetItem 13 | - dynamodb:PutItem 14 | - dynamodb:UpdateItem 15 | - dynamodb:DeleteItem 16 | Resource: 17 | - { "Fn::GetAtt": ["vxTiktokDynamoTable", "Arn" ] } 18 | environment: 19 | APP_NAME: vxTiktok 20 | EMBED_COLOR: \#EE1D52 21 | REPO_URL: https://github.com/dylanpdx/vxtiktok 22 | DOMAINNAME: vxtiktok.com 23 | SLIDESHOW_RENDERER: local 24 | CACHE_METHOD: dynamodb 25 | DATABASE_URL: none 26 | CACHE_TABLE: ${self:custom.tableName} 27 | CACHE_TTL: 86400 28 | 29 | RUNNING_SERVERLESS: 1 30 | 31 | package: 32 | patterns: 33 | - '!node_modules/**' 34 | - '!venv/**' 35 | 36 | plugins: 37 | - serverless-wsgi 38 | - serverless-python-requirements 39 | - serverless-plugin-common-excludes 40 | - serverless-plugin-include-dependencies 41 | 42 | functions: 43 | vxTiktokApp: 44 | handler: wsgi_handler.handler 45 | url: true 46 | timeout: 15 47 | memorySize: 1000 48 | layers: 49 | - Ref: PythonRequirementsLambdaLayer 50 | 51 | 52 | custom: 53 | tableName: 'tiktok-table-${self:provider.stage}' 54 | wsgi: 55 | app: vxtiktok.app 56 | pythonRequirements: 57 | layer: true 58 | dockerizePip: true 59 | 60 | 61 | resources: 62 | Resources: 63 | vxTiktokDynamoTable: 64 | Type: 'AWS::DynamoDB::Table' 65 | Properties: 66 | AttributeDefinitions: 67 | - 68 | AttributeName: url 69 | AttributeType: S 70 | KeySchema: 71 | - 72 | AttributeName: url 73 | KeyType: HASH 74 | TableName: ${self:custom.tableName} 75 | BillingMode: PAY_PER_REQUEST 76 | TimeToLiveSpecification: 77 | AttributeName: ttl 78 | Enabled: true -------------------------------------------------------------------------------- /slideshowBuilder/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import subprocess 4 | import json 5 | import sys 6 | import tempfile 7 | 8 | def generateVideo(slideshowVideoData): 9 | # slideshowData = {..., "slideshowData":{"musicUrl":"","imageUrls":[]}} 10 | slideshowData = slideshowVideoData["slideshowData"] 11 | txtData = "" 12 | 13 | if len(slideshowData["imageURLs"]) == 1: 14 | slideshowData["imageURLs"].append(slideshowData["imageURLs"][0]) 15 | 16 | for url in slideshowData["imageURLs"]: 17 | txtData += f"file '{url}'\nduration 2\n" 18 | with tempfile.TemporaryDirectory() as tmpdirname: 19 | txtPath = os.path.join(tmpdirname, "test.txt") 20 | with open(txtPath, "w") as f: 21 | f.write(txtData) 22 | musicPath = slideshowData["musicURL"] 23 | outputFilePath = os.path.join(tmpdirname, "out.mp4") 24 | subprocess.run(["ffmpeg", "-f", "concat", "-safe", "0", "-protocol_whitelist", "file,http,tcp,https,tls", "-i", txtPath, "-i", musicPath, "-map", "0:v", "-map", "1:a","-vf","scale=1280:720:force_original_aspect_ratio=decrease:eval=frame,pad=1280:720:-1:-1:eval=frame,format=yuv420p", "-filter_complex", "[1:0] apad","-shortest", outputFilePath]) 25 | with open(outputFilePath, "rb") as vid_file: 26 | encoded_string = base64.b64encode(vid_file.read()).decode('ascii') 27 | return encoded_string -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {% block head %}{% endblock %} 6 | 7 | 8 | 9 | {% block body %}{% endblock %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/message.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block body %} 10 |