feat(tiktok): 初始提交

抖音批量下载与去水印工具
This commit is contained in:
imgyh 2023-01-30 16:57:28 +08:00
commit 74f9c91e75
27 changed files with 16623 additions and 0 deletions

63
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,63 @@
# 工作流名称
name: Build Docker Image
# push tag 时触发执行
on:
push:
tags:
- v*
# 定义环境变量, 后面会使用
# 定义 APP_NAME 用于 docker build-args
# 定义 DOCKERHUB_REPO 标记 docker hub repo 名称
env:
APP_NAME: TikTokWeb
DOCKERHUB_REPO: imgyh/TikTokWeb
jobs:
main:
# 在 Ubuntu 上运行
runs-on: ubuntu-latest
steps:
# git checkout 代码
- name: Checkout
uses: actions/checkout@v3
# 设置 QEMU, 后面 docker buildx 依赖此.
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# 设置 Docker buildx, 方便构建 Multi platform 镜像
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 登录 docker hub
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
# GitHub Repo => Settings => Secrets 增加 docker hub 登录密钥信息
# DOCKERHUB_USERNAME 是 docker hub 账号名.
# DOCKERHUB_TOKEN: docker hub => Account Setting => Security 创建.
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 通过 git 命令获取当前 tag 信息, 存入环境变量 APP_VERSION
- name: Generate App Version
run: echo APP_VERSION=`git describe --tags --always` >> $GITHUB_ENV
# 构建 Docker 并推送到 Docker hub
- name: Build and push
uses: docker/build-push-action@v3
id: docker_build
with:
# 是否 docker push
push: true
# 生成多平台镜像, 看 基础镜像 有哪些平台的镜像,下面就写哪些。
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
# docker build arg, 注入 APP_NAME/APP_VERSION
build-args: |
APP_NAME=${{ env.APP_NAME }}
APP_VERSION=${{ env.APP_VERSION }}
# 生成两个 docker tag: ${APP_VERSION} 和 latest
tags: |
${{ env.DOCKERHUB_REPO }}:latest
${{ env.DOCKERHUB_REPO }}:${{ env.APP_VERSION }}

50
.github/workflows/pyinstaller.yml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Package Application with Pyinstaller
# push tag 时触发执行
on:
push:
tags:
- v* # Push events to matching v*, i.e. v1.0, v20.15.10
# 需要添加权限才能 Release 成功
permissions:
contents: write # Release 需要的基本权限,不然会出错
pull-requests: write # Release中 discussion_category_name 需要的权限, 这个设置暂时用不到
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# 设置python环境
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies and Package Application with Pyinstaller
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
pyinstaller -i ./static/img/favicon.ico -F TikTokCommand.py
- name: Create Release and Upload Release Asset
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
# tag_name: ${{ github.ref }} # 标签名称. 默认为github.ref ${{ github.ref }}=refs/tags/+tag_name
# name: ${{ env.APP_VERSION }} # 发布的名称. 默认为标签名称
# 发布的更新内容 body 和 body_path 同时使用时, 优先 body_path 失败再尝试 body
# body_path: ${{ github.workspace }}-CHANGELOG.md
body: TODO New Release.
draft: false # 此版本是否为草稿
prerelease: false # 是否为预发布
generate_release_notes: false # 是否为本次发布自动生成名称(name)和正文(body)。
# 如果指定了名称,将使用指定的名称;否则,将自动生成一个名称。
# 如果指定了正文,正文将被添加到自动生成的注释中。
files: | # 多个文件要加 |
dist/TikTokCommand.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 默认为${{ github.token }}

160
.gitignore vendored Normal file
View File

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# This Dockerfile is used to build an Python environment
FROM python:3.9-slim-bullseye
LABEL maintainer="imgyh<admin@imgyh.com>"
WORKDIR /app
ADD . $WORKDIR
RUN pip3 install -r requirements.txt
CMD ["python3", "TikTokWeb.py"]

89
DouYinSelenium.py Normal file
View File

@ -0,0 +1,89 @@
import requests, re, os, time
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
class TikTok(object):
# 利用selenium可以获取cookies
def __init__(self):
option = webdriver.ChromeOptions()
# option.add_argument('headless') # 设置option
# option.add_argument("--headless")
# option.add_argument('--disable-gpu') # 一些情况下使用headless GPU会有问题我没遇到
# option.add_argument('window-size=1920x1080') # 页面部分内容是动态加载得时候无头模式默认size为0x0需要设置最大化窗口并设置windowssize不然会出现显示不全的问题
# option.add_argument('--start-maximized') # 页面部分内容是动态加载得时候无头模式默认size为0x0需要设置最大化窗口并设置windowssize不然会出现显示不全的问题
self.driver = webdriver.Chrome(ChromeDriverManager().install(), options=option)
# self.driver.get("https://www.douyin.com")
# # 获取cookie
# cookie_items = self.driver.get_cookies()
# cookie_str = ""
# # 组装cookie字符串
# for item_cookie in cookie_items:
# item_str = item_cookie["name"] + "=" + item_cookie["value"] + "; "
# cookie_str += item_str
self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
}
def videoShareLinkConvert(self, shareLink="https://v.douyin.com/kcvMpuN/"):
temp = shareLink.split("com/")[1].split("/")[0]
shareUrl = "https://v.douyin.com/" + temp
# 获取 awemeId
r = requests.get(shareUrl, self.headers)
awemeId = r.url.split('/')[5]
# print(awemeId)
return "https://www.douyin.com/video/" + awemeId
# 视频基本信息
def oneVideoInfo(self, url="https://www.douyin.com/video/6915675899241450760"):
self.driver.get(url)
html = self.driver.page_source
# print(html)
soup = BeautifulSoup(html, 'html.parser')
# 视频资源地址
list = soup.findAll(name="source")
# print(list)
videoRealUrl = list[2].get("src")
videoRealUrl = "https:" + videoRealUrl.split('&')[0] + "&ratio=1080p&line=0"
print(videoRealUrl)
return videoRealUrl
def userShareLinkConvert(self, shareLink="https://v.douyin.com/kcvSCe9/"):
temp = shareLink.split("com/")[1].split("/")[0]
shareUrl = "https://v.douyin.com/" + temp
# 获取 userId
r = requests.get(shareUrl, self.headers)
userId = r.url.split("?")[0].split("user/")[1]
# print(userId)
return "https://www.douyin.com/user/" + userId
# 用户基本信息
def userVideoInfo(self, url="https://www.douyin.com/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek"):
self.driver.get(url)
# 模拟鼠标下滑
js = "var q=document.documentElement.scrollTop=100000"
while True:
self.driver.execute_script(js)
html = self.driver.page_source
soup = BeautifulSoup(html, 'html.parser')
# print(len(soup.findAll(name="div", attrs={"class": "Sr905S5u"})))
# 滑到底部 Sr905S5u 这个div会消失
if len(soup.findAll(name="div", attrs={"class": "Sr905S5u"})) == 0:
break
time.sleep(1)
# 视频资源地址
list = soup.findAll(name="a", attrs={"class": "B3AsdZT9 chmb2GX8"})
userVideoUrls = []
for i in list:
# print("https://www.douyin.com" + i.get("href"))
videoRealUrl = self.oneVideoInfo("https://www.douyin.com" + i.get("href"))
userVideoUrls.append(videoRealUrl)
return userVideoUrls
tk = TikTok()
# tk.oneVideoInfo()
tk.userVideoInfo()
tk.driver.quit()

634
TikTok.py Normal file
View File

@ -0,0 +1,634 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import re
import requests
import json
import time
import os
import copy
import TikTokUtils
'''
作品详情
https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id=%s&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333
1080p视频
https://aweme.snssdk.com/aweme/v1/play/?video_id=%s&ratio=1080p&line=0
主页作品
https://www.iesdouyin.com/aweme/v1/web/aweme/post/?sec_user_id=%s&count=%s&max_cursor=%s&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333
主页喜欢
https://www.iesdouyin.com/web/api/v2/aweme/like/?sec_uid=%s&count=%s&max_cursor=%s&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333
'''
class TikTok(object):
def __init__(self):
self.headers = {
'user-agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36 Edg/87.0.664.66',
'Cookie': 'msToken=%s' % TikTokUtils.generate_random_str(107)
}
# 作者信息
self.authorDict = {
"avatar_thumb": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"avatar": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"cover_url": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
# 喜欢的作品数
"favoriting_count": "",
# 粉丝数
"follower_count": "",
# 关注数
"following_count": "",
# 昵称
"nickname": "",
# 是否允许下载
"prevent_download": "",
# 用户 url id
"sec_uid": "",
# 是否私密账号
"secret": "",
# 短id
"short_id": "",
# 签名
"signature": "",
# 总获赞数
"total_favorited": "",
# 用户id
"uid": "",
# 用户自定义唯一id 抖音号
"unique_id": "",
# 年龄
"user_age": "",
}
# 图片信息
self.picDict = {
"height": "",
"mask_url_list": "",
"uri": "",
"url_list": [],
"width": ""
}
# 音乐信息
self.musicDict = {
"cover_hd": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"cover_large": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"cover_medium": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"cover_thumb": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
# 音乐作者抖音号
"owner_handle": "",
# 音乐作者id
"owner_id": "",
# 音乐作者昵称
"owner_nickname": "",
"play_url": {
"height": "",
"uri": "",
"url_key": "",
"url_list": [],
"width": ""
},
# 音乐名字
"title": "",
}
# 视频信息
self.videoDict = {
"play_addr": {
"uri": "",
"url_list": "",
},
"cover_original_scale": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"dynamic_cover": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"origin_cover": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
},
"cover": {
"height": "",
"uri": "",
"url_list": [],
"width": ""
}
}
# 作品信息
self.awemeDict = {
# 作品创建时间
"create_time":"",
# awemeType=0 视频, awemeType=1 图集
"awemeType": "",
# 作品 id
"aweme_id": "",
# 作者信息
"author": self.authorDict,
# 作品描述
"desc": "",
# 图片
"images": [],
# 音乐
"music": self.musicDict,
# 视频
"video": self.videoDict,
# 作品信息统计
"statistics": {
"admire_count": "",
"collect_count": "",
"comment_count": "",
"digg_count": "",
"play_count": "",
"share_count": ""
}
}
# 用户作品信息
self.awemeList = []
# 直播信息
self.liveDict = {
# 是否在播
"status": "",
# 直播标题
"title": "",
# 观看人数
"user_count": "",
# 昵称
"nickname": "",
# sec_uid
"sec_uid": "",
# 直播间观看状态
"display_long": "",
# 推流
"flv_pull_url": "",
# 分区
"partition": "",
"sub_partition": ""
}
# 从分享链接中提取网址
def getShareLink(self, string):
# findall() 查找匹配正则表达式的字符串
return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', string)[0]
# 得到 作品id 或者 用户id
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
def getKey(self, url):
key = None
key_type = None
try:
r = requests.get(url=url, headers=self.headers)
except Exception as e:
print('[ 警告 ]:输入链接有误!\r')
return key_type, key
# 抖音把图集更新为note
# 作品 第一步解析出来的链接是share/video/{aweme_id}
# https://www.iesdouyin.com/share/video/7037827546599263488/?region=CN&mid=6939809470193126152&u_code=j8a5173b&did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&titleType=title&schema_type=37&from_ssr=1&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
# 用户 第一步解析出来的链接是share/user/{sec_uid}
# https://www.iesdouyin.com/share/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek?did=MS4wLjABAAAA1DICF9-A9M_CiGqAJZdsnig5TInVeIyPdc2QQdGrq58xUgD2w6BqCHovtqdIDs2i&iid=MS4wLjABAAAAomGWi4n2T0H9Ab9x96cUZoJXaILk4qXOJlJMZFiK6b_aJbuHkjN_f0mBzfy91DX1&with_sec_did=1&sec_uid=MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek&from_ssr=1&u_code=j8a5173b&timestamp=1674540164&ecom_share_track_params=%7B%22is_ec_shopping%22%3A%221%22%2C%22secuid%22%3A%22MS4wLjABAAAA-jD2lukp--I21BF8VQsmYUqJDbj3FmU-kGQTHl2y1Cw%22%2C%22enter_from%22%3A%22others_homepage%22%2C%22share_previous_page%22%3A%22others_homepage%22%7D&utm_source=copy&utm_campaign=client_share&utm_medium=android&app=aweme
urlstr = str(r.request.path_url)
if "/share/user/" in urlstr:
# 获取用户 sec_uid
if '?' in r.request.path_url:
for one in re.finditer(r'user\/([\d\D]*)([?])', str(r.request.path_url)):
key = one.group(1)
else:
for one in re.finditer(r'user\/([\d\D]*)', str(r.request.path_url)):
key = one.group(1)
key_type = "user"
elif "/share/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "live.douyin.com" in r.url:
key = r.url.replace('https://live.douyin.com/', '')
key_type = "live"
if key is None or key_type is None:
print('[ 警告 ]:输入链接有误!无法获取 id\r')
return key_type, key
print('[ 提示 ]:作品或者用户的 id = %s\r' % key)
return key_type, key
# 将得到的json数据dataRaw精简成自己定义的数据dataNew
# 转换得到的数据
def dataConvert(self, awemeType, dataNew, dataRaw):
for item in dataNew:
try:
# 作品创建时间
if item == "create_time":
dataNew['create_time'] = time.strftime(
"%Y-%m-%d %H.%M.%S", time.localtime(dataRaw['create_time']))
continue
# 设置 awemeType
if item == "awemeType":
dataNew["awemeType"] = awemeType
continue
# 当 解析的链接 是图片时
if item == "images":
if awemeType == 1:
for image in dataRaw[item]:
for i in image:
self.picDict[i] = image[i]
# 字典要深拷贝
self.awemeDict["images"].append(copy.deepcopy(self.picDict))
continue
# 当 解析的链接 是视频时
if item == "video":
if awemeType == 0:
self.dataConvert(awemeType, dataNew[item], dataRaw[item])
continue
# 将小头像放大
if item == "avatar":
for i in dataNew[item]:
if i == "url_list":
for j in self.awemeDict["author"]["avatar_thumb"]["url_list"]:
dataNew[item][i].append(j.replace("100x100", "1080x1080"))
elif i == "uri":
dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i].replace("100x100",
"1080x1080")
else:
dataNew[item][i] = self.awemeDict["author"]["avatar_thumb"][i]
continue
# 原来的json是[{}] 而我们的是 {}
if item == "cover_url":
self.dataConvert(awemeType, dataNew[item], dataRaw[item][0])
continue
# 根据 uri 获取 1080p 视频
if item == "play_addr":
dataNew[item]["uri"] = dataRaw["bit_rate"][0]["play_addr"]["uri"]
# 使用 这个api 可以获得1080p
dataNew[item]["url_list"] = "https://aweme.snssdk.com/aweme/v1/play/?video_id=%s&ratio=1080p&line=0" \
% dataNew[item]["uri"]
continue
# 常规 递归遍历 字典
if isinstance(dataNew[item], dict):
self.dataConvert(awemeType, dataNew[item], dataRaw[item])
else:
# 赋值
dataNew[item] = dataRaw[item]
except Exception as e:
print("[ 警告 ]:转换数据时在接口中未找到 %s\r" % (item))
def clearDict(self, data):
for item in data:
# 常规 递归遍历 字典
if isinstance(data[item], dict):
self.clearDict(data[item])
elif isinstance(data[item], list):
data[item] = []
else:
data[item] = ""
# 传入 aweme_id
# 返回 数据 字典
def getAwemeInfo(self, aweme_id):
if aweme_id is None:
return None
# 官方接口
# 旧接口22/12/23失效
# jx_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={self.aweme_id[i]}'
# 23/01/11
# 此ies domian暂时不需要xg参数
# 单作品接口返回 'aweme_detail'
# 主页作品接口返回 'aweme_list'->['aweme_detail']
jx_url = f'https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id={aweme_id}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333'
raw = requests.get(url=jx_url, headers=self.headers).text
datadict = json.loads(raw)
# 清空self.awemeDict
self.clearDict(self.awemeDict)
if datadict['aweme_detail'] is None:
print('[ 错误 ]:作品不存在, 请检查后重新运行!\r')
return None
# 默认为视频
awemeType = 0
try:
# datadict['aweme_detail']["images"] 不为 None 说明是图集
if datadict['aweme_detail']["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.dataConvert(awemeType, self.awemeDict, datadict['aweme_detail'])
return self.awemeDict
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
# mode : post | like 模式选择 like为用户点赞 post为用户发布
def getUserInfo(self, sec_uid, mode="post", count=35):
if sec_uid is None:
return None
# 旧接口于22/12/23失效
# post_url = 'https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=%s&count=35&max_cursor=0&aid=1128&_signature=PDHVOQAAXMfFyj02QEpGaDwx1S&dytk=' % (
# self.sec)
# 23/1/11
# 暂时使用不需要xg的接口
max_cursor = 0
self.awemeList = []
while True:
if mode == "post":
post_url = 'https://www.iesdouyin.com/aweme/v1/web/aweme/post/?sec_user_id=%s&count=%s&max_cursor=%s&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333' % (
sec_uid, count, max_cursor)
elif mode == "like":
post_url = 'https://www.iesdouyin.com/web/api/v2/aweme/like/?sec_uid=%s&count=%s&max_cursor=%s&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333' % (
sec_uid, count, max_cursor)
res = requests.get(url=post_url, headers=self.headers)
datadict = json.loads(res.text)
if not datadict["aweme_list"]:
print("[ 错误 ]:未找到数据, 请检查后重新运行!\r")
return None
print("[ 提示 ]:正在获取接口数据请稍后...\r\n")
for aweme in datadict["aweme_list"]:
# 获取 aweme_id 使用这个接口 https://www.iesdouyin.com/aweme/v1/web/aweme/detail/
aweme_id = aweme["aweme_id"]
# 深拷贝 dict 不然list里面全是同样的数据
self.awemeList.append(copy.deepcopy(self.getAwemeInfo(aweme_id)))
# time.sleep(0.5)
# 更新 max_cursor
max_cursor = datadict["max_cursor"]
# 退出条件
if datadict["has_more"] != 1:
break
return self.awemeList
def getLiveInfo(self, web_rid: str):
# web_rid = live_url.replace('https://live.douyin.com/', '')
live_api = 'https://live.douyin.com/webcast/web/enter/?aid=6383&web_rid=%s' % (web_rid)
# 必须用这个 headers
headers = {
'Cookie': 'msToken=tsQyL2_m4XgtIij2GZfyu8XNXBfTGELdreF1jeIJTyktxMqf5MMIna8m1bv7zYz4pGLinNP2TvISbrzvFubLR8khwmAVLfImoWo3Ecnl_956MgOK9kOBdwM=; odin_tt=6db0a7d68fd2147ddaf4db0b911551e472d698d7b84a64a24cf07c49bdc5594b2fb7a42fd125332977218dd517a36ec3c658f84cebc6f806032eff34b36909607d5452f0f9d898810c369cd75fd5fb15; ttwid=1%7CfhiqLOzu_UksmD8_muF_TNvFyV909d0cw8CSRsmnbr0%7C1662368529%7C048a4e969ec3570e84a5faa3518aa7e16332cfc7fbcb789780135d33a34d94d2'
}
response = requests.get(live_api, headers=headers)
live_json = json.loads(response.text)
if live_json == {} or live_json['status_code'] != 0:
print("[ 警告 ]:接口未返回信息\r")
return None
# 清空字典
self.clearDict(self.liveDict)
# 是否在播
self.liveDict["status"] = live_json['data']['data'][0]['status']
if self.liveDict["status"] == 4:
print('[ 📺 ]:当前直播已结束,按回车退出')
return self.liveDict
# 直播标题
self.liveDict["title"] = live_json['data']['data'][0]['title']
# 观看人数
self.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.liveDict["sub_partition"] = live_json['data']['partition_road_map']['sub_partition']['partition'][
'title']
except Exception as e:
self.liveDict["partition"] = ''
self.liveDict["sub_partition"] = ''
info = '[ 💻 ]:直播间:%s 当前%s 主播:%s 分区:%s-%s\r' % (
self.liveDict["title"], self.liveDict["display_long"], self.liveDict["nickname"],
self.liveDict["partition"], self.liveDict["sub_partition"])
print(info)
flv = []
print('[ 🎦 ]:直播间清晰度')
for i, f in enumerate(self.liveDict["flv_pull_url"].keys()):
print('[ %s ]: %s' % (i, f))
flv.append(f)
rate = int(input('[ 🎬 ]输入数字选择推流清晰度:'))
# 显示清晰度列表
print('[ %s ]:%s' % (flv[rate], self.liveDict["flv_pull_url"][flv[rate]]))
print('[ 📺 ]:复制链接使用下载工具下载')
return self.liveDict
# 来自 https://blog.csdn.net/weixin_43347550/article/details/105248223
def progressBarDownload(self, url, filepath):
start = time.time() # 下载开始时间
response = requests.get(url, stream=True, headers=self.headers)
size = 0 # 初始化已下载大小
chunk_size = 1024 # 每次下载的数据大小
content_size = int(response.headers['content-length']) # 下载文件总大小
try:
if response.status_code == 200: # 判断是否响应成功
print('[ 开始下载 ]:文件大小:{size:.2f} MB'.format(
size=content_size / chunk_size / 1024)) # 开始下载,显示下载文件大小
with open(filepath, 'wb') as file: # 显示进度条
for data in response.iter_content(chunk_size=chunk_size):
file.write(data)
size += len(data)
print('\r' + '[ 下载进度 ]:%s%.2f%%' % (
'>' * int(size * 50 / content_size), float(size / content_size * 100)), end=' ')
end = time.time() # 下载结束时间
print('\n' + '[ 下载完成 ]:耗时: %.2f\n' % (
end - start)) # 输出下载用时时间
except Exception as e:
# 下载异常 删除原来下载的文件, 可能未下成功
if os.path.exists(filepath):
os.remove(filepath)
print("[ 错误 ]:下载出错\r")
def awemeDownload(self, awemeDict: dict, music=True, cover=True, avatar=True, savePath=os.getcwd()):
if awemeDict is None:
return
try:
# 使用作品 创建时间+描述 当文件夹
file_name = TikTokUtils.replaceStr(awemeDict["create_time"] + awemeDict["desc"])
aweme_path = os.path.join(savePath, file_name)
if not os.path.exists(aweme_path):
os.mkdir(aweme_path)
# 保存获取到的字典信息
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(aweme_path, "result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(awemeDict, ensure_ascii=False, indent=2))
f.close()
# 下载 视频
if awemeDict["awemeType"] == 0:
print("[ 提示 ]:正在下载视频...\r")
video_path = os.path.join(aweme_path, file_name + ".mp4")
if os.path.exists(video_path):
print("[ 提示 ]:视频已存在为您跳过...\r\n")
else:
try:
url = awemeDict["video"]["play_addr"]["url_list"]
if url != "":
self.progressBarDownload(url, video_path)
except Exception as e:
print("[ 错误 ]:无法获取到视频url\r\n")
# 下载 图集
if awemeDict["awemeType"] == 1:
print("[ 提示 ]:正在下载图集...\r")
for ind, image in enumerate(awemeDict["images"]):
image_path = os.path.join(aweme_path, "image" + str(ind) + ".jpeg")
if os.path.exists(image_path):
print("[ 提示 ]:图片已存在为您跳过...\r\n")
else:
try:
url = image["url_list"][0]
if url != "":
self.progressBarDownload(url, image_path)
except Exception as e:
print("[ 错误 ]:无法获取到图片url\r\n")
# 下载 音乐
if music:
print("[ 提示 ]:正在下载音乐...\r")
music_name = TikTokUtils.replaceStr(awemeDict["music"]["title"])
music_path = os.path.join(aweme_path, music_name + ".mp3")
if os.path.exists(music_path):
print("[ 提示 ]:音乐已存在为您跳过...\r\n")
else:
try:
url = awemeDict["music"]["play_url"]["url_list"][0]
if url != "":
self.progressBarDownload(url, music_path)
except Exception as e:
print("[ 错误 ]:无法获取到音乐url\r\n")
# 下载 cover
if cover and awemeDict["awemeType"] == 0:
print("[ 提示 ]:正在下载视频cover图...\r")
cover_path = os.path.join(aweme_path, "cover.jpeg")
if os.path.exists(cover_path):
print("[ 提示 ]:cover 已存在为您跳过...\r\n")
else:
try:
url = awemeDict["video"]["cover_original_scale"]["url_list"][0]
if url != "":
self.progressBarDownload(url, cover_path)
except Exception as e:
print("[ 错误 ]:无法获取到cover url\r\n")
# 下载 avatar
if avatar:
print("[ 提示 ]:正在下载用户头像...\r")
avatar_path = os.path.join(aweme_path, "avatar.jpeg")
if os.path.exists(avatar_path):
print("[ 提示 ]:avatar 已存在为您跳过...\r\n")
else:
try:
url = awemeDict["author"]["avatar"]["url_list"][0]
if url != "":
self.progressBarDownload(url, avatar_path)
except Exception as e:
print("[ 错误 ]:无法获取到avatar url\r\n")
except Exception as e:
print("[ 错误 ]:请检查json信息是否正确\r\n")
def userDownload(self, awemeList: list, music=True, cover=True, avatar=True, savePath=os.getcwd()):
if awemeList is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
for ind, aweme in enumerate(awemeList):
print("[ 提示 ]:正在下载 %s 的作品 %s/%s\r"
% (aweme["author"]["nickname"], str(ind + 1), len(awemeList)))
self.awemeDownload(aweme, music, cover, avatar, savePath)
time.sleep(0.5)
if __name__ == "__main__":
pass

71
TikTokCommand.py Normal file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import argparse
import os
import json
from TikTok import TikTok
def argument():
parser = argparse.ArgumentParser(description='抖音批量下载工具 使用帮助')
parser.add_argument("--link", "-l",
help="1.作品(视频或图集)与个人主页抖音分享链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/)\r\n"
"2.解析直播网页版网址(https://live.douyin.com/802939216127)",
type=str, required=True)
parser.add_argument("--path", "-p", help="下载保存位置",
type=str, required=True)
parser.add_argument("--music", "-m", help="是否下载视频中的音乐(True/False)",
type=bool, required=False, default=True)
parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 当下载视频时有效",
type=bool, required=False, default=True)
parser.add_argument("--avatar", "-a", help="是否下载作者的头像(True/False)",
type=bool, required=False, default=True)
parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)",
type=str, required=False, default="post")
args = parser.parse_args()
return args
def main():
args = argument()
tk = TikTok()
url = tk.getShareLink(args.sharelink)
key_type, key = tk.getKey(url)
if key is None or key_type is None:
exit(0)
elif key_type == "user":
datalist = tk.getUserInfo(key, args.mode, 35)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.savepath)
elif key_type == "aweme":
datadict = tk.getAwemeInfo(key)
tk.awemeDownload(awemeDict=datadict, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.savepath)
elif key_type == "live":
live_json = tk.getLiveInfo(key)
if not os.path.exists(args.savepath):
os.mkdir(args.savepath)
# 保存获取到json
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(args.savepath, "result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(live_json, ensure_ascii=False, indent=2))
f.close()
if __name__ == "__main__":
main()

49
TikTokUtils.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import random
import re
def generate_random_str(randomlength=16):
"""
根据传入长度产生随机字符串
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789='
length = len(base_str) - 1
for _ in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
def replaceStr(filenamestr: str):
"""
替换非法字符缩短字符长度使其能成为文件名
"""
# 匹配 汉字 字母 数字 空格
match = "([0-9A-Za-z\u4e00-\u9fa5 -._]+)"
result = re.findall(match, filenamestr)
result = "".join(result).strip()
if len(result) > 80:
result = result[:80]
# 去除前后空格
return result
if __name__ == "__main__":
pass

57
TikTokWeb.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/01/27 19:36:18
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
from flask import *
from TikTok import TikTok
def work(share_link):
tk = TikTok()
url = tk.getShareLink(share_link)
key_type, key = tk.getKey(url)
datadict = tk.getAwemeInfo(key)
return datadict
app = Flask(__name__)
# 设置编码
app.config['JSON_AS_ASCII'] = False
@app.route("/douyin", methods=["POST"])
def douyin():
usefuldict = {}
if request.method == "POST":
result = request.form
else:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
try:
usefuldict = work(result["share_link"])
usefuldict["status_code"] = 200
except Exception as error:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=5000)

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
requests==2.28.2
flask==2.2.2
pyinstaller==5.7.0

2337
static/css/font-awesome.css vendored Executable file

File diff suppressed because it is too large Load Diff

1
static/css/naranja.min.css vendored Normal file
View File

@ -0,0 +1 @@
.naranja-notification{height:0;box-sizing:content-box;padding:10px 0;transition:padding .7s cubic-bezier(0, .5, 0, 1),height .7s cubic-bezier(0, .5, 0, 1)}.naranja-notification *{box-sizing:border-box}.naranja-notification .narj-log{background-color:#F9F9F9}.naranja-notification .narj-log button{border:1px solid #D2D2D2;background-color:white}.naranja-notification .narj-log button:first-of-type{color:#0099E5}.naranja-notification .narj-success{background-color:#B8F4BC}.naranja-notification .narj-success button{border:1px solid #6ED69A;background-color:#B8F4BC;opacity:.9;color:#11B674}.naranja-notification .narj-success button:first-of-type{opacity:1}.naranja-notification .narj-warn{background-color:#FFDD85}.naranja-notification .narj-warn button{border:1px solid #F5CE69;background-color:#FFDD85;opacity:.9;color:#D9993F}.naranja-notification .narj-warn button:first-of-type{opacity:1}.naranja-notification .narj-error{background-color:#ED9286}.naranja-notification .narj-error button{border:1px solid #ED8476;background-color:#ED9286;opacity:.9;color:#C24343}.naranja-notification .narj-error button:first-of-type{opacity:1}.naranja-notification .naranja-body-notification{animation:.4s fadeUpIn 1 cubic-bezier(0, .5, 0, 1);position:relative;display:flex;width:310px;border-radius:4px;padding:7px;box-shadow:0 5px 10px rgba(0,0,0,0.16);margin-bottom:7px;margin-top:12px;opacity:1;transition:opacity .15s ease,marginTop .3s ease,marginBottom .3s ease,padding .3s ease}.naranja-notification .naranja-body-notification:hover .naranja-close-icon{opacity:.7}.naranja-notification .naranja-body-notification:hover .naranja-close-icon:hover{opacity:1}.naranja-notification .naranja-body-notification>div{display:inline-flex;justify-content:center;align-items:center}.naranja-notification .naranja-body-notification .naranja-text-and-title{padding-left:15px;flex-direction:column;justify-content:center;align-items:flex-start}.naranja-notification .naranja-body-notification .naranja-text-and-title>p{margin:5px;font-family:'Open Sans'}.naranja-notification .naranja-body-notification .naranja-text-and-title>div{width:100%}.naranja-notification .naranja-body-notification .naranja-text-and-title>div button{float:right;margin-left:6px;margin-top:10px;margin-bottom:2px;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;border-radius:3px;padding:2px 11px;font-size:14px;text-align:center;outline:none;border-width:1px;box-shadow:0 2px 4px -2px rgba(0,0,0,0.2);cursor:pointer}.naranja-notification .naranja-body-notification .naranja-text-and-title>div button:active{opacity:.7}.naranja-notification .naranja-body-notification .naranja-title{font-size:20px;opacity:1}.naranja-notification .naranja-body-notification .naranja-parragraph{font-size:14px;opacity:.6;padding-right:30px;line-height:1.4em}.naranja-close-icon{position:absolute;right:7px;top:7px;opacity:0;cursor:pointer;transition:opacity .25s ease}@keyframes fadeUpIn{from{opacity:.2;box-shadow:0 0 0 rgba(0,0,0,0.5);transform:scale(.95)}75%{opacity:1}to{opacity:1;box-shadow:0 5px 10px rgba(0,0,0,0.16);transform:scale(1)}}.naranja-notification-box{box-sizing:content-box;display:flex;flex-direction:column-reverse;position:fixed;bottom:0;right:0;width:315px;height:auto;max-height:100vh;overflow:auto;padding:8px;padding-top:20px}.naranja-notification-box .naranja-notification-advice{position:fixed;right:138px;top:-39px;transform:translateY(0);cursor:pointer;transition:transform .3s ease}.naranja-notification-box .naranja-notification-advice.active{transform:translateY(60px)}

632
static/css/style.css Executable file
View File

@ -0,0 +1,632 @@
html {
scroll-behavior: smooth;
}
body,
html {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
* {
box-sizing: border-box;
}
.d-grid {
display: grid;
}
.d-flex {
display: flex;
display: -webkit-flex;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
button,
input,
select {
-webkit-appearance: none;
outline: none;
}
button,
.btn,
select {
cursor: pointer;
}
a {
text-decoration: none;
}
iframe {
border: none;
}
ul {
margin: 0;
padding: 0
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
padding: 0
}
p {
color: #485460;
}
.p-relative {
position: relative;
}
.p-absolute {
position: absolute;
}
.p-fixed {
position: fixed;
}
.p-sticky {
position: sticky;
}
.btn,
button,
.actionbg {
border-radius: 2px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-o-border-radius: 2px;
-ms-border-radius: 2px;
}
.btn:hover,
button:hover {
transition: 0.5s ease;
-webkit-transition: 0.5s ease;
-o-transition: 0.5s ease;
-ms-transition: 0.5s ease;
-moz-transition: 0.5s ease;
}
/*--/wrapper--*/
.wrapper {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.wrapper {
max-width: 540px;
}
}
@media (min-width: 768px) {
.wrapper {
max-width: 720px;
}
}
@media (min-width: 992px) {
.wrapper {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.wrapper {
max-width: 1140px;
}
}
.wrapper-full {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
/*--//wrapper--*/
.error-61-mian {
background: url(https://testingcf.jsdelivr.net/gh/imgyh/tiktok/static/img/banner.jpg) no-repeat bottom;
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
-moz-background-size: cover;
height: 100vh;
z-index: 0;
position: relative;
display: grid;
align-items: center;
}
.error-61-mian:before {
content: "";
background: rgba(66, 21, 2, 0.45);
position: absolute;
top: 0;
min-height: 100%;
left: 0;
right: 0;
z-index: -1;
}
.errors-16-top {
max-width: 600px;
margin: 0 auto;
text-align: center
}
.errors-16-mid {
max-width: 600px;
margin: 0 auto;
text-align: center
}
.error-61-mian h3 {
font-size: 223px;
line-height: 200px;
color: #fff;
font-weight: bold;
margin-bottom: 20px;
text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
.error-61-mian p {
font-size: 18px;
line-height: 30px;
color: #fff;
letter-spacing: 1px;
}
.error-page-form {
border: 2px solid #fff;
border-radius: 25px;
padding: 5px;
margin: 20px 0;
}
.error-page-form input {
color: #fff;
font-size: 18px;
border: none;
outline: none;
display: block;
padding: 0.4em 1em;
background: rgba(114, 51, 23, 0.75);
letter-spacing: 1px;
width: 79%;
border-radius: 25px;
margin-right: 1%;
}
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: #fff;
background: transparent;
}
::-moz-placeholder { /* Firefox 19+ */
color: #fff;
background: transparent;
}
:-ms-input-placeholder { /* IE 10+ */
color: #fff;
background: transparent;
}
:-moz-placeholder { /* Firefox 18- */
color: #fff;
background: transparent;
}
.error-page-form button {
color: var(--theme-green);
background: #fff;
border: none;
padding: 10px 15px;
text-decoration: none;
cursor: pointer;
font-size: 18px;
width: 20%;
display: block;
border-radius: 25px;
}
.error-page-form button:hover {
color: #fff;
background: #0099CC; /*#394247*/
}
.social-coming-icons a {
background: transparent;
border-radius: 50%;
width: 34px;
height: 34px;
text-align: center;
display: inline-block;
margin-right: 12px;
border: 2px solid #fff;
color: #fff;
}
.social-coming-icons a span {
font-size: 18px;
line-height: 30px;
}
.social-coming-icons a:hover {
color: #0099CC; /*#394247*/
border: 2px solid #0099CC; /*#394247*/
transition: 0.5s ease;
-webkit-transition: 0.5s ease;
-o-transition: 0.5s ease;
-ms-transition: 0.5s ease;
-moz-transition: 0.5s ease;
}
.copy-right {
margin: 0 auto;
text-align: center
}
.copy-right p {
font-size: 18px;
line-height: 29px;
color: #f9f9f9;
}
.copy-right a {
color: #00c4b6;
}
@media (max-width: 1280px) {
.error-61-mian h3 {
font-size: 180px;
line-height: 170px;
}
}
@media (max-width: 1280px) {
.error-61-mian h3 {
font-size: 125px;
line-height: 120px;
color: #fff;
font-weight: 700;
margin-bottom: 20px;
}
}
@media (max-width: 990px) {
.error-61-mian h3 {
font-size: 125px;
line-height: 110px;
}
}
@media (max-width: 667px) {
.error-61-mian h3 {
font-size: 115px;
line-height: 110px;
}
}
@media (max-width: 600px) {
.error-61-mian {
background: url(https://testingcf.jsdelivr.net/gh/imgyh/tiktok/static/img/banner.jpg) no-repeat right;
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
-moz-background-size: cover;
}
}
@media (max-width: 440px) {
.error-61-mian h3 {
font-size: 105px;
line-height: 100px;
}
}
@media (max-width: 384px) {
.error-61-mian h3 {
font-size: 95px;
line-height: 90px;
}
}
/*加载动画*/
.loading {
width: 30px;
height: 30px;
position: relative;
transform: rotate(45deg);
animation: animationContainer 1s ease infinite;
margin: 0 auto;
display: none;
}
.shape {
width: 10px;
height: 10px;
border-radius: 50%;
position: absolute;
}
.shape-1 {
background-color: #1875e5;
left: 0;
animation: animationShape1 0.3s ease infinite alternate;
}
.shape-2 {
background-color: #c5523f;
right: 0;
animation: animationShape2 0.3s ease infinite 0.3s alternate;
}
.shape-3 {
background-color: #499255;
bottom: 0;
animation: animationShape3 0.3s ease infinite 0.3s alternate;
}
.shape-4 {
background-color: #f2b736;
right: 0;
bottom: 0;
animation: animationShape4 0.3s ease infinite alternate;
}
@keyframes animationContainer {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
@keyframes animationShape1 {
0% {
transform: translate(5px, 5px);
}
100% {
transform: translate(-3px, -3px);
}
}
@keyframes animationShape2 {
0% {
transform: translate(-5px, 5px);
}
100% {
transform: translate(3px, -3px);
}
}
@keyframes animationShape3 {
0% {
transform: translate(5px, -5px);
}
100% {
transform: translate(-3px, 3px);
}
}
@keyframes animationShape4 {
0% {
transform: translate(-5px, -5px);
}
100% {
transform: translate(3px, 3px);
}
}
/*加载动画*/
/*按钮样式*/
.btn {
/* 文字颜色 */
color: #0099CC;
/* 清除背景色 */
background: transparent;
/* 边框样式、颜色、宽度 */
border: 2px solid #0099CC;
/* 给边框添加圆角 */
border-radius: 6px;
/* 字母转大写 */
border: none;
color: white;
padding: 10px 20px;
text-align: center;
display: inline-block;
font-size: 18px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
text-decoration: none;
text-transform: uppercase;
}
.btn1 {
background-color: white;
color: black;
border: 2px solid #0099CC;
}
/* 悬停样式 */
.btn1:hover {
background-color: #0099CC;
color: white;
}
/*按钮样式*/
/*头像框*/
.avatar {
width: 100%;
height: 100%;
display: flex;
border-radius: 50%;
align-items: center;
justify-content: center;
overflow: hidden;
}
.nickname {
display: flex;
/*border-radius: 50%;*/
align-items: center;
justify-content: center;
overflow: hidden;
}
.photo {
float: left;
width: 30%;
height: 100%;
}
.info {
float: right;
width: 70%;
height: 100%;
}
.icons {
display: -webkit-flex; /* Safari */
display: flex;
font-size: 30px;
}
.icon {
flex: 1;
}
/*头像框*/
/*select 样式*/
.select {
display: inline-block;
width: 100px;
position: relative;
vertical-align: middle;
padding: 0;
overflow: hidden;
background-color: #fff;
color: #555;
border: 1px solid #aaa;
text-shadow: none;
border-radius: 25px;
transition: box-shadow 0.25s ease;
z-index: 2;
}
.select:hover {
box-shadow: 0 1px 4px #0099CC; /*rgba(0, 0, 0, 0.15)*/
background-color: #0099CC;
color: white;
}
.select:before {
content: "";
position: absolute;
width: 0;
height: 0;
border: 10px solid transparent;
border-top-color: #ccc;
top: 14px;
right: 10px;
cursor: pointer;
z-index: -2;
}
.select select {
cursor: pointer;
padding: 10px;
width: 100%;
border: none;
background: transparent;
background-image: none;
-webkit-appearance: none;
-moz-appearance: none;
font-size: 18px;
}
.select select:focus {
outline: none;
}
/*select 样式*/
/*视频播放相关*/
#show-video {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 10 !important;
background: rgba(0, 0, 0, .85);
display: none;
}
.video-close {
width: 45px;
height: 45px;
color: #211d1e;
position: absolute;
right: 50px;
top: 50px;
z-index: 10 ;
cursor: pointer;
}
#show-video video {
outline: none;
max-width: 85%;
max-height: 88vh;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 20px 40px rgb(0 0 0 / 50%);
}

9
static/css/viewer.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/fonts/FontAwesome.otf Executable file

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/img/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
static/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

163
static/js/custom.js Normal file
View File

@ -0,0 +1,163 @@
// 发 post 请求
function SendAjax() {
var data = {};
data = $('#form1').serialize();
$.ajax({
type: 'POST',
url: "/douyin",
data: data,
dataType: 'json',
beforeSend: function () {
$("#loading").attr("style", "display:block;");//在请求后台数据之前显示loading图标
$("#download").attr("style", "display:none;");//隐藏 download
},
success: function (result) {
// console.log(result);//打印服务端返回的数据(调试用)
if (result.status_code === 200) {
if (result.awemeType === 0) {
$("#awemeType").html("预览视频");
$("#video").attr("href", result.video.play_addr.url_list);
$("#pre_video").attr("src", result.video.play_addr.url_list);
$("#video").attr("style", "display:inline;");//显示 video
}
if (result.awemeType === 1) {
$("#awemeType").html("预览图集");
var images = result.images;
var licontent = ""; // 拼接输入的 li 标签的字符串
for (var i = 0; i < images.length; i++) {
licontent += "<li><img src= " + images[i].url_list[0] + "></li>"
}
document.getElementById("images").innerHTML = licontent;
$("#video").attr("style", "display:none;");//隐藏 video
}
$("#cover").attr("href", result.video.cover_original_scale.url_list[0]);
$("#pre_video").attr("poster", result.video.dynamic_cover.url_list[0]);
$("#music").attr("href", result.music.play_url.url_list[0]);
$("#avatar").attr("src", result.author.avatar.url_list[0]);
$("#avatar").attr("alt", result.author.nickname);
$("#nickname").html(result.author.nickname);
$("#desc").html(result.desc);
var count = result.statistics.digg_count;
var digg_count;
if (count < 1000) {
digg_count = count
} else if (count >= 1000 && count < 10000) {
digg_count = (count / 1000).toFixed(1) + "K"
} else {
digg_count = (count / 10000).toFixed(1) + "W"
}
$("#aweme_digg_count").html(digg_count);
count = result.statistics.comment_count;
var comment_count;
if (count < 1000) {
comment_count = count
} else if (count >= 1000 && count < 10000) {
comment_count = (count / 1000).toFixed(1) + "K"
} else {
comment_count = (count / 10000).toFixed(1) + "W"
}
$("#aweme_comment_count").html(comment_count);
count = result.statistics.collect_count;
var collect_count;
if (count < 1000) {
collect_count = count
} else if (count >= 1000 && count < 10000) {
collect_count = (count / 1000).toFixed(1) + "K"
} else {
collect_count = (count / 10000).toFixed(1) + "W"
}
$("#aweme_collect_count").html(collect_count);
count = result.statistics.share_count;
var share_count;
if (count < 1000) {
share_count = count
} else if (count >= 1000 && count < 10000) {
share_count = (count / 1000).toFixed(1) + "K"
} else {
share_count = (count / 10000).toFixed(1) + "W"
}
$("#aweme_share_count").html(share_count);
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:block;");//显示 download
// alert("SUCCESS");
// 执行弹框
narnSuccess();
} else {
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:none;");//隐藏 download
// 执行弹框
narnFail();
}
;
},
error: function (xhr, type) {
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:none;");//隐藏 download
// alert("异常!");
// 执行弹框
narnFail();
}
});
}
// 右上角弹框
function narnSuccess() {
naranja().success({
title: '解析成功',
text: '请及时下载音视频',
icon: true,
timeout: 5000,
buttons: []
})
}
function narnFail() {
naranja().error({
title: '解析失败',
text: '视频不存在或接口失效',
icon: true,
timeout: 5000,
buttons: []
})
}
window.addEventListener('DOMContentLoaded', function () {
document.getElementById('view_aweme').addEventListener('click', function () {
var awemeType = document.getElementById("awemeType").innerText;
if (awemeType === "预览视频") {
// 调小音量
var videoElement = document.getElementById("pre_video");
videoElement.volume = 0.6
/*弹出视频播放层*/
$("#show-video").show();
}
// 图片查看器
if (awemeType === "预览图集") {
var viewer = new Viewer(document.getElementById('images'), {
hidden: function () {
viewer.destroy();
},
});
// image.click();
viewer.show();
}
});
/*关闭视频播放层*/
$(".video-close").click(function () {
var videoElement = document.getElementById("pre_video");
videoElement.pause()
$("#show-video").hide();
})
});

9440
static/js/jquery-1.8.2.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
static/js/naranja.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
static/js/viewer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

170
templates/index.html Executable file
View File

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="zhxx">
<head>
<title>抖音去水印工具</title>
<!-- Meta tag Keywords -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"/>
<meta name="keywords" content="抖音去水印工具"/>
<meta name="referrer" content="never">
<script>
addEventListener("load", function () {
setTimeout(hideURLbar, 0);
}, false);
function hideURLbar() {
window.scrollTo(0, 1);
}
</script>
<!-- //Meta tag Keywords -->
<!-- /Favicons -->
<link href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/img/favicon.ico" rel="shortcut icon" type="image/x-icon">
<!-- //Favicons -->
<!--/Style-CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/style.css" type="text/css" media="all"/>
<!--//Style-CSS -->
<!-- font-awesome-icons -->
<link href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/font-awesome.css" rel="stylesheet">
<!-- //font-awesome-icons -->
<!-- naranja 右下角弹框提示 https://github.com/e1016/naranja-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/naranja.min.css" type="text/css"/>
<!-- //naranja -->
<!-- viewerjs 图片查看器 https://github.com/fengyuanchen/viewerjs-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/css/viewer.min.css" type="text/css"/>
<!-- //viewerjs -->
</head>
<body>
<div class="error-61-mian">
<div class="wrapper">
<div class="errors-16-top">
<p style="color:#00c4b6;font-size:32px;">抖音去水印工具</p>
<br>
<p>温馨提示: 粘贴分享链接时 无需删除文案 但如果链接正确但解析失败请删掉文案后重试 https://v.douyin.com/kcvMpuN/</p>
<p>关于抖音批量下载与去水印工具的更多实现细节请点击: <a href="https://www.imgyh.com/archives/41.html" target="_blank" style="color:#00c4b6;">抖音批量下载与去水印工具</a></p>
<form id="form1" onsubmit="return false" action="#" method="post" class="d-flex error-page-form">
{# 以前需要手动选择 图片 或者 视频 现在加了自动判断#}
{# <div class="select">#}
{# <select name="awemeType" required="required">#}
{# <option value="0" selected="selected">视频</option>#}
{# <option value="1">图集</option>#}
{# </select>#}
{# </div>#}
<input type="text" placeholder="抖音视频分享地址,复制后粘贴到此处" name="share_link" required="required">
<button type="reset" onclick="SendAjax()">解析</button>
</form>
{# <div class="social-coming-icons">#}
{# <a href="#" title="Facebook" class="footer-fb"><span class="fa fa-facebook"#}
{# aria-hidden="true"></span></a>#}
{# <a href="#" title="Twitter" class="footer-tw"><span class="fa fa-twitter"#}
{# aria-hidden="true"></span></a>#}
{# <a href="#" title="Google Plus" class="footer-gg"><span class="fa fa-google-plus"#}
{# aria-hidden="true"></span></a>#}
{# <a href="#" title="Linkedin" class="footer-lin"><span class="fa fa-linkedin"#}
{# aria-hidden="true"></span></a>#}
{# </div>#}
</div>
<div class="errors-16-mid">
<div class="loading" id="loading">
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div class="shape shape-3"></div>
<div class="shape shape-4"></div>
</div>
<div id="download" style="display: none;">
<div class="photo">
<img id="avatar" src="#" class="avatar" alt="avatar">
<br>
<p id="nickname" class="nickname"></p>
</div>
<div class="info">
<div class="icons">
<div class="icon">
<i class="fa fa-heart" style="color:#fd325c;" aria-hidden="true"></i>
<p id="aweme_digg_count"></p>
</div>
<div class="icon">
<i class="fa fa-comment" style="color:#efeeec;" aria-hidden="true"></i>
<p id="aweme_comment_count"></p>
</div>
<div class="icon">
<i class="fa fa-star" style="color:#fcb505;" aria-hidden="true"></i>
<p id="aweme_collect_count"></p>
</div>
<div class="icon">
<i class="fa fa-share" style="color:#e7e8e6;" aria-hidden="true"></i>
<p id="aweme_share_count"></p>
</div>
</div>
<br>
<p id="desc"></p>
<br>
<a id="cover" href="#" target="_blank">
<button type="button" class="btn btn1 ">
<span>下载封面</span>
</button>
</a>
<a id="video" href="#" target="_blank">
<button type="button" class="btn btn1">
<span>下载视频</span>
</button>
</a>
<a id="music" href="#" target="_blank">
<button type="button" class="btn btn1">
<span>下载音乐</span>
</button>
</a>
<button id="view_aweme" type="button" class="btn btn1">
<span id="awemeType"></span>
</button>
</div>
</div>
</div>
</div>
<div class="copy-right">
<p>Copyright &copy; 2021-2023.我的博客 <a href="https://www.imgyh.com/" target="_blank">GYH's Blog</a> && 项目地址 <a
href="https://github.com/imgyh/douyin" target="_blank">Github</a>
All rights reserved.</p>
</div>
</div>
{# 视频预览效果 https://blog.csdn.net/qq_45140694/article/details/115266928 #}
<div id="show-video">
<a class="video-close">
<span>
<svg t="1614676844098" class="icon" viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg" p-id="2082"
width="30" height="30">
<path d="M591.506286 511.853714l417.133714-416.914285a54.601143 54.601143 0 0 0 0-76.8l-2.267429-2.267429a54.601143 54.601143 0 0 0-76.8 0L512.438857 433.481143 95.305143 15.798857a54.601143 54.601143 0 0 0-76.8 0L16.237714 18.066286a53.577143 53.577143 0 0 0 0 76.8l417.097143 416.987428L16.201143 929.097143a54.601143 54.601143 0 0 0 0 76.8l2.267428 2.267428a54.601143 54.601143 0 0 0 76.8 0l417.170286-417.060571 417.097143 417.097143a54.601143 54.601143 0 0 0 76.8 0l2.267429-2.267429a54.601143 54.601143 0 0 0 0-76.8z"
p-id="2083" fill="#e6e6e6"></path>
</svg>
</span>
</a>
{# https://blog.csdn.net/seeeeeeeeeee/article/details/119981594 #}
<video src="" id="pre_video" controls="controls" poster=""></video>
</div>
<div style="display: none">
<ul id="images">
{# <li><img src="picture-1.jpg" alt="Picture 1"></li>#}
{# <li><img src="picture-2.jpg" alt="Picture 2"></li>#}
{# <li><img src="picture-3.jpg" alt="Picture 3"></li>#}
</ul>
</div>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/jquery-1.8.2.min.js" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/naranja.min.js" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/viewer.min.js" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/js/custom.js" type="text/javascript"></script>
</body>
</html>