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) 中。 ![image](https://user-images.githubusercontent.com/11046969/182412269-8ac2dee8-fb30-40b1-b4b3-190c99496759.png) - 支持下载无水印视频 - 支持下载某个账号号的所有视频 ## 下载软件 软件采用 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) ## 使用 如下方式使用。 ### 下载单个视频 ![image](https://user-images.githubusercontent.com/11046969/182413296-1a97050c-f7fd-4912-bf09-e064d67c888f.png) 手机端、网页端都可,点击分享按钮,把口令复制到本软件中,进行解析即可。 口令类似 `1.20 fBt:/ 拿好纸巾(有双倍福利呦) # 美女合集 # 气质美女 # 变装 @抖音小助手 https://v.douyin.com/23FsM5g/ 复制此链接,打开Dou音搜索,直接观看视频!` ![image](https://user-images.githubusercontent.com/11046969/182413713-7d540831-44cc-42ef-99d9-a30c54300da1.png) ### 下载某个账号号的所有视频 网页版,进入个人页,网址类似 `https://www.douyin.com/user/MS4wLjABAAAAWiOs23d6NtmiUg98zONd6wQhmPsy1WLwZn0jEaCbDL8`: ![image](https://user-images.githubusercontent.com/11046969/182414514-e2e15549-ec85-4dad-b821-3382b16f4abd.png) 复制网址,粘贴到 “用户所有视频” 类型下,解析即可: ![image](https://user-images.githubusercontent.com/11046969/182414926-10d4526d-fff3-495a-8b9b-e2949e3018e4.png) 点击 “全部下载” 按钮,就可以进行全部下载了: ![image](https://user-images.githubusercontent.com/11046969/182415286-851f802d-305b-4684-b6a2-c10976c1338d.png) 一键下载完成: ![image](https://user-images.githubusercontent.com/11046969/182416193-f009597e-9ee4-4c41-aca4-eecbfeafe76d.png) ================================================ 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); }} > {videoInfo?.length > 0 ? ( <>
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" ? ( ) : ( )}    
), }, ]} pagination={false} >
) : (
)} ); } 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 } ] } }