Compare commits

..

109 Commits
v1.5.0 ... main

Author SHA1 Message Date
imgyh
8158292756 docs(changelog): 更新日志 2023-05-28 15:49:30 +08:00
imgyh
a790aad8df docs(readme): 完善文档 2023-05-28 15:48:20 +08:00
imgyh
1e69ee8a14 fix(webapi): 修复作品列表为空时不返回cursor
fix #40
2023-05-28 15:47:34 +08:00
imgyh
ff72578a9c docs(issue): 增加issue模板 2023-05-24 11:05:21 +08:00
imgyh
f7a3acc754 ci(pyinstaller): webApi打包增加静态文件
fix #39
2023-05-23 22:06:32 +08:00
imgyh
b38e210927 feat(douyin): 修改文件名命名风格 2023-05-22 16:39:53 +08:00
imgyh
ce385d9608 ci(docker): 更新自动化部署配置 2023-05-21 16:44:38 +08:00
imgyh
d87cf4f837 docs(readme): 更新文档 2023-05-21 16:44:02 +08:00
imgyh
336267fac4 docs(changelog): 更新日志 2023-05-21 16:27:53 +08:00
imgyh
2114bc925e feat(tiktok): 删除多余文件 2023-05-21 16:25:02 +08:00
imgyh
54e50598de docs(readme): 更新文档
re #36
2023-05-21 16:23:55 +08:00
imgyh
fc90897396 ci(tiktok): 修改打包配置 2023-05-21 16:20:36 +08:00
imgyh
942bc35912 feat(tiktok): 规范包目录结构 2023-05-21 16:17:21 +08:00
imgyh
3b1f9e7b5c docs(changelog): 更新日志 2023-04-24 20:46:59 +08:00
imgyh
40b4462464 docs(readme): 更新文档 2023-04-24 20:45:00 +08:00
imgyh
ee06c65269 docs(config): 更新配置文件 2023-04-24 20:43:18 +08:00
imgyh
ebf6671d33 feat(tiktok): 增加数据库功能与增量更新功能
re #24
2023-04-24 20:42:20 +08:00
imgyh
87b48aec06 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:50:24 +08:00
imgyh
a140c2f3f8 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:48:36 +08:00
imgyh
1c48f64b75 fix(web): 解决浏览器提示不安全问题 2023-04-21 20:20:31 +08:00
imgyh
321d4622a1 fix(tiktok): 优化视频链接获取 2023-04-21 20:18:56 +08:00
imgyh
a984186542 docs(changelog): 更新日志 2023-04-21 17:59:28 +08:00
imgyh
72aca75960 feat(web): 前段静态文件使用国内cdn 2023-04-21 17:58:34 +08:00
imgyh
064ae7dc63 style(tiktok): 整理代码格式 2023-04-21 16:53:37 +08:00
imgyh
48fcf902be test(test): 测试接口 2023-04-21 16:41:19 +08:00
imgyh
ada1851e86 fix(result): 修复1080p清晰度链接 2023-04-21 16:40:48 +08:00
imgyh
9aaab2f691 fix(tiktok): 修复接口失效问题
fix #33
2023-04-21 16:37:10 +08:00
imgyh
5c6dd6fb45 docs(changelog): 更新日志 2023-04-16 19:51:22 +08:00
imgyh
8f6c1ea70d fix(tiktok): 增加请求重试机制 2023-04-16 19:50:20 +08:00
imgyh
59b24472c7 ci(docker): 适配基础镜像平台 2023-04-16 19:09:22 +08:00
imgyh
25614f9a67 docs(changelog): 更新日志 2023-04-16 19:02:42 +08:00
imgyh
6808355e9f docs(readme): 更新文档 2023-04-16 19:00:59 +08:00
imgyh
1ef56da99b build(dockerfile): 增加node环境 2023-04-16 19:00:30 +08:00
imgyh
596a0fc633 feat(command): 未传入cookie则使用默认值 2023-04-16 18:59:09 +08:00
imgyh
2f13bd5122 fix(web): 前端适配新接口 2023-04-16 18:58:21 +08:00
imgyh
d3d091d9dd feat(web): 所有接口支持json与form两种格式, cookie不传使用默认值
fix #31
2023-04-16 18:56:15 +08:00
imgyh
372450b4d7 docs(readme): 更新文档 2023-04-14 22:42:25 +08:00
imgyh
918d6c9eba feat(web): 增加解析接口
fix #28
2023-04-14 22:41:56 +08:00
imgyh
2ea2ac07a8 fix(tiktok): 更改多作品接口请求逻辑, 不再调用单个作品的方法 2023-04-14 20:22:52 +08:00
imgyh
aebc14429d fix(command): 修复mode命令解析问题 2023-04-04 13:11:43 +08:00
imgyh
72a57668c7 docs(readme): 更新日志 2023-03-29 19:00:03 +08:00
imgyh
99c62922ca fix(tiktok): 缩短文件名长度,只使用数字字母汉字作为文件名
fix #19
2023-03-29 18:59:41 +08:00
imgyh
f2c1587e53 docs(changelog): 更新日志 2023-03-28 22:16:36 +08:00
imgyh
f0d8223f14 ci(pyinstaller): 增加配置文件下载 2023-03-28 22:15:39 +08:00
imgyh
01fbd743f8 fix(tiktok): 缩短文件名长度 2023-03-28 22:11:31 +08:00
imgyh
47b7662a66 docs(changelog): 更新日志 2023-03-28 21:39:58 +08:00
imgyh
0314e2520d docs(readme): 更新文档 2023-03-28 21:37:47 +08:00
imgyh
8ce13c0d72 fix(tiktok): 修改文件名匹配正则表达式 2023-03-28 21:08:35 +08:00
imgyh
2496ccd90c fix(tiktok): 修复配置文件路径获取问题 2023-03-28 20:17:02 +08:00
imgyh
f55dd004e9 fix(tiktok): 使用tqdm进度条 2023-03-28 20:16:24 +08:00
imgyh
cbfd137184 chore(requirements): 添加tqdm 2023-03-28 20:15:44 +08:00
imgyh
262c10495c docs(changelog): 更新日志 2023-03-28 17:24:14 +08:00
imgyh
e900d1d2ab docs(readme): 更新文档 2023-03-28 17:22:12 +08:00
imgyh
a6c86629e6 test(test): 删除以前的测试命令 2023-03-28 17:21:34 +08:00
imgyh
ec559e2913 feat(tiktok): 增加配置文件, 手动传入自己的cookie
fix #16
2023-03-28 17:21:04 +08:00
imgyh
9fc37f1048 fix(tiktok): 重试失败后返回已经获取到的数据, 而不是抛异常 2023-03-28 17:19:41 +08:00
imgyh
6b6978df49 chore(requirements): 添加PyYAML 2023-03-28 17:16:37 +08:00
imgyh
c9ece0bf50 feat(tiktok): 支持电脑网页版url作为链接 2023-03-27 21:21:23 +08:00
imgyh
20350b8889 feat(tiktok): 进度条滚动显示 2023-03-27 14:19:37 +08:00
imgyh
794632d6c5 fix(tiktok): 增加文件下载失败重试机制 2023-03-27 12:55:30 +08:00
imgyh
38fc76826d feat(tiktok): 单个作品使用多线程下载 2023-03-27 10:42:45 +08:00
imgyh
0fb3739b47 feat(tiktok): 使用rich进度条 2023-03-27 10:42:07 +08:00
imgyh
ebb60c8ee7 chore(requirements): 使用rich替换tqdm 2023-03-27 10:41:32 +08:00
imgyh
def3dd77e7 docs(readme): 更新文档 2023-03-25 21:31:19 +08:00
imgyh
8ea8871cc8 feat(tiktok): 增加json数据是否保存的开关 2023-03-25 21:20:58 +08:00
imgyh
c5b9ec5faf fix(tiktok): 修复主页作品获取失败
fix #15
2023-03-25 21:00:34 +08:00
imgyh
f753c5c52e fix(tiktok): 修复获取单个合集id失败 2023-03-25 20:30:50 +08:00
imgyh
132ab70094 docs(changelog): 更新日志 2023-03-22 19:01:37 +08:00
imgyh
04b3279975 test(test): 更改失效的链接 2023-03-22 18:59:18 +08:00
imgyh
939a417654 fix(tiktok): 单个作品不使用多线程 2023-03-22 18:58:49 +08:00
imgyh
f8ec5e3745 fix(tiktok): 获取x-bogus错误后重试, 单个作品不使用多线程 2023-03-22 18:58:22 +08:00
imgyh
89d4c7cd42 feat(tiktok): 使用线程池代替手动创建线程 2023-03-22 16:46:35 +08:00
imgyh
f224557461 fix(utils): 优化获取x-bogus时出错的提示信息 2023-03-22 16:44:30 +08:00
imgyh
4c1a4ed950 fix(tiktok): 修改文件夹名的正则匹配逻辑
fix #11
2023-03-19 14:09:59 +08:00
imgyh
e194a2818b fix(tiktok): 修改命令行帮助提示信息 2023-03-19 14:07:58 +08:00
imgyh
eb570b183c docs(readme): 更新文档 2023-03-19 13:16:00 +08:00
imgyh
d9c224bd2d feat(web): 前端提示信息优化 2023-03-19 13:15:12 +08:00
imgyh
e4a0ebba4f feat(live): 直播解析支持APP端分享链接 2023-03-19 13:07:17 +08:00
imgyh
39f3920e54 docs(changelog): 更新日志 2023-03-16 16:57:35 +08:00
imgyh
14f51a62aa docs(readme): 更新文档 2023-03-16 16:55:43 +08:00
imgyh
83e46322ee feat(tiktok): 适配多线程下载 2023-03-16 16:55:15 +08:00
imgyh
af63aadcfc chore(requirements): 添加tqdm 2023-03-16 16:54:20 +08:00
imgyh
85942f2ffc fix(tiktok): 修复无法获取图集id
fix #10
2023-03-16 12:33:21 +08:00
imgyh
322f004e69 docs(readme): 修改说明文档 2023-03-11 20:45:26 +08:00
imgyh
723da7640e ci(pyinstaller): 增加TikTokWeb打包 2023-03-11 20:44:49 +08:00
imgyh
8c5863d44f fix(tiktok): 修复接口未返回数据时重复请求的死循环(10s后还是未返回就抛异常) 2023-03-11 20:25:09 +08:00
imgyh
3fcacbd14c refactor(tiktok): 优化提示信息 2023-03-07 21:10:38 +08:00
imgyh
e608e5431b docs(changelog): 更新日志 2023-03-05 22:05:30 +08:00
imgyh
1b85b19891 fix(web): 取消指定端口为必选项 2023-03-05 22:02:59 +08:00
imgyh
3c2ed3cd78 docs(changelog): 更新日志 2023-03-05 21:34:57 +08:00
imgyh
50beb45228 docs(readme): 更新README 2023-03-05 21:34:27 +08:00
imgyh
7c44b9426d feat(url): 添加速度更快的X-Bogus接口 2023-03-05 21:31:12 +08:00
imgyh
469fe2c521 feat(live): 增加直播解析方法的提示信息开启或关闭选项 2023-03-05 20:50:28 +08:00
imgyh
98a9f74d4b feat(web): 增加命令行指定web端口 2023-03-05 20:48:38 +08:00
imgyh
80825b2fc2 refactor(web): 优化前端细节 2023-03-05 20:27:08 +08:00
imgyh
ee4ec700b8 docs(changelog): 更新日志 2023-03-04 22:33:01 +08:00
imgyh
04ec0f1c40 fix(live): 修复无法播放http的直播地址 2023-03-04 22:30:57 +08:00
imgyh
42436948f7 docs(changelog): 更新日志 2023-03-04 22:06:40 +08:00
imgyh
44670186af feat(utils): 重新加入远程调用X-Bogus接口作为备用,防止本地没有JS环境 2023-03-04 22:06:07 +08:00
imgyh
fb82300a27 docs(readme): 更新README 2023-03-04 20:29:11 +08:00
imgyh
6f2908c116 docs(changelog): 更新日志 2023-03-04 20:23:17 +08:00
imgyh
90aa10515f feat(web): 增加web端直播解析 2023-03-04 20:06:59 +08:00
imgyh
f7fdcd141b feat(live): 适配web端的直播解析 2023-03-04 20:05:17 +08:00
imgyh
2183ebdb41 refactor(utils): 删除冗余代码 2023-03-03 17:49:03 +08:00
imgyh
5536380d53 ci(pyinstaller): 增加打包exe时加入js文件 2023-03-03 17:33:20 +08:00
B1gM8c
a4328cd53b
feat(tiktok): 支持本地生成X-Bogus (#3)
* 支持本地生成X-Bogus

由于使用第三方接口必然会产生网络延迟等问题,故支持本地生成X-Bogus

参考项目https://github.com/B1gM8c/tiktok
2023-03-03 17:27:36 +08:00
imgyh
8f9fe9f1e7 docs(changelog): 更新日志 2023-03-02 21:32:39 +08:00
imgyh
2eae2e182d test(test): 测试passport_csrf_token 2023-03-02 21:28:50 +08:00
imgyh
ebe3be43ef fix(tiktok): cookies增加passport_csrf_token 2023-03-02 21:28:10 +08:00
48 changed files with 4649 additions and 2533 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: Bug report
about: 报告出现的Bug
title: "[BUG]"
labels: bug
assignees: imgyh
---
**Bug 描述**
对bug的清晰而简洁的描述。
**Bug 复现**
复现这次行为的步骤:
1. 更改了 '...'
2. 点击了 '....'
3. '....'
**预期行为**
简单描述预期结果。
**截图**
如果适用,请添加屏幕截图以帮助解释您的问题。
**环境信息 (请填写以下信息):**
- 操作系统: [e.g. windows]
- 命令: [e.g. DouyinCommand.py, WebApi.py, DouyinCommand.exe, WebApi.exe]
- 版本 [e.g. v1.6.4]
**附文**
在此处添加有关该问题的其他文字。

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: 对该项目的建议
title: "[Feature]"
labels: enhancement
assignees: imgyh
---
**描述您想要的功能**
对你想要加入的功能的清晰而简洁的描述。
**描述你想要替换的功能或需要修改的解决方案**
对您考虑过的任何替代解决方案或功能的清晰简洁的描述。
**附文**
在此处添加有关功能请求的任何其他文字或屏幕截图。

22
.github/ISSUE_TEMPLATE/help_want.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Help Want
about: 想要得到帮助非Bug问题如程序不会用、部署相关等问题
title: "[Help]"
labels: help wanted
assignees: imgyh
---
**问题描述**
对您的遇到的问题清晰而简洁的描述。
**截图**
如果适用,请添加屏幕截图以帮助解释您的问题。
**环境信息 (请填写以下信息):**
- 操作系统: [e.g. windows]
- 命令: [e.g. DouyinCommand.py, WebApi.py, DouyinCommand.exe, WebApi.exe]
- 版本 [e.g. v1.6.4]
**附文**
在此处添加有关该问题的其他文字。

View File

@ -51,6 +51,7 @@ jobs:
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
# docker build arg, 注入 APP_NAME/APP_VERSION
build-args: |

View File

@ -28,7 +28,8 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
pyinstaller -i ./static/img/favicon.ico -F TikTokCommand.py
pyinstaller -i ./static/img/favicon.ico -F DouYinCommand.py
pyinstaller -i ./static/img/favicon.ico -F WebApi.py --add-data "templates;templates" --add-data "static;static"
- name: Create Release and Upload Release Asset
uses: softprops/action-gh-release@v1
@ -45,6 +46,8 @@ jobs:
# 如果指定了名称,将使用指定的名称;否则,将自动生成一个名称。
# 如果指定了正文,正文将被添加到自动生成的注释中。
files: | # 多个文件要加 |
dist/TikTokCommand.exe
dist/DouYinCommand.exe
dist/WebApi.exe
config.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 默认为${{ github.token }}

321
.gitignore vendored
View File

@ -1,160 +1,161 @@
# 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/
# 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/
*.db

View File

@ -1,3 +1,180 @@
# [](https://github.com/imgyh/tiktok/compare/v1.6.4...v) (2023-05-28)
### Bug Fixes
* **webapi:** 修复作品列表为空时不返回cursor ([1e69ee8](https://github.com/imgyh/tiktok/commit/1e69ee8a140b7761f3ebdebf1b1d9e31af79bd2c)), closes [#40](https://github.com/imgyh/tiktok/issues/40)
### Features
* **douyin:** 修改文件名命名风格 ([b38e210](https://github.com/imgyh/tiktok/commit/b38e2109271a07e408bbbad2454ebc99f48437fe))
# [](https://github.com/imgyh/tiktok/compare/v1.6.3...v) (2023-05-21)
### Features
* **tiktok:** 规范包目录结构 ([942bc35](https://github.com/imgyh/tiktok/commit/942bc359126a0ce9241b44576063bf42b161d02a))
* **tiktok:** 删除多余文件 ([2114bc9](https://github.com/imgyh/tiktok/commit/2114bc925edf8c050834f1ba2f3ddb4be22b6ed3))
# [](https://github.com/imgyh/tiktok/compare/v1.6.2...v) (2023-04-24)
### Bug Fixes
* **tiktok:** 优化视频链接获取 ([321d462](https://github.com/imgyh/tiktok/commit/321d4622a16a26cc8587e16c0e5b9f8601c16f99))
* **web:** 解决浏览器提示不安全问题 ([87b48ae](https://github.com/imgyh/tiktok/commit/87b48aec06bce777c427d03ab02590fd7089bb9b))
### Features
* **tiktok:** 增加数据库功能与增量更新功能 ([ebf6671](https://github.com/imgyh/tiktok/commit/ebf6671d336767595a01941562db397546ab2fe9)), closes [#24](https://github.com/imgyh/tiktok/issues/24)
# [](https://github.com/imgyh/tiktok/compare/v1.6.1...v) (2023-04-21)
### Bug Fixes
* **result:** 修复1080p清晰度链接 ([ada1851](https://github.com/imgyh/tiktok/commit/ada1851e869ae4a65a43fb07eff3c22baeced087))
* **tiktok:** 修复接口失效问题 ([9aaab2f](https://github.com/imgyh/tiktok/commit/9aaab2f691adff5f43c0f40899b1eb37cd4665d5)), closes [#33](https://github.com/imgyh/tiktok/issues/33)
### Features
* **web:** 前段静态文件使用国内cdn ([72aca75](https://github.com/imgyh/tiktok/commit/72aca75960b0f2fe4ef918682debec5c9af8cb59))
# [](https://github.com/imgyh/tiktok/compare/v1.6.0...v) (2023-04-16)
### Bug Fixes
* **command:** 修复mode命令解析问题 ([aebc144](https://github.com/imgyh/tiktok/commit/aebc14429ded01203b2d3b3385d7f1bdcbafae1a))
* **tiktok:** 增加请求重试机制 ([8f6c1ea](https://github.com/imgyh/tiktok/commit/8f6c1ea70dd4da63c772f45f0326047deb3daef7))
* **tiktok:** 更改多作品接口请求逻辑, 不再调用单个作品的方法 ([2ea2ac0](https://github.com/imgyh/tiktok/commit/2ea2ac07a844421d665684e3273c3b6b8e7064a6))
* **tiktok:** 缩短文件名长度,只使用数字字母汉字作为文件名 ([99c6292](https://github.com/imgyh/tiktok/commit/99c62922ca701de2e7eccb68b3c3e67b98d9fcef)), closes [#19](https://github.com/imgyh/tiktok/issues/19)
* **web:** 前端适配新接口 ([2f13bd5](https://github.com/imgyh/tiktok/commit/2f13bd5122b25e507cf55a58aea24396016414da))
### Features
* **command:** 未传入cookie则使用默认值 ([596a0fc](https://github.com/imgyh/tiktok/commit/596a0fc63308c2ea515c305161f115e6914f7504))
* **web:** 增加解析接口 ([918d6c9](https://github.com/imgyh/tiktok/commit/918d6c9ebaa47ce7259fc2f23efbd53b320095a9)), closes [#28](https://github.com/imgyh/tiktok/issues/28)
* **web:** 所有接口支持json与form两种格式, cookie不传使用默认值 ([d3d091d](https://github.com/imgyh/tiktok/commit/d3d091d9ddadf1f01588b7e273b40356ed09cfa2)), closes [#31](https://github.com/imgyh/tiktok/issues/31)
# [](https://github.com/imgyh/tiktok/compare/v1.5.5...v) (2023-03-28)
### Bug Fixes
* **tiktok:** 使用tqdm进度条 ([f55dd00](https://github.com/imgyh/tiktok/commit/f55dd004e9eea039fea04ed9e8e1325bca62c363))
* **tiktok:** 修复主页作品获取失败 ([c5b9ec5](https://github.com/imgyh/tiktok/commit/c5b9ec5faf90c4d00c5cc24b48a774404344be19)), closes [#15](https://github.com/imgyh/tiktok/issues/15)
* **tiktok:** 修复获取单个合集id失败 ([f753c5c](https://github.com/imgyh/tiktok/commit/f753c5c52e4fa54f04f05d555d61b22b544a2169))
* **tiktok:** 修复配置文件路径获取问题 ([2496ccd](https://github.com/imgyh/tiktok/commit/2496ccd90ca5232c61a2ed213f85235fa26354b3))
* **tiktok:** 修改文件名匹配正则表达式 ([8ce13c0](https://github.com/imgyh/tiktok/commit/8ce13c0d72379accbfd38ba083728df87719d471))
* **tiktok:** 增加文件下载失败重试机制 ([794632d](https://github.com/imgyh/tiktok/commit/794632d6c5dce68b9eade8094e74231aa90421f0))
* **tiktok:** 缩短文件名长度 ([01fbd74](https://github.com/imgyh/tiktok/commit/01fbd743f82552e52099637871246ddc21949fc2))
* **tiktok:** 重试失败后返回已经获取到的数据, 而不是抛异常 ([9fc37f1](https://github.com/imgyh/tiktok/commit/9fc37f1048fbd182e00eea89b71ec644f7d9df56))
### Features
* **tiktok:** 使用rich进度条 ([0fb3739](https://github.com/imgyh/tiktok/commit/0fb3739b4734910b6a0d35fdb0033921ef854adb))
* **tiktok:** 单个作品使用多线程下载 ([38fc768](https://github.com/imgyh/tiktok/commit/38fc76826d20257ec63bb7fcdea6eeca38a8aa6d))
* **tiktok:** 增加json数据是否保存的开关 ([8ea8871](https://github.com/imgyh/tiktok/commit/8ea8871cc88b1199bfcb6c2ff7aef16fd1f733c3))
* **tiktok:** 增加配置文件, 手动传入自己的cookie ([ec559e2](https://github.com/imgyh/tiktok/commit/ec559e2913c70836b97ea2634604ac6ca6734a60)), closes [#16](https://github.com/imgyh/tiktok/issues/16)
* **tiktok:** 支持电脑网页版url作为链接 ([c9ece0b](https://github.com/imgyh/tiktok/commit/c9ece0bf502c1a6a6e6b2e12c8ffcbce3303ce6a))
* **tiktok:** 进度条滚动显示 ([20350b8](https://github.com/imgyh/tiktok/commit/20350b8889343bb93ec60081e6369f96d868203b))
# [](https://github.com/imgyh/tiktok/compare/v1.5.4...v) (2023-03-22)
### Bug Fixes
* **tiktok:** 修改命令行帮助提示信息 ([e194a28](https://github.com/imgyh/tiktok/commit/e194a2818b64aaa9b2a5ab172302c87a4f4a5790))
* **tiktok:** 修改文件夹名的正则匹配逻辑 ([4c1a4ed](https://github.com/imgyh/tiktok/commit/4c1a4ed950476c0945bf7986254d75d108d3019f)), closes [#11](https://github.com/imgyh/tiktok/issues/11)
* **tiktok:** 单个作品不使用多线程 ([939a417](https://github.com/imgyh/tiktok/commit/939a417654183a2ac2766a25bdecc13575752e61))
* **tiktok:** 获取x-bogus错误后重试, 单个作品不使用多线程 ([f8ec5e3](https://github.com/imgyh/tiktok/commit/f8ec5e3745d587d08dcba6464a0f990913f55ce5))
* **utils:** 优化获取x-bogus时出错的提示信息 ([f224557](https://github.com/imgyh/tiktok/commit/f2245574611f125f69be9bbe9a2d376c0241bc87))
### Features
* **live:** 直播解析支持APP端分享链接 ([e4a0ebb](https://github.com/imgyh/tiktok/commit/e4a0ebba4f39cc55e8d070e12b6597ab5c3745d3))
* **tiktok:** 使用线程池代替手动创建线程 ([89d4c7c](https://github.com/imgyh/tiktok/commit/89d4c7cd4253cd3ac885b55774dff2853c0d6e4f))
* **web:** 前端提示信息优化 ([d9c224b](https://github.com/imgyh/tiktok/commit/d9c224bd2d5fd382fb22b37f5e68a85894bb9aef))
# [](https://github.com/imgyh/tiktok/compare/v1.5.3...v) (2023-03-16)
### Bug Fixes
* **tiktok:** 修复接口未返回数据时重复请求的死循环(10s后还是未返回就抛异常) ([8c5863d](https://github.com/imgyh/tiktok/commit/8c5863d44f4ae4a5242c8191f98bc0f3936e8e84))
* **tiktok:** 修复无法获取图集id ([85942f2](https://github.com/imgyh/tiktok/commit/85942f2ffce97d853dab96da87737d98f450347e)), closes [#10](https://github.com/imgyh/tiktok/issues/10)
### Features
* **tiktok:** 适配多线程下载 ([83e4632](https://github.com/imgyh/tiktok/commit/83e46322ee13bd14841332538fef22e27e2f0e59))
# [](https://github.com/imgyh/tiktok/compare/v1.5.2...v) (2023-03-05)
### Bug Fixes
* **web:** 取消指定端口为必选项 ([1b85b19](https://github.com/imgyh/tiktok/commit/1b85b1989119cb35888e4e2d4f4018ed276f68d2))
### Features
* **live:** 增加直播解析方法的提示信息开启或关闭选项 ([469fe2c](https://github.com/imgyh/tiktok/commit/469fe2c5217ff22e42c1523d089a119415266c23))
* **url:** 添加速度更快的X-Bogus接口 ([7c44b94](https://github.com/imgyh/tiktok/commit/7c44b9426dc30f3be97d6e5824bf628c47276a87))
* **web:** 增加命令行指定web端口 ([98a9f74](https://github.com/imgyh/tiktok/commit/98a9f74d4b0b319ea1aeb3883c105c0b805105f1))
# [](https://github.com/imgyh/tiktok/compare/v1.5.1...v) (2023-03-04)
### Bug Fixes
* **live:** 修复无法播放http的直播地址 ([04ec0f1](https://github.com/imgyh/tiktok/commit/04ec0f1c400adb5bfacf74dca0114ec9d625e3cc))
### Features
* **live:** 适配web端的直播解析 ([f7fdcd1](https://github.com/imgyh/tiktok/commit/f7fdcd141b3a9877f5fd888383bfa48875d023bf))
* **tiktok:** 支持本地生成X-Bogus ([#3](https://github.com/imgyh/tiktok/issues/3)) ([a4328cd](https://github.com/imgyh/tiktok/commit/a4328cd53bd8a0342cf053050a8066130e008cde))
* **utils:** 重新加入远程调用X-Bogus接口作为备用,防止本地没有JS环境 ([4467018](https://github.com/imgyh/tiktok/commit/44670186afdcb1314194b0c00f39d1baa9681985))
* **web:** 增加web端直播解析 ([90aa105](https://github.com/imgyh/tiktok/commit/90aa10515f7bc90ed35c9484e2993083a533d6cc))
# [](https://github.com/imgyh/tiktok/compare/v1.5.0...v) (2023-03-02)
### Bug Fixes
* **tiktok:** cookies增加passport_csrf_token ([ebe3be4](https://github.com/imgyh/tiktok/commit/ebe3be43ef676c39cd1cd63cc606df1a9e5b1995))
# (2023-03-02)

View File

@ -1,13 +1,16 @@
# 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"]
# 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 sed -i s/deb.debian.org/mirrors.aliyun.com/g /etc/apt/sources.list
RUN pip3 install -r requirements_docker.txt
ENV TZ=Asia/Shanghai
CMD ["python3", "WebApi.py"]

376
DouYinCommand.py Normal file
View File

@ -0,0 +1,376 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : DouYinCommand.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 16:01
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import argparse
import os
import sys
import json
import yaml
import time
from apiproxy.douyin.douyin import Douyin
from apiproxy.douyin.download import Download
from apiproxy.douyin import douyin_headers
from apiproxy.common import utils
configModel = {
"link": [],
"path": os.getcwd(),
"music": True,
"cover": True,
"avatar": True,
"json": True,
"folderstyle": True,
"mode": ["post"],
"number": {
"post": 0,
"like": 0,
"allmix": 0,
"mix": 0,
"music": 0,
},
'database': True,
"increase": {
"post": False,
"like": False,
"allmix": False,
"mix": False,
"music": False,
},
"thread": 5,
"cookie": None
}
def argument():
parser = argparse.ArgumentParser(description='抖音批量下载工具 使用帮助')
parser.add_argument("--cmd", "-C", help="使用命令行(True)或者配置文件(False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--link", "-l",
help="作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址, 可以设置多个链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)",
type=str, required=False, default=[], action="append")
parser.add_argument("--path", "-p", help="下载保存位置, 默认当前文件位置",
type=str, required=False, default=os.getcwd())
parser.add_argument("--music", "-m", help="是否下载视频中的音乐(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 默认为True, 当下载视频时有效",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--avatar", "-a", help="是否下载作者的头像(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--json", "-j", help="是否保存获取到的数据(True/False), 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--folderstyle", "-fs", help="文件保存风格, 默认为True",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式",
type=str, required=False, default=[], action="append")
parser.add_argument("--postnumber", help="主页下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--likenumber", help="主页下喜欢下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--allmixnumber", help="主页下合集下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--mixnumber", help="单个合集下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--musicnumber", help="音乐(原声)下作品下载个数设置, 默认为0 全部下载",
type=int, required=False, default=0)
parser.add_argument("--database", "-d", help="是否使用数据库, 默认为True 使用数据库; 如果不使用数据库, 增量更新不可用",
type=utils.str2bool, required=False, default=True)
parser.add_argument("--postincrease", help="是否开启主页作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--likeincrease", help="是否开启主页喜欢增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--allmixincrease", help="是否开启主页合集增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--mixincrease", help="是否开启单个合集下作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--musicincrease", help="是否开启音乐(原声)下作品增量下载(True/False), 默认为False",
type=utils.str2bool, required=False, default=False)
parser.add_argument("--thread", "-t",
help="设置线程数, 默认5个线程",
type=int, required=False, default=5)
parser.add_argument("--cookie", help="设置cookie, 格式: \"name1=value1; name2=value2;\" 注意要加冒号",
type=str, required=False, default='')
args = parser.parse_args()
if args.thread <= 0:
args.thread = 5
return args
def yamlConfig():
curPath = os.path.dirname(os.path.realpath(sys.argv[0]))
yamlPath = os.path.join(curPath, "config.yml")
f = open(yamlPath, 'r', encoding='utf-8')
cfg = f.read()
configDict = yaml.load(stream=cfg, Loader=yaml.FullLoader)
try:
if configDict["link"] != None:
configModel["link"] = configDict["link"]
except Exception as e:
print("[ 警告 ]:link未设置, 程序退出...\r\n")
try:
if configDict["path"] != None:
configModel["path"] = configDict["path"]
except Exception as e:
print("[ 警告 ]:path未设置, 使用当前路径...\r\n")
try:
if configDict["music"] != None:
configModel["music"] = configDict["music"]
except Exception as e:
print("[ 警告 ]:music未设置, 使用默认值True...\r\n")
try:
if configDict["cover"] != None:
configModel["cover"] = configDict["cover"]
except Exception as e:
print("[ 警告 ]:cover未设置, 使用默认值True...\r\n")
try:
if configDict["avatar"] != None:
configModel["avatar"] = configDict["avatar"]
except Exception as e:
print("[ 警告 ]:avatar未设置, 使用默认值True...\r\n")
try:
if configDict["json"] != None:
configModel["json"] = configDict["json"]
except Exception as e:
print("[ 警告 ]:json未设置, 使用默认值True...\r\n")
try:
if configDict["folderstyle"] != None:
configModel["folderstyle"] = configDict["folderstyle"]
except Exception as e:
print("[ 警告 ]:folderstyle未设置, 使用默认值True...\r\n")
try:
if configDict["mode"] != None:
configModel["mode"] = configDict["mode"]
except Exception as e:
print("[ 警告 ]:mode未设置, 使用默认值post...\r\n")
try:
if configDict["number"]["post"] != None:
configModel["number"]["post"] = configDict["number"]["post"]
except Exception as e:
print("[ 警告 ]:post number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["like"] != None:
configModel["number"]["like"] = configDict["number"]["like"]
except Exception as e:
print("[ 警告 ]:like number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["allmix"] != None:
configModel["number"]["allmix"] = configDict["number"]["allmix"]
except Exception as e:
print("[ 警告 ]:allmix number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["mix"] != None:
configModel["number"]["mix"] = configDict["number"]["mix"]
except Exception as e:
print("[ 警告 ]:mix number未设置, 使用默认值0...\r\n")
try:
if configDict["number"]["music"] != None:
configModel["number"]["music"] = configDict["number"]["music"]
except Exception as e:
print("[ 警告 ]:music number未设置, 使用默认值0...\r\n")
try:
if configDict["database"] != None:
configModel["database"] = configDict["database"]
except Exception as e:
print("[ 警告 ]:database未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["post"] != None:
configModel["increase"]["post"] = configDict["increase"]["post"]
except Exception as e:
print("[ 警告 ]:post 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["like"] != None:
configModel["increase"]["like"] = configDict["increase"]["like"]
except Exception as e:
print("[ 警告 ]:like 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["allmix"] != None:
configModel["increase"]["allmix"] = configDict["increase"]["allmix"]
except Exception as e:
print("[ 警告 ]:allmix 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["mix"] != None:
configModel["increase"]["mix"] = configDict["increase"]["mix"]
except Exception as e:
print("[ 警告 ]:mix 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["increase"]["music"] != None:
configModel["increase"]["music"] = configDict["increase"]["music"]
except Exception as e:
print("[ 警告 ]:music 增量更新未设置, 使用默认值False...\r\n")
try:
if configDict["thread"] != None:
configModel["thread"] = configDict["thread"]
except Exception as e:
print("[ 警告 ]:thread未设置, 使用默认值5...\r\n")
try:
if configDict["cookies"] != None:
cookiekey = configDict["cookies"].keys()
cookieStr = ""
for i in cookiekey:
cookieStr = cookieStr + i + "=" + configDict["cookies"][i] + "; "
configModel["cookie"] = cookieStr
except Exception as e:
pass
try:
if configDict["cookie"] != None:
configModel["cookie"] = configDict["cookie"]
except Exception as e:
pass
def main():
start = time.time() # 开始时间
args = argument()
if args.cmd:
configModel["link"] = args.link
configModel["path"] = args.path
configModel["music"] = args.music
configModel["cover"] = args.cover
configModel["avatar"] = args.avatar
configModel["json"] = args.json
configModel["folderstyle"] = args.folderstyle
if args.mode == None or args.mode == []:
args.mode = []
args.mode.append("post")
configModel["mode"] = list(set(args.mode))
configModel["number"]["post"] = args.postnumber
configModel["number"]["like"] = args.likenumber
configModel["number"]["allmix"] = args.allmixnumber
configModel["number"]["mix"] = args.mixnumber
configModel["number"]["music"] = args.musicnumber
configModel["database"] = args.database
configModel["increase"]["post"] = args.postincrease
configModel["increase"]["like"] = args.likeincrease
configModel["increase"]["allmix"] = args.allmixincrease
configModel["increase"]["mix"] = args.mixincrease
configModel["increase"]["music"] = args.musicincrease
configModel["thread"] = args.thread
configModel["cookie"] = args.cookie
else:
yamlConfig()
if configModel["link"] == []:
return
if configModel["cookie"] is not None and configModel["cookie"] != "":
douyin_headers["Cookie"] = configModel["cookie"]
configModel["path"] = os.path.abspath(configModel["path"])
print("[ 提示 ]:数据保存路径 " + configModel["path"])
if not os.path.exists(configModel["path"]):
os.mkdir(configModel["path"])
dy = Douyin(database=configModel["database"])
dl = Download(thread=configModel["thread"], music=configModel["music"], cover=configModel["cover"],
avatar=configModel["avatar"], resjson=configModel["json"],
folderstyle=configModel["folderstyle"])
for link in configModel["link"]:
print("--------------------------------------------------------------------------------")
print("[ 提示 ]:正在请求的链接: " + link + "\r\n")
url = dy.getShareLink(link)
key_type, key = dy.getKey(url)
if key_type == "user":
print("[ 提示 ]:正在请求用户主页下作品\r\n")
data = dy.getUserDetailInfo(sec_uid=key)
nickname = ""
if data is not None and data != {}:
nickname = utils.replaceStr(data['user']['nickname'])
userPath = os.path.join(configModel["path"], "user_" + nickname + "_" + key)
if not os.path.exists(userPath):
os.mkdir(userPath)
for mode in configModel["mode"]:
print("--------------------------------------------------------------------------------")
print("[ 提示 ]:正在请求用户主页模式: " + mode + "\r\n")
if mode == 'post' or mode == 'like':
datalist = dy.getUserInfo(key, mode, 35, configModel["number"][mode], configModel["increase"][mode])
if datalist is not None and datalist != []:
modePath = os.path.join(userPath, mode)
if not os.path.exists(modePath):
os.mkdir(modePath)
dl.userDownload(awemeList=datalist, savePath=modePath)
elif mode == 'mix':
mixIdNameDict = dy.getUserAllMixInfo(key, 35, configModel["number"]["allmix"])
if mixIdNameDict is not None and mixIdNameDict != {}:
for mix_id in mixIdNameDict:
print(f'[ 提示 ]:正在下载合集 [{mixIdNameDict[mix_id]}] 中的作品\r\n')
mix_file_name = utils.replaceStr(mixIdNameDict[mix_id])
datalist = dy.getMixInfo(mix_id, 35, 0, configModel["increase"]["allmix"], key)
if datalist is not None and datalist != []:
modePath = os.path.join(userPath, mode)
if not os.path.exists(modePath):
os.mkdir(modePath)
dl.userDownload(awemeList=datalist, savePath=os.path.join(modePath, mix_file_name))
print(f'[ 提示 ]:合集 [{mixIdNameDict[mix_id]}] 中的作品下载完成\r\n')
elif key_type == "mix":
print("[ 提示 ]:正在请求单个合集下作品\r\n")
datalist = dy.getMixInfo(key, 35, configModel["number"]["mix"], configModel["increase"]["mix"], "")
if datalist is not None and datalist != []:
mixname = utils.replaceStr(datalist[0]["mix_info"]["mix_name"])
mixPath = os.path.join(configModel["path"], "mix_" + mixname + "_" + key)
if not os.path.exists(mixPath):
os.mkdir(mixPath)
dl.userDownload(awemeList=datalist, savePath=mixPath)
elif key_type == "music":
print("[ 提示 ]:正在请求音乐(原声)下作品\r\n")
datalist = dy.getMusicInfo(key, 35, configModel["number"]["music"], configModel["increase"]["music"])
if datalist is not None and datalist != []:
musicname = utils.replaceStr(datalist[0]["music"]["title"])
musicPath = os.path.join(configModel["path"], "music_" + musicname + "_" + key)
if not os.path.exists(musicPath):
os.mkdir(musicPath)
dl.userDownload(awemeList=datalist, savePath=musicPath)
elif key_type == "aweme":
print("[ 提示 ]:正在请求单个作品\r\n")
datanew, dataraw = dy.getAwemeInfo(key)
if datanew is not None and datanew != {}:
datalist = []
datalist.append(datanew)
awemePath = os.path.join(configModel["path"], "aweme")
if not os.path.exists(awemePath):
os.mkdir(awemePath)
dl.userDownload(awemeList=datalist, savePath=awemePath)
elif key_type == "live":
print("[ 提示 ]:正在进行直播解析\r\n")
live_json = dy.getLiveInfo(key)
if configModel["json"]:
livePath = os.path.join(configModel["path"], "live")
if not os.path.exists(livePath):
os.mkdir(livePath)
live_file_name = utils.replaceStr(key + live_json["nickname"])
# 保存获取到json
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(livePath, live_file_name + ".json"), "w", encoding='utf-8') as f:
f.write(json.dumps(live_json, ensure_ascii=False, indent=2))
f.close()
end = time.time() # 结束时间
print('\n' + '[下载完成]:总耗时: %d分钟%d\n' % (int((end - start) / 60), ((end - start) % 60))) # 输出下载用时时间
if __name__ == "__main__":
main()

View File

@ -83,6 +83,7 @@ class TikTok(object):
userVideoUrls.append(videoRealUrl)
return userVideoUrls
tk = TikTok()
# tk.oneVideoInfo()
tk.userVideoInfo()

433
README.md
View File

@ -8,42 +8,76 @@
开源地址https://github.com/imgyh/tiktok
博客文档https://www.imgyh.com/archives/41.html
抖音去水印工具Web demohttps://dy.gyh.im/
**联系方式:**
> [TG](https://t.me/gyh9527)
> [TG群组](https://t.me/GYHgroup)
> [Email](mailto:admin@imgyh.com)
> [Blog](https://www.imgyh.com)
## 抖音去水印工具 Feature
* 通过作品分享链接获取去水印作品、音乐、封面图、头像
* 获取点赞数、评论数、收藏数、分享数、作品描述等信息
* 支持直播解析
* 基于Flask实现 Web 交互界面
* 提供相关接口,支持单个作品、直播、主页喜欢、主页作品、主页合集、合集、音乐(原声)通过接口获取
![tiktokweb](img/tiktokweb.jpg)
![tiktokweb video](img/tiktokwebvideo.jpg)
![tiktokweb preview video](img/tiktokwebpreviewvideo.jpg)
![tiktokweb image](img/tiktokwebimage.jpg)
![tiktokweb preview image](img/tiktokwebpreviewimage.jpg)
![WebApi](img/WebApi.jpg)
![WebApi video](img/WebApivideo.jpg)
![WebApi preview video](img/WebApipreviewvideo.jpg)
![WebApi image](img/WebApiimage.jpg)
![WebApi preview image](img/WebApipreviewimage.jpg)
## 抖音批量下载工具 Feature
* 支持个人主页链接、作品分享链接、抖音直播Web链接、合集链接、音乐集合链接
* 支持单个作品下载、主页作品下载、主页喜欢下载、单个合集下载、主页所有合集下载、音乐集合下载
* 支持个人主页链接、作品分享链接、抖音直播Web链接、合集链接、音乐(原声)集合链接
* 支持单个作品下载、主页作品下载、主页喜欢下载、直播解析、单个合集下载、主页所有合集下载、音乐(原声)集合下载
* 下载视频、视频封面、音乐、头像
* 去水印下载
* 自动跳过已下载
* 支持指定下载作品数量
* 多线程下载
* 支持多链接下载
* 增量更新与数据持久化到数据库, 保存每条作品信息到数据库, 并根据数据库是否存在来增量请求下载
![](img/tiktokcommand1.jpg)
![](img/tiktokcommand2.jpg)
![tiktokcommandl ive](img/tiktokcommandlive.jpg)
![tiktokcommand download](img/tiktokcommanddownload.jpg)
![tiktokcommand download detail](img/tiktokcommanddownloaddetail.jpg)
![DouYinCommand1](img/DouYinCommand1.jpg)
![DouYinCommand2](img/DouYinCommand2.jpg)
![DouYinCommandl ive](img/DouYinCommandlive.jpg)
![DouYinCommand download](img/DouYinCommanddownload.jpg)
![DouYinCommand download detail](img/DouYinCommanddownloaddetail.jpg)
# 使用方法
- 支持的地址格式, 形如
```
抖音app分享链接:
1. 作品(视频或图集)、直播、合集、音乐集合、个人主页 https://v.douyin.com/BugmVVD/
抖音网页版浏览器URL:
2. 单个视频 https://www.douyin.com/video/6915675899241450760
3. 单个图集 https://www.douyin.com/note/7014363562642623777
4. 用户主页 https://www.douyin.com/user/MS4wLjABAAAA06y3Ctu8QmuefqvUSU7vr0c_ZQnCqB0eaglgkelLTek
5. 单个合集 https://www.douyin.com/collection/7208829743762769975
6. 音乐(原声)下的视频 https://www.douyin.com/music/7149936801028131598
7. 直播 https://live.douyin.com/759547612580
```
## 抖音去水印工具
使用抖音去水印工具有三种方式
### 使用方式
使用抖音去水印工具有4种方式
1. (推荐)直接使用我搭建的抖音去水印工具https://dy.gyh.im/
2. 使用docker运行
```
@ -55,14 +89,33 @@ docker run -d -p 5000:5000 --name tiktok --restart=always imgyh/tiktokweb
```
cd /path/to/tiktok
python -m pip install -r requirements.txt
python TikTokWeb.py
python WebApi.py
```
4. windows用户也可以下载 Releases 中的 [WebApi.exe](https://github.com/imgyh/tiktok/releases) 文件双击运行
5. 指定端口运行
```
# 指定端口运行
python WebApi.py -p 5001
.\WebApi.exe -p 5001
```
访问: http://localhost:5000
## 抖音批量下载工具
windows用户下载 Releases 中的 [TikTokCommand.exe](https://github.com/imgyh/tiktok/releases) 文件在cmd中运行
批量下载有两种方式运行, 配置文件和命令行
默认使用配置文件方式
> !!!!!! 请先获取cookie再使用
### 安装依赖
windows用户下载 Releases 中的 [DouYinCommand.exe](https://github.com/imgyh/tiktok/releases) 文件运行
windows用户本地有`python3.9`环境, 也可按照linux与mac用户的方式运行
linux与mac用户下载本项目, 在本地`python3.9`环境中运行, 首先需要安装依赖, 安装命令
@ -72,165 +125,409 @@ cd /path/to/tiktok
python -m pip install -r requirements.txt
```
运行示例:
### 使用Docker
> !!!!!! 请先获取cookie再使用
请映射以下两个目录(三个位置需要修改), 根据实际情况修改目录地址
`/path/to/tiktok` 源代码目录
`/path/to/downloads` 下载位置
```
docker run -d -p 5000:5000 --name tiktok --restart=always -v /path/to/tiktok:/app -v /path/to/downloads:/path/to/downloads imgyh/tiktokweb
```
将所有用到 `python DouYinCommand.py` 替换成 `docker exec -it tiktok python3 DouYinCommand.py`
### 配置文件方式
> !!!!!! 请自己登录网页版抖音后F12获取cookie
> !!!!!! 请仔细阅读配置示例[config.yml](./config.yml)中的描述
配置文件名必须叫 `config.yml`, 并将其放在DouYinCommand.py或者DouYinCommand.exe同一个目录下
直接运行DouYinCommand.py或者DouYinCommand.exe, 无需在命令中加入任何参数, 所有参数都从配置文件中读取
基本配置示例[config.yml](./config.yml)
### 命令行方式
> 以下为命令行的一些信息, 每个选项的详细说明参见配置示例[config.yml](./config.yml)中的描述
- 获取帮助信息
```
windows用户:
.\TikTokCommand.exe -h
.\DouYinCommand.exe -h
linux与mac用户:
python TikTokCommand.py -h
python DouYinCommand.py -h
```
- 参数介绍
```
-h, --help 展示帮助页
--link LINK, -l LINK 1.作品(视频或图集)、合集、音乐集合、个人主页抖音分享链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/)
2.解析直播网页版网址(https://live.douyin.com/802939216127)
--path PATH, -p PATH 下载保存位置
--music MUSIC, -m MUSIC 是否下载视频中的音乐(True/False), 默认为True
--cover COVER, -c COVER 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效
--avatar AVATAR, -a AVATAR 是否下载作者的头像(True/False), 默认为True
--mode MODE, -M MODE link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post
--number NUMBER, -n NUMBER 1.当下载单个合集、音乐集合、主页作品(post模式)和喜欢(like模式)时, 可设置下载前n个作品, 默认为0全部下载
2.当下载主页下所有合集(mix模式)时, 设置下载前n个合集下所有作品, 默认为0全部下载
-h, --help 展示帮助信息
--cmd CMD, -C CMD 使用命令行(True)或者配置文件(False), 默认为False
--link LINK, -l LINK 作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址, 可以设置多个链接
(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)
--path PATH, -p PATH 下载保存位置, 默认当前文件位置
--music MUSIC, -m MUSIC 是否下载视频中的音乐(True/False), 默认为True
--cover COVER, -c COVER 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效
--avatar AVATAR, -a AVATAR 是否下载作者的头像(True/False), 默认为True
--json JSON, -j JSON 是否保存获取到的数据(True/False), 默认为True
--folderstyle FOLDERSTYLE, -fs FOLDERSTYLE 文件保存风格, 默认为True
--mode MODE, -M MODE link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式
--postnumber POSTNUMBER 主页下作品下载个数设置, 默认为0 全部下载
--likenumber LIKENUMBER 主页下喜欢下载个数设置, 默认为0 全部下载
--allmixnumber ALLMIXNUMBER 主页下合集下载个数设置, 默认为0 全部下载
--mixnumber MIXNUMBER 单个合集下作品下载个数设置, 默认为0 全部下载
--musicnumber MUSICNUMBER 音乐(原声)下作品下载个数设置, 默认为0 全部下载
--database DATABASE, -d DATABASE 是否使用数据库, 默认为True 使用数据库; 如果不使用数据库, 增量更新不可用
--postincrease POSTINCREASE 是否开启主页作品增量下载(True/False), 默认为False
--likeincrease LIKEINCREASE 是否开启主页喜欢增量下载(True/False), 默认为False
--allmixincrease ALLMIXINCREASE 是否开启主页合集增量下载(True/False), 默认为False
--mixincrease MIXINCREASE 是否开启单个合集下作品增量下载(True/False), 默认为False
--musicincrease MUSICINCREASE 是否开启音乐(原声)下作品增量下载(True/False), 默认为False
--thread THREAD, -t THREAD 设置线程数, 默认5个线程
--cookie COOKIE 设置cookie, 格式: "name1=value1; name2=value2;" 注意要加冒号
```
- 多链接多模式混合下载, 可以传入多个链接和多个模式(post、like、mix)
```
windows用户:
.\DouYinCommand.exe -C True `
-l https://live.douyin.com/759547612580 `
-l https://v.douyin.com/BugmVVD/ `
-l https://v.douyin.com/BugrFTN/ `
-l https://v.douyin.com/B72pdU5/ `
-l https://v.douyin.com/B72QgDw/ `
-l https://v.douyin.com/AJp8D3f/ `
-l https://v.douyin.com/B38oovu/ `
-l https://v.douyin.com/S6YMNXs/ `
-p C:\project\test `
-M post `
-M like `
-M mix `
--postnumber 5 `
--likenumber 5 `
--allmixnumber 1 `
--mixnumber 5 `
--musicnumber 5 `
--cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True \
-l https://live.douyin.com/759547612580 \
-l https://v.douyin.com/BugmVVD/ \
-l https://v.douyin.com/BugrFTN/ \
-l https://v.douyin.com/B72pdU5/ \
-l https://v.douyin.com/B72QgDw/ \
-l https://v.douyin.com/AJp8D3f/ \
-l https://v.douyin.com/B38oovu/ \
-l https://v.douyin.com/S6YMNXs/ \
-p /path/to/downdir \
-M post \
-M like \
-M mix \
--postnumber 5 \
--likenumber 5 \
--allmixnumber 1 \
--mixnumber 5 \
--musicnumber 5 \
--cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载单个作品
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvMpuN/ -p C:\project\test
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvMpuN/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvMpuN/ -p /path/to/downdir
python DouYinCommand.py -C True -l https://v.douyin.com/kcvMpuN/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部作品
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个作品
- 增量更新主页下作品(postincrease 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -n 30
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --postincrease True --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -n 30
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --postincrease True --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部喜欢
- 关闭数据库, 增量更新不可用(database 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --database False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --database False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个喜欢
- 所有视频在一个文件夹下(folderstyle 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like -n 30
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --folderstyle False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like -n 30
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --folderstyle False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个作品(postnumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test --postnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir --postnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部喜欢(-M like 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个喜欢(-M like --likenumber 选项)
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -M like --likenumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -M like --likenumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载单个合集全部作品
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/B3J63Le/ -p C:\project\test
.\DouYinCommand.exe -C True -l https://v.douyin.com/B3J63Le/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir
python DouYinCommand.py -C True -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载单个合集前n个作品
- 下载单个合集前n个作品(--mixnumber 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/B3J63Le/ -p C:\project\test -n 30
.\DouYinCommand.exe -C True -l https://v.douyin.com/B3J63Le/ -p C:\project\test --mixnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir -n 30
python DouYinCommand.py -C True -l https://v.douyin.com/B3J63Le/ -p /path/to/downdir --mixnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页全部合集下所有作品
- 下载主页全部合集下所有作品(-M mix 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix
.\DouYinCommand.exe -C True -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix
python DouYinCommand.py -C True -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载主页前n个合集下所有作品
- 下载主页前n个合集下所有作品(-M mix --allmixnumber 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix -n 2
.\DouYinCommand.exe -C True -l https://v.douyin.com/B38oovu/ -p C:\project\test -M mix --allmixnumber 2 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix -n 2
python DouYinCommand.py -C True -l https://v.douyin.com/B38oovu/ -p /path/to/downdir -M mix --allmixnumber 2 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载音乐集合下所有作品
- 下载音乐(原声)集合下所有作品
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/S6YMNXs/ -p C:\project\test
.\DouYinCommand.exe -C True -l https://v.douyin.com/S6YMNXs/ -p C:\project\test --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir
python DouYinCommand.py -C True -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 下载音乐集合下前n个作品
- 下载音乐(原声)集合下前n个作品(--musicnumber 选项)
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/S6YMNXs/ -p C:\project\test -n 30
.\DouYinCommand.exe -C True -l https://v.douyin.com/S6YMNXs/ -p C:\project\test --musicnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir -n 30
python DouYinCommand.py -C True -l https://v.douyin.com/S6YMNXs/ -p /path/to/downdir --musicnumber 30 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 关闭头像下载, cover, music 也是一样的设置对应选项为 False
- 关闭头像下载, cover, music json数据也是一样的设置对应选项为 False
```
windows用户:
.\TikTokCommand.exe -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -a False
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -a False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -a False
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -a False --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 多线程设置, 默认5个线程, 可以自己调节线程数
```
windows用户:
.\DouYinCommand.exe -C True -l https://v.douyin.com/kcvSCe9/ -p C:\project\test -t 8 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python DouYinCommand.py -C True -l https://v.douyin.com/kcvSCe9/ -p /path/to/downdir -t 8 --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
- 直播推流地址解析
```
windows用户:
.\TikTokCommand.exe -l https://live.douyin.com/802939216127 -p /path/to/downdir
.\DouYinCommand.exe -C True -l https://live.douyin.com/802939216127 -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
或者
.\DouYinCommand.exe -C True -l https://v.douyin.com/SnXMoh2/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
linux与mac用户:
python TikTokCommand.py -l https://live.douyin.com/802939216127 -p /path/to/downdir
python DouYinCommand.py -C True -l https://live.douyin.com/802939216127 -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
或者
python DouYinCommand.py -C True -l https://v.douyin.com/SnXMoh2/ -p /path/to/downdir --cookie "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"
```
# Web版接口
> !!!!!! 请传入cookie再使用
1. 单个作品、图集接口
```
接口地址: 127.0.0.1:5000/douyin/aweme
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/kcvMpuN/",
"cookie":"xxxx"
}
```
2. 直播解析接口
```
接口地址: 127.0.0.1:5000/douyin/live
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/DdWaSBd/",
"cookie":"xxxx"
}
```
3. 主页作品
```
接口地址: 127.0.0.1:5000/douyin/user/post
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/B72pdU5/",
"cursor":0,
"cookie":"xxxx"
}
```
4. 主页喜欢
```
接口地址: 127.0.0.1:5000/douyin/user/like
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/AoWVvYH/",
"cursor":0,
"cookie":"xxxx"
}
```
5. 主页合集
```
接口地址: 127.0.0.1:5000/douyin/user/mix
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/B38oovu/",
"cursor":0,
"cookie":"xxxx"
}
```
6. 单个合集
```
接口地址: 127.0.0.1:5000/douyin/mix
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://www.douyin.com/collection/7217644759668492345", // https://v.douyin.com 这种类型也可以
"cursor":0,
"cookie":"xxxx"
}
```
7. 音乐(原声)
```
接口地址: 127.0.0.1:5000/douyin/music
请求方式: POST
请求参数, JSON或form表单:
{
"share_link":"https://v.douyin.com/S6YMNXs/",
"cursor":0,
"cookie":"xxxx"
}
```
# ToDo
- [x] 单个合集下载
- [x] 主页所有合集下载
- [x] 获取分享的音乐链接下的所有作品
- [x] 获取分享的音乐(原声)链接下的所有作品
- [x] 指定下载作品数量
- [ ] 获取热搜榜数据
- [ ] 多主页链接批量下载
- [ ] 多线程下载
- [ ] 保存数据至数据库
- [ ] 制作成接口
- [x] 多链接批量下载
- [x] 多线程下载
- [x] 保存数据至数据库
- [x] 制作成接口
- [ ] 获取收藏与观看历史
- [ ] 直播间数据
# 鸣谢
本项目部分思路来自[TikTokDownload](https://github.com/Johnserf-Seed/TikTokDownload)
# 赞赏
## 支付宝
![alipay](./img/alipay.jpg)
## 微信
![wechat](./img/wechat.jpg)
# 申明
本项目只作为学习用途, 切勿他用

593
TikTok.py
View File

@ -1,593 +0,0 @@
#!/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 : 2023/02/11 修改接口
-------------------------------------------------
'''
import re
import requests
import json
import time
import os
import copy
from TikTokUtils import Utils
from TikTokUrls import Urls
from TikTokResult import Result
class TikTok(object):
def __init__(self):
self.urls = Urls()
self.utils = Utils()
self.result = Result()
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',
'referer': 'https://www.douyin.com/',
'Cookie': f"msToken={self.utils.generate_random_str(107)}; ttwid={self.utils.getttwid()}; odin_tt=324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69;"
}
# 从分享链接中提取网址
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
# 合集
# https://www.douyin.com/collection/7093490319085307918
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 "/collection/" in urlstr:
# 获取作品 aweme_id
key = re.findall('collection/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/music/" in urlstr:
# 获取作品 aweme_id
key = re.findall('music/(\d+)?', urlstr)[0]
key_type = "music"
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
return key_type, key
# 传入 aweme_id
# 返回 数据 字典
def getAwemeInfo(self, aweme_id):
print('[ 提示 ]:正在请求的作品 id = %s\r' % aweme_id)
if aweme_id is None:
return None
# 单作品接口返回 'aweme_detail'
# 主页作品接口返回 'aweme_list'->['aweme_detail']
jx_url = self.urls.POST_DETAIL + self.utils.getXbogus(
url=f'aweme_id={aweme_id}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333')
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
raw = requests.get(url=jx_url, headers=self.headers).text
datadict = json.loads(raw)
if datadict is not None and datadict['aweme_detail'] is not None and datadict["status_code"] == 0:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
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.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
# mode : post | like 模式选择 like为用户点赞 post为用户发布
def getUserInfo(self, sec_uid, mode="post", count=35, number=0):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
max_cursor = 0
awemeList = []
print("[ 提示 ]:正在获取所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [主页] 进行第 " + str(times) + " 次请求...\r")
if mode == "post":
url = self.urls.USER_POST + self.utils.getXbogus(
url=f'device_platform=webapp&aid=6383&os_version=10&version_name=17.4.0&sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + self.utils.getXbogus(
url=f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333')
else:
print("[ 错误 ]:模式选择错误, 仅支持post、like、mix, 请检查后重新运行!\r")
return None
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
res = requests.get(url=url, headers=self.headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
print('[ 提示 ]:开始对 ' + str(len(datadict["aweme_list"])) + ' 条数据请求作品详情\r\n')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
for aweme in datadict["aweme_list"]:
# 获取 aweme_id
aweme_id = aweme["aweme_id"]
# 深拷贝 dict 不然list里面全是同样的数据
datanew, dataraw = self.getAwemeInfo(aweme_id)
awemeList.append(copy.deepcopy(datanew))
if numflag:
number-=1
if number==0:
break
if numflag and number==0:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
max_cursor = datadict["max_cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]: [主页] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[主页] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getLiveInfo(self, web_rid: str):
print('[ 提示 ]:正在请求的直播间 id = %s\r\n' % web_rid)
# web_rid = live_url.replace('https://live.douyin.com/', '')
live_api = self.urls.LIVE + self.utils.getXbogus(
url=f'aid=6383&device_platform=web&web_rid={web_rid}')
try:
response = requests.get(live_api, headers=self.headers)
live_json = json.loads(response.text)
except Exception as e:
print("[ 错误 ]:接口未返回数据, 请检查后重新运行!\r")
return None
if live_json == {} or live_json['status_code'] != 0:
print("[ 错误 ]:接口未返回信息\r")
return None
# 清空字典
self.result.clearDict(self.result.liveDict)
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
print('[ 📺 ]:当前直播已结束,正在退出')
return self.result.liveDict
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = live_json['data']['partition_road_map']['sub_partition']['partition'][
'title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
info = '[ 💻 ]:直播间:%s 当前%s 主播:%s 分区:%s-%s\r' % (
self.result.liveDict["title"], self.result.liveDict["display_long"], self.result.liveDict["nickname"],
self.result.liveDict["partition"], self.result.liveDict["sub_partition"])
print(info)
flv = []
print('[ 🎦 ]:直播间清晰度')
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
print('[ %s ]: %s' % (i, f))
flv.append(f)
rate = int(input('[ 🎬 ]输入数字选择推流清晰度:'))
# 显示清晰度列表
print('[ %s ]:%s' % (flv[rate], self.result.liveDict["flv_pull_url"][flv[rate]]))
print('[ 📺 ]:复制链接使用下载工具下载')
return self.result.liveDict
def getMixInfo(self, mix_id: str, count=35, number=0):
print('[ 提示 ]:正在请求的合集 id = %s\r\n' % mix_id)
if mix_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
print("[ 提示 ]:正在获取合集下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集] 进行第 " + str(times) + " 次请求...\r")
url = self.urls.USER_MIX + self.utils.getXbogus(
url=f'device_platform=webapp&aid=6383&os_version=10&version_name=17.4.0&mix_id={mix_id}&cursor={cursor}&count={count}')
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
res = requests.get(url=url, headers=self.headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
print('[ 提示 ]:开始对 ' + str(len(datadict["aweme_list"])) + ' 条数据请求作品详情\r\n')
if datadict is not None:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
for aweme in datadict["aweme_list"]:
# 获取 aweme_id
aweme_id = aweme["aweme_id"]
# 深拷贝 dict 不然list里面全是同样的数据
datanew, dataraw = self.getAwemeInfo(aweme_id)
awemeList.append(copy.deepcopy(datanew))
if numflag:
number -= 1
if number == 0:
break
if numflag and number == 0:
print("\r\n[ 提示 ]:[合集] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[合集] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getUserAllMixInfo(self, sec_uid, count=35, number=0):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
mixIdNameDict = {}
print("[ 提示 ]:正在获取主页下所有合集 id 数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集列表] 进行第 " + str(times) + " 次请求...\r")
url = self.urls.USER_MIX_LIST + self.utils.getXbogus(
url=f'device_platform=webapp&aid=6383&os_version=10&version_name=17.4.0&sec_user_id={sec_uid}&count={count}&cursor={cursor}')
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
res = requests.get(url=url, headers=self.headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["mix_infos"])) + ' 条数据\r')
print('[ 提示 ]:开始对 ' + str(len(datadict["mix_infos"])) + ' 条数据请求作品详情\r\n')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
for mix in datadict["mix_infos"]:
mixIdNameDict[mix["mix_id"]] = mix["mix_name"]
if numflag:
number -= 1
if number == 0:
break
if numflag and number == 0:
print("\r\n[ 提示 ]:[合集列表] 下指定数量合集数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("[ 提示 ]:[合集列表] 下所有合集 id 数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集列表] 第 " + str(times) + " 次请求成功...\r\n")
return mixIdNameDict
def getMusicInfo(self, music_id: str, count=35, number=0):
print('[ 提示 ]:正在请求的音乐集合 id = %s\r\n' % music_id)
if music_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
print("[ 提示 ]:正在获取音乐集合下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [音乐集合] 进行第 " + str(times) + " 次请求...\r")
url = self.urls.MUSIC + self.utils.getXbogus(
url=f'device_platform=webapp&aid=6383&os_version=10&version_name=17.4.0&music_id={music_id}&cursor={cursor}&count={count}')
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
res = requests.get(url=url, headers=self.headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
print('[ 提示 ]:开始对 ' + str(len(datadict["aweme_list"])) + ' 条数据请求作品详情\r\n')
if datadict is not None:
break
except Exception as e:
print("[ 警告 ]:接口未返回数据, 正在重新请求!\r")
for aweme in datadict["aweme_list"]:
# 获取 aweme_id
aweme_id = aweme["aweme_id"]
# 深拷贝 dict 不然list里面全是同样的数据
datanew, dataraw = self.getAwemeInfo(aweme_id)
awemeList.append(copy.deepcopy(datanew))
if numflag:
number -= 1
if number == 0:
break
if numflag and number == 0:
print("\r\n[ 提示 ]:[音乐集合] 下指定数量作品数据获取完成...\r\n")
break
# 更新 cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[音乐集合] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[音乐集合] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
# 来自 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
if not os.path.exists(savePath):
os.mkdir(savePath)
try:
# 使用作品 创建时间+描述 当文件夹
file_name = self.utils.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 = self.utils.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\n"
% (aweme["author"]["nickname"], str(ind + 1), len(awemeList)))
self.awemeDownload(aweme, music, cover, avatar, savePath)
# time.sleep(0.5)
if __name__ == "__main__":
pass

View File

@ -1,97 +0,0 @@
#!/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
from TikTokUtils import Utils
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), 默认为True",
type=Utils().str2bool, required=False, default=True)
parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 默认为True, 当下载视频时有效",
type=Utils().str2bool, required=False, default=True)
parser.add_argument("--avatar", "-a", help="是否下载作者的头像(True/False), 默认为True",
type=Utils().str2bool, required=False, default=True)
parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post",
type=str, required=False, default="post")
parser.add_argument("--number", "-n",
help="1.当下载单个合集、音乐集合、主页作品(post模式)和喜欢(like模式)时, 可设置下载前n个作品, 默认为0全部下载\r\n" +
"2.当下载主页下所有合集(mix模式)时, 设置下载前n个合集下所有作品, 默认为0全部下载",
type=int, required=False, default=0)
args = parser.parse_args()
return args
def main():
utils = Utils()
args = argument()
tk = TikTok()
url = tk.getShareLink(args.link)
key_type, key = tk.getKey(url)
if key is None or key_type is None:
return
elif key_type == "user" and args.mode != 'mix':
datalist = tk.getUserInfo(key, args.mode, 35, args.number)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "user" and args.mode == 'mix':
if not os.path.exists(args.path):
os.mkdir(args.path)
mixIdNameDict = tk.getUserAllMixInfo(key, 35, args.number)
for mix_id in mixIdNameDict:
print(f'[ 提示 ]:正在下载合集 [{mixIdNameDict[mix_id]}] 中的作品\r\n')
mix_file_name = utils.replaceStr(mixIdNameDict[mix_id])
datalist = tk.getMixInfo(mix_id, 35)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=os.path.join(args.path, mix_file_name))
print(f'[ 提示 ]:合集 [{mixIdNameDict[mix_id]}] 中的作品下载完成\r\n')
elif key_type == "mix":
datalist = tk.getMixInfo(key,35, args.number)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "music":
datalist = tk.getMusicInfo(key,35, args.number)
tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "aweme":
datanew, dataraw = tk.getAwemeInfo(key)
tk.awemeDownload(awemeDict=datanew, music=args.music, cover=args.cover, avatar=args.avatar,
savePath=args.path)
elif key_type == "live":
live_json = tk.getLiveInfo(key)
if not os.path.exists(args.path):
os.mkdir(args.path)
# 保存获取到json
print("[ 提示 ]:正在保存获取到的信息到result.json\r\n")
with open(os.path.join(args.path, "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()

View File

@ -1,131 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/02/11 13:06:23
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import TikTokUtils
from TikTok import TikTok
def getAwemeInfo():
share_link_video = "3.56 uSy:/ 复制打开抖音,看看【小透明的作品】没有女朋友就用我的吧哈哈哈哈 # 表情包锁屏 https://v.douyin.com/BugmVVD/"
share_link_pic = "8.20 MJI:/ 复制打开抖音,看看【舍溪的图文作品】我又来放图集啦~还有你们要的小可爱大图也放啦~# ... https://v.douyin.com/BugrFTN/"
tk = TikTok()
url = tk.getShareLink(share_link_pic)
key_type, key = tk.getKey(url)
datanew, dataraw = tk.getAwemeInfo(key)
print(datanew)
def getUserInfo():
share_link_post = "1- 长按复制此条消息打开抖音搜索查看TA的更多作品。 https://v.douyin.com/BupCppt/"
share_link_like = "2- 长按复制此条消息打开抖音搜索查看TA的更多作品。 https://v.douyin.com/BusJrfr/"
tk = TikTok()
url = tk.getShareLink(share_link_like)
key_type, key = tk.getKey(url)
awemeList = tk.getUserInfo(key, mode="like", count=35)
print(awemeList)
def getLiveInfo():
live_link = "https://live.douyin.com/40768897856"
tk = TikTok()
url = tk.getShareLink(live_link)
key_type, key = tk.getKey(url)
live_json = tk.getLiveInfo(key)
print(live_json)
def getMixInfo():
mix_link = 'https://v.douyin.com/B3J63Le/'
tk = TikTok()
url = tk.getShareLink(mix_link)
key_type, key = tk.getKey(url)
awemeList = tk.getMixInfo(key, count=35)
print(len(awemeList))
def getUserAllMixInfo():
user_all_mix_link = 'https://v.douyin.com/B38oovu/'
tk = TikTok()
url = tk.getShareLink(user_all_mix_link)
key_type, key = tk.getKey(url)
mixIdNameDict = tk.getUserAllMixInfo(key, count=35)
print(mixIdNameDict)
def getMusicInfo():
music_link = 'https://v.douyin.com/S6YMNXs/'
tk = TikTok()
url = tk.getShareLink(music_link)
key_type, key = tk.getKey(url)
awemeList = tk.getMusicInfo(key,count=35)
print(len(awemeList))
def test():
utils=TikTokUtils.Utils()
user_all_mix_link = 'https://www.douyin.com/aweme/v1/web/aweme/post/?'+\
utils.getXbogus(url='device_platform=webapp&aid=6383&os_version=10&version_name=17.4.0&sec_user_id=MS4wLjABAAAA7G3S983y0b0m_7bcTSK7UzY-3hFJdHvMv-aVbbz0kIg&max_cursor=1626149119000&count=10')
headers1 = {
'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',
'referer': 'https://www.douyin.com/',
'Cookie': 'ttwid=1%7CWBuxH_bhbuTENNtACXoesI5QHV2Dt9-vkMGVHSRRbgY%7C1677118712%7C1d87ba1ea2cdf05d80204aea2e1036451dae638e7765b8a4d59d87fa05dd39ff; bd_ticket_guard_client_data=eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWNsaWVudC1jc3IiOiItLS0tLUJFR0lOIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLVxyXG5NSUlCRFRDQnRRSUJBREFuTVFzd0NRWURWUVFHRXdKRFRqRVlNQllHQTFVRUF3d1BZbVJmZEdsamEyVjBYMmQxXHJcbllYSmtNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVKUDZzbjNLRlFBNUROSEcyK2F4bXAwNG5cclxud1hBSTZDU1IyZW1sVUE5QTZ4aGQzbVlPUlI4NVRLZ2tXd1FJSmp3Nyszdnc0Z2NNRG5iOTRoS3MvSjFJc3FBc1xyXG5NQ29HQ1NxR1NJYjNEUUVKRGpFZE1Cc3dHUVlEVlIwUkJCSXdFSUlPZDNkM0xtUnZkWGxwYmk1amIyMHdDZ1lJXHJcbktvWkl6ajBFQXdJRFJ3QXdSQUlnVmJkWTI0c0RYS0c0S2h3WlBmOHpxVDRBU0ROamNUb2FFRi9MQnd2QS8xSUNcclxuSURiVmZCUk1PQVB5cWJkcytld1QwSDZqdDg1czZZTVNVZEo5Z2dmOWlmeTBcclxuLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tXHJcbiJ9'
}
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',
'referer': 'https://www.douyin.com/',
'Cookie': 'ttwid=1|sGp2L-Krm46cXHcK7BsKghavVeVQIIOYtQInA1LV0-w|1676899557|3e483426230c481bd34f4d6529d6252372c154b75be7d4a2baec8edbfd0a742c; __ac_signature=_02B4Z6wo00f01CEKaogAAIDBqkHxaCCYIyghKm4AAGu9c3; s_v_web_id=verify_ledo1j1t_0NwhDQFJ_nLca_42o5_8tAA_T8CWm5E2M6LF; msToken=%s;odin_tt=324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69;' % utils.generate_random_str(
107)
}
import requests
res = requests.get(user_all_mix_link,headers=headers1)
print(res.text)
if __name__ == "__main__":
# test()
# getMusicInfo()
# getUserAllMixInfo()
# getMixInfo()
# getAwemeInfo()
# getUserInfo()
# getLiveInfo()
pass
################################# 测试命令 ######################################################
# 直播
# python TikTokCommand.py -l https://live.douyin.com/759547612580 -p /mnt/c/project/test0
# .\TikTokCommand.exe -l https://live.douyin.com/759547612580 -p .\test0
# 视频
# python TikTokCommand.py -l https://v.douyin.com/BugmVVD/ -p /mnt/c/project/test1
# .\TikTokCommand.exe -l https://v.douyin.com/BugmVVD/ -p .\test1
# 图集
# python TikTokCommand.py -l https://v.douyin.com/BugrFTN/ -p /mnt/c/project/test2
# .\TikTokCommand.exe -l https://v.douyin.com/BugrFTN/ -p .\test2
# 主页作品(视频)
# python TikTokCommand.py -l https://v.douyin.com/BupCppt/ -p /mnt/c/project/test3
# .\TikTokCommand.exe -l https://v.douyin.com/BupCppt/ -p .\test3
# 主页作品(视频与图集混合)
# python TikTokCommand.py -l https://v.douyin.com/B72pdU5/ -p /mnt/c/project/test4
# .\TikTokCommand.exe -l https://v.douyin.com/B72pdU5/ -p .\test4
# 主页喜欢(视频)
# python TikTokCommand.py -l https://v.douyin.com/B72QgDw/ -p /mnt/c/project/test5 -M like
# .\TikTokCommand.exe -l https://v.douyin.com/B72QgDw/ -p .\test5 -M like
# 单个合集
# python TikTokCommand.py -l https://v.douyin.com/B3J63Le/ -p /mnt/c/project/test6
# .\TikTokCommand.exe -l https://v.douyin.com/B3J63Le/ -p .\test6
# 用户主页下所有合集
# python TikTokCommand.py -l https://v.douyin.com/B38oovu/ -p /mnt/c/project/test7 -M mix
# .\TikTokCommand.exe -l https://v.douyin.com/B38oovu/ -p .\test7 -M mix
# 音乐集合
# python TikTokCommand.py -l https://v.douyin.com/S6YMNXs/ -p /mnt/c/project/test8
# .\TikTokCommand.exe -l https://v.douyin.com/S6YMNXs/ -p .\test8
#################################################################################################

View File

@ -1,87 +0,0 @@
#!/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
import json
import requests
from TikTokUrls import Urls
class Utils(object):
def __init__(self):
pass
def generate_random_str(self, 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(self, 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
def getXbogus(self, url, headers=None):
urls = Urls()
try:
response = json.loads(requests.post(
url=urls.GET_XB_PATH, data={"param": url}, headers=headers).text)
params = response["param"]
xb = response["X-Bogus"]
except Exception as e:
print('[ 错误 ]:X-Bogus接口异常, 可能是访问流量高, 接口限流请稍等几分钟再次尝试')
return
return params # , xb
def str2bool(self, v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
return True
# https://www.52pojie.cn/thread-1589242-1-1.html
def getttwid(self):
url = 'https://ttwid.bytedance.com/ttwid/union/register/'
data = '{"region":"cn","aid":1768,"needFid":false,"service":"www.ixigua.com","migrate_info":{"ticket":"","source":"node"},"cbUrlProtocol":"https","union":true}'
res = requests.post(url=url, data=data)
for i,j in res.cookies.items():
return j
if __name__ == "__main__":
pass

View File

@ -1,57 +0,0 @@
#!/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)
datanew, dataraw = tk.getAwemeInfo(key)
return datanew
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)

159
WebApi.py Normal file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : WebApi.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 18:52
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
from flask import *
from apiproxy.douyin.douyinapi import DouyinApi
from apiproxy.douyin import douyin_headers
import argparse
def douyinwork(share_link, max_cursor, mode, cookie):
dy = DouyinApi()
if cookie is not None and cookie != "":
douyin_headers["Cookie"] = cookie
url = dy.getShareLink(share_link)
key_type, key = dy.getKey(url)
data = None
rawdata = None
cursor = None
has_more = None
if key_type == "user":
if mode == 'post' or mode == 'like':
data, rawdata, cursor, has_more = dy.getUserInfoApi(sec_uid=key, mode=mode, count=35,
max_cursor=max_cursor)
elif mode == 'mix':
data, rawdata, cursor, has_more = dy.getUserAllMixInfoApi(sec_uid=key, count=35, cursor=max_cursor)
elif mode == 'detail':
rawdata = dy.getUserDetailInfoApi(sec_uid=key)
data = rawdata
elif key_type == "mix":
data, rawdata, cursor, has_more = dy.getMixInfoApi(mix_id=key, count=35, cursor=max_cursor)
elif key_type == "music":
data, rawdata, cursor, has_more = dy.getMusicInfoApi(music_id=key, count=35, cursor=max_cursor)
elif key_type == "aweme":
data, rawdata = dy.getAwemeInfoApi(aweme_id=key)
elif key_type == "live":
data, rawdata = dy.getLiveInfoApi(web_rid=key)
datadict = {}
datadict["data"] = data
datadict["rawdata"] = rawdata
datadict["cursor"] = cursor
datadict["has_more"] = has_more
return datadict
def deal(mode=None):
usefuldict = {}
if request.headers.get("content_type") == "application/json":
result = request.get_json(force=True)
else:
result = request.form
share_link = None
cursor = 0
cookie = None
try:
share_link = result["share_link"]
cursor = result["cursor"]
cookie = result["cookie"]
except Exception as e:
usefuldict["status_code"] = 500
try:
if share_link is not None and share_link != "":
usefuldict = douyinwork(share_link, cursor, mode, cookie)
usefuldict["status_code"] = 200
except Exception as e:
usefuldict["status_code"] = 500
return jsonify(usefuldict)
app = Flask(__name__)
# 设置编码
app.config['JSON_AS_ASCII'] = False
def argument():
parser = argparse.ArgumentParser(description='抖音去水印工具 使用帮助')
parser.add_argument("--port", "-p", help="Web端口",
type=int, required=False, default=5000)
args = parser.parse_args()
return args
@app.route("/douyin/music", methods=["POST"])
def douyinMusic():
return deal()
@app.route("/douyin/mix", methods=["POST"])
def douyinMix():
return deal()
@app.route("/douyin/user/mix", methods=["POST"])
def douyinUserMix():
return deal(mode="mix")
@app.route("/douyin/user/like", methods=["POST"])
def douyinUserLike():
return deal(mode="like")
@app.route("/douyin/user/post", methods=["POST"])
def douyinUserPost():
return deal(mode="post")
@app.route("/douyin/user/detail", methods=["POST"])
def douyinUserDetail():
return deal(mode="detail")
@app.route("/douyin/aweme", methods=["POST"])
def douyinAweme():
return deal()
@app.route("/douyin/live", methods=["POST"])
def douyinLive():
return deal()
@app.route("/douyin", methods=["POST"])
def douyin():
return deal()
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
if __name__ == "__main__":
args = argument()
app.run(debug=False, host="0.0.0.0", port=args.port)

20
apiproxy/__init__.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:32
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 16:10
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
from .utils import Utils
utils = Utils()

201
apiproxy/common/utils.py Normal file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : utils.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:18
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import random
import requests
import re
import os
import sys
import hashlib
import base64
import time
import apiproxy
class Utils(object):
def __init__(self):
pass
def replaceStr(self, filenamestr: str):
"""
替换非法字符缩短字符长度使其能成为文件名
"""
# 匹配 汉字 字母 数字 空格
match = "([0-9A-Za-z\u4e00-\u9fa5]+)"
result = re.findall(match, filenamestr)
result = "".join(result).strip()
if len(result) > 20:
result = result[:20]
# 去除前后空格
return result
def resource_path(self, relative_path):
if getattr(sys, 'frozen', False): # 是否Bundle Resource
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
def str2bool(self, v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
return True
def generate_random_str(self, 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
# https://www.52pojie.cn/thread-1589242-1-1.html
def getttwid(self):
url = 'https://ttwid.bytedance.com/ttwid/union/register/'
data = '{"region":"cn","aid":1768,"needFid":false,"service":"www.ixigua.com","migrate_info":{"ticket":"","source":"node"},"cbUrlProtocol":"https","union":true}'
res = requests.post(url=url, data=data)
for i, j in res.cookies.items():
return j
def getXbogus(self, payload, form='', ua=apiproxy.ua):
xbogus = self.get_xbogus(payload, ua, form)
params = payload + "&X-Bogus=" + xbogus
return params
def get_xbogus(self, payload, ua, form):
short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="
arr2 = self.get_arr2(payload, ua, form)
garbled_string = self.get_garbled_string(arr2)
xbogus = ""
for i in range(0, 21, 3):
char_code_num0 = garbled_string[i]
char_code_num1 = garbled_string[i + 1]
char_code_num2 = garbled_string[i + 2]
base_num = char_code_num2 | char_code_num1 << 8 | char_code_num0 << 16
str1 = short_str[(base_num & 16515072) >> 18]
str2 = short_str[(base_num & 258048) >> 12]
str3 = short_str[(base_num & 4032) >> 6]
str4 = short_str[base_num & 63]
xbogus += str1 + str2 + str3 + str4
return xbogus
def get_garbled_string(self, arr2):
p = [
arr2[0], arr2[10], arr2[1], arr2[11], arr2[2], arr2[12], arr2[3], arr2[13], arr2[4], arr2[14],
arr2[5], arr2[15], arr2[6], arr2[16], arr2[7], arr2[17], arr2[8], arr2[18], arr2[9]
]
char_array = [chr(i) for i in p]
f = []
f.extend([2, 255])
tmp = ['ÿ']
bytes_ = self._0x30492c(tmp, "".join(char_array))
for i in range(len(bytes_)):
f.append(bytes_[i])
return f
def get_arr2(self, payload, ua, form):
salt_payload_bytes = hashlib.md5(hashlib.md5(payload.encode()).digest()).digest()
salt_payload = [byte for byte in salt_payload_bytes]
salt_form_bytes = hashlib.md5(hashlib.md5(form.encode()).digest()).digest()
salt_form = [byte for byte in salt_form_bytes]
ua_key = ['\u0000', '\u0001', '\u000e']
salt_ua_bytes = hashlib.md5(base64.b64encode(self._0x30492c(ua_key, ua))).digest()
salt_ua = [byte for byte in salt_ua_bytes]
timestamp = int(time.time())
canvas = 1489154074
arr1 = [
64, # 固定
0, # 固定
1, # 固定
14, # 固定 这个还要再看一下14,12,0都出现过
salt_payload[14], # payload 相关
salt_payload[15],
salt_form[14], # form 相关
salt_form[15],
salt_ua[14], # ua 相关
salt_ua[15],
(timestamp >> 24) & 255,
(timestamp >> 16) & 255,
(timestamp >> 8) & 255,
(timestamp >> 0) & 255,
(canvas >> 24) & 255,
(canvas >> 16) & 255,
(canvas >> 8) & 255,
(canvas >> 0) & 255,
64, # 校验位
]
for i in range(1, len(arr1) - 1):
arr1[18] ^= arr1[i]
arr2 = [arr1[0], arr1[2], arr1[4], arr1[6], arr1[8], arr1[10], arr1[12], arr1[14], arr1[16], arr1[18], arr1[1],
arr1[3], arr1[5], arr1[7], arr1[9], arr1[11], arr1[13], arr1[15], arr1[17]]
return arr2
def _0x30492c(self, a, b):
d = [i for i in range(256)]
c = 0
result = bytearray(len(b))
for i in range(256):
c = (c + d[i] + ord(a[i % len(a)])) % 256
e = d[i]
d[i] = d[c]
d[c] = e
t = 0
c = 0
for i in range(len(b)):
t = (t + 1) % 256
c = (c + d[t]) % 256
e = d[t]
d[t] = d[c]
d[c] = e
result[i] = ord(b[i]) ^ d[(d[t] + d[c]) % 256]
return result
if __name__ == "__main__":
pass

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:44
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import apiproxy
from apiproxy.common import utils
douyin_headers = {
'User-Agent': apiproxy.ua,
'referer': 'https://www.douyin.com/',
'accept-encoding': None,
'Cookie': f"msToken={utils.generate_random_str(107)}; ttwid={utils.getttwid()}; odin_tt=324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69; passport_csrf_token=f61602fc63757ae0e4fd9d6bdcee4810;"
}

172
apiproxy/douyin/database.py Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : database.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:15
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import sqlite3
import json
class DataBase(object):
def __init__(self):
self.conn = sqlite3.connect('data.db')
self.cursor = self.conn.cursor()
self.create_user_post_table()
self.create_user_like_table()
self.create_mix_table()
self.create_music_table()
def create_user_post_table(self):
sql = """CREATE TABLE if not exists t_user_post (
id integer primary key autoincrement,
sec_uid varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_user_post(self, sec_uid: str, aweme_id: int):
sql = """select id, sec_uid, aweme_id, rawdata from t_user_post where sec_uid=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_user_post(self, sec_uid: str, aweme_id: int, data: dict):
insertsql = """insert into t_user_post (sec_uid, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_user_like_table(self):
sql = """CREATE TABLE if not exists t_user_like (
id integer primary key autoincrement,
sec_uid varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_user_like(self, sec_uid: str, aweme_id: int):
sql = """select id, sec_uid, aweme_id, rawdata from t_user_like where sec_uid=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_user_like(self, sec_uid: str, aweme_id: int, data: dict):
insertsql = """insert into t_user_like (sec_uid, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_mix_table(self):
sql = """CREATE TABLE if not exists t_mix (
id integer primary key autoincrement,
sec_uid varchar(200),
mix_id varchar(200),
aweme_id integer,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_mix(self, sec_uid: str, mix_id: str, aweme_id: int):
sql = """select id, sec_uid, mix_id, aweme_id, rawdata from t_mix where sec_uid=? and mix_id=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (sec_uid, mix_id, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_mix(self, sec_uid: str, mix_id: str, aweme_id: int, data: dict):
insertsql = """insert into t_mix (sec_uid, mix_id, aweme_id, rawdata) values(?,?,?,?);"""
try:
self.cursor.execute(insertsql, (sec_uid, mix_id, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
def create_music_table(self):
sql = """CREATE TABLE if not exists t_music (
id integer primary key autoincrement,
music_id varchar(200),
aweme_id integer unique,
rawdata json
);"""
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
pass
def get_music(self, music_id: str, aweme_id: int):
sql = """select id, music_id, aweme_id, rawdata from t_music where music_id=? and aweme_id=?;"""
try:
self.cursor.execute(sql, (music_id, aweme_id))
self.conn.commit()
res = self.cursor.fetchone()
return res
except Exception as e:
pass
def insert_music(self, music_id: str, aweme_id: int, data: dict):
insertsql = """insert into t_music (music_id, aweme_id, rawdata) values(?,?,?);"""
try:
self.cursor.execute(insertsql, (music_id, aweme_id, json.dumps(data)))
self.conn.commit()
except Exception as e:
pass
if __name__ == '__main__':
pass

688
apiproxy/douyin/douyin.py Normal file
View File

@ -0,0 +1,688 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : douyin.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:52
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import re
import requests
import json
import time
import copy
from apiproxy.douyin import douyin_headers
from apiproxy.douyin.urls import Urls
from apiproxy.douyin.result import Result
from apiproxy.douyin.database import DataBase
from apiproxy.common import utils
class Douyin(object):
def __init__(self, database=False):
self.urls = Urls()
self.result = Result()
self.database = database
if database:
self.db = DataBase()
# 用于设置重复请求某个接口的最大时间
self.timeout = 10
# 从分享链接中提取网址
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=douyin_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
# 合集
# https://www.douyin.com/collection/7093490319085307918
urlstr = str(r.request.path_url)
if "/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 "/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/note/" in urlstr:
# 获取note aweme_id
key = re.findall('note/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/mix/detail/" in urlstr:
# 获取合集 id
key = re.findall('/mix/detail/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/collection/" in urlstr:
# 获取合集 id
key = re.findall('/collection/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/music/" in urlstr:
# 获取原声 id
key = re.findall('music/(\d+)?', urlstr)[0]
key_type = "music"
elif "/webcast/reflow/" in urlstr:
key1 = re.findall('reflow/(\d+)?', urlstr)[0]
url = self.urls.LIVE2 + utils.getXbogus(
f'live_id=1&room_id={key1}&app_id=1128')
res = requests.get(url, headers=douyin_headers)
resjson = json.loads(res.text)
key = resjson['data']['room']['owner']['web_rid']
key_type = "live"
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
return key_type, key
# 传入 aweme_id
# 返回 数据 字典
def getAwemeInfo(self, aweme_id):
print('[ 提示 ]:正在请求的作品 id = %s\r' % aweme_id)
if aweme_id is None:
return None
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
# 单作品接口返回 'aweme_detail'
# 主页作品接口返回 'aweme_list'->['aweme_detail']
jx_url = self.urls.POST_DETAIL + utils.getXbogus(
f'aweme_id={aweme_id}&device_platform=webapp&aid=6383')
raw = requests.get(url=jx_url, headers=douyin_headers).text
datadict = json.loads(raw)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return {}, {}
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
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.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
# 传入 url 支持 https://www.iesdouyin.com 与 https://v.douyin.com
# mode : post | like 模式选择 like为用户点赞 post为用户发布
def getUserInfo(self, sec_uid, mode="post", count=35, number=0, increase=False):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
max_cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [主页] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
if mode == "post":
url = self.urls.USER_POST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
else:
print("[ 错误 ]:模式选择错误, 仅支持post、like、mix, 请检查后重新运行!\r")
return None
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if mode == "post":
if self.db.get_user_post(sec_uid=sec_uid, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_user_post(sec_uid=sec_uid, aweme_id=aweme['aweme_id'], data=aweme)
elif mode == "like":
if self.db.get_user_like(sec_uid=sec_uid, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_user_like(sec_uid=sec_uid, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [主页] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [主页] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
max_cursor = datadict["max_cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]: [主页] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[主页] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getLiveInfo(self, web_rid: str):
print('[ 提示 ]:正在请求的直播间 id = %s\r\n' % web_rid)
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
live_api = self.urls.LIVE + utils.getXbogus(
f'aid=6383&device_platform=web&web_rid={web_rid}')
response = requests.get(live_api, headers=douyin_headers)
live_json = json.loads(response.text)
if live_json != {} and live_json['status_code'] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return {}
# 清空字典
self.result.clearDict(self.result.liveDict)
# 类型
self.result.liveDict["awemeType"] = 2
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
print('[ 📺 ]:当前直播已结束,正在退出')
return self.result.liveDict
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 直播cover
self.result.liveDict["cover"] = live_json['data']['data'][0]['cover']['url_list'][0]
# 头像
self.result.liveDict["avatar"] = live_json['data']['data'][0]['owner']['avatar_thumb']['url_list'][0].replace(
"100x100", "1080x1080")
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = \
live_json['data']['partition_road_map']['sub_partition']['partition']['title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
info = '[ 💻 ]:直播间:%s 当前%s 主播:%s 分区:%s-%s\r' % (
self.result.liveDict["title"], self.result.liveDict["display_long"], self.result.liveDict["nickname"],
self.result.liveDict["partition"], self.result.liveDict["sub_partition"])
print(info)
flv = []
print('[ 🎦 ]:直播间清晰度')
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
print('[ %s ]: %s' % (i, f))
flv.append(f)
rate = int(input('[ 🎬 ]输入数字选择推流清晰度:'))
self.result.liveDict["flv_pull_url0"] = self.result.liveDict["flv_pull_url"][flv[rate]]
# 显示清晰度列表
print('[ %s ]:%s' % (flv[rate], self.result.liveDict["flv_pull_url"][flv[rate]]))
print('[ 📺 ]:复制链接使用下载工具下载')
return self.result.liveDict
def getMixInfo(self, mix_id: str, count=35, number=0, increase=False, sec_uid=''):
print('[ 提示 ]:正在请求的合集 id = %s\r\n' % mix_id)
if mix_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取合集下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_MIX + utils.getXbogus(
f'mix_id={mix_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if self.db.get_mix(sec_uid=sec_uid, mix_id=mix_id, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_mix(sec_uid=sec_uid, mix_id=mix_id, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [合集] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [合集] 下指定数量作品数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[合集] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getUserAllMixInfo(self, sec_uid, count=35, number=0):
print('[ 提示 ]:正在请求的用户 id = %s\r\n' % sec_uid)
if sec_uid is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
mixIdNameDict = {}
print("[ 提示 ]:正在获取主页下所有合集 id 数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [合集列表] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_MIX_LIST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&cursor={cursor}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["mix_infos"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return mixIdNameDict
for mix in datadict["mix_infos"]:
mixIdNameDict[mix["mix_id"]] = mix["mix_name"]
if numflag:
number -= 1
if number == 0:
break
if numflag and number == 0:
print("\r\n[ 提示 ]:[合集列表] 下指定数量合集数据获取完成...\r\n")
break
# 更新 max_cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("[ 提示 ]:[合集列表] 下所有合集 id 数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[合集列表] 第 " + str(times) + " 次请求成功...\r\n")
return mixIdNameDict
def getMusicInfo(self, music_id: str, count=35, number=0, increase=False):
print('[ 提示 ]:正在请求的音乐集合 id = %s\r\n' % music_id)
if music_id is None:
return None
if number <= 0:
numflag = False
else:
numflag = True
cursor = 0
awemeList = []
increaseflag = False
numberis0 = False
print("[ 提示 ]:正在获取音乐集合下的所有作品数据请稍后...\r")
print("[ 提示 ]:会进行多次请求,等待时间较长...\r\n")
times = 0
while True:
times = times + 1
print("[ 提示 ]:正在对 [音乐集合] 进行第 " + str(times) + " 次请求...\r")
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.MUSIC + utils.getXbogus(
f'music_id={music_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
print('[ 提示 ]:本次请求返回 ' + str(len(datadict["aweme_list"])) + ' 条数据\r')
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return awemeList
for aweme in datadict["aweme_list"]:
if self.database:
# 退出条件
if increase is False and numflag and numberis0:
break
if increase and numflag and numberis0 and increaseflag:
break
# 增量更新, 找到非置顶的最新的作品发布时间
if self.db.get_music(music_id=music_id, aweme_id=aweme['aweme_id']) is not None:
if increase and aweme['is_top'] == 0:
increaseflag = True
else:
self.db.insert_music(music_id=music_id, aweme_id=aweme['aweme_id'], data=aweme)
# 退出条件
if increase and numflag is False and increaseflag:
break
if increase and numflag and numberis0 and increaseflag:
break
else:
if numflag and numberis0:
break
if numflag:
number -= 1
if number == 0:
numberis0 = True
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
print("[ 警告 ]:接口中未找到 images\r")
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
if self.database:
if increase and numflag is False and increaseflag:
print("\r\n[ 提示 ]: [音乐集合] 下作品增量更新数据获取完成...\r\n")
break
elif increase is False and numflag and numberis0:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成...\r\n")
break
elif increase and numflag and numberis0 and increaseflag:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成, 增量更新数据获取完成...\r\n")
break
else:
if numflag and numberis0:
print("\r\n[ 提示 ]: [音乐集合] 下指定数量作品数据获取完成...\r\n")
break
# 更新 cursor
cursor = datadict["cursor"]
# 退出条件
if datadict["has_more"] == 0 or datadict["has_more"] == False:
print("\r\n[ 提示 ]:[音乐集合] 下所有作品数据获取完成...\r\n")
break
else:
print("\r\n[ 提示 ]:[音乐集合] 第 " + str(times) + " 次请求成功...\r\n")
return awemeList
def getUserDetailInfo(self, sec_uid):
if sec_uid is None:
return None
datadict = {}
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_DETAIL + utils.getXbogus(
f'sec_user_id={sec_uid}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
return datadict
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
print("[ 提示 ]:重复请求该接口" + str(self.timeout) + "s, 仍然未获取到数据")
return datadict
if __name__ == "__main__":
pass

View File

@ -0,0 +1,398 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : douyinapi.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/20 22:13
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import re
import requests
import json
import time
import copy
from apiproxy.douyin import douyin_headers
from apiproxy.douyin.urls import Urls
from apiproxy.douyin.result import Result
from apiproxy.common import utils
class DouyinApi(object):
def __init__(self):
self.urls = Urls()
self.result = Result()
# 用于设置重复请求某个接口的最大时间
self.timeout = 10
# 从分享链接中提取网址
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=douyin_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
# 合集
# https://www.douyin.com/collection/7093490319085307918
urlstr = str(r.request.path_url)
if "/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 "/video/" in urlstr:
# 获取作品 aweme_id
key = re.findall('video/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/note/" in urlstr:
# 获取note aweme_id
key = re.findall('note/(\d+)?', urlstr)[0]
key_type = "aweme"
elif "/mix/detail/" in urlstr:
# 获取合集 id
key = re.findall('/mix/detail/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/collection/" in urlstr:
# 获取合集 id
key = re.findall('/collection/(\d+)?', urlstr)[0]
key_type = "mix"
elif "/music/" in urlstr:
# 获取原声 id
key = re.findall('music/(\d+)?', urlstr)[0]
key_type = "music"
elif "/webcast/reflow/" in urlstr:
key1 = re.findall('reflow/(\d+)?', urlstr)[0]
url = self.urls.LIVE2 + utils.getXbogus(
f'live_id=1&room_id={key1}&app_id=1128')
res = requests.get(url, headers=douyin_headers)
resjson = json.loads(res.text)
key = resjson['data']['room']['owner']['web_rid']
key_type = "live"
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
return key_type, key
def getAwemeInfoApi(self, aweme_id):
if aweme_id is None:
return None
start = time.time() # 开始时间
while True:
try:
jx_url = self.urls.POST_DETAIL + utils.getXbogus(
f'aweme_id={aweme_id}&device_platform=webapp&aid=6383')
raw = requests.get(url=jx_url, headers=douyin_headers).text
datadict = json.loads(raw)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if datadict['aweme_detail']["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, datadict['aweme_detail'])
return self.result.awemeDict, datadict
def getUserInfoApi(self, sec_uid, mode="post", count=35, max_cursor=0):
if sec_uid is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
if mode == "post":
url = self.urls.USER_POST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
elif mode == "like":
url = self.urls.USER_FAVORITE_A + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&max_cursor={max_cursor}&device_platform=webapp&aid=6383')
else:
return None
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["max_cursor"], datadict["has_more"]
def getLiveInfoApi(self, web_rid: str):
start = time.time() # 开始时间
while True:
try:
live_api = self.urls.LIVE + utils.getXbogus(
f'aid=6383&device_platform=web&web_rid={web_rid}')
response = requests.get(live_api, headers=douyin_headers)
live_json = json.loads(response.text)
if live_json != {} and live_json['status_code'] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
# 清空字典
self.result.clearDict(self.result.liveDict)
# 类型
self.result.liveDict["awemeType"] = 2
# 是否在播
self.result.liveDict["status"] = live_json['data']['data'][0]['status']
if self.result.liveDict["status"] == 4:
return self.result.liveDict, live_json
# 直播标题
self.result.liveDict["title"] = live_json['data']['data'][0]['title']
# 直播cover
self.result.liveDict["cover"] = live_json['data']['data'][0]['cover']['url_list'][0]
# 头像
self.result.liveDict["avatar"] = live_json['data']['data'][0]['owner']['avatar_thumb']['url_list'][0].replace(
"100x100", "1080x1080")
# 观看人数
self.result.liveDict["user_count"] = live_json['data']['data'][0]['user_count_str']
# 昵称
self.result.liveDict["nickname"] = live_json['data']['data'][0]['owner']['nickname']
# sec_uid
self.result.liveDict["sec_uid"] = live_json['data']['data'][0]['owner']['sec_uid']
# 直播间观看状态
self.result.liveDict["display_long"] = live_json['data']['data'][0]['room_view_stats']['display_long']
# 推流
self.result.liveDict["flv_pull_url"] = live_json['data']['data'][0]['stream_url']['flv_pull_url']
try:
# 分区
self.result.liveDict["partition"] = live_json['data']['partition_road_map']['partition']['title']
self.result.liveDict["sub_partition"] = \
live_json['data']['partition_road_map']['sub_partition']['partition']['title']
except Exception as e:
self.result.liveDict["partition"] = ''
self.result.liveDict["sub_partition"] = ''
flv = []
for i, f in enumerate(self.result.liveDict["flv_pull_url"].keys()):
flv.append(f)
self.result.liveDict["flv_pull_url0"] = self.result.liveDict["flv_pull_url"][flv[0]]
return self.result.liveDict, live_json
def getMixInfoApi(self, mix_id: str, count=35, cursor=0):
if mix_id is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.USER_MIX + utils.getXbogus(
f'mix_id={mix_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["cursor"], datadict["has_more"]
def getUserAllMixInfoApi(self, sec_uid, count=35, cursor=0):
if sec_uid is None:
return None
mixIdlist = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.USER_MIX_LIST + utils.getXbogus(
f'sec_user_id={sec_uid}&count={count}&cursor={cursor}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for mix in datadict["mix_infos"]:
mixIdNameDict = {}
mixIdNameDict["https://www.douyin.com/collection/" + mix["mix_id"]] = mix["mix_name"]
mixIdlist.append(mixIdNameDict)
return mixIdlist, datadict, datadict["cursor"], datadict["has_more"]
def getMusicInfoApi(self, music_id: str, count=35, cursor=0):
if music_id is None:
return None
awemeList = []
start = time.time() # 开始时间
while True:
try:
url = self.urls.MUSIC + utils.getXbogus(
f'music_id={music_id}&cursor={cursor}&count={count}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
break
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
for aweme in datadict["aweme_list"]:
# 清空self.awemeDict
self.result.clearDict(self.result.awemeDict)
# 默认为视频
awemeType = 0
try:
if aweme["images"] is not None:
awemeType = 1
except Exception as e:
pass
# 转换成我们自己的格式
self.result.dataConvert(awemeType, self.result.awemeDict, aweme)
if self.result.awemeDict is not None and self.result.awemeDict != {}:
awemeList.append(copy.deepcopy(self.result.awemeDict))
return awemeList, datadict, datadict["cursor"], datadict["has_more"]
def getUserDetailInfoApi(self, sec_uid):
if sec_uid is None:
return None
start = time.time() # 开始时间
while True:
# 接口不稳定, 有时服务器不返回数据, 需要重新获取
try:
url = self.urls.USER_DETAIL + utils.getXbogus(
f'sec_user_id={sec_uid}&device_platform=webapp&aid=6383')
res = requests.get(url=url, headers=douyin_headers)
datadict = json.loads(res.text)
if datadict is not None and datadict["status_code"] == 0:
return datadict
except Exception as e:
end = time.time() # 结束时间
if end - start > self.timeout:
return None
if __name__ == "__main__":
pass

205
apiproxy/douyin/download.py Normal file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : download.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:18
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import os
import json
import time
import requests
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
from apiproxy.douyin import douyin_headers
from apiproxy.common import utils
class Download(object):
def __init__(self, thread=5, music=True, cover=True, avatar=True, resjson=True, folderstyle=True):
self.thread = thread
self.music = music
self.cover = cover
self.avatar = avatar
self.resjson = resjson
self.folderstyle = folderstyle
def progressBarDownload(self, url, filepath, desc):
response = requests.get(url, stream=True, headers=douyin_headers)
chunk_size = 1024 # 每次下载的数据大小
content_size = int(response.headers['content-length']) # 下载文件总大小
try:
if response.status_code == 200: # 判断是否响应成功
with open(filepath, 'wb') as file, tqdm(total=content_size,
unit="iB",
desc=desc,
unit_scale=True,
unit_divisor=1024,
) as bar: # 显示进度条
for data in response.iter_content(chunk_size=chunk_size):
size = file.write(data)
bar.update(size)
except Exception as e:
# 下载异常 删除原来下载的文件, 可能未下成功
if os.path.exists(filepath):
os.remove(filepath)
print("[ 错误 ]:下载出错\r")
def awemeDownload(self, awemeDict: dict, savePath=os.getcwd()):
if awemeDict is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
try:
# 使用作品 创建时间+描述 当文件夹
file_name = awemeDict["create_time"] + "_" + utils.replaceStr(awemeDict["desc"])
if self.folderstyle:
aweme_path = os.path.join(savePath, file_name)
if not os.path.exists(aweme_path):
os.mkdir(aweme_path)
else:
aweme_path = savePath
# 保存获取到的字典信息
if self.resjson:
try:
with open(os.path.join(aweme_path, file_name + "_result.json"), "w", encoding='utf-8') as f:
f.write(json.dumps(awemeDict, ensure_ascii=False, indent=2))
f.close()
except Exception as e:
print("[ 错误 ]:保存 result.json 失败... 作品名: " + file_name + "\r\n")
desc = file_name[:30]
# 下载 视频
if awemeDict["awemeType"] == 0:
video_path = os.path.join(aweme_path, file_name + "_video.mp4")
if os.path.exists(video_path):
pass
else:
try:
url = awemeDict["video"]["play_addr"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, video_path, "[ 视频 ]:" + desc))
except Exception as e:
print("[ 警告 ]:视频下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 图集
if awemeDict["awemeType"] == 1:
for ind, image in enumerate(awemeDict["images"]):
image_path = os.path.join(aweme_path, file_name + "_image_" + str(ind) + ".jpeg")
if os.path.exists(image_path):
pass
else:
try:
url = image["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, image_path, "[ 图集 ]:" + desc))
except Exception as e:
print("[ 警告 ]:图片下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 音乐
if self.music:
music_name = utils.replaceStr(awemeDict["music"]["title"])
music_path = os.path.join(aweme_path, file_name + "_music_" + music_name + ".mp3")
if os.path.exists(music_path):
pass
else:
try:
url = awemeDict["music"]["play_url"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, music_path, "[ 原声 ]:" + desc))
except Exception as e:
print("[ 警告 ]:音乐(原声)下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 cover
if self.cover and awemeDict["awemeType"] == 0:
cover_path = os.path.join(aweme_path, file_name + "_cover.jpeg")
if os.path.exists(cover_path):
pass
else:
try:
url = awemeDict["video"]["cover"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, cover_path, "[ 封面 ]:" + desc))
except Exception as e:
print("[ 警告 ]:cover下载失败,请重试... 作品名: " + file_name + "\r\n")
# 下载 avatar
if self.avatar:
avatar_path = os.path.join(aweme_path, file_name + "_avatar.jpeg")
if os.path.exists(avatar_path):
pass
else:
try:
url = awemeDict["author"]["avatar"]["url_list"][0]
if url != "":
self.isdwownload = False
self.alltask.append(
self.pool.submit(self.progressBarDownload, url, avatar_path, "[ 头像 ]:" + desc))
except Exception as e:
print("[ 警告 ]:avatar下载失败,请重试... 作品名: " + file_name + "\r\n")
except Exception as e:
print("[ 错误 ]:下载作品时出错\r\n")
def userDownload(self, awemeList: list, savePath=os.getcwd()):
if awemeList is None:
return
if not os.path.exists(savePath):
os.mkdir(savePath)
self.alltask = []
self.pool = ThreadPoolExecutor(max_workers=self.thread)
start = time.time() # 开始时间
for aweme in awemeList:
self.awemeDownload(awemeDict=aweme, savePath=savePath)
wait(self.alltask, return_when=ALL_COMPLETED)
# 检查下载是否完成
while True:
print("[ 提示 ]:正在检查下载是否完成...")
self.isdwownload = True
# 下载上一步失败的
for aweme in awemeList:
self.awemeDownload(awemeDict=aweme, savePath=savePath)
wait(self.alltask, return_when=ALL_COMPLETED)
if self.isdwownload:
break
end = time.time() # 结束时间
print('\n' + '[下载完成]:耗时: %d分钟%d\n' % (int((end - start) / 60), ((end - start) % 60))) # 输出下载用时时间
if __name__ == "__main__":
pass

View File

@ -1,274 +1,315 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/02/11 13:06:23
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
import time
import copy
class Result(object):
def __init__(self):
# 作者信息
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": ""
}
# 将得到的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))
pass
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] = ""
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : result.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:16
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
import time
import copy
class Result(object):
def __init__(self):
# 作者信息
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": ""
}
}
# mix信息
self.mixInfo = {
"cover_url": {
"height": "",
"uri": "",
"url_list": [],
"width": 720
},
"ids": "",
"is_serial_mix": "",
"mix_id": "",
"mix_name": "",
"mix_pic_type": "",
"mix_type": "",
"statis": {
"current_episode": "",
"updated_to_episode": ""
}
}
# 作品信息
self.awemeDict = {
# 作品创建时间
"create_time": "",
# awemeType=0 视频, awemeType=1 图集, awemeType=2 直播
"awemeType": "",
# 作品 id
"aweme_id": "",
# 作者信息
"author": self.authorDict,
# 作品描述
"desc": "",
# 图片
"images": [],
# 音乐
"music": self.musicDict,
# 合集
"mix_info": self.mixInfo,
# 视频
"video": self.videoDict,
# 作品信息统计
"statistics": {
"admire_count": "",
"collect_count": "",
"comment_count": "",
"digg_count": "",
"play_count": "",
"share_count": ""
}
}
# 用户作品信息
self.awemeList = []
# 直播信息
self.liveDict = {
# awemeType=0 视频, awemeType=1 图集, awemeType=2 直播
"awemeType": "",
# 是否在播
"status": "",
# 直播标题
"title": "",
# 直播cover
"cover": "",
# 头像
"avatar": "",
# 观看人数
"user_count": "",
# 昵称
"nickname": "",
# sec_uid
"sec_uid": "",
# 直播间观看状态
"display_long": "",
# 推流
"flv_pull_url": "",
# 分区
"partition": "",
"sub_partition": "",
# 最清晰的地址
"flv_pull_url0": "",
}
# 将得到的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"]
dataNew[item]["url_list"] = copy.deepcopy(dataRaw["bit_rate"][0]["play_addr"]["url_list"])
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))
pass
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] = ""
if __name__ == '__main__':
pass

View File

@ -1,84 +1,80 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Description:TikTok.py
@Date :2023/02/11 13:06:23
@Author :imgyh
@version :1.0
@Github :https://github.com/imgyh
@Mail :admin@imgyh.com
-------------------------------------------------
Change Log :
-------------------------------------------------
'''
class Urls(object):
def __init__(self):
# https://langyue.cc/APIdocV1.0.html
######################################### WEB #########################################
# 首页推荐
self.TAB_FEED = 'https://www.douyin.com/aweme/v1/web/tab/feed/?'
# 用户短信息给多少个用户secid就返回多少的用户信息
self.USER_SHORT_INFO = 'https://www.douyin.com/aweme/v1/web/im/user/info/?'
# 用户详细信息
self.USER_DETAIL = 'https://www.douyin.com/aweme/v1/web/user/profile/other/?'
# 用户作品
# cookies 暂时只需要 __ac_signature, s_v_web_id两个参数, 好像会过期
# url 暂时不需要携带 msToken, X-Bogus, _signature
# 每次返回数据很少
# self.USER_POST = 'https://m.douyin.com/web/api/v2/aweme/post/?'
# 2023/02/19 失效
self.USER_POST = 'https://www.douyin.com/aweme/v1/web/aweme/post/?'
# 作品信息
self.POST_DETAIL = 'https://www.douyin.com/aweme/v1/web/aweme/detail/?'
# 用户喜欢A
# 需要 odin_tt
self.USER_FAVORITE_A = 'https://www.douyin.com/aweme/v1/web/aweme/favorite/?'
# 用户喜欢B
self.USER_FAVORITE_B = 'https://www.iesdouyin.com/web/api/v2/aweme/like/?'
# 用户历史
self.USER_HISTORY = 'https://www.douyin.com/aweme/v1/web/history/read/?'
# 用户收藏
self.USER_COLLECTION = 'https://www.douyin.com/aweme/v1/web/aweme/listcollection/?'
# 用户评论
self.COMMENT = 'https://www.douyin.com/aweme/v1/web/comment/list/?'
# 首页朋友作品
self.FRIEND_FEED = 'https://www.douyin.com/aweme/v1/web/familiar/feed/?'
# 关注用户作品
self.FOLLOW_FEED = 'https://www.douyin.com/aweme/v1/web/follow/feed/?'
# 合集下所有作品
# 只需要X-Bogus
self.USER_MIX = 'https://www.douyin.com/aweme/v1/web/mix/aweme/?'
# 用户所有合集列表
# 需要 ttwid
self.USER_MIX_LIST = 'https://www.douyin.com/aweme/v1/web/mix/list/?'
# 直播
self.LIVE = 'https://live.douyin.com/webcast/room/web/enter/?'
# 音乐
self.MUSIC = 'https://www.douyin.com/aweme/v1/web/music/aweme/?'
# X-Bogus Path
# 60 秒内,请求同一URI累计超过 600 次,封锁IP 300 秒
self.GET_XB_PATH = 'https://tiktok.199933.xyz/xb'
#######################################################################################
if __name__ == '__main__':
Urls()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : urls.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 15:04
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''
class Urls(object):
def __init__(self):
######################################### WEB #########################################
# 首页推荐
self.TAB_FEED = 'https://www.douyin.com/aweme/v1/web/tab/feed/?'
# 用户短信息给多少个用户secid就返回多少的用户信息
self.USER_SHORT_INFO = 'https://www.douyin.com/aweme/v1/web/im/user/info/?'
# 用户详细信息
self.USER_DETAIL = 'https://www.douyin.com/aweme/v1/web/user/profile/other/?'
# 用户作品
self.USER_POST = 'https://www.douyin.com/aweme/v1/web/aweme/post/?'
# 作品信息
self.POST_DETAIL = 'https://www.douyin.com/aweme/v1/web/aweme/detail/?'
# 用户喜欢A
# 需要 odin_tt
self.USER_FAVORITE_A = 'https://www.douyin.com/aweme/v1/web/aweme/favorite/?'
# 用户喜欢B
self.USER_FAVORITE_B = 'https://www.iesdouyin.com/web/api/v2/aweme/like/?'
# 用户历史
self.USER_HISTORY = 'https://www.douyin.com/aweme/v1/web/history/read/?'
# 用户收藏
self.USER_COLLECTION = 'https://www.douyin.com/aweme/v1/web/aweme/listcollection/?'
# 用户评论
self.COMMENT = 'https://www.douyin.com/aweme/v1/web/comment/list/?'
# 首页朋友作品
self.FRIEND_FEED = 'https://www.douyin.com/aweme/v1/web/familiar/feed/?'
# 关注用户作品
self.FOLLOW_FEED = 'https://www.douyin.com/aweme/v1/web/follow/feed/?'
# 合集下所有作品
# 只需要X-Bogus
self.USER_MIX = 'https://www.douyin.com/aweme/v1/web/mix/aweme/?'
# 用户所有合集列表
# 需要 ttwid
self.USER_MIX_LIST = 'https://www.douyin.com/aweme/v1/web/mix/list/?'
# 直播
self.LIVE = 'https://live.douyin.com/webcast/room/web/enter/?'
self.LIVE2 = 'https://webcast.amemv.com/webcast/room/reflow/info/?'
# 音乐
self.MUSIC = 'https://www.douyin.com/aweme/v1/web/music/aweme/?'
#######################################################################################
if __name__ == '__main__':
pass

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@FileName : __init__.py.py
@Project : apiproxy
@Description:
@Author : imgyh
@Mail : admin@imgyh.com
@Github : https://github.com/imgyh
@Site : https://www.imgyh.com
@Date : 2023/5/12 14:43
@Version : v1.0
@ChangeLog
------------------------------------------------
------------------------------------------------
'''

111
config.yml Normal file
View File

@ -0,0 +1,111 @@
#######################################
# 说明:
# 1. 井号(#)为注释
# 2. 缩进严格对齐,使用空格缩进, 注意有些冒号后面有一个空格, 有些没有空格
# 3. 请使用英文字符
# 4. 更多yaml语法请上网查看
#######################################
# 作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址
# (删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)
# 可以设置多个链接, 确保至少一个链接
# 必选
link:
- https://live.douyin.com/759547612580
- https://v.douyin.com/BugmVVD/
- https://v.douyin.com/BugrFTN/
- https://v.douyin.com/B38oovu/
- https://v.douyin.com/S6YMNXs/
# 下载保存位置, 默认当前文件位置
# 必选
path: C:\project\test333
# 是否下载视频中的音乐(True/False), 默认为True
# 可选
music: True
# 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效
# 可选
cover: True
# 是否下载作者的头像(True/False), 默认为True
# 可选
avatar: True
# 是否保存获取到的数据(True/False), 默认为True
# 可选
json: True
folderstyle: True # True -> 每个视频是一个单独的文件夹; False -> 所有视频共用一个文件夹
# True
# user_xxx_xxx
# - like/post/mix
# - 2022-11-28 13.09.56_xxx
# - 2022-11-28 13.09.56_xxx.mp4
# - 2022-11-29 12.09.56_xxx
# - 2022-11-29 12.09.56_xxx.mp4
# False
# user_xxx_xxx
# - like/post/mix
# - 2022-11-28 13.09.56_xxx.mp4
# - 2022-11-29 12.09.56_xxx.mp4
# link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式
# 可选
mode:
- post
- like
- mix
# 下载作品个数设置
# 可选
number:
post: 5 # 主页下作品下载个数设置, 默认为0 全部下载
like: 5 # 主页下喜欢下载个数设置, 默认为0 全部下载
allmix: 1 # 主页下合集下载个数设置, 默认为0 全部下载
mix: 5 # 单个合集下作品下载个数设置, 默认为0 全部下载
music: 5 # 音乐(原声)下作品下载个数设置, 默认为0 全部下载
database: True # 如果不使用数据库, 增量更新将不可用
# 增量下载, 下载作品范围: 抖音最新作品到本地的最新作品之间的作品, 如果本地没有该链接的任何视频则全部下载
# 可配合 number 选项一起使用
# 情况1: number(假如设置5) 和 increase(假如抖音博主更新了3条作品,本地并未下载) 则会获取5条数据并下载
# 情况2: number(假如设置5) 和 increase(假如抖音博主更新了6条作品,本地并未下载) 则会获取6条数据并下载
# 情况3: number(假如设置5) 和 increase(假如本地并未下载该博主视频) 则会获取所有的视频
# 情况4: 当获取主页所有mix时(mode是mix模式)比较特殊, number(allmix) 控制下载多少个合集, increase(allmix) 对每个合集进行增量更新
# 可选
increase:
post: False # 是否开启主页作品增量下载(True/False), 默认为False
like: False # 是否开启主页喜欢增量下载(True/False), 默认为False
allmix: False # 是否开启主页合集增量下载(True/False), 默认为False
mix: False # 是否开启单个合集下作品增量下载(True/False), 默认为False
music: False # 是否开启音乐(原声)下作品增量下载(True/False), 默认为False
# 设置线程数, 默认5个线程
# 可选
thread: 5
# cookie 请登录网页抖音后F12查看
# cookies 和 cookie 二选一, 要使用这种形式, 请注释下面的cookie
# 目前只需要msToken、ttwid、odin_tt、passport_csrf_token、sid_guard
# 可以动态添加, 程序会根据填的键查找,并没有写死, 如果抖音需要更多的cookie自己加上就行了
cookies:
msToken: xxx
ttwid: xxx
odin_tt: xxx
passport_csrf_token: xxx
sid_guard: xxx
# cookie 请登录网页抖音后F12查看
# cookies 和 cookie 二选一, 要使用这种形式, 请注释上面的cookies及包含的所有键值对
# 设置了这个后上面的cookies选项自动失效, 这个优先级更高
# 格式: "name1=value1; name2=value2;" 注意要加冒号
# 冒号中的内容包括不限于以下键值对, 如果抖音需要更多的cookie自己加上就行了
#cookie: "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;"

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

BIN
img/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
img/wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,3 +1,5 @@
requests==2.28.2
flask==2.2.2
pyinstaller==5.7.0
pyinstaller==5.7.0
tqdm==4.65.0
PyYAML==6.0

4
requirements_docker.txt Normal file
View File

@ -0,0 +1,4 @@
requests==2.28.2
flask==2.2.2
tqdm==4.65.0
PyYAML==6.0

View File

@ -6,8 +6,8 @@
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('https://cdn.jsdelivr.net/gh/imgyh/tiktok/static/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,163 +1,237 @@
// 发 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();
})
});
// 发 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) {
result = result.data
if (result.awemeType === 0) {
$("#awemeType").html("预览视频");
$("#AwemeOrLive").html("下载视频");
$("#video").attr("href", removeHttp(result.video.play_addr.url_list[0]));
$("#pre_video").attr("src", removeHttp(result.video.play_addr.url_list[0]));
$("#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= " + removeHttp(images[i].url_list[0]) + "></li>"
}
document.getElementById("images").innerHTML = licontent;
$("#video").attr("style", "display:none;");//隐藏 video
}
if (result.awemeType === 0 || result.awemeType === 1) {
$("#cover").attr("href", removeHttp(result.video.cover_original_scale.url_list[0]));
$("#pre_video").attr("poster", removeHttp(result.video.dynamic_cover.url_list[0]));
$("#music").attr("href", removeHttp(result.music.play_url.url_list[0]));
$("#avatar").attr("src", removeHttp(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);
$("#icons").attr("style", "display:flex;");//显示 icons
$("#icon").attr("style", "display:table-row;");//显示 icon
$("#music").attr("style", "display:inline;");//显示 music
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:block;");//显示 download
// alert("SUCCESS");
// 执行弹框
narnSuccess();
}
if (result.awemeType === 2) {
if (result.status === 4) {
$("#loading").attr("style", "display:none;");//隐藏 loading
$("#download").attr("style", "display:none;");//隐藏 download
// 执行弹框
narnWarn()
} else {
$("#AwemeOrLive").html("下载直播");
$("#awemeType").html("预览直播");
$("#video").attr("href", removeHttp(result.flv_pull_url0));
$("#pre_video").attr("src", removeHttp(result.flv_pull_url0));
$("#cover").attr("href", removeHttp(result.cover));
$("#pre_video").attr("poster", result.cover);
$("#avatar").attr("src", removeHttp(result.avatar));
$("#avatar").attr("alt", result.nickname);
$("#nickname").html(result.nickname);
$("#desc").html(result.title);
$("#video").attr("style", "display:inline;");//显示 video
$("#icons").attr("style", "display:none;");//隐藏 icons
$("#icon").attr("style", "display:none;");//隐藏 icon
$("#music").attr("style", "display:none;");//隐藏 music
$("#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: []
})
}
function narnWarn() {
naranja().warn({
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();
}
// 预览直播
if (awemeType === "预览直播") {
if (flvjs.isSupported()) {//检查flvjs能否正常使用
var videoElement = document.getElementById('pre_video');//使用id选择器找到第二步设置的dom元素
var flvPlayer = flvjs.createPlayer({//创建一个新的flv播放器对象
type: 'flv',//类型flv
url: $("#video").attr("href")//flv文件地址
});
flvPlayer.attachMediaElement(videoElement);//将flv视频装载进video元素内
flvPlayer.load();//载入视频
flvPlayer.play();//播放视频,如果不想要自动播放,去掉本行
/*弹出视频播放层*/
$("#show-video").show();
}
}
});
/*关闭视频播放层*/
$(".video-close").click(function () {
var videoElement = document.getElementById("pre_video");
videoElement.pause()
$("#show-video").hide();
})
});
function removeHttp(url) {
if (typeof (url) == 'string') {
url = url.replace(/^https?:/, '');
}
return url;
}

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

File diff suppressed because one or more lines are too long

View File

@ -1,170 +1,175 @@
<!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>
<!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="../static/img/favicon.ico" rel="shortcut icon"
type="image/x-icon">
<!-- //Favicons -->
<!--/Style-CSS -->
<link rel="stylesheet" href="../static/css/style.css" type="text/css"
media="all"/>
<!--//Style-CSS -->
<!-- font-awesome-icons -->
<link href="//lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- //font-awesome-icons -->
<!-- naranja 右下角弹框提示 https://github.com/e1016/naranja-->
<link rel="stylesheet" href="//unpkg.com/naranja@1.0.1/lib/naranja.min.css" type="text/css"/>
<!-- //naranja -->
<!-- viewerjs 图片查看器 https://github.com/fengyuanchen/viewerjs-->
<link rel="stylesheet" href="//lib.baomitu.com/viewerjs/1.11.2/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>2.支持直播解析,需要网页版直播链接 https://live.douyin.com/343806013144</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 id="icons" class="icons">
<div id="icon" class="icon">
<i class="fa fa-heart" style="color:#fd325c;" aria-hidden="true"></i>
<p id="aweme_digg_count"></p>
</div>
<div id="icon" class="icon">
<i class="fa fa-comment" style="color:#efeeec;" aria-hidden="true"></i>
<p id="aweme_comment_count"></p>
</div>
<div id="icon" class="icon">
<i class="fa fa-star" style="color:#fcb505;" aria-hidden="true"></i>
<p id="aweme_collect_count"></p>
</div>
<div id="icon" 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 id="AwemeOrLive"></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="//lib.baomitu.com/jquery/1.8.2/jquery.min.js" type="text/javascript"></script>
<script src="//unpkg.com/naranja@1.0.1/lib/naranja.min.js" type="text/javascript"></script>
<script src="//lib.baomitu.com/viewerjs/1.11.2/viewer.min.js" type="text/javascript"></script>
<script src="../static/js/custom.js" type="text/javascript"></script>
<script src="//lib.baomitu.com/flv.js/0.0.2/flv.min.js" type="text/javascript"></script>
</body>
</html>