Full Code of lecepin/douyin-downloader for AI

master 308dd2e9ea62 cached
19 files
33.1 KB
9.7k tokens
17 symbols
1 requests
Download .txt
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
================================================
<?php
error_reporting(0);

$real_url = $g_url = $_GET['url'];
preg_match('/https:\/\/v.douyin.com\/[^\s ]*/', $real_url, $match);

if ($match[0]) {
    file_get_contents($match[0]);
    preg_match('/https:[^\s]*/', $http_response_header[6], $match);
    $real_url = $match[0];
}

preg_match('/https:\/\/www.(douyin.com|iesdouyin.com\/share)\/video\/([^?&=\s\/]+)/', $real_url, $match);
$real_id = $match[2];

if ($real_id) {
    $video_info = json_decode(file_get_contents('https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=' . $real_id));
    $v_url = str_replace('playwm', 'play', $video_info->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];
}
?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h2>抖音视频下载</h2>
    <div>
        <form>
            <input name="url" autocomplete="off" placeholder="请输入抖音分享的地址" style="width: 300px;" />
            <button>解析</button>
        </form>

        <?php echo $g_url ? '解析地址:' . $g_url : ''; ?>

        <?php if (!$v_url && $g_url) {
            echo '<br /> 解析失败,请重!';
        } ?>

        <?php if ($v_url) {
            echo '<hr />';
            echo $v_title;
            echo '<br />' . $v_ratio;
            echo '<br /> 下载地址:<a target="_blank" href="' . $v_url . '">' . $v_url . '</a>';
            echo '<br /><img src="' . $v_cover . '" />';
        } ?>
        <a href="https://github.com/lecepin/douyin-downloader" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
                <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
                <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
                <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
            </svg></a>
        <style>
            .github-corner:hover .octo-arm {
                animation: octocat-wave 560ms ease-in-out
            }

            @keyframes octocat-wave {

                0%,
                100% {
                    transform: rotate(0)
                }

                20%,
                60% {
                    transform: rotate(-25deg)
                }

                40%,
                80% {
                    transform: rotate(10deg)
                }
            }

            @media (max-width:500px) {
                .github-corner:hover .octo-arm {
                    animation: none
                }

                .github-corner .octo-arm {
                    animation: octocat-wave 560ms ease-in-out
                }
            }
        </style>
    </div>

</body>

</html>


================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script>
      document.addEventListener('contextmenu', event => event.preventDefault());
    </script>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>


================================================
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 (
    <div className="App">
      <Space className="App-topbar">
        <span>类型</span>
        <Popover
          placement="bottomLeft"
          content={
            <div style={{ maxWidth: 520, wordBreak: "break-all" }}>
              <p>单个视频:如 “9.94 Eho:/ 我把事情拖到最后一分钟做不是因为我懒而是那个时候我更老了 做事情也更成熟了# 叮叮当当舞 # 杰星编舞 https://v.douyin.com/2vLYnCp/ 复制此链接,打开Dou音搜索,直接观看视频!”</p>
              <hr />
              <p>用户所有视频:如“https://www.douyin.com/user/MS4wLjABAAAABsXrboCFzZqd2HrqUMBCUmMWRHDqjMdrW0WndNDaFAbO924AWWF7fk8YJUdZYmjk”</p>
            </div>
          }
          trigger="hover"
        >
          <QuestionCircleOutlined />
        </Popover>
        <Select
          value={parseType}
          disabled={false}
          onChange={(value) => setParseType(value)}
        >
          <Select.Option key="video">单 个 视 频</Select.Option>
          <Select.Option key="userVideo">用户所有视频</Select.Option>
        </Select>
        <Input
          placeholder={ parseType === "video" ? "请填入分享的视频链接" : "请填入用户的页面网址" }
          disabled={false}
          value={url}
          onChange={({ target }) => { setUrl(target.value); }}
        ></Input>
        <Button
          type="primary"
          loading={isParseLoading}
          onClick={async () => {
            setIsParseLoading(true);

            try {
              if (parseType === "video") {
                const id = await invoke("get_url_id", { addr: url });
                const info = await invoke("get_video_info_by_id", { id });

                setVideoInfo([info]);
              } else {
                const { video_count, uid } = await invoke("get_user_info_by_url", { addr: url, });
                const info = await invoke("get_list_by_user_id", { uid, count: video_count, maxCursor: 0 });

                setVideoInfo(info);
              }
            } catch (error) {
              message.error(error);
            }

            setIsParseLoading(false);
          }}
        >
          解析{parseType === "video" ? "单个视频" : "所有视频"}
        </Button>
        <Button
          icon={<GithubFilled />}
          onClick={() => open_url("https://github.com/lecepin/douyin-downloader") }
        >
          <b> Star</b>
        </Button>
      </Space>

      {videoInfo?.length > 0 ? (
        <>
          <div>
            <Button
              loading={allDownloading}
              icon={<CloudDownloadOutlined />}
              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);
              }}
            >
              全部下载
            </Button>
          </div>
          <Table
            sticky
            rowKey="id"
            dataSource={videoInfo}
            columns={[
              {
                title: "序号",
                dataIndex: "index",
                key: "index",
                ellipsis: true,
                width: 80,
                render: (_a, _b, index) => index + 1,
              },
              {
                title: "封面",
                dataIndex: "cover",
                key: "cover",
                render: (value) =>
                  value ? (
                    <img
                      style={{ maxHeight: 100, maxWidth: 100 }}
                      src={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 }) => (
                  <div>
                    {status[id]?.status == "done" ? (
                      <Button
                        icon={<EyeOutlined />}
                        type="primary"
                        onClick={() => {
                          status[id].filePath &&
                            openFile(status[id].filePath).catch(() => {});
                        }}
                        size="small"
                        ghost
                      >
                        查看
                      </Button>
                    ) : (
                      <Button
                        icon={<DownloadOutlined />}
                        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,
                            });
                          }
                        }}
                      >
                        下载
                      </Button>
                    )}
                    &nbsp; &nbsp;
                    <Button
                      icon={<PlaySquareOutlined />}
                      onClick={() => open_url(url)}
                      size="small"
                    >
                      预览
                    </Button>
                  </div>
                ),
              },
            ]}
            pagination={false}
          ></Table>
        </>
      ) : (
        <div className="App-logo">
          <img src={imgLogo} />
          <Button
            icon={<GithubFilled />}
            size="large"
            onClick={() =>
              open_url("https://github.com/lecepin/douyin-downloader")
            }
          >
            <b> Star</b>
          </Button>
        </div>
      )}
      <BackTop style={{ left: 50 }} />
    </div>
  );
}

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(<App />);


================================================
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<String, String> {
    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<VideoInfo, String> {
    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::<serde_json::Value>(&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<serde_json::Value, String> {
    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::<serde_json::Value>(&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<String, String> {
    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<UserInfo, String> {
    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::<serde_json::Value>(&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<serde_json::Value, String> {
    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::<serde_json::Value>(&res_text).map_err(|_| "解析错误")?)
}

// 取用户下的所有个人视频
#[tauri::command]
#[async_recursion]
pub async fn get_list_by_user_id(
    uid: &str,
    count: u64,
    max_cursor: u64,
) -> Result<Vec<VideoInfo>, String> {
    let mut res: Vec<VideoInfo> = 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::<serde_json::Value>(&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::<Vec<VideoInfo>>()
            .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
      }
    ]
  }
}
Download .txt
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
Download .txt
SYMBOL INDEX (17 symbols across 4 files)

FILE: src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: src-tauri/src/command.rs
  type VideoInfo (line 9) | pub struct VideoInfo {
  type UserInfo (line 18) | pub struct UserInfo {
  type DownloadProgress (line 26) | pub struct DownloadProgress {
  function get_url_id (line 34) | pub async fn get_url_id(addr: String) -> Result<String, String> {
  function get_video_info_by_id (line 71) | pub async fn get_video_info_by_id(id: &str) -> Result<VideoInfo, String> {
  function get_video_full_info_by_id (line 110) | pub async fn get_video_full_info_by_id(id: &str) -> Result<serde_json::V...
  function download_video (line 125) | pub async fn download_video(
  function get_user_info_by_url (line 175) | pub async fn get_user_info_by_url(addr: &str) -> Result<UserInfo, String> {
  function get_user_full_info_by_url (line 214) | pub async fn get_user_full_info_by_url(addr: &str) -> Result<serde_json:...
  function get_list_by_user_id (line 235) | pub async fn get_list_by_user_id(
  function get_list_like_by_user_id (line 289) | pub fn get_list_like_by_user_id() {}
  function get_list_favorite_by_user_id (line 293) | pub fn get_list_favorite_by_user_id() {}
  function get_list_by_hash_tag (line 297) | pub fn get_list_by_hash_tag() {}

FILE: src-tauri/src/main.rs
  function main (line 9) | fn main() {

FILE: src/App.js
  function App (line 10) | function App() {
  function open_url (line 285) | function open_url(url) {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
  {
    "path": ".github/workflows/main.yml",
    "chars": 1266,
    "preview": "name: Release\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n\njobs:\n  release:\n    strategy:\n      fail-fast: f"
  },
  {
    "path": ".gitignore",
    "chars": 335,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "README.md",
    "chars": 2260,
    "preview": "## 抖音下载器\n\n⚠ 接口挂了,暂时没时间更新软件了。着急下载的话,可以直接在 电脑 浏览器查看抖音,进入 Devtools,执行以下代码进行下载:\n\n```js\nopen(\n  document.querySelectorAll(\"vi"
  },
  {
    "path": "config-overrides.js",
    "chars": 332,
    "preview": "const { override, addLessLoader, adjustStyleLoaders } = require('customize-cra');\n\nmodule.exports = override(\n  addLessL"
  },
  {
    "path": "package.json",
    "chars": 876,
    "preview": "{\n  \"name\": \"douyin-downloader\",\n  \"version\": \"0.1.1\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@ant-design/icons\": \""
  },
  {
    "path": "php_ver.php",
    "chars": 3807,
    "preview": "<?php\nerror_reporting(0);\n\n$real_url = $g_url = $_GET['url'];\npreg_match('/https:\\/\\/v.douyin.com\\/[^\\s ]*/', $real_url,"
  },
  {
    "path": "public/index.html",
    "chars": 1829,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
  },
  {
    "path": "public/manifest.json",
    "chars": 492,
    "preview": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n     "
  },
  {
    "path": "public/robots.txt",
    "chars": 67,
    "preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "src/App.js",
    "chars": 9504,
    "preview": "import { Button, Input, Space, Select, Popover, Table, message, BackTop } from \"antd\";\nimport { useState } from \"react\";"
  },
  {
    "path": "src/App.less",
    "chars": 421,
    "preview": ".App {\n  padding: 20px;\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n\n  &-topbar {\n    width: 100%;\n    p"
  },
  {
    "path": "src/index.js",
    "chars": 197,
    "preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport \"antd/dist/antd.min.css\";\nimport App from \"./"
  },
  {
    "path": "src-tauri/.gitignore",
    "chars": 73,
    "preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "chars": 938,
    "preview": "[package]\nname = \"app\"\nversion = \"0.1.0\"\ndescription = \"A Tauri App\"\nauthors = [\"lecepin\"]\nlicense = \"\"\nrepository = \"\"\n"
  },
  {
    "path": "src-tauri/build.rs",
    "chars": 37,
    "preview": "fn main() {\n  tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/src/command.rs",
    "chars": 8522,
    "preview": "use async_recursion::async_recursion;\nuse futures_util::StreamExt;\nuse std::fs::File;\nuse std::io::Write;\nuse std::path:"
  },
  {
    "path": "src-tauri/src/main.rs",
    "chars": 1427,
    "preview": "#![cfg_attr(\n    all(not(debug_assertions), target_os = \"windows\"),\n    windows_subsystem = \"windows\"\n)]\n\nuse tauri::{Ab"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "chars": 1474,
    "preview": "{\n  \"$schema\": \"../node_modules/@tauri-apps/cli/schema.json\",\n  \"build\": {\n    \"beforeBuildCommand\": \"npm run build\",\n  "
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the lecepin/douyin-downloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (33.1 KB), approximately 9.7k tokens, and a symbol index with 17 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!