# YTdown **Repository Path**: aloot-cold/ytdown ## Basic Information - **Project Name**: YTdown - **Description**: 解析引擎内置、下载全原生、产物进公共媒体库;界面换成液态玻璃底栏 + App Store 式「卡片浮现」转场。 - **Primary Language**: Java - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-30 - **Last Updated**: 2026-10-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # YTDown 3.0.0 > Android 原生 YouTube 视频下载器。Kotlin + Jetpack Compose + Coroutines + StateFlow + WorkManager + OkHttp。 > 版本 `3.0.0`(versionCode `300`)。 **上游仓库**:`github.com/Alootcold/YouTubeDownForAndroid`(同系列另有 PC 版 `YouTubeDownloader`、macOS 版 `YouTubeMacDownloader`、浏览器扩展 `Youtube-Downloader-Extension`,官网 `videokeep.pages.dev`)。 > ⚠️ 上游账号 `Alootcold` 已于 **2026-07-15** 被 GitHub 暂停(依据 DSA 第 16 条), > 仓库对外一律返回 404,源码与历史 release notes 目前均无法下载。 > 已向 GitHub 提交申诉工单 **#4660609**,等待人工复核。 > 详见 [`CHANGELOG.md`](CHANGELOG.md) 顶部的说明。 --- ## 1. 这个工程解决什么 一句话:**用内置的轻量引擎换直链,用 Android 原生能力把文件稳定下载下来。** ``` 粘贴链接 → 解析引擎(只出直链) → OkHttp(原生下载) → WorkManager(前台服务化) → StateFlow(UI) ``` 两条硬约束在代码里的落点: | 约束 | 落点 | |---|---| | 引擎只解析,不下载 | `data/resolver/YtDlpVideoResolver.kt` 只产出直链,拿到 URL 就退出 | | 进度不许乱跳 | `DownloadState.progress` 是**纯派生值** `(视频已下+音频已下)/(视频总+音频总)`,分母在下载前一次性冻结 | --- ## 2. 目录结构 ``` YTDown/ ├── PRODUCT.md 产品说明书(红线映射表、验收标准) ├── DESIGN.md 设计规范(令牌、组件、页面规格、无障碍) ├── CHANGELOG.md 更新日志(按版本倒序) ├── settings.gradle.kts ├── build.gradle.kts ├── gradle.properties ├── gradle/libs.versions.toml 依赖版本集中管理 └── app/ ├── build.gradle.kts versionName 3.0.0 / versionCode 300 └── src/main/ ├── AndroidManifest.xml ├── java/com/ytdown/app/ │ ├── YtDownApplication.kt │ ├── MainActivity.kt │ ├── core/ AppContainer、格式化、下载目录 │ ├── data/ │ │ ├── model/ DownloadState(唯一状态载体)等 │ │ ├── local/ DataStore 设置 + JSON 任务/历史落盘 │ │ ├── engine/ 引擎安装、版本检测、热更新 │ │ ├── resolver/ VideoResolver 契约 + 引擎实现 │ │ └── repository/ DownloadRepository(唯一状态写入方)、CatalogRepository │ ├── download/ DownloadWorker、SegmentDownloader、SpeedMeter、MediaMerger │ └── ui/ │ ├── theme/ 颜色 / 排印 / 间距圆角动效令牌 │ ├── components/ LiquidBottomTabs(官方液态玻璃底栏逐字移植)、GlassSurface、VideoRow、进度行、折叠文本 │ ├── navigation/ YTDownApp(返回转场与路由)、Routes、采样节流 │ └── screens/ home / detail / downloads / history / settings └── res/ ├── values/ 英文 + 主题 + 颜色 ├── values-zh-rCN/ 简体中文 ├── values-zh-rTW/ 繁體中文 ├── drawable/ 通知图标与离线占位封面 └── xml/ backup / file_paths ``` > **应用图标**:本轮不提供图标资源,清单临时使用系统默认图标 > `@android:drawable/sym_def_app_icon`;相关自适应图标文件已移除,无悬空引用。 > 后续按 `DESIGN.md` 第 9 节的规格补齐。 --- ## 3. 在 Android Studio 里打开 1. `File → Open` 选择本目录,等待 Gradle Sync。 2. **若提示缺少 `gradle-wrapper.jar`**(仓库只分发文本文件,未附带二进制): - 方案 A(推荐):`Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle distribution` 选择 `gradle-8.11.1-all.zip`,Sync 即可; - 方案 B:本机装有 Gradle 8.11+ 时执行 `gradle wrapper --gradle-version 8.11.1` 生成 wrapper。 3. 编译产物为 debug APK,版本信息可在「设置 → 关于」查看。 环境要求:JDK 17、Android SDK Platform 36、Build Tools 34+。 --- ## 4. 版本与兼容性 | 项 | 值 | 说明 | |---|---|---| | `minSdk` | 31 | Android 12 起 | | `targetSdk` | 36 | 保持现有运行时行为;Android 17 (API 37) 设备上行为不变 | | `compileSdk` | 37 | Android 17;`backdrop 2.0.1` 与 Compose 1.12.x 强制要求 | | AGP | 9.4.1 | 内建 Kotlin(KGP 2.4.10 已在根 buildscript 钉版) | | Gradle | 9.6.0 | AGP 9.4 的最低/默认 Gradle 版本 | | Build-Tools | 36.0.0 | AGP 9.4 默认值 | **为什么必须升级到 AGP 9 / compileSdk 37**: `backdrop:2.0.1`(以及它带来的 Compose 1.12.1、core 1.19.1、navigation 2.10.2、lifecycle 2.11.0) 在 AAR metadata 中声明了 `compileSdk 37 + AGP ≥ 9.1.0`,用 AGP 8.x 会在 `:app:checkDebugAarMetadata` 阶段直接失败——这不是能绕过的配置问题。 **AGP 9 的内建 Kotlin**: AGP 9.0 起默认启用内建 Kotlin,`org.jetbrains.kotlin.android` 插件与新 DSL 不兼容。 本工程因此**不应用**该插件,只用 `com.android.application` + `org.jetbrains.kotlin.plugin.compose`, 并在根 `buildscript` 里把 KGP 钉在 **2.4.10**(与 Compose 编译器插件严格同版); AGP 9 内建 Kotlin 默认只带 KGP 2.2.10,版本过低会报 "compiled with an incompatible version of Kotlin"。 预测式返回需要 Android 13+;13 以下自动退化为传统返回(系统按版本处理,无需额外分支)。 应用层不注册任何 `BackHandler` / `PredictiveBackHandler`:手势由 Navigation Compose 2.10 的 `NavHost` 内建处理器接管,转场为跟手横向滑动(见 `DESIGN.md`)。 --- ## 5. 解析引擎(唯一外部依赖) 引擎**已内置进 APK**(`youtubedl-android` 的 `library` + `ffmpeg` 0.19.0, 含 Python 运行时 + yt-dlp 脚本),**开箱即用**,无需先下载安装。 引擎只负责换直链,**不参与任何字节下载**。 版本检查地址在 `app/build.gradle.kts` 的 `ENGINE_MANIFEST_URL`, 默认指向 yt-dlp 官方 GitHub Release API: ``` https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest ``` - 自动检测:冷启动时检查一次,24 小时节流,**只检测不静默安装**; - 手动更新:设置页「立即检查 / 立即更新」,下载后校验 SHA-256、原子替换,失败自动回滚到内置版本; - 想换别的解析后端:实现 `VideoResolver` 接口后在 `AppContainer.videoResolver` 处替换实现即可,UI 与下载层零改动。 下载目录:成品经 MediaStore 发布到 `Movies/YTDown`(视频)与 `Music/YTDown`(音频), 相册 / 音乐 App 里直接可见,无需任何存储权限。 --- ## 6. 三个关键算法 **进度(字节加权,单调不回退)** ```kotlin progress = (videoBytes + audioBytes) / (videoTotalBytes + audioTotalBytes) ``` - 分母在进入 `DOWNLOADING` 前冻结:分片响应的 `Content-Length` 只接受第一次有效值; - 字节计数只增不减,跨进程重试同样单调; - UI 侧 `animateFloatAsState` 200ms 过渡,状态流 `sample(500)` 节流。 **网速(1 秒滑动窗口瞬时值)** ```kotlin speed = (当前已下载字节 - 1 秒前已下载字节) / 实际间隔 ``` 首次采样直接返回 `0`,不做全程平均、不延迟显示;采样点每秒刷新一次。 **重试语义** - 失败后「重试」:清空分片文件与计数,从头再来; - 暂停后「继续」:保留分片文件与计数,走 HTTP Range 断点续传,进度从原处接着走。 --- ## 7. 设计约束速查 | 项 | 值 | |---|---| | 间距 | 4 / 8 / 12 / 16 / 24 | | 圆角 | 6 / 10 / 16 | | 动效 | `CubicBezierEasing(0.2f, 0f, 0f, 1f)`,120 / 200 / 320ms | | 主色 | 朱砂 `#C2410C` + 青玉 `#256E62`(≤2 个),其余全中性灰 | | 亚克力 Dock | **官方 `LiquidBottomTabs` 的 5 个文件逐字移植**(backdrop 2.0.1 + kyant-shapes 1.2.1):三层叠加(玻璃条 / 隐形图标行录背景 / 水滴玻璃块消费 `CombinedBackdrop`)→ Tab 图标被玻璃折射;手势与逆变换逻辑一字未改,只替换 3 Tab / 中文 / Material 图标 / 配色,色散模糊折射用官方默认值 | | 触控区 | ≥ 48dp | | 字体 | 平台系统字型,不打包第三方字体 | 详见 `DESIGN.md`(含对比度校验表与组件规格)。 --- ## 8. 当前 mock 与待接入项 - 首页列表:只放**用户自己解析出来的**条目(`CatalogRepository` 只接受 `upsert`, 没有预置假数据);空列表时显示空状态; - 搜索 / 排行榜:接口已抽象(`CatalogRepository`),接入真实 API 时替换实现; - 音视频合并:**内置 ffmpeg**,`-c copy` 无转码合并为单文件; 合并失败则保留「视频 + 音频」双文件,不让任务失败; - 内置播放器:3.0.0 不做。 --- ## 9. 依赖版本一览 Kotlin 2.4.10 · AGP 9.4.1 · Gradle 9.6.0 · Compose BOM 2026.09.00(UI 1.12.1 / Material3 1.4.0)· activity 1.13.0 / activity-compose 1.13.0(预测式返回要求 activity ≥ 1.6.0、activity-compose ≥ 1.8.0)· navigation-compose 2.10.2 · navigationevent 1.1.2 · work-runtime 2.12.0 · datastore-preferences 1.1.2 · okhttp 4.12.0 · coroutines 1.10.1 · coil 2.7.0 · **backdrop 2.0.1**(Kyant0 / AndroidLiquidGlass,液态玻璃:真实折射 + 景深模糊 + 色散) > **为什么工具链升了级**:`backdrop:2.0.1` 的二进制元数据是 Kotlin **2.4.10**、要求 Compose UI **≥ 1.12.0**, > 与原来的 Kotlin 2.1.0 / Compose 1.7.8 不兼容(会报 "compiled with an incompatible version of Kotlin")。 > 因此 Kotlin / Compose BOM / AGP / Gradle 一并升级到满足其要求的版本。 --- ## 10. 前台服务类型(Android 14+ 合规) 下载任务跑在 WorkManager 的 `SystemForegroundService` 里。**从 Android 14(API 34)起, 面向 34+ 的应用必须为每一个前台服务声明类型**,否则调用 `startForeground()` 会抛 `SecurityException`。 官方对 `dataSync` 类型的定义就是「Data upload or download」——正好对应本 App 「用户点击下载 → 原生 OkHttp 写入文件」的行为,所以选 `dataSync`(**不需要** `specialUse`, 那个类型要额外写 `` 使用场景说明)。 ### 代码侧三要件(缺一不可) | # | 位置 | 内容 | | --- | --- | --- | | ① | `AndroidManifest.xml` | `` | | ② | `AndroidManifest.xml` | `` | | ③ | `download/DownloadWorker.kt` | `ForegroundInfo(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)` | ①里的 `tools:node="merge"` 是关键:`androidx.work` 库**自带**了同名 service 的声明, 但**不含** `foregroundServiceType`,所以必须由应用侧合并补上;少了 `merge` 会与库的声明冲突。 ### Android 15+ 的额外限制 官方明确:**面向 Android 15+ 的应用不允许从 `BOOT_COMPLETED` 广播接收器启动 `dataSync` 前台服务**。 设备重启后 WorkManager 会恢复未完成的任务,此时 `setForeground()` 可能抛 `ForegroundServiceStartNotAllowedException`。`DownloadWorker` 已兜住这一条:把任务明确标记为 **失败并提示重试**,而不是让 worker 崩掉、在下载列表里留下永远卡住的僵尸任务。 ### Google Play 上架时还要做的一件事(不在代码里) Play Console 要求为**每一个**你声明的前台服务类型提供 **justification(用途说明)**, 否则审核会被打回。对本 App: - 声明的类型:`dataSync` - 建议的 justification:**「用户主动发起的文件下载 / 用户发起的传输操作」** (对应 Play 对 `dataSync` 的可选理由 "User-initiated file transfer" / "Transferring files") - 同时在 Play Console 的 **Data safety** 里如实申报:下载流量会**离开设备**到 YouTube(CDN), 应用**不收集**任何数据。 另外提醒:`dataSync` 在 Android 15+ 有**每 24 小时累计 6 小时**的运行上限(超时后系统会 `onTimeout` 并停掉服务)。单个视频下载远达不到,但批量下载时需要留意。