FlyNarwhal 是一个基于 Compose Multiplatform 的飞牛影视跨平台客户端。本文档详细分析了其核心架构,特别是 fntv-proxy 组件的作用以及登录后的功能关联关系。
| 属性 | 值 |
|---|---|
| 类型 | 本地前置代理 (Local Forward Proxy) |
| 默认端口 | 1999 |
| 监听地址 | http://127.0.0.1:1999 |
| 实现语言 | Go (编译后的二进制文件) |
| 启动方式 | 应用启动时自动启动,关闭时自动停止 |
fntv-proxy 可执行文件按以下优先级查找:
<user.dir>/fntv-proxy<user.dir>/../fntv-proxy<compose.application.resources.dir>/fntv-proxy<exeDir>/app/resources/fntv-proxy<exeDir>/fntv-proxy<exeDir>/app/resources/fntv-proxy~/Library/Application Support/fly-narwhal/proxy~/.local/share/fly-narwhal/proxy| 操作系统 | 架构 | 可执行文件名 |
|---|---|---|
| Windows | AMD64/ARM64/386 | fntv-proxy.exe |
| macOS | AMD64/ARM64 | fntv-proxy |
| Linux | AMD64/ARM64 | fntv-proxy |
fntv-proxy 的主要职责是为飞牛影视的 API 请求添加鉴权相关的请求头,具体包括:
┌─────────────────────────────────────────────────────────────┐ │ FlyNarwhal 应用 │ │ (Kotlin + Compose) │ └────────────────────┬────────────────────────────────────────┘ │ │ HTTP 请求 (携带基础头) │ Authorization, Cookie, User-Agent ↓ ┌─────────────────────────────────────────────────────────────┐ │ fntv-proxy (端口 1999) │ │ (Go) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 1. 接收应用请求 │ │ │ │ 2. 添加鉴权请求头(签名、设备标识等) │ │ │ │ 3. 转发到飞牛影视官方 API │ │ │ │ 4. 返回响应给应用 │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ HTTPS 请求 (完整鉴权) ↓ ┌─────────────────────────────────────────────────────────────┐ │ 飞牛影视官方 API 服务器 │ │ (feiniu.com / fnos.net) │ └─────────────────────────────────────────────────────────────┘
FlyNarwhal 支持两种登录方式:
5666http://[IP地址]:5666 或 https://[域名]:5666http://192.168.1.100:5666https://my-fnos.local:5666https://5ddd.com/[FN_ID] 或 https://[域名].fnos.netmode=relay 表示使用中继模式┌─────────────────────────────────────────────────────────────┐ │ 登录界面 (LoginScreen) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 输入字段: │ │ │ │ - 主机地址 (host) │ │ │ │ - 端口 (port, 默认 5666) │ │ │ │ - 用户名/邮箱 (username) │ │ │ │ - 密码 (password) │ │ │ │ - HTTPS 开关 (isHttps) │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ performLogin() ↓ ┌─────────────────────────────────────────────────────────────┐ │ LoginStateManager.handleLogin() │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 1. 验证输入完整性 │ │ │ │ 2. 保存显示地址 (displayHost, displayPort) │ │ │ │ 3. 处理主机地址: │ │ │ │ - 如果是 FN ID: 转换为 "5ddd.com/[FN_ID]" │ │ │ │ - 如果是域名: 保持原样 │ │ │ │ - 如果是 IP: 保持原样 │ │ │ │ 4. 中继模式判断: │ │ │ │ - 如果包含 5ddd.com 或 fnos.net │ │ │ │ → isHttps = true │ │ │ │ → 添加 cookie "mode=relay" │ │ │ │ → port = 0 (不使用端口) │ │ │ │ 5. 保存登录信息到 PreferencesManager │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ loginViewModel.login() ↓ ┌─────────────────────────────────────────────────────────────┐ │ FnOfficialApiImpl.login() │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 构建登录请求: │ │ │ │ POST {baseUrl}/api/login │ │ │ │ Headers: │ │ │ │ - Authorization: [token] │ │ │ │ - Cookie: [Trim-MC-token] │ │ │ │ - Accept: application/json │ │ │ │ - User-Agent: Mozilla/5.0... │ │ │ │ Body: {username, password} │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ 登录成功 ↓ ┌─────────────────────────────────────────────────────────────┐ │ 处理登录响应 │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 1. AccountDataCache.authorization = token │ │ │ │ 2. AccountDataCache.insertCookie("Trim-MC-token", token)│ │ │ 3. LoginStateManager.updateLoginStatus(true) │ │ │ │ 4. 保存 token 到持久化存储 │ │ │ │ 5. 跳转到首页 │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
object AccountDataCache {
var authorization: String // 登录 token
var cookieMap: MutableMap<String, String> // Cookie
var userName: String // 用户名
var password: String // 密码
var isHttps: Boolean // 是否使用 HTTPS
var host: String // 实际使用的主机地址
var port: Int // 实际使用的端口
var displayHost: String // 显示的主机地址
var displayPort: Int // 显示的端口
var isLoggedIn: Boolean // 登录状态
var isNasLogin: Boolean // 是否 NAS 登录
var fnId: String // FN ID
fun getFnOfficialBaseUrl(): String {
// 构建飞牛影视 API 基础 URL
// http://[host]:[port] 或 https://[host]:[port]
}
fun getProxyBaseUrl(): String {
// fntv-proxy 地址
return "http://127.0.0.1:1999"
}
}
| 功能模块 | 是否使用 fntv-proxy | 说明 |
|---|---|---|
| 视频播放 | ✅ 是 | fntv-proxy 为视频请求添加鉴权头 |
| 字幕下载 | ✅ 是 | 字幕请求需要鉴权 |
| 媒体库列表 | ✅ 是 | 获取媒体库需要鉴权 |
| 用户信息 | ✅ 是 | 查询用户信息需要鉴权 |
| 收藏/标记 | ✅ 是 | 操作需要鉴权 |
| 观看记录 | ✅ 是 | 记录观看状态需要鉴权 |
┌─────────────────────────────────────────────────────────────┐ │ 业务功能模块 │ │ (MediaListViewModel, UserInfoViewModel, etc.) │ └────────────────────┬────────────────────────────────────────┘ │ │ 调用 API ↓ ┌─────────────────────────────────────────────────────────────┐ │ FnOfficialApiImpl │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ val response = fnOfficialClient.post( │ │ │ │ "${AccountDataCache.getFnOfficialBaseUrl()}/api/xxx",│ │ │ │ body │ │ │ │ ) │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ fnOfficialClient 发送请求 ↓ ┌─────────────────────────────────────────────────────────────┐ │ BaseHttpClient (Ktor) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ defaultRequest { │ │ │ │ header(HttpHeaders.Authorization, authorization) │ │ │ │ header(HttpHeaders.Cookie, cookieState) │ │ │ │ header(HttpHeaders.UserAgent, "Mozilla/5.0...") │ │ │ │ header(HttpHeaders.Accept, "application/json") │ │ │ │ } │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ HTTP 请求 (带基础鉴权头) ↓ ┌─────────────────────────────────────────────────────────────┐ │ fntv-proxy (127.0.0.1:1999) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ 1. 接收请求 │ │ │ │ 2. 添加额外鉴权头: │ │ │ │ - X-Signature (请求签名) │ │ │ │ - X-Timestamp (时间戳) │ │ │ │ - X-Device-Id (设备 ID) │ │ │ │ - X-Client-Version (客户端版本) │ │ │ │ - 其他加密/鉴权相关头 │ │ │ │ 3. 转发到飞牛影视官方 API │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────────┘ │ │ HTTPS 请求 (完整鉴权) ↓ ┌─────────────────────────────────────────────────────────────┐ │ 飞牛影视官方 API (鉴权验证) │ └────────────────────┬────────────────────────────────────────┘ │ │ 响应数据 ↓ ┌─────────────────────────────────────────────────────────────┐ │ fntv-proxy (返回响应) │ └────────────────────┬────────────────────────────────────────┘ │ │ 响应数据 ↓ ┌─────────────────────────────────────────────────────────────┐ │ 业务模块 (处理响应数据) │ └─────────────────────────────────────────────────────────────┘
// MediaDbListViewModel
val result = fnOfficialApi.getMediaDbList()
// 内部调用
GET http://[host]:[port]/api/media/db/list
Headers:
- Authorization: [token]
- Cookie: Trim-MC-token=[token]
- User-Agent: Mozilla/5.0...
// 经过 fntv-proxy 后添加
GET https://api.feiniu.com/api/media/db/list
Headers:
- Authorization: [token]
- Cookie: Trim-MC-token=[token]
- User-Agent: Mozilla/5.0...
- X-Signature: [签名]
- X-Timestamp: [时间戳]
- X-Device-Id: [设备ID]
// 视频播放 URL 生成
val playUrl = fnOfficialApi.getPlayUrl(mediaId)
// 字幕下载
HlsSubtitleUtil(fnOfficialClient, playUrl, subtitle)
// 请求经过 fntv-proxy,添加视频流鉴权头
Headers:
- Range: bytes=0-...
- 视频流鉴权相关头
应用启动 (main.kt) ↓ LaunchedEffect(Unit) ↓ ProxyManager.start() ↓ 1. 查找 fntv-proxy 可执行文件(按优先级顺序) 2. 检查进程是否已存在,存在则清理 3. 使用 ProcessBuilder 启动代理进程 4. 创建守护线程处理 stdout/stderr 5. 注册 JVM shutdown hook 确保进程关闭 ↓ fntv-proxy 运行在 1999 端口
应用关闭或 onDispose 触发 ↓ ProxyManager.stop() ↓ 1. 尝试优雅关闭(发送停止信号) 2. 等待最多 3 秒 3. 如果未关闭,强制终止进程 4. 清理守护线程 ↓ fntv-proxy 进程结束
| 原因 | 说明 |
|---|---|
| 安全隔离 | 鉴权算法封装在独立进程中,避免在 Kotlin 代码中暴露 |
| 防逆向 | Go 编译的二进制文件比字节码更难逆向分析 |
| 算法更新 | 鉴权算法更新时只需替换 fntv-proxy,无需重新发布应用 |
| 跨平台统一 | 不同平台的鉴权逻辑在 Go 代码中统一实现 |
| 性能优化 | Go 处理网络请求更高效 |
fntv-proxy 是一个本地代理程序,会:
这些行为与恶意软件的特征相似,因此会被安全软件(如 360 杀毒)误报。
// fntv-proxy 地址(硬编码)
Proxy Base URL: http://127.0.0.1:1999
// FNOS 默认端口
Default Port: 5666
登录信息存储在本地配置中(使用 Settings API):
| 键 | 说明 |
|---|---|
username | 用户名 |
password | 密码(如果选择记住) |
token | 登录 token |
isHttps | 是否使用 HTTPS |
host | 主机地址 |
port | 端口号 |
cookie | Cookie 状态 |
isLoggedIn | 登录状态 |
loginHistory | 登录历史记录 |
┌─────────────────────────────────────────────────────────────┐ │ FlyNarwhal 应用 │ │ (Compose Multiplatform) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ UI 层 │ │ ViewModel │ │ Manager │ │ │ │ - LoginScreen│ │ - LoginVM │ │ - LoginState│ │ │ │ - HomePage │ │ - MediaList │ │ - Proxy │ │ │ │ - Settings │ │ - UserInfo │ │ Manager │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────────┴──────────────────┘ │ │ │ │ │ ┌───────▼────────┐ │ │ │ Data Layer │ │ │ │ - API Impl │ │ │ │ - HTTP Client│ │ │ │ - Cache │ │ │ └───────┬────────┘ │ └───────────────────────────┼──────────────────────────────────┘ │ HTTP (基础头) ┌───────────────────────────▼──────────────────────────────────┐ │ fntv-proxy (1999) │ │ 添加完整鉴权请求头 │ └───────────────────────────┬──────────────────────────────────┘ │ HTTPS (完整鉴权) ┌───────────────────────────▼──────────────────────────────────┐ │ 飞牛影视官方 API 服务器 │ │ (feiniu.com / fnos.net / 5ddd.com) │ └─────────────────────────────────────────────────────────────┘
| 组件 | 技术 |
|---|---|
| UI 框架 | Compose Multiplatform |
| 网络库 | Ktor Client + OkHttp |
| 依赖注入 | Koin |
| 持久化 | Settings (Multiplatform Settings) |
| 日志 | Kermit |
| fntv-proxy | Go |
文档版本: 1.0 生成日期: 2025