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