Skip to content

Lux 资料整理

  • 依据 lux 仓库: branch master (dd00f6d)

学习前先克隆项目:

cd external
git clone --depth 1 https://github.com/iawia002/lux.git

项目概述

Lux 是一个用 Go 编写的快速、简单的视频下载器,支持从多个视频网站下载视频和音频。

  • GitHub: https://github.com/iawia002/lux
  • 本地路径: external/lux

项目结构

lux/
├── main.go           # 程序入口
├── app/              # CLI 应用逻辑 (使用 urfave/cli)
├── config/           # 配置相关
├── downloader/       # 下载器核心
├── extractors/        # 视频网站提取器 (支持 40+ 网站)
├── parser/           # URL 解析器
├── request/          # HTTP 网络请求封装
├── utils/            # 工具函数
└── test/             # 测试文件

学习阶段

阶段 1: 基础入门

  1. 运行项目

  2. 安装 Go 1.21+

  3. cd external/lux
  4. go run . --help 查看帮助

  5. 理解程序入口

  6. 阅读 external/lux/main.go

  7. 阅读 external/lux/app/app.go 了解 CLI 结构

阶段 2: 核心模块

建议学习顺序: request → extractors → downloader (由底层到上层)

  1. 网络请求 (request/)

  2. 了解如何封装 HTTP 请求

  3. 处理 cookie、proxy 等

  4. 提取器模块 (extractors/)

  5. external/lux/extractors/extractors.go - 提取器接口

  6. external/lux/extractors/types.go - 数据类型定义

  7. 下载器模块 (downloader/)

  8. external/lux/downloader/downloader.go - 下载核心逻辑

  9. external/lux/downloader/types.go - 数据结构

阶段 3: 深入理解

  1. 学习一个具体提取器

  2. 推荐从简单的开始: extractors/youtube/extractors/bilibili/

  3. 理解如何解析视频 URL
  4. 理解如何提取视频流信息

  5. 理解 URL 解析 (parser/)

  6. 如何识别不同的网站

  7. 如何路由到正确的提取器

阶段 4: 实践

  1. 尝试修改
  2. 添加一个新的网站支持
  3. 修改下载逻辑
  4. 添加单元测试

关键概念

  • Extractor: 提取器接口,每个网站一个实现
  • Stream: 视频流 (如 720P, 1080P)
  • Part: 视频分段 (有些视频需要分段下载后合并)
  • Data: 提取的完整数据 (包含多个 Stream)
  • Mux: 音视频合并 (使用 ffmpeg)

第三方库

用途
urfave/cli/v2 CLI 框架,构建命令行应用
gocolly/colly/v2 网页爬虫框架,用于抓取网页内容
PuerkitoBio/goquery HTML 解析库,类似 jQuery 的 DOM 操作
kkdai/youtube/v2 YouTube 专用提取器
dop251/goja JavaScript 引擎,用于执行 JS 代码
robertkrimen/otto 另一个 JavaScript 引擎
fatih/color 彩色终端输出
cheggaaa/pb/v3 终端进度条
buger/jsonparser 高性能 JSON 解析
json-iterator/go 高性能 JSON 序列化/反序列化
itchyny/gojq jq 风格的 JSON 查询
EDDYCJY/fake-useragent 随机 User-Agent 生成
MercuryEngineering/CookieMonster Cookie 管理
kr/pretty 格式化输出 (用于调试)
pkg/errors 错误处理增强

本地实验

参考资源

  • external/lux/README.md - 项目文档
  • external/lux/CONTRIBUTING.md - 贡献指南

Downloader 模块流程分析

本文档分析 lux 项目中 downloader 包的主要下载流程。

1. 核心数据结构

Downloader 结构体

type Downloader struct {
    Bar    *pb.ProgressBar  // 进度条
    option Options         // 下载选项
}

Options 配置项

字段 说明
InfoOnly 仅显示信息,不下载
Silent 静默模式,不输出信息
Stream 指定要下载的流类型
AudioOnly 仅下载音频
MultiThread 是否启用多线程下载
ThreadNumber 线程数量
ChunkSizeMB 分块大小(MB)
UseAria2RPC 使用 Aria2 RPC 下载
EmbedSubtitle 内嵌字幕到视频

2. 主要下载流程

入口方法: Download()

┌─────────────────────────────────────────────────────────────┐
│                      Download(data)                         │
├─────────────────────────────────────────────────────────────┤
│  1. 验证 streams 不为空                                     │
│  2. 按 Size 排序所有 streams                                │
│  3. 如果 InfoOnly: 打印信息后返回                            │
│  4. 获取输出文件名 (title)                                   │
│  5. 选择要下载的 stream                                      │
│  6. 下载字幕 (Caption)                                      │
│  7. 检查是否使用 Aria2 RPC                                  │
│  8. 检查文件是否已存在                                       │
│  9. 初始化进度条                                            │
│ 10. 下载视频/音频                                           │
│ 11. 合并分片 (如果有多个 parts)                              │
│ 12. 内嵌字幕 (如果启用)                                     │
└─────────────────────────────────────────────────────────────┘

单文件 vs 多分片下载

单文件流程 (len(stream.Parts) == 1)

┌─────────────────────────────────────┐
│           单文件下载                  │
├─────────────────────────────────────┤
│ if MultiThread:                     │
│     multiThreadSave()               │
│ else:                               │
│     save()                          │
└─────────────────────────────────────┘

多分片流程 (len(stream.Parts) > 1)

┌─────────────────────────────────────────────┐
│              多分片下载                       │
├─────────────────────────────────────────────┤
│  1. 使用 WaitGroupPool 并行下载各分片         │
│  2. 每个分片调用 save() 或 multiThreadSave() │
│  3. 等待所有分片下载完成                       │
│  4. 合并所有分片为完整文件                      │
│  5. 内嵌字幕 (可选)                           │
└─────────────────────────────────────────────┘

3. 核心下载方法

3.1 save() - 单线程下载

func (downloader *Downloader) save(part *extractors.Part, refer, fileName string) error

流程:

┌─────────────────────────────────────────────────────────┐
│                      save()                             │
├─────────────────────────────────────────────────────────┤
│  1. 生成最终文件路径                                      │
│  2. 检查文件是否已完整下载 (跳过)                          │
│  3. 创建临时文件 (xxx.download)                           │
│  4. 检查临时文件是否已存在 (断点续传)                       │
│  5. 设置 HTTP Headers (Referer, Range)                   │
│  6. 下载数据到临时文件                                    │
│     - 如果 ChunkSizeMB > 0: 分块下载                       │
│     - 否则: 单次下载                                      │
│  7. 支持重试 (RetryTimes)                                │
│  8. 关闭文件并重命名为最终文件名                           │
└─────────────────────────────────────────────────────────┘

断点续传支持:

  • 下载前检查 xxx.download 临时文件是否存在
  • 如果存在,读取已下载大小,设置 Range: bytes={size}- 头部
  • 从断点位置继续下载

3.2 multiThreadSave() - 多线程下载

func (downloader *Downloader) multiThreadSave(dataPart *extractors.Part, refer, fileName string) error

流程:

┌─────────────────────────────────────────────────────────────┐
│                  multiThreadSave()                         │
├─────────────────────────────────────────────────────────────┤
│  1. 检查最终文件和临时文件是否存在                           │
│  2. 扫描已有的分片文件 (.part0, .part1, ...)               │
│  3. 分析已下载状态:                                         │
│     - 找出已完成的分片                                      │
│     - 找出未完成的分片                                      │
│     - 计算已下载总大小                                      │
│  4. 如果已下载大小 == 总大小: 合并并返回                     │
│  5. 使用 WaitGroupPool 并行下载未完成的分片                 │
│  6. 每个分片独立下载,支持断点续传                          │
│  7. 合并所有分片                                            │
└─────────────────────────────────────────────────────────────┘

分片文件结构:

  • 每个分片存储为 xxx.part{index} 文件
  • 文件头包含 FilePartMeta 元数据 (Index, Start, End, Cur)
  • 实际数据从元数据之后开始

3.3 writeFile() - HTTP 写入文件

func (downloader *Downloader) writeFile(url string, file *os.File, headers map[string]string) (int64, error)
  • 发起 HTTP GET 请求
  • 使用 progress bar 包装 writer 追踪进度
  • 返回写入的字节数

4. 字幕下载

func (downloader *Downloader) caption(url, fileName, ext string, transform func([]byte) ([]byte, error)) error
  • 下载字幕/弹幕文件
  • 支持格式转换 (如 XML -> SRT)
  • 如果启用 EmbedSubtitle: 内嵌到视频中

5. Aria2 RPC 支持

func (downloader *Downloader) aria2(title string, stream *extractors.Stream) error
  • 通过 Aria2 JSON-RPC 接口添加下载任务
  • 支持分片并行下载
  • 需要配置 Aria2Token, Aria2Method, Aria2Addr

6. 文件合并 (FFmpeg)

当视频有多个分片时,需要调用 utils 包的 ffmpeg 函数合并:

// 通用合并 (支持音视频合并)
// 使用 ffmpeg: -c:v copy -c:a copy
utils.MergeFilesWithSameExtension(parts, mergedFilePath)

// MP4 合并 (使用 concat demuxer)
// 使用 ffmpeg concat 模式,自动处理 aac_adtstoasc bitstream filter
utils.MergeToMP4(parts, mergedFilePath, title)

// 内嵌字幕到视频
// 根据容器格式选择字幕 codec (mp4 -> mov_text, webm -> webvtt)
utils.EmbedSubtitles(mergedFilePath, subtitlePaths, subtitleLangs)

FFmpeg 相关函数位于 utils/ffmpeg.go:

函数 用途
MergeFilesWithSameExtension() 合并相同扩展名文件,音视频合成
MergeToMP4() 合并 MP4 分片,添加 aac_adtstoasc 滤镜
EmbedSubtitles() 内嵌字幕到视频容器

7. 关键文件

文件 说明
../external/lux/downloader/downloader.go 主下载逻辑
../external/lux/downloader/types.go 类型定义
../external/lux/downloader/utils.go 辅助函数
../external/lux/downloader/downloader_test.go 测试用例
../external/lux/utils/ffmpeg.go FFmpeg 合并/转码

8. 流程图

用户调用 Download()
┌──────────────────┐
│  检查 InfoOnly   │
└────────┬─────────┘
         │ 是
┌──────────────────┐
│   打印视频信息    │
└────────┬─────────┘
         │ 否
┌──────────────────┐
│  下载字幕文件    │
└────────┬─────────┘
┌──────────────────┐
│ 检查 Aria2 RPC   │
└────────┬─────────┘
         │ 是
┌──────────────────┐
│  调用 aria2()    │
└────────┬─────────┘
         │ 否
┌──────────────────┐
│ 检查文件已存在   │
└────────┬─────────┘
         │ 是
┌──────────────────┐
│    跳过下载      │
└────────┬─────────┘
         │ 否
┌──────────────────┐
│ 初始化进度条     │
└────────┬─────────┘
    ┌────┴────┐
    │         │
  单文件    多分片
    │         │
    ▼         ▼
┌────────┐  ┌────────────────┐
│ save() │  │ 并行下载各分片 │
│ 或     │  │   (WaitGroup)  │
│multi   │  └────────┬───────┘
│Thread  │           │
│Save()  │           ▼
└────────┘  ┌────────────────┐
            │  合并分片文件   │
            └────────┬───────┘
            ┌────────────────┐
            │  内嵌字幕(可选) │
            └────────────────┘
Backlinks (1)
flowchart LR
  n0["AI"]
  n1["Database"]
  n2["Dev Tools"]
  n3["Emoji"]
  n4["Frontend"]
  n5["Game Dev"]
  n6["Collection"]
  n7["Languages"]
  n8["Maps"]
  n9["Media"]
  n10["Monitor"]
  n11["Plans"]
  n12["对象存储基本用法(Bucket / Object / 常用操作)"]
  n13["对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"]
  n14["Object Storage"]
  n15["挂载 Bucket 为本地文件系统(FUSE Mount)"]
  n16["对象存储签名 URL(Signed URL)原理与实战"]
  n17["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
  n18["Prototypes"]
  n19["Research"]
  n20["Better Auth 源码阅读指南"]
  n21["DuckDB 环境与基本使用"]
  n22["DuckDB 实战研究"]
  n23["DuckDB 模拟数据"]
  n24["PostgreSQL 数据用 DuckDB 加速查询"]
  n25["HK 角色阅读"]
  n26["HK 台词阅读"]
  n27["Hollow Knight 英语主题"]
  n28["HK 物品阅读"]
  n29["HK 地点阅读"]
  n30["HK 世界观 / Lore 阅读"]
  n31["HK 英语学习素材清单"]
  n32["英语学习 Dashboard"]
  n33["English Scraps Archive"]
  n34["English Scraps 使用指南"]
  n35["Jellyfin 源码阅读指南"]
  n36["Lux 资料整理"]
  n37["Nest Commander 学习资料"]
  n38["NestJS 源码阅读指南"]
  n39["Protomaps 自建底图研究"]
  n40["自制 PMTiles 地图(最简单例子)"]
  n41["MapLibre 集成 Protomaps"]
  n42["PMTiles 格式与工具链"]
  n43["上海地区底图项目"]
  n44["Redash 源码阅读指南"]
  n45["Rust 学习计划"]
  n46["shadcn/ui 源码阅读指南"]
  n47["TRIP 项目核心原理与代码阅读指南"]
  n1 --> n22
  n6 --> n0
  n6 --> n1
  n6 --> n2
  n6 --> n3
  n6 --> n4
  n6 --> n5
  n6 --> n7
  n6 --> n8
  n6 --> n9
  n6 --> n10
  n6 --> n11
  n6 --> n19
  n12 --> n13
  n12 --> n16
  n12 --> n17
  n12 --> n18
  n13 --> n12
  n13 --> n15
  n13 --> n16
  n13 --> n17
  n14 --> n12
  n14 --> n13
  n14 --> n15
  n14 --> n16
  n14 --> n17
  n15 --> n12
  n15 --> n16
  n15 --> n17
  n16 --> n18
  n17 --> n12
  n17 --> n13
  n17 --> n16
  n17 --> n18
  n18 --> n12
  n18 --> n15
  n18 --> n16
  n18 --> n17
  n18 --> n39
  n19 --> n20
  n19 --> n22
  n19 --> n32
  n19 --> n35
  n19 --> n36
  n19 --> n37
  n19 --> n38
  n19 --> n39
  n19 --> n44
  n19 --> n45
  n19 --> n46
  n19 --> n47
  n21 --> n23
  n22 --> n1
  n22 --> n19
  n22 --> n21
  n22 --> n23
  n22 --> n24
  n23 --> n21
  n23 --> n24
  n24 --> n22
  n24 --> n23
  n27 --> n25
  n27 --> n26
  n27 --> n28
  n27 --> n29
  n27 --> n30
  n27 --> n31
  n27 --> n34
  n31 --> n26
  n32 --> n27
  n32 --> n33
  n32 --> n34
  n34 --> n32
  n34 --> n33
  n39 --> n8
  n39 --> n40
  n39 --> n41
  n39 --> n42
  n39 --> n43
  n40 --> n42
  n41 --> n18
  n41 --> n43
  n42 --> n40
  n42 --> n43
  n43 --> n41
  n43 --> n42
  click n0 "../../../collection/ai/" "AI"
  click n1 "../../../collection/database/" "Database"
  click n2 "../../../collection/dev-tools/" "Dev Tools"
  click n3 "../../../collection/emoji/" "Emoji"
  click n4 "../../../collection/frontend/" "Frontend"
  click n5 "../../../collection/game-dev/" "Game Dev"
  click n6 "../../../collection/" "Collection"
  click n7 "../../../collection/languages/" "Languages"
  click n8 "../../../collection/maps/" "Maps"
  click n9 "../../../collection/media/" "Media"
  click n10 "../../../collection/monitor/" "Monitor"
  click n11 "../../../collection/scraps/plans/" "Plans"
  click n12 "../../../knowledge/infrastructure/cloud/object-storage/basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
  click n13 "../../../knowledge/infrastructure/cloud/object-storage/data-migration/" "对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"
  click n14 "../../../knowledge/infrastructure/cloud/object-storage/" "Object Storage"
  click n15 "../../../knowledge/infrastructure/cloud/object-storage/mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
  click n16 "../../../knowledge/infrastructure/cloud/object-storage/signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
  click n17 "../../../knowledge/infrastructure/cloud/object-storage/vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
  click n18 "../../../prototypes/" "Prototypes"
  click n19 "../../" "Research"
  click n20 "../better-auth/" "Better Auth 源码阅读指南"
  click n21 "../duckdb/basic-usage/" "DuckDB 环境与基本使用"
  click n22 "../duckdb/" "DuckDB 实战研究"
  click n23 "../duckdb/mock-data/" "DuckDB 模拟数据"
  click n24 "../duckdb/postgresql-acceleration/" "PostgreSQL 数据用 DuckDB 加速查询"
  click n25 "../english/hollow-knight/characters/" "HK 角色阅读"
  click n26 "../english/hollow-knight/dialogues/" "HK 台词阅读"
  click n27 "../english/hollow-knight/" "Hollow Knight 英语主题"
  click n28 "../english/hollow-knight/items/" "HK 物品阅读"
  click n29 "../english/hollow-knight/locations/" "HK 地点阅读"
  click n30 "../english/hollow-knight/lore/" "HK 世界观 / Lore 阅读"
  click n31 "../english/hollow-knight/resources/" "HK 英语学习素材清单"
  click n32 "../english/" "英语学习 Dashboard"
  click n33 "../english/scraps/archive/" "English Scraps Archive"
  click n34 "../english/scraps/" "English Scraps 使用指南"
  click n35 "../jellyfin/" "Jellyfin 源码阅读指南"
  click n36 "./" "Lux 资料整理"
  click n37 "../nest-commander/" "Nest Commander 学习资料"
  click n38 "../nestjs/" "NestJS 源码阅读指南"
  click n39 "../protomaps/" "Protomaps 自建底图研究"
  click n40 "../protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
  click n41 "../protomaps/maplibre/" "MapLibre 集成 Protomaps"
  click n42 "../protomaps/pmtiles/" "PMTiles 格式与工具链"
  click n43 "../protomaps/shanghai-map/" "上海地区底图项目"
  click n44 "../redash/" "Redash 源码阅读指南"
  click n45 "../rust/" "Rust 学习计划"
  click n46 "../shadcn-ui/" "shadcn/ui 源码阅读指南"
  click n47 "../trip/" "TRIP 项目核心原理与代码阅读指南"