Repository: lecepin/douyin-downloader
Branch: master
Commit: 308dd2e9ea62
Files: 19
Total size: 33.1 KB
Directory structure:
gitextract_iq12ra4d/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── README.md
├── config-overrides.js
├── package.json
├── php_ver.php
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src/
│ ├── App.js
│ ├── App.less
│ └── index.js
└── src-tauri/
├── .gitignore
├── Cargo.toml
├── build.rs
├── icons/
│ └── icon.icns
├── src/
│ ├── command.rs
│ └── main.rs
└── tauri.conf.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Node.js setup
uses: actions/setup-node@v1
with:
node-version: 16
- name: Rust setup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies and build web
run: npm i && npm run tauri build
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
with:
tagName: v__VERSION__ # tauri-action replaces \_\_VERSION\_\_ with the app version
releaseName: 'v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: true
prerelease: false
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea/
package-lock.json
================================================
FILE: README.md
================================================
## 抖音下载器
⚠ 接口挂了,暂时没时间更新软件了。着急下载的话,可以直接在 电脑 浏览器查看抖音,进入 Devtools,执行以下代码进行下载:
```js
open(
document.querySelectorAll("video")[
document.querySelectorAll("video").length == 1 ? 0 : 1
].children[0].src
);
```
---
> 在线解析版本:[https://apis.leping.fun/dy/](https://apis.leping.fun/dy/),代码在 [php_ver.php](./php_ver.php) 中。

- 支持下载无水印视频
- 支持下载某个账号号的所有视频
## 下载软件
软件采用 Rust + Tauri 编写,安装包非常小,只有 5MB 左右。
- Windows 下载地址:[douyin-downloader_0.1.0_x64_en-US.msi](https://github.com/lecepin/douyin-downloader/releases/download/v0.1.0/douyin-downloader_0.1.0_x64_en-US.msi)
- Mac 下载地址:[douyin-downloader_0.1.0_x64.dmg](https://github.com/lecepin/douyin-downloader/releases/download/v0.1.0/douyin-downloader_0.1.0_x64.dmg)
> 国内访问速度慢,可以使用以下加速地址:
> - Windows 下载地址:[douyin-downloader_0.1.0_x64_en-US.msi](https://github.91chi.fun/https://github.com//lecepin/douyin-downloader/releases/download/v0.1.0/douyin-downloader_0.1.0_x64_en-US.msi)
> - Mac 下载地址:[douyin-downloader_0.1.0_x64.dmg](https://github.91chi.fun/https://github.com//lecepin/douyin-downloader/releases/download/v0.1.0/douyin-downloader_0.1.0_x64.dmg)
## 使用
如下方式使用。
### 下载单个视频

手机端、网页端都可,点击分享按钮,把口令复制到本软件中,进行解析即可。
口令类似 `1.20 fBt:/ 拿好纸巾(有双倍福利呦) # 美女合集 # 气质美女 # 变装 @抖音小助手 https://v.douyin.com/23FsM5g/ 复制此链接,打开Dou音搜索,直接观看视频!`

### 下载某个账号号的所有视频
网页版,进入个人页,网址类似 `https://www.douyin.com/user/MS4wLjABAAAAWiOs23d6NtmiUg98zONd6wQhmPsy1WLwZn0jEaCbDL8`:

复制网址,粘贴到 “用户所有视频” 类型下,解析即可:

点击 “全部下载” 按钮,就可以进行全部下载了:

一键下载完成:

================================================
FILE: config-overrides.js
================================================
const { override, addLessLoader, adjustStyleLoaders } = require('customize-cra');
module.exports = override(
addLessLoader({ lessOptions: { javascriptEnabled: true } }),
adjustStyleLoaders(({ use: [, , postcss] }) => {
postcss.options = { postcssOptions: postcss.options };
}),
function (config) { return config; },
);
================================================
FILE: package.json
================================================
{
"name": "douyin-downloader",
"version": "0.1.1",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@tauri-apps/api": "^1.0.2",
"antd": "^4.21.7",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"start": "cross-env BROWSER=none react-app-rewired start",
"build": "react-app-rewired build",
"tauri": "tauri"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@tauri-apps/cli": "^1.0.5",
"cross-env": "^7.0.3",
"customize-cra": "^1.0.0",
"less": "^4.1.2",
"less-loader": "^11.0.0",
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1"
}
}
================================================
FILE: php_ver.php
================================================
item_list[0]->video->play_addr->url_list[0]);
$v_title = $video_info->item_list[0]->desc;
$v_ratio = $video_info->item_list[0]->video->ratio;
$v_cover = $video_info->item_list[0]->video->cover->url_list[0];
}
?>
Document
抖音视频下载
解析失败,请重!';
} ?>
';
echo $v_title;
echo '
' . $v_ratio;
echo '
下载地址:
' . $v_url . '';
echo '

';
} ?>
================================================
FILE: public/index.html
================================================
React App
================================================
FILE: public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: src/App.js
================================================
import { Button, Input, Space, Select, Popover, Table, message, BackTop } from "antd";
import { useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import { open as openFile } from "@tauri-apps/api/shell";
import { QuestionCircleOutlined, PlaySquareOutlined, GithubFilled, EyeOutlined, DownloadOutlined, CloudDownloadOutlined } from "@ant-design/icons";
import imgLogo from "./logo.png";
import "./App.less";
export default function App() {
const [parseType, setParseType] = useState("video");
const [url, setUrl] = useState("");
const [videoInfo, setVideoInfo] = useState([]);
const [isParseLoading, setIsParseLoading] = useState(false);
const [status, setStatus] = useState({});
const [allDownloading, setAllDownloading] = useState(false);
return (
类型
单个视频:如 “9.94 Eho:/ 我把事情拖到最后一分钟做不是因为我懒而是那个时候我更老了 做事情也更成熟了# 叮叮当当舞 # 杰星编舞 https://v.douyin.com/2vLYnCp/ 复制此链接,打开Dou音搜索,直接观看视频!”
用户所有视频:如“https://www.douyin.com/user/MS4wLjABAAAABsXrboCFzZqd2HrqUMBCUmMWRHDqjMdrW0WndNDaFAbO924AWWF7fk8YJUdZYmjk”
}
trigger="hover"
>
{ setUrl(target.value); }}
>
}
onClick={() => open_url("https://github.com/lecepin/douyin-downloader") }
>
Star
{videoInfo?.length > 0 ? (
<>
}
type="primary"
ghost
onClick={async () => {
const dir = await open({ directory: true });
if (!dir) {
return;
}
setAllDownloading(true);
for (let _index = 0; _index < videoInfo.length; _index++) {
const { id, title, url } = videoInfo[_index];
const fileName = `${title}${Date.now()}.mp4`;
try {
setStatus((status) => ({
...status,
[id]: {
status: "downloading",
},
}));
const filePath = await invoke("download_video", {
url,
writePath: dir,
fileName,
id: id,
});
setStatus((status) => ({
...status,
[id]: {
status: "done",
filePath,
},
}));
} catch (error) {
message.error(error);
setStatus({
...status,
[id]: null,
});
}
}
setAllDownloading(false);
}}
>
全部下载
index + 1,
},
{
title: "封面",
dataIndex: "cover",
key: "cover",
render: (value) =>
value ? (
) : null,
width: 100,
},
{
title: "标题",
dataIndex: "title",
key: "title",
ellipsis: true,
},
{
title: "分辨率",
dataIndex: "ratio",
key: "ratio",
width: 100,
ellipsis: true,
},
{
title: "操作",
dataIndex: "action",
key: "action",
width: "180px",
render: (_, { url, title, id }) => (
{status[id]?.status == "done" ? (
}
type="primary"
onClick={() => {
status[id].filePath &&
openFile(status[id].filePath).catch(() => {});
}}
size="small"
ghost
>
查看
) : (
}
loading={
status[id]?.status == "downloading" || allDownloading
}
type="primary"
size="small"
onClick={async () => {
const fileName = `${title}${Date.now()}.mp4`;
const dir = await open({ directory: true });
if (!dir) {
return;
}
try {
setStatus({
...status,
[id]: {
status: "downloading",
},
});
const filePath = await invoke("download_video", {
url,
writePath: dir,
fileName,
id: id,
});
setStatus({
...status,
[id]: {
status: "done",
filePath,
},
});
} catch (error) {
message.error(error);
setStatus({
...status,
[id]: null,
});
}
}}
>
下载
)}
}
onClick={() => open_url(url)}
size="small"
>
预览
),
},
]}
pagination={false}
>
>
) : (
}
size="large"
onClick={() =>
open_url("https://github.com/lecepin/douyin-downloader")
}
>
Star
)}
);
}
function open_url(url) {
const el = document.createElement("a");
el.style.display = "none";
el.setAttribute("target", "_blank");
el.href = url;
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}
================================================
FILE: src/App.less
================================================
.App {
padding: 20px;
height: 100vh;
display: flex;
flex-direction: column;
&-topbar {
width: 100%;
padding-bottom: 20px;
& > .ant-space-item {
&:nth-last-child(3) {
flex-grow: 1;
}
}
}
&-logo {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
& > img {
margin-bottom: 10px;
}
}
}
================================================
FILE: src/index.js
================================================
import React from "react";
import ReactDOM from "react-dom/client";
import "antd/dist/antd.min.css";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render();
================================================
FILE: src-tauri/.gitignore
================================================
# Generated by Cargo
# will have compiled files and executables
/target/
================================================
FILE: src-tauri/Cargo.toml
================================================
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["lecepin"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.0.4", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.5", features = ["api-all"] }
reqwest = { version = "0.11.11", features = ["stream"] }
futures-util = "0.3.21"
async-recursion = "1.0.0"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
================================================
FILE: src-tauri/build.rs
================================================
fn main() {
tauri_build::build()
}
================================================
FILE: src-tauri/src/command.rs
================================================
use async_recursion::async_recursion;
use futures_util::StreamExt;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tauri::regex::Regex;
#[derive(serde::Serialize)]
pub struct VideoInfo {
title: String,
ratio: String,
cover: String,
url: String,
id: String,
}
#[derive(serde::Serialize)]
pub struct UserInfo {
nick_name: String,
video_count: u64,
avatar: String,
uid: String,
}
#[derive(Clone, serde::Serialize)]
pub struct DownloadProgress {
current: u64,
total: u64,
id: String,
}
// 取各种 url 的 id
#[tauri::command]
pub async fn get_url_id(addr: String) -> Result {
let mut _addr = addr;
let mut result = "".to_string();
let reg_get_share_url = Regex::new(r#"https://v.douyin.com/[^\s ]*"#).unwrap();
let reg_get_id = Regex::new(r#"https://www.douyin.com/video/([^?&=\s]+)"#).unwrap();
match reg_get_share_url.captures(&_addr) {
Some(cap) => {
let url = cap.get(0).map_or("", |value| value.as_str());
if url.len() > 0 {
_addr = reqwest::get(url)
.await
.map_err(|_| "网络错误")?
.url()
.as_str()
.to_string();
}
}
_ => (),
}
if let Some(cap) = reg_get_id.captures(&_addr) {
result = cap
.get(1)
.map_or("".to_string(), |value| value.as_str().to_string());
}
if result.len() > 0 {
return Ok(result);
}
Err("解析失败".into())
}
// 取视频信息
#[tauri::command]
pub async fn get_video_info_by_id(id: &str) -> Result {
let res_text = reqwest::get(
"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=".to_string() + id,
)
.await
.map_err(|_| "网络错误")?
.text()
.await
.map_err(|_| "网络错误")?;
let raw_info = serde_json::from_str::(&res_text).map_err(|_| "解析错误")?;
let url = raw_info["item_list"][0]["video"]["play_addr"]["url_list"][0]
.as_str()
.unwrap_or("")
.replace("playwm", "play");
if url.len() == 0 {
return Err("此视频地址无效".into());
}
Ok(VideoInfo {
title: raw_info["item_list"][0]["desc"]
.as_str()
.unwrap_or("")
.to_string(),
ratio: raw_info["item_list"][0]["video"]["ratio"]
.as_str()
.unwrap_or("")
.to_string(),
cover: raw_info["item_list"][0]["video"]["cover"]["url_list"][0]
.as_str()
.unwrap_or("")
.to_string(),
id: raw_info["aweme_id"].as_str().unwrap_or("").to_string(),
url,
})
}
// 取完整视频信息
#[tauri::command]
pub async fn get_video_full_info_by_id(id: &str) -> Result {
let res_text = reqwest::get(
"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=".to_string() + id,
)
.await
.map_err(|_| "网络错误")?
.text()
.await
.map_err(|_| "网络错误")?;
Ok(serde_json::from_str::(&res_text).map_err(|_| "解析错误")?)
}
// 视频下载
#[tauri::command]
pub async fn download_video(
url: &str,
write_path: &str,
file_name: &str,
id: &str,
window: tauri::Window,
) -> Result {
let file_path = Path::new(write_path).join(file_name.replace(
|item: char| ['\\', '/', ':', '?', '*', '"', '<', '>', '|'].contains(&item),
"_",
));
let res = reqwest::Client::new()
.get(url)
.header("user-agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36")
.send()
.await
.map_err(|_| "网络错误")?;
let res_len = res.content_length().unwrap_or(0);
if res_len == 0 {
return Err("视频长度为 0".into());
}
let mut downloaded_len = 0_u64;
let mut stream = res.bytes_stream();
let mut file = File::create(&file_path).map_err(|_| "文件创建失败")?;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|_| "网络错误")?;
file.write_all(&chunk).map_err(|_| "文件写入失败")?;
downloaded_len += chunk.len() as u64;
window
.emit(
"e_download_progress",
DownloadProgress {
current: downloaded_len,
total: res_len,
id: id.into(),
},
)
.unwrap();
}
Ok(file_path.to_str().unwrap().into())
}
// 取用户信息
#[tauri::command]
pub async fn get_user_info_by_url(addr: &str) -> Result {
let reg_get_user_id = Regex::new(r#"https://www.douyin.com/user/([\w-]+)"#).unwrap();
let uid = reg_get_user_id
.captures(addr)
.map_or(Err("地址错误"), |cap| {
Ok(cap.get(1).map_or("", |value| value.as_str()))
})?;
let res_text =
reqwest::get("https://www.iesdouyin.com/web/api/v2/user/info/?sec_uid=".to_string() + uid)
.await
.map_err(|_| "网络错误")?
.text()
.await
.map_err(|_| "网络错误")?;
let raw_info = serde_json::from_str::(&res_text).map_err(|_| "解析错误")?;
let video_count = raw_info["user_info"]["aweme_count"]
.as_u64()
.unwrap_or(0_u64);
if video_count == 0 {
return Err("用户视频数为 0".into());
}
Ok(UserInfo {
nick_name: raw_info["user_info"]["nickname"]
.as_str()
.unwrap_or("")
.to_string(),
video_count,
avatar: raw_info["user_info"]["avatar_larger"]["url_list"][0]
.as_str()
.unwrap_or("")
.to_string(),
uid: uid.into(),
})
}
// 取完整用户信息
#[tauri::command]
pub async fn get_user_full_info_by_url(addr: &str) -> Result {
let reg_get_user_id = Regex::new(r#"https://www.douyin.com/user/(\w+)"#).unwrap();
let uid = reg_get_user_id
.captures(addr)
.map_or(Err("地址错误"), |cap| {
Ok(cap.get(1).map_or("", |value| value.as_str()))
})?;
let res_text =
reqwest::get("https://www.iesdouyin.com/web/api/v2/user/info/?sec_uid=".to_string() + uid)
.await
.map_err(|_| "网络错误")?
.text()
.await
.map_err(|_| "网络错误")?;
Ok(serde_json::from_str::(&res_text).map_err(|_| "解析错误")?)
}
// 取用户下的所有个人视频
#[tauri::command]
#[async_recursion]
pub async fn get_list_by_user_id(
uid: &str,
count: u64,
max_cursor: u64,
) -> Result, String> {
let mut res: Vec = vec![];
let res_text =
reqwest::get(format!("https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid={uid}&count={count}&max_cursor={max_cursor}"))
.await
.map_err(|_| "网络错误")?
.text()
.await
.map_err(|_| "网络错误")?;
let raw_info = serde_json::from_str::(&res_text).map_err(|_| "解析错误")?;
let has_more = raw_info["has_more"].as_bool().unwrap_or(false);
let max_cursor = raw_info["max_cursor"].as_u64().unwrap_or(0_u64);
let video_list = match raw_info["aweme_list"].is_array() {
true => raw_info["aweme_list"].as_array().unwrap(),
_ => {
return Err("用户视频数为 0".into());
}
};
res.append(
video_list
.iter()
.map(|item| VideoInfo {
title: item["desc"].as_str().unwrap_or("").to_string(),
ratio: item["video"]["ratio"].as_str().unwrap_or("").to_string(),
cover: item["video"]["cover"]["url_list"][0]
.as_str()
.unwrap_or("")
.to_string(),
url: item["video"]["play_addr"]["url_list"][0]
.as_str()
.unwrap_or("")
.replace("playwm", "play"),
id: item["aweme_id"].as_str().unwrap_or("").to_string(),
})
.collect::>()
.as_mut(),
);
if !has_more {
return Ok(res);
}
res.append(get_list_by_user_id(uid, count, max_cursor).await?.as_mut());
Ok(res)
}
// 取用户下的所有点赞视频
#[allow(dead_code)]
pub fn get_list_like_by_user_id() {}
// 取用户下的所有收藏视频
#[allow(dead_code)]
pub fn get_list_favorite_by_user_id() {}
// 取 #tag 下的所有视频
#[allow(dead_code)]
pub fn get_list_by_hash_tag() {}
================================================
FILE: src-tauri/src/main.rs
================================================
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use tauri::{AboutMetadata, Menu, MenuItem, Submenu};
mod command;
fn main() {
let mut menu = Menu::new();
#[cfg(target_os = "macos")]
{
menu = menu.add_submenu(Submenu::new(
"抖音视频下载",
Menu::new()
.add_native_item(MenuItem::About("".into(), AboutMetadata::default()))
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::Quit),
));
}
menu = menu.add_submenu(Submenu::new(
"文件",
Menu::new()
.add_native_item(MenuItem::CloseWindow)
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste),
));
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
command::get_url_id,
command::get_video_info_by_id,
command::get_video_full_info_by_id,
command::download_video,
command::get_user_info_by_url,
command::get_user_full_info_by_url,
command::get_list_by_user_id,
])
.menu(menu)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
================================================
FILE: src-tauri/tauri.conf.json
================================================
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run start",
"devPath": "http://localhost:3000",
"distDir": "../build",
"withGlobalTauri": true
},
"package": {
"productName": "douyin-downloader",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": true
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "lecepin",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.lecepin.douyindownloader",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "抖音视频下载",
"width": 800
}
]
}
}