Skip to content

feat: sync external playlist#194

Merged
roitium merged 9 commits into
devfrom
feat/sync-external-playlist
Feb 1, 2026
Merged

feat: sync external playlist#194
roitium merged 9 commits into
devfrom
feat/sync-external-playlist

Conversation

@roitium
Copy link
Copy Markdown
Collaborator

@roitium roitium commented Feb 1, 2026

Closes #128

Summary by CodeRabbit

发布说明

  • 新功能

    • 支持酷狗音乐歌词搜索
    • 支持从网易云音乐/QQ音乐导入歌单并自动匹配B站视频
    • 新增桌面歌词显示(仅Android)及歌词时间偏移调整
  • 问题修复

    • 修复ToastContext未初始化导致应用崩溃的问题
    • 修复Cookie键名包含无效字符导致的应用崩溃,并添加自动修复提示
  • 文档

    • 添加外部歌单导入使用指南
    • 更新歌词功能文档

✏️ Tip: You can customize this high-level summary in your review settings.

@roitium roitium added the enhancement New feature or request label Feb 1, 2026
@safedep
Copy link
Copy Markdown

safedep Bot commented Feb 1, 2026

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

No dependency changes detected. Nothing to scan.

This report is generated by SafeDep Github App

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bbplayer-docs Ready Ready Preview, Comment Feb 1, 2026 11:28am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 1, 2026

Caution

Review failed

The pull request is closed.

概述

该 PR 实现了从网易云音乐和 QQ 音乐导入歌单的功能,并支持自动匹配到 B 站视频。同时添加了酷狗音乐作为歌词来源,并删除了大量 React Native 最佳实践文档。

变更

文件/目录 摘要
外部歌单导入功能
apps/mobile/src/app/playlist/external-sync.tsx, apps/mobile/src/components/modals/playlist/InputExternalPlaylistInfo.tsx, apps/mobile/src/components/modals/playlist/ManualMatchExternalSync.tsx
实现了完整的外部歌单同步流程:包括歌单信息输入、逐曲手动匹配、进度跟踪和保存功能。支持暂停/继续、错误处理和 ETA 显示。
外部歌单服务层
apps/mobile/src/lib/services/externalPlaylistService.ts, apps/mobile/src/lib/facades/syncExternalPlaylist.ts
新增服务层处理外部歌单数据获取、视频匹配(含进度回调)和持久化保存。包含评分算法用于选择最佳视频匹配。
歌词来源扩展
apps/mobile/src/lib/api/kugou/api.ts, apps/mobile/src/lib/services/lyricService.ts, apps/mobile/src/hooks/queries/lyrics/index.ts
添加酷狗音乐 API 支持,实现平行查询多个歌词源,支持取消机制以优化性能。
API 类型定义
apps/mobile/src/types/apis/kugou.ts, apps/mobile/src/types/apis/kuwo.ts, apps/mobile/src/types/apis/netease.ts, apps/mobile/src/types/apis/qqmusic.ts, apps/mobile/src/types/apis/bilibili.ts, apps/mobile/src/types/apis/baidu.ts
新增和扩展多个音乐平台及 B 站 API 的类型定义,支持歌单和歌词数据结构。
工具函数
apps/mobile/src/utils/matching.ts, apps/mobile/src/utils/time.ts, apps/mobile/src/lib/utils/playlistUrlParser.ts
新增匹配算法工具(高斯分布、LCS 算法)、时间解析工具和歌单 URL 解析器。
状态管理
apps/mobile/src/hooks/stores/useExternalPlaylistSyncStore.tsx, apps/mobile/src/hooks/useExternalPlaylist.ts
新增 Zustand 基础的同步状态存储和 React Query hook 用于获取外部歌单。
UI 集成
apps/mobile/src/features/library/local/LocalPlaylistList.tsx, apps/mobile/src/components/ModalRegistry.tsx, apps/mobile/src/features/playlist/remote/components/PlaylistHeader.tsx
在本地歌单列表新增菜单项支持导入外部歌单,扩展 PlaylistHeader 支持二级按钮。
路由和导航
apps/mobile/src/app/_layout.tsx, apps/mobile/src/app/test.tsx, apps/mobile/src/app/(tabs)/settings/playback.tsx
新增 /playlist/external-sync 路由,添加酷狗音乐选项,集成外部歌单测试入口。
同步 facade 重构
apps/mobile/src/lib/facades/syncBilibiliPlaylist.ts, apps/mobile/src/features/playlist/remote/hooks/useRemotePlaylist.ts, 等多个导入源
将原 SyncFacade 重命名为 SyncBilibiliPlaylistFacade 并更新所有引用,为新的外部歌单同步做准备。
文档和配置更新
apps/mobile/CHANGELOG.md, README.md, apps/docs/docs/guides/external-playlist.md, apps/docs/docs/guides/lyrics.md
更新文档说明新增功能(外部歌单导入和酷狗歌词)并记录 bug 修复。
网络安全配置
apps/mobile/app.config.ts, packages/orpheus/android/src/main/AndroidManifest.xml, packages/orpheus/android/src/main/res/xml/network_security_config.xml
启用明文 HTTP 流量支持(usesCleartextTraffic),移除网络安全配置限制。
全局状态和初始化
apps/mobile/index.js, apps/mobile/src/types/global.ts, apps/mobile/src/hooks/stores/useAppStore.ts
新增 global.isUIReady 标志用于条件化 toast 显示,增强 cookie 校验和修复机制。
Bilibili API 增强
apps/mobile/src/lib/api/bilibili/api.ts, apps/mobile/src/lib/api/bilibili/client.ts
支持 skipCookie 选项以在某些请求中禁用 cookie 注入。
数据库层
apps/mobile/src/lib/services/trackService.ts, apps/mobile/src/hooks/mutations/db/playlist.ts
优化 findOrCreateManyTracks 中的重复检测逻辑,更新导入源。
已删除的文档文件
.agents/skills/react-native-best-practices/, .agents/skills/vercel-react-native-skills/, 相关引用文件
移除 93 个 React Native 最佳实践和 Vercel React Native 技能文档文件及其符号链接。

序列图

sequenceDiagram
    participant User as 用户
    participant UI as 外部歌单 UI
    participant Service as ExternalPlaylistService
    participant API as 网易/QQ API
    participant BilibiliAPI as Bilibili API
    participant DB as 本地数据库
    
    User->>UI: 输入歌单链接
    UI->>Service: fetchExternalPlaylist(id, source)
    Service->>API: 获取歌单元数据
    API-->>Service: 返回歌曲列表
    Service-->>UI: 显示歌曲列表
    
    User->>UI: 开始匹配
    UI->>Service: matchExternalPlaylist(tracks, onProgress)
    loop 每首歌曲
        Service->>BilibiliAPI: 搜索视频
        BilibiliAPI-->>Service: 返回候选视频
        Service->>Service: 评分并选择最佳匹配
        Service-->>UI: 更新进度和结果
    end
    
    User->>UI: 手动调整或保存
    UI->>DB: 保存匹配结果到本地歌单
    DB-->>UI: 保存完成
    UI-->>User: 导航回歌单列表
Loading

预计代码审查工作量

🎯 4 (复杂) | ⏱️ ~60 分钟

可能相关的 PR

  • PR #194: 实现了同一个"同步外部歌单"功能,修改相同的模块和类型定义
  • PR #147: 修改了同一文件 LocalPlaylistList.tsx,用于添加/导入歌单菜单功能
  • PR #140: 修改歌词源选择和手动搜索集成(设置 UI、ManualSearchLyrics、查询 hook)

诗句

🐰 兔子跳过山丘,外来歌单归家,
酷狗咕咕唱歌,哔哩哔哩映画,
手指点一点,歌单藏心底,
网易、QQ 都有缘,B 站视频配上歌! 🎵

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 变更范围与#128要求相符。然而存在部分超出导入歌单功能本身的改动:删除了大量React Native最佳实践文档、调整网络配置(cleartext traffic)、删除Xiami音乐支持等,这些改动与导入歌单功能关系不大。 应分离核心功能实现和文档/配置删除为独立PR。建议在单独PR中处理文档清理、网络配置变更和依赖版本更新。
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题「feat: sync external playlist」准确概括了本次变更的主要内容——同步外部音乐平台的歌单功能。标题简洁且清晰表达了功能新增。
Linked Issues check ✅ Passed 代码变更完整实现了#128中要求的功能:支持导入QQ音乐、网易云、酷我、酷狗歌单,并自动批量搜索匹配B站视频,保存为本地歌单。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/sync-external-playlist

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@packages/external-playlist-sync/meting.d.ts`:
- Line 1: The ambient module declaration is too minimal and causes the Meting
class and its methods to be typed as any; update the declaration for module
'@roitium/meting' to export a minimal set of types so static analysis can
type-check usage: declare and export a Meting class (symbol: Meting) with a
typed constructor (e.g., constructor(options?: Record<string, any>)), and add
method signatures used by the codebase such as init(): Promise<void>,
search(query: string, source?: string): Promise<any>, getSong(id: string):
Promise<any>, getPlaylist(id: string): Promise<any> (and any other methods your
index.ts calls), plus export the class as the module default or named export to
match how it is imported; include simple interfaces/types for option objects if
referenced so the compiler no longer infers any.

In `@packages/external-playlist-sync/package.json`:
- Around line 10-12: The package `@roitium/meting` is currently listed under
devDependencies but is imported at runtime in index.ts (import of
`@roitium/meting` at top and use around line 8); move "@roitium/meting" from
devDependencies into the dependencies object in package.json so it is installed
in production builds (ensure the exact package key "@roitium/meting" is added
under "dependencies" and removed from "devDependencies").

In `@packages/meting/src/meting.js`:
- Around line 77-126: The current retry/timeout logic reuses a single
AbortController and timeoutId across attempts and clears the timeout before
response.text() finishes, so retries immediately fail and response.text()
remains unprotected; fix by moving creation of AbortController, timeoutId and
assignment requestOptions.signal inside each attempt (inside makeRequest) so
each retry gets a fresh controller and timeout, start the per-attempt setTimeout
that aborts that controller, await both fetch and response.text() before
clearing that attempt's timeout (so response.text() is also covered), and ensure
you set requestOptions.signal = controller.signal for that attempt and
clearTimeout(timeoutId) in both the success and error paths for that attempt to
avoid leaks; reference symbols: AbortController, controller, timeoutId,
requestOptions.signal, makeRequest, fetch, response.text(), retries.

In `@packages/meting/src/providers/tencent.js`:
- Around line 254-266: Before accessing data.data[0] inside the qualityMap loop,
add a guard to verify the API response contains an array with at least one
element (e.g., check data && Array.isArray(data.data) && data.data.length > 0)
and handle the empty case (return null/undefined or skip processing) to avoid
TypeError; update the loop using the existing symbols (qualityMap, vkeys,
response.req_0, vkeys[i].purl, and the accesses to .file and .mid) so you only
read data.data[0].file and data.data[0].mid after the presence check and provide
a clear fallback path.
🟡 Minor comments (22)
apps/mobile/CHANGELOG.md-10-13 (1)

10-13: ⚠️ Potential issue | 🟡 Minor

CHANGELOG 条目与 PR 主要功能不一致。

当前 CHANGELOG 仅记录了"支持 Kuwo、Kugou、Baidu 音乐的歌词搜索",但根据 PR 标题("feat: sync external playlist")和关联的 issue #128,本 PR 的核心功能是导入/同步外部平台歌单。建议补充一条更能体现主要功能的条目。

📝 建议的 CHANGELOG 条目补充
 ### Added
 
+- 支持导入外部音乐平台歌单(Kuwo、Kugou、QQ 音乐、网易云音乐等)
 - 支持 Kuwo、Kugou、Baidu 音乐的歌词搜索
apps/mobile/src/lib/utils/playlistUrlParser.ts-12-18 (1)

12-18: ⚠️ Potential issue | 🟡 Minor

QQ 域名匹配过于宽泛。

.qq.com 会匹配所有 QQ 相关域名(如 news.qq.com、mail.qq.com),而 QQ 音乐的实际域名是 y.qq.com。建议使用更精确的匹配。

建议修复
 // QQ Music
-if (text.includes('.qq.com')) {
+if (text.includes('y.qq.com')) {
   const result = /id=(\d+)/.exec(text)
   if (result?.[1]) {
     return { id: result[1], source: 'qq' }
   }
 }
packages/meting/CHANGELOG.md-47-47 (1)

47-47: ⚠️ Potential issue | 🟡 Minor

版本日期包含占位符。

2025-01-XX 应该替换为实际的发布日期。

建议修复
-## [1.5.14] - 2025-01-XX
+## [1.5.14] - 2025-01-15
packages/meting/package.json-61-66 (1)

61-66: ⚠️ Potential issue | 🟡 Minor

更新过期的开发依赖版本。

@rollup/plugin-babel 应更新至 6.1.0(当前 6.0.4),@rollup/plugin-node-resolve 应更新至 16.0.3(当前 16.0.1),rollup 应更新至 4.57.1(当前 4.49.0)。@rollup/plugin-terser 已是最新版本。当前无已知安全漏洞。

packages/meting/src/providers/tencent.js-197-198 (1)

197-198: ⚠️ Potential issue | 🟡 Minor

JSON.parse 缺少错误处理。

如果响应格式异常,JSON.parse(result) 会抛出异常。建议添加 try-catch 或在调用前验证数据格式。

apps/mobile/src/utils/time.ts-38-46 (1)

38-46: ⚠️ Potential issue | 🟡 Minor

建议对非数字输入添加 NaN 校验。

durationStr 包含非数字字符(如 "ab:cd")时,Number() 会返回 NaN,导致后续计算结果也为 NaN,而非预期的 0

🛡️ 建议的修复方案
 export function parseDurationString(durationStr: string): number {
-	const parts = durationStr.split(':').map(Number)
+	const parts = durationStr.split(':').map(Number)
+	if (parts.some(isNaN)) return 0
 	if (parts.length === 3) {
 		return parts[0] * 3600 + parts[1] * 60 + parts[2]
 	} else if (parts.length === 2) {
 		return parts[0] * 60 + parts[1]
 	}
 	return 0
 }
apps/mobile/src/lib/facades/syncExternalPlaylist.ts-119-127 (1)

119-127: ⚠️ Potential issue | 🟡 Minor

静默跳过失败的 key 生成可能导致数据丢失

generateUniqueTrackKey(payload) 返回错误时,代码会静默跳过该曲目,不会有任何日志或警告。这可能导致用户保存的歌单中缺少某些曲目,但用户无法得知原因。

建议在跳过时至少记录一条警告日志。

🔧 建议的修改
 				const orderedTrackIds: number[] = []
 				for (const payload of trackPayloads) {
 					const keyResult = generateUniqueTrackKey(payload)
 					if (keyResult.isOk()) {
 						const id = trackIdsMap.get(keyResult.value)
 						if (id) {
 							orderedTrackIds.push(id)
+						} else {
+							logger.warn('Track ID not found in map', { key: keyResult.value })
 						}
+					} else {
+						logger.warn('Failed to generate unique track key', { payload, error: keyResult.error })
 					}
 				}
apps/mobile/src/lib/api/kugou/api.ts-73-79 (1)

73-79: ⚠️ Potential issue | 🟡 Minor

缺少 HTTP 错误检查和请求头

这个 fetch 调用缺少两个在 search() 方法中存在的处理:

  1. 没有检查 res.ok 状态
  2. 没有使用 this.getHeaders() 设置请求头

如果 Kugou 歌词 API 对请求头有同样的要求,可能会导致请求失败。

🔧 建议的修改
 		return ResultAsync.fromPromise(
-			fetch(searchUrl).then(
-				(res) => res.json() as Promise<KugouLyricSearchResponse>,
-			),
+			fetch(searchUrl, { headers: this.getHeaders() }).then((res) => {
+				if (!res.ok) {
+					throw new Error(`Kugou lyric search error: ${res.statusText}`)
+				}
+				return res.json() as Promise<KugouLyricSearchResponse>
+			}),
 			(e) =>
 				new Error('Failed to search lyric candidate on Kugou', { cause: e }),
 		).andThen((searchRes) => {
apps/mobile/src/lib/api/kugou/api.ts-97-101 (1)

97-101: ⚠️ Potential issue | 🟡 Minor

歌词下载请求同样缺少错误检查和请求头

与上面的歌词搜索请求一样,这个 fetch 调用也缺少 HTTP 错误检查和请求头。

🔧 建议的修改
 			return ResultAsync.fromPromise(
-				fetch(downloadUrl).then(
-					(res) => res.json() as Promise<KugouLyricDownloadResponse>,
-				),
+				fetch(downloadUrl, { headers: this.getHeaders() }).then((res) => {
+					if (!res.ok) {
+						throw new Error(`Kugou lyric download error: ${res.statusText}`)
+					}
+					return res.json() as Promise<KugouLyricDownloadResponse>
+				}),
 				(e) => new Error('Failed to download lyric from Kugou', { cause: e }),
 			).map((downloadRes) => {
packages/external-playlist-sync/index.ts-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

顶层 Promise 未处理,可能导致未捕获的拒绝错误。

静态分析正确标记了此问题。当 main() 内部抛出异常时(如 try-catch 之外的错误),会导致 unhandled promise rejection。

🐛 建议修复
-main()
+main().catch((error) => {
+	console.error('Fatal error:', error)
+	process.exitCode = 1
+})

或使用 void 操作符显式标记忽略:

-main()
+void main()
apps/mobile/src/hooks/stores/useExternalPlaylistSyncStore.tsx-26-26 (1)

26-26: ⚠️ Potential issue | 🟡 Minor

setProgress 可能产生除零错误。

total 为 0 时,current / total 会返回 InfinityNaN,可能导致 UI 显示异常。

🛡️ 建议修改
-		setProgress: (current, total) => set({ progress: current / total }),
+		setProgress: (current, total) => set({ progress: total > 0 ? current / total : 0 }),
packages/external-playlist-sync/matcher.ts-32-41 (1)

32-41: ⚠️ Potential issue | 🟡 Minor

parseDuration 缺少对无效输入的防护。

当解析包含非数字字符的时长字符串时,Number() 会返回 NaN,导致后续计算出现问题。建议添加校验。

🛡️ 建议修改
 function parseDuration(durationStr: string): number {
 	const parts = durationStr.split(':').map(Number)
+	if (parts.some(isNaN)) {
+		return 0
+	}
 	if (parts.length === 3) {
 		return parts[0] * 3600 + parts[1] * 60 + parts[2]
 	} else if (parts.length === 2) {
 		return parts[0] * 60 + parts[1]
 	}
 	return 0
 }
packages/external-playlist-sync/matcher.ts-17-23 (1)

17-23: ⚠️ Potential issue | 🟡 Minor

修复静态分析警告:异步方法缺少 await 表达式。

静态分析工具报告 search 方法是 async 但没有 await 表达式。对于模拟实现,应该移除 async 关键字或添加 await

🔧 建议修改
 // 模拟 B站 API
 const bilibiliAPI: BilibiliAPI = {
-	search: async (query: string, page: number) => {
+	search: async (query: string, _page: number) => {
+		await Promise.resolve() // Placeholder for actual async operation
 		console.log(`[Mock] Searching for: ${query}, page: ${page}`)
 		return []
 	},
 }

或者移除 async

 const bilibiliAPI: BilibiliAPI = {
-	search: async (query: string, page: number) => {
+	search: (query: string, page: number): Promise<BilibiliSearchVideo[]> => {
 		console.log(`[Mock] Searching for: ${query}, page: ${page}`)
-		return []
+		return Promise.resolve([])
 	},
 }
packages/meting/src/providers/kuwo.js-15-23 (1)

15-23: ⚠️ Potential issue | 🟡 Minor

硬编码的 Cookie 和 CSRF Token 可能导致请求失败。

Headers 中包含硬编码的 Cookie 和 CSRF token (kw_token=3E7JFQ7MRPL)。这些值可能会过期或被服务端拒绝,导致 API 请求失败。建议考虑动态获取这些凭证或在请求失败时提供明确的错误提示。

packages/meting/src/providers/kugou.js-264-291 (1)

264-291: ⚠️ Potential issue | 🟡 Minor

歌词解码缺少对 response.content 的空值检查。

如果 API 返回的响应中 content 字段不存在或为空,Buffer.from(response.content, 'base64') 会抛出错误。

🛡️ 建议添加空值检查
     const response = JSON.parse(await this.meting._exec(api));
+    if (!response.content) {
+      return JSON.stringify({ lyric: '', tlyric: '' });
+    }
     const lyricData = {
       lyric: Buffer.from(response.content, 'base64').toString(),
       tlyric: ''
     };
apps/mobile/src/components/modals/playlist/ManualMatchExternalSync.tsx-29-30 (1)

29-30: ⚠️ Potential issue | 🟡 Minor

在渲染函数中抛出错误可能导致应用崩溃。

extraData 不存在时直接抛出错误会导致整个列表渲染失败。建议返回 null 并记录警告日志,而非抛出异常。

🛡️ 建议改为安全处理
 const renderItem = ({
 	item,
 	extraData,
 }: ListRenderItemInfoWithExtraData<...>) => {
-	if (!extraData) throw new Error('Extradata 不存在')
+	if (!extraData) {
+		console.warn('ManualMatchExternalSync: extraData is missing')
+		return null
+	}
 	return (
packages/meting/src/providers/netease.js-239-262 (1)

239-262: ⚠️ Potential issue | 🟡 Minor

URL 解码缺少空值检查,可能导致运行时错误。

data.data 为空数组或 data.data[0] 不存在时,访问 data.data[0].ufdata.data[0].url 会抛出 TypeError。

🛡️ 建议添加防御性检查
   urlDecode(result) {
     const data = JSON.parse(result);
     let url;
-    
-    if (data.data[0].uf && data.data[0].uf.url) {
-      data.data[0].url = data.data[0].uf.url;
-    }
-    
-    if (data.data[0].url) {
+
+    const item = data.data?.[0];
+    if (!item) {
+      return JSON.stringify({ url: '', size: 0, br: -1 });
+    }
+
+    if (item.uf?.url) {
+      item.url = item.uf.url;
+    }
+
+    if (item.url) {
       url = {
-        url: data.data[0].url,
-        size: data.data[0].size,
-        br: data.data[0].br / 1000
+        url: item.url,
+        size: item.size,
+        br: item.br / 1000
       };
     } else {
packages/meting/src/providers/baidu.js-231-255 (1)

231-255: ⚠️ Potential issue | 🟡 Minor

urlDecode 缺少空值检查,可能导致运行时错误。

如果 API 返回的数据中 songurlsongurl.url 不存在,forEach 调用会抛出 TypeError。

🛡️ 建议添加防御性检查
   urlDecode(result) {
     const data = JSON.parse(result);
     
     let maxBr = 0;
     let url;
+
+    const urls = data.songurl?.url ?? [];
     
-    data.songurl.url.forEach(item => {
+    urls.forEach(item => {
       if (item.file_bitrate <= this.meting.temp.br && item.file_bitrate > maxBr) {
         maxBr = item.file_bitrate;
         url = {
           url: item.file_link,
           br: item.file_bitrate
         };
       }
     });
apps/mobile/src/app/playlist/external-sync.tsx-313-323 (1)

313-323: ⚠️ Potential issue | 🟡 Minor

同步重启逻辑存在潜在问题。

startIndex >= data.tracks.length 时调用 reset(),但由于 React 状态更新是异步的,下面计算 effectiveStartIndexresults 可能仍是旧值,导致 effectiveStartIndex 不会变为 0。

建议简化此逻辑或显式传递起始索引。

🛡️ 建议修复逻辑
-	const startIndex = Object.keys(results).length
-	if (startIndex >= data.tracks.length) {
-		if (startIndex === data.tracks.length) {
-			reset()
-		}
-	}
-
-	const effectiveStartIndex =
-		Object.keys(results).length === data.tracks.length
-			? 0
-			: Object.keys(results).length
+	const currentResultCount = Object.keys(results).length
+	const isCompleted = currentResultCount >= data.tracks.length
+	
+	if (isCompleted) {
+		reset()
+	}
+	
+	const effectiveStartIndex = isCompleted ? 0 : currentResultCount
packages/meting/src/providers/kugou.js-217-248 (1)

217-248: ⚠️ Potential issue | 🟡 Minor

urlDecode 中缺少错误处理且存在潜在空引用。

  1. 访问 this.meting.temp.br 时未检查 this.meting.temp 是否存在
  2. 循环中的 this.meting._exec(api) 调用未包含 try-catch,单个请求失败可能导致整个方法失败
  3. 如果 data.data[0].relate_goods 不存在,for...of 循环会抛出 TypeError
🛡️ 建议添加防御性检查
   async urlDecode(result) {
     const data = JSON.parse(result);
     
     let maxBr = 0;
     let url;
+
+    const targetBr = this.meting?.temp?.br ?? 320;
+    const relateGoods = data.data?.[0]?.relate_goods ?? [];
     
-    for (const item of data.data[0].relate_goods) {
-      if (item.info.bitrate <= this.meting.temp.br && item.info.bitrate > maxBr) {
+    for (const item of relateGoods) {
+      if (item.info.bitrate <= targetBr && item.info.bitrate > maxBr) {
         const api = {
           // ...
         };
         
-        const response = JSON.parse(await this.meting._exec(api));
-        if (response.url) {
+        try {
+          const response = JSON.parse(await this.meting._exec(api));
+          if (response.url) {
+            // ...
+          }
+        } catch (e) {
+          // Continue to next candidate on failure
+          continue;
+        }
apps/mobile/src/components/modals/playlist/ManualMatchExternalSync.tsx-65-70 (1)

65-70: ⚠️ Potential issue | 🟡 Minor

视频时长解析假设固定的 MM:SS 格式,可能导致运行时错误。

当前实现假设 item.duration 格式为 MM:SS,但如果格式为 HH:MM:SS 或其他格式,parseInt 会返回 NaN,导致显示异常。

🛡️ 建议添加格式检查
+const parseDuration = (duration: string): number => {
+	const parts = duration.split(':').map(Number)
+	if (parts.length === 2) {
+		return parts[0] * 60 + parts[1]
+	}
+	if (parts.length === 3) {
+		return parts[0] * 3600 + parts[1] * 60 + parts[2]
+	}
+	return 0
+}

 <Text variant='bodySmall' numberOfLines={1}>
 	{item.author} -{' '}
-	{formatDurationToHHMMSS(
-		Math.round(
-			parseInt(item.duration.split(':')[0]) * 60 +
-				parseInt(item.duration.split(':')[1]),
-		),
-	)}
+	{formatDurationToHHMMSS(parseDuration(item.duration))}
 </Text>
packages/meting/src/providers/base.js-219-223 (1)

219-223: ⚠️ Potential issue | 🟡 Minor

format 方法检查逻辑存在问题

typeof this.format === 'function' 始终为 true,因为基类已定义 format 方法。但如果子类未覆盖该方法,调用时仍会抛出 Error

建议改用标志位或检测是否为基类实现:

🐛 建议修复
     // 使用当前 provider 的格式化方法
-    if (typeof this.format === 'function') {
+    // 检查子类是否覆盖了 format 方法
+    if (this.format !== BaseProvider.prototype.format) {
       const result = data.map(item => this.format(item));
       return JSON.stringify(result);
     }
🧹 Nitpick comments (25)
apps/mobile/src/features/playlist/remote/components/PlaylistHeader.tsx (2)

47-47: 建议直接解构新增的 props,而非使用 rest 语法。

当前使用 ...props 捕获剩余属性,然后通过 props.disableMainButtonprops.secondaryButtonText 等方式访问,这种模式不够直观。既然这些属性已在接口中明确定义,建议直接解构以保持代码风格一致。

♻️ 建议的修改
 export const PlaylistHeader = memo(function PlaylistHeader({
 	coverUri,
 	title,
 	subtitles,
 	description,
 	onClickMainButton,
 	mainButtonIcon,
 	mainButtonText,
 	linkedPlaylistId,
 	id,
-	...props
+	disableMainButton,
+	secondaryButtonText,
+	secondaryButtonIcon,
+	onClickSecondaryButton,
+	disableSecondaryButton,
 }: PlaylistHeaderProps) {

然后将后续代码中的 props.xxx 替换为直接引用:

  • props.disableMainButtondisableMainButton
  • props.secondaryButtonTextsecondaryButtonText
  • props.secondaryButtonIconsecondaryButtonIcon
  • props.onClickSecondaryButtononClickSecondaryButton
  • props.disableSecondaryButtondisableSecondaryButton

109-109: 内联样式会在每次渲染时创建新对象。

style={{ marginLeft: 8 }} 在每次渲染时都会创建新的对象引用,虽然影响较小,但建议将其移至 StyleSheet 中以保持一致性并避免不必要的重渲染。

♻️ 建议的修改

styles 对象中添加:

 const styles = StyleSheet.create({
 	// ...existing styles...
+	secondaryButton: {
+		marginLeft: 8,
+	},
 })

然后修改按钮样式引用:

 <Button
 	mode='outlined'
 	icon={props.secondaryButtonIcon}
 	onPress={props.onClickSecondaryButton}
-	style={{ marginLeft: 8 }}
+	style={styles.secondaryButton}
 	disabled={props.disableSecondaryButton}
 >
packages/meting/ARCHITECTURE.md (1)

9-21: 建议为代码块添加语言标识符。

目录结构代码块缺少语言标识符,可以添加 textplaintext 以符合 markdown lint 规范。

建议修复
-```
+```text
 src/
 ├── meting.js                 # 主入口文件(重构后)
packages/meting/CLAUDE.md (1)

73-75: 建议为流程图代码块添加语言标识符。

可以使用 textplaintext 标识符。

建议修复
-```
+```text
 用户 API 调用 → 主 Meting 类 → Provider.executeRequest() → 平台特定处理 → 返回标准化结果
 ```
packages/meting/src/providers/tencent.js (1)

317-319: JSONP 解析使用魔法数字,易出错。

result.substring(18, result.length - 1) 假设响应格式为固定的 JSONP 包装。如果腾讯 API 变更包装格式,此处会静默失败。建议使用更健壮的正则提取 JSON 内容。

♻️ 建议的改进方案
   lyricDecode(result) {
-    const jsonStr = result.substring(18, result.length - 1);
+    // 提取 JSONP 回调中的 JSON 内容
+    const match = result.match(/\((\{.*\})\)$/s);
+    if (!match) {
+      return JSON.stringify({ lyric: '', tlyric: '' });
+    }
+    const jsonStr = match[1];
     const data = JSON.parse(jsonStr);
apps/mobile/src/hooks/useExternalPlaylist.ts (1)

9-23: 冗余的空值检查

第 12 行的 if (!playlistId) return null 检查是多余的,因为第 22 行的 enabled: !!playlistId 已经确保了当 playlistId 为空时不会执行 queryFn。可以移除这个检查以简化代码。

♻️ 建议的修改
 	return useQuery({
 		queryKey: ['external-playlist', source, playlistId],
 		queryFn: async () => {
-			if (!playlistId) return null
 			const result = await externalPlaylistService.fetchExternalPlaylist(
 				playlistId,
 				source,
 			)
 			if (result.isErr()) {
 				throw result.error
 			}
 			return result.value
 		},
 		enabled: !!playlistId,
 	})
apps/mobile/src/lib/api/kugou/api.ts (1)

142-144: 不必要的类型断言

bestMatch.remoteId as string 是多余的,因为根据 LyricSearchResult 类型定义,当 source'kugou' 时,remoteId 已经是 string 类型。

♻️ 建议的修改
-			return this.getLyrics(bestMatch.remoteId as string).map((content) =>
+			return this.getLyrics(bestMatch.remoteId).map((content) =>
 				this.parseLyrics(content),
 			)
apps/mobile/src/hooks/queries/lyrics/index.ts (1)

54-54: 移除调试用的 console.log

建议移除或替换这些 console.log 语句。可以使用项目中已有的 log 工具进行调试日志记录,或者直接删除这些调试语句。

同样适用于第 71 行和第 88 行的 console.log

apps/mobile/src/app/(tabs)/settings/playback.tsx (1)

217-243: LGTM!

新增的三个歌词源选项实现与现有选项保持一致,逻辑正确。

如有需要,可以考虑将重复的 Checkbox.Item 配置抽取为数组映射,以减少代码重复:

♻️ 可选重构:抽取为配置驱动
const lyricSources = [
  { key: 'netease', label: '网易云音乐' },
  { key: 'qqmusic', label: 'QQ 音乐' },
  { key: 'kuwo', label: '酷我音乐' },
  { key: 'kugou', label: '酷狗音乐' },
  { key: 'baidu', label: '百度音乐' },
] as const

// 在 JSX 中:
{lyricSources.map(({ key, label }) => (
  <Checkbox.Item
    key={key}
    mode='ios'
    label={label}
    status={lyricSource === key ? 'checked' : 'unchecked'}
    onPress={() => {
      setSettings({ lyricSource: key })
      setLyricSourceMenuVisible(false)
    }}
  />
))}
packages/external-playlist-sync/tsconfig.json (1)

1-17: LGTM!

TypeScript 配置合理,NodeNext 模块解析与代码中使用的 .js 扩展名导入匹配。

如果该包需要被工作区内其他包消费类型,可考虑添加声明文件输出:

💡 可选:启用声明文件生成
 {
 	"compilerOptions": {
 		"target": "ESNext",
 		"module": "NodeNext",
 		"moduleResolution": "NodeNext",
 		"lib": ["ESNext"],
 		"strict": true,
 		"esModuleInterop": true,
 		"skipLibCheck": true,
 		"forceConsistentCasingInFileNames": true,
 		"outDir": "./dist",
 		"rootDir": "./",
-		"resolveJsonModule": true
+		"resolveJsonModule": true,
+		"declaration": true,
+		"declarationMap": true
 	},
 	"include": ["./**/*.ts"],
 	"exclude": ["node_modules", "dist"]
 }
packages/external-playlist-sync/index.ts (1)

14-14: JSON.parse 结果直接类型断言存在风险。

如果 result 不是有效的 JSON 或结构不符合 NeteasePlaylistResponse,运行时会出错但 TypeScript 无法捕获。建议添加基本的运行时校验。

🛡️ 建议添加基础校验
-		const data = JSON.parse(result) as NeteasePlaylistResponse
+		const data: unknown = JSON.parse(result)
+		if (
+			!data ||
+			typeof data !== 'object' ||
+			!('playlist' in data) ||
+			!data.playlist?.tracks
+		) {
+			throw new Error('Invalid playlist response structure')
+		}
+		const playlistData = data as NeteasePlaylistResponse

或考虑使用 zod 等库进行 schema 验证。

packages/external-playlist-sync/src/types/netease.ts (1)

40-45: 建议将 tns 字段标记为可选。

根据网易云音乐 API 实际返回的数据,tns(翻译名称)字段在大多数情况下可能不存在。建议与 NeteaseSong 中的 tns?: string[] 保持一致。

♻️ 建议修改
 export interface NeteaseArtist {
 	id: number
 	name: string
-	tns: string[]
+	tns?: string[]
 	alias: string[]
 }

 export interface NeteaseAlbum {
 	id: number
 	name: string
 	picUrl: string
-	tns: string[]
+	tns?: string[]
 }

Also applies to: 47-52

apps/mobile/src/components/modals/playlist/InputExternalPlaylistInfo.tsx (1)

55-69: onValueChange 缺少类型断言。

SegmentedButtonsonValueChange 回调参数类型为 string,但 setSource 期望 'netease' | 'qq'。建议添加类型断言以确保类型安全。

♻️ 建议修改
 <SegmentedButtons
 	value={source}
-	onValueChange={(value) => setSource(value)}
+	onValueChange={(value) => setSource(value as 'netease' | 'qq')}
 	buttons={[
 		{
 			value: 'netease',
 			label: '网易云音乐',
 		},
 		{
 			value: 'qq',
 			label: 'QQ音乐',
 		},
 	]}
 	style={styles.segmentedButtons}
 />
packages/meting/rollup.config.js (1)

28-36: 版本注入可考虑使用更健壮的方案。

当前使用简单的 code.replace('__VERSION__', ...) 可能会匹配到字符串字面量中的意外内容。可以考虑使用 @rollup/plugin-replace 或更精确的正则表达式。

♻️ 使用 `@rollup/plugin-replace` 的替代方案
import replace from '@rollup/plugin-replace';

// 在 plugins 数组中替换自定义插件
replace({
  preventAssignment: true,
  values: {
    __VERSION__: JSON.stringify(packageInfo.version)
  }
})
apps/mobile/src/lib/services/lyricService.ts (1)

183-184: 考虑修复类型定义而非使用 @ts-expect-error

使用 @ts-expect-error 来抑制类型错误表明 settings.lyricSource 的类型定义与 getBestMatchedLyricssource 参数类型不匹配。建议更新 lyricSource 设置的类型定义,使其与函数签名一致,而非抑制类型检查。

Also applies to: 197-198

packages/meting/src/providers/netease.js (2)

5-6: EAPI_IV 常量未被使用。

EAPI_IV 被定义但从未在代码中使用。eapiEncrypt 方法使用的是 AES-128-ECB 模式(第 220 行),该模式不需要 IV。如果不需要此常量,建议移除以保持代码整洁。

♻️ 建议移除未使用的常量
 // eapi 相关常量
 const EAPI_KEY = 'e82ckenh8dichen8';
-const EAPI_IV = Buffer.from('0102030405060708');

277-293: 存在未使用的私有方法。

以下方法在当前实现中未被调用:

  • _generateRandomIP() (Lines 282-293)
  • _bchexdec() (Lines 339-341)
  • _str2hex() (Lines 343-345)
  • _powMod() (Lines 350-362)

如果这些是为未来功能预留的,建议添加注释说明;否则考虑移除以减少代码维护负担。

Also applies to: 339-362

packages/meting/src/providers/kuwo.js (2)

111-122: br 参数未被使用。

url(id, br = 320) 方法接收比特率参数但在请求体中未使用该值,与其他 Provider 的实现不一致。如果酷我 API 不支持指定比特率,建议在方法注释中说明。


204-217: 歌词时间戳精度可能存在问题。

msec 的计算使用 toFixed(0) 进行四舍五入,可能导致歌词同步时出现细微偏差。考虑使用 Math.floorMath.round 明确取整策略。

const msec = Math.floor((time % 1) * 100).toString().padStart(2, '0');
apps/mobile/src/app/playlist/external-sync.tsx (1)

51-60: 建议移除注释掉的样式代码。

Lines 56-59 包含被注释掉的样式代码,这些应该被移除以保持代码整洁。

♻️ 建议移除注释代码
 			<View
 				style={[
 					styles.itemContainer,
-					{
-						// backgroundColor: theme.colors.elevation.level1, // Removed card style
-						// marginBottom: 12, // Removed spacing
-						// borderRadius: 12, // Removed border radius
-					},
 				]}
 			>
packages/meting/src/meting.js (1)

62-62: 未使用的参数 headerOnly

参数 headerOnly 已声明但从未在函数体中使用,应移除或实现相关逻辑。

♻️ 建议修复
-  async _curl(url, payload = null, headerOnly = false) {
+  async _curl(url, payload = null) {
apps/mobile/src/types/apis/netease.ts (3)

43-48: 建议将 tnsalias 设为可选属性

根据网易云 API 的实际响应,tns(翻译名)和 alias(别名)字段可能不存在或为空。将这些属性设为必填可能导致类型不匹配。

♻️ 建议修复
 export interface NeteaseArtist {
 	id: number
 	name: string
-	tns: string[]
-	alias: string[]
+	tns?: string[]
+	alias?: string[]
 }

50-55: 建议将 tns 设为可选属性

NeteaseArtist 相同,专辑的翻译名字段可能不存在。

♻️ 建议修复
 export interface NeteaseAlbum {
 	id: number
 	name: string
 	picUrl: string
-	tns: string[]
+	tns?: string[]
 }

57-67: 建议将 tlyric 设为可选属性

翻译歌词 (tlyric) 并非所有歌曲都有,API 可能返回空对象或 null。

♻️ 建议修复
 export interface NeteaseLyricResponse {
 	lrc: {
 		version: number
 		lyric: string
 	}
-	tlyric: {
+	tlyric?: {
 		version: number
 		lyric: string
 	}
 	code: number
 }
packages/meting/src/providers/base.js (1)

130-164: 避免直接修改传入的 api 对象

executeRequest 方法直接修改了传入的 api 参数(第 133、139、140 行),这可能导致调用方复用该对象时出现意外行为。

♻️ 建议:使用浅拷贝避免副作用
   async executeRequest(api, meting) {
+    // 避免修改原始对象
+    api = { ...api };
+    
     // 如果有编码方法,先进行编码
     if (api.encode) {
       api = await this.handleEncode(api);
     }

@@ -0,0 +1 @@
declare module '@roitium/meting'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

ambient 声明过于简单,导致类型丢失。

当前声明未导出任何类型,导致 Meting 类及其方法被推断为 any,这正是 index.ts 中静态分析报错的根源(Unsafe construction of an any typed value 等)。

建议补充最小类型定义:

🔧 建议添加类型定义
-declare module '@roitium/meting'
+declare module '@roitium/meting' {
+	type Source = 'netease' | 'tencent' | 'kugou' | 'kuwo' | 'baidu'
+
+	export default class Meting {
+		constructor(source: Source)
+		playlist(id: string): Promise<string>
+		// 根据实际使用补充其他方法
+	}
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
declare module '@roitium/meting'
declare module '@roitium/meting' {
type Source = 'netease' | 'tencent' | 'kugou' | 'kuwo' | 'baidu'
export default class Meting {
constructor(source: Source)
playlist(id: string): Promise<string>
// 根据实际使用补充其他方法
}
}
🤖 Prompt for AI Agents
In `@packages/external-playlist-sync/meting.d.ts` at line 1, The ambient module
declaration is too minimal and causes the Meting class and its methods to be
typed as any; update the declaration for module '@roitium/meting' to export a
minimal set of types so static analysis can type-check usage: declare and export
a Meting class (symbol: Meting) with a typed constructor (e.g.,
constructor(options?: Record<string, any>)), and add method signatures used by
the codebase such as init(): Promise<void>, search(query: string, source?:
string): Promise<any>, getSong(id: string): Promise<any>, getPlaylist(id:
string): Promise<any> (and any other methods your index.ts calls), plus export
the class as the module default or named export to match how it is imported;
include simple interfaces/types for option objects if referenced so the compiler
no longer infers any.

Comment on lines +10 to +12
"devDependencies": {
"@roitium/meting": "workspace:*"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "@roitium/meting" packages/external-playlist-sync -g "*.ts"

Repository: bbplayer-app/BBPlayer

Length of output: 223


🏁 Script executed:

head -20 packages/external-playlist-sync/package.json
cat -n packages/external-playlist-sync/index.ts | head -10

Repository: bbplayer-app/BBPlayer

Length of output: 720


@roitium/meting 从 devDependencies 移到 dependencies。

该包在 index.ts(主入口点)第 1 行进行运行时导入,并在第 8 行实际使用。将其放在 devDependencies 会导致生产环境安装(--production 或 --omit=dev)时模块缺失,引发模块解析失败。

建议的修改
-	"devDependencies": {
-		"@roitium/meting": "workspace:*"
-	}
+	"dependencies": {
+		"@roitium/meting": "workspace:*"
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"devDependencies": {
"@roitium/meting": "workspace:*"
}
"dependencies": {
"@roitium/meting": "workspace:*"
}
🤖 Prompt for AI Agents
In `@packages/external-playlist-sync/package.json` around lines 10 - 12, The
package `@roitium/meting` is currently listed under devDependencies but is
imported at runtime in index.ts (import of `@roitium/meting` at top and use around
line 8); move "@roitium/meting" from devDependencies into the dependencies
object in package.json so it is installed in production builds (ensure the exact
package key "@roitium/meting" is added under "dependencies" and removed from
"devDependencies").

Comment thread packages/meting/src/meting.js Outdated
Comment on lines +77 to +126
// 添加超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 20000);
requestOptions.signal = controller.signal;

let retries = 3;
const makeRequest = async () => {
try {
const response = await fetch(url, requestOptions);

clearTimeout(timeoutId);

// 存储响应信息
this.info = {
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries())
};

// 获取响应数据
const data = await response.text();
this.raw = data;
this.error = null;
this.status = '';

return this;
} catch (err) {
clearTimeout(timeoutId);

// 处理错误
if (err.name === 'AbortError') {
this.error = 'TIMEOUT';
this.status = 'Request timeout';
} else {
this.error = err.code || err.name;
this.status = err.message;
}

// 重试机制
if (retries > 0) {
retries--;
await new Promise(resolve => setTimeout(resolve, 1000));
return makeRequest();
} else {
return this;
}
}
};

return await makeRequest();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

超时控制和重试逻辑存在问题

  1. AbortController 无法跨重试复用AbortController 一旦调用 abort() 后就无法重置。当前实现在重试时仍使用同一个已中止的 controller,导致后续重试请求会立即失败。

  2. 超时未覆盖 response.text()clearTimeoutfetch 返回后立即调用,但 response.text() 的读取过程不受超时保护,可能导致长时间挂起。

🐛 建议修复
-    // 添加超时控制
-    const controller = new AbortController();
-    const timeoutId = setTimeout(() => controller.abort(), 20000);
-    requestOptions.signal = controller.signal;
-
     let retries = 3;
     const makeRequest = async () => {
+      // 每次重试创建新的 AbortController
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 20000);
+      
       try {
-        const response = await fetch(url, requestOptions);
-        
-        clearTimeout(timeoutId);
+        const response = await fetch(url, { ...requestOptions, signal: controller.signal });
         
         // 存储响应信息
         this.info = {
           statusCode: response.status,
           headers: Object.fromEntries(response.headers.entries())
         };

         // 获取响应数据
         const data = await response.text();
+        clearTimeout(timeoutId);
         this.raw = data;
         this.error = null;
         this.status = '';
         
         return this;
       } catch (err) {
         clearTimeout(timeoutId);
🤖 Prompt for AI Agents
In `@packages/meting/src/meting.js` around lines 77 - 126, The current
retry/timeout logic reuses a single AbortController and timeoutId across
attempts and clears the timeout before response.text() finishes, so retries
immediately fail and response.text() remains unprotected; fix by moving creation
of AbortController, timeoutId and assignment requestOptions.signal inside each
attempt (inside makeRequest) so each retry gets a fresh controller and timeout,
start the per-attempt setTimeout that aborts that controller, await both fetch
and response.text() before clearing that attempt's timeout (so response.text()
is also covered), and ensure you set requestOptions.signal = controller.signal
for that attempt and clearTimeout(timeoutId) in both the success and error paths
for that attempt to avoid leaks; reference symbols: AbortController, controller,
timeoutId, requestOptions.signal, makeRequest, fetch, response.text(), retries.

Comment on lines +254 to +266
for (let i = 0; i < qualityMap.length; i++) {
const [sizeKey, br, prefix, ext] = qualityMap[i];
if (data.data[0].file[sizeKey] && br <= this.meting.temp.br) {
if (vkeys[i].vkey) {
url = {
url: response.req_0.data.sip[0] + vkeys[i].purl,
size: data.data[0].file[sizeKey],
br: br
};
break;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

访问 data.data[0] 前应检查数组是否为空。

若 API 返回空数组,data.data[0]undefined,后续访问 .file.mid 会抛出 TypeError。

🛡️ 建议的修复方案
     const response = JSON.parse(await this.meting._exec(api));
     const vkeys = response.req_0.data.midurlinfo;
     
+    if (!data.data || data.data.length === 0) {
+      return JSON.stringify({ url: '', size: 0, br: -1 });
+    }
+
     let url;
     for (let i = 0; i < qualityMap.length; i++) {
🤖 Prompt for AI Agents
In `@packages/meting/src/providers/tencent.js` around lines 254 - 266, Before
accessing data.data[0] inside the qualityMap loop, add a guard to verify the API
response contains an array with at least one element (e.g., check data &&
Array.isArray(data.data) && data.data.length > 0) and handle the empty case
(return null/undefined or skip processing) to avoid TypeError; update the loop
using the existing symbols (qualityMap, vkeys, response.req_0, vkeys[i].purl,
and the accesses to .file and .mid) so you only read data.data[0].file and
data.data[0].mid after the presence check and provide a clear fallback path.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@apps/mobile/src/hooks/queries/lyrics/index.ts`:
- Around line 125-143: The effect currently lists processedProviders in its
dependency array while also mutating it inside (via setProcessedProviders),
which risks unnecessary reruns and closure bugs; change to track processed
providers with a useRef (e.g., processedProvidersRef) instead of state so you
can remove processedProviders from the useEffect deps, keep neteaseData, qqData,
kugouData in the dependency array, and update results via setResults(prev =>
[...prev, ...data]) only when processedProvidersRef.current lacks the provider;
after appending results mark processedProvidersRef.current.add(provider) rather
than calling setProcessedProviders to avoid the circular dependency.

In `@apps/mobile/src/lib/api/kugou/api.ts`:
- Around line 101-111: Before decoding downloadRes.content, validate the
response status and presence of content: inside the
ResultAsync.fromPromise().map callback that receives downloadRes (from the fetch
-> KugouLyricDownloadResponse flow), check downloadRes.status for success (e.g.
200) and ensure downloadRes.content is non-empty; if invalid, throw a
descriptive Error so the ResultAsync rejects rather than calling
CryptoJS.enc.Base64.parse on an empty/invalid value. Update the branch that
currently decodes using CryptoJS.enc.Base64.parse(raw) /
CryptoJS.enc.Utf8.stringify(word) to run only after these checks.

In `@apps/mobile/src/lib/api/qqmusic/api.ts`:
- Around line 173-203: The getPlaylist method is missing the optional
AbortSignal parameter used elsewhere; update the public getPlaylist(id: string)
signature to accept signal?: AbortSignal and pass that signal into the fetch
call options so it behaves like search(), getLyrics(), and
searchBestMatchedLyrics(); keep the same error handling and
ResultAsync.fromPromise wrapper but include the signal in the fetch options to
enable request cancellation.

In `@apps/mobile/src/lib/services/externalPlaylistService.ts`:
- Around line 63-85: The mapping in the qqMusicApi.getPlaylist response (inside
the .map callback that builds playlist and tracks) assumes playlist.songlist,
each track.singer, and track.album exist and will throw if any are undefined;
update the code to defensively access these fields using optional chaining and
defaults: treat playlist.songlist as an empty array if falsy, treat track.singer
as an empty array, use track.album?.name and a safe album object fallback,
coerce duration with a numeric fallback, and only build coverUrl when
track.album?.mid exists (or use a default image); ensure translatedTitle falls
back to an empty string and that tracks mapping always returns a valid object
structure so the function degrades gracefully on missing QQ fields.
- Around line 102-112: 当前在 externalPlaylistService.ts 的返回分支使用 okAsync
返回一个默认“空”结果(函数中返回的 okAsync({...})会掩盖不支持的 source 情况);请将该分支改为返回 errAsync(...)
并传入明确错误信息(例如 "Unsupported source" 或包含 source 值的描述),同时在文件顶部新增 errAsync
的导入以替换/补充现有 okAsync 使用,确保调用方能接收到失败语义而非假成功。

In `@apps/mobile/src/lib/services/lyricService.ts`:
- Around line 206-207: The code currently casts lyricSource with
`@ts-expect-error` to call getBestMatchedLyrics while getBestMatchedLyrics only
accepts 'auto' | 'netease' | 'qqmusic' | 'kugou'; either extend
getBestMatchedLyrics' signature to include 'kuwo' and 'baidu' and implement
their provider search logic (and the underlying API client/provider
implementations) and ensure callers like the site using lyricSource pass the new
union, or remove 'kuwo' and 'baidu' from the settings in appStore.ts so they
never reach getBestMatchedLyrics; also remove the `@ts-expect-error`, update all
type annotations/usages of getBestMatchedLyrics, and guard against
Promise.any([]) by ensuring at least one provider is added or by handling the
empty-array case in getBestMatchedLyrics.
🧹 Nitpick comments (9)
apps/mobile/app.config.ts (1)

136-139: 明文 HTTP 被全量启用,存在传输安全风险

usesCleartextTraffic 直接设为 true 会让所有域名走明文 HTTP,容易被中间人攻击。建议仅在开发/测试环境启用,或通过显式环境变量开关,并在生产保持 false(必要时再用域名白名单配置)。

✅ 建议的最小改动(仅开发启用)
-					usesCleartextTraffic: true,
+					usesCleartextTraffic: IS_DEV && process.env.ALLOW_CLEARTEXT === 'true',
apps/mobile/src/lib/api/netease/api.ts (1)

128-143: 建议为 getPlaylist 增加可选 AbortSignal,统一取消行为。
同步外部歌单场景可能较长,建议与 getLyrics/search 保持一致。

♻️ 建议修改
 getPlaylist(
 	id: string,
+	signal?: AbortSignal,
 ): ResultAsync<NeteasePlaylistResponse, NeteaseApiError> {
 	const data = {
 		s: '0',
 		id: id,
 		n: '1000',
 		t: '0',
 	}
 	const requestOptions: RequestOptions = createOption({}, 'eapi')
+	if (signal) {
+		requestOptions.signal = signal
+	}
 	return createRequest<object, NeteasePlaylistResponse>(
 		'/api/v6/playlist/detail',
 		data,
 		requestOptions,
 	).map((res) => res.body)
 }
apps/mobile/src/lib/api/kugou/api.ts (2)

147-149: 不必要的类型断言

bestMatch.remoteId as string 是多余的。根据 LyricSearchResult 类型定义,当 source: 'kugou' 时,remoteId 已经是 string 类型。

♻️ 建议移除类型断言
-return this.getLyrics(bestMatch.remoteId as string, signal).map(
+return this.getLyrics(bestMatch.remoteId, signal).map(
   (content) => this.parseLyrics(content),
 )

42-42: 所有 Kugou API 端点都使用 HTTP 而非 HTTPS,存在安全风险

三个端点(mobilecdn.kugou.comkrcs.kugou.comlyrics.kugou.com)均采用明文 HTTP 协议:

  • 请求/响应数据易被中间人攻击窃听或篡改
  • 在非信任网络环境下可能被拦截

虽然 Kugou 官方文档明确声明官方 API 均使用 HTTPS,建议确认这些移动端点是否也支持 HTTPS 并相应迁移。

apps/mobile/src/lib/services/lyricService.ts (2)

69-98: 并发取消逻辑存在边界问题

createProviderPromise 中的取消逻辑在首个 provider 成功后会中止其他请求,这是合理的。但有两个潜在问题:

  1. apiCall 因被 abort 而抛出 AbortError 时,这个错误会被传递到 Promise.any,可能污染错误聚合
  2. 如果所有 provider 都被 abort(理论上不应发生),Promise.any 会收到全是 AbortError 的聚合错误

当前实现在正常场景下工作正常,但建议在错误消息中过滤掉 AbortError,以提供更清晰的失败信息。


140-155: Promise.any 错误处理基本正确

使用 Promise.any 配合 AggregateError 处理是正确的模式。建议增加对 aggregateError.errors 不存在情况的防御性处理。

🛡️ 建议增加防御性检查
 return ResultAsync.fromPromise(Promise.any(providers), (e) => {
   // All failed
   // e will be an AggregateError if using Promise.any
   const aggregateError = e as AggregateError
-  const errors = Array.from(aggregateError.errors || [])
+  const errors = Array.isArray(aggregateError.errors) 
+    ? aggregateError.errors 
+    : [aggregateError]
   const errorMessages = errors
apps/mobile/src/hooks/queries/lyrics/index.ts (2)

52-55: 状态更新链可以合并优化

静态分析工具提示避免链式状态更新。当 searchQuery 变化时,setResultssetProcessedProviders 可以合并为单次更新,或使用 useReducer 来管理相关状态。

♻️ 建议使用函数式更新或合并状态
+// 方案1: 合并为单一状态对象
+const [searchState, setSearchState] = useState<{
+  results: LyricSearchResult
+  processedProviders: Set<string>
+}>({ results: [], processedProviders: new Set() })

 // Effect to reset results when query changes
 useEffect(() => {
-  setResults([])
-  setProcessedProviders(new Set())
+  setSearchState({ results: [], processedProviders: new Set() })
 }, [searchQuery])

66-66: 调试用的 console.log 应移除或替换为 logger

生产代码中不应保留 console.log 调试语句。建议移除或使用项目中统一的 log 工具(如 @/utils/log)。

♻️ 建议移除或替换
-console.log('Searching Netease:', searchQuery)
-console.log('Searching QQ:', searchQuery)
-console.log('Searching Kugou:', searchQuery)

Also applies to: 86-86, 103-103

apps/mobile/src/lib/services/externalPlaylistService.ts (1)

126-137: 取消信号的响应可以更及时。
当前会至少等待 MIN_DELAY 才响应 abort,用户取消后仍会卡住一段时间。建议改为可中断的 wait。

♻️ 建议改为可中断等待
-          await wait(MIN_DELAY)
+          await waitWithAbort(MIN_DELAY, options?.signal)
const waitWithAbort = (ms: number, signal?: AbortSignal) =>
  new Promise<void>((resolve, reject) => {
    const t = setTimeout(resolve, ms)
    if (!signal) return
    if (signal.aborted) {
      clearTimeout(t)
      return reject(new Error('Aborted'))
    }
    const onAbort = () => {
      clearTimeout(t)
      signal.removeEventListener('abort', onAbort)
      reject(new Error('Aborted'))
    }
    signal.addEventListener('abort', onAbort, { once: true })
  })

Comment thread apps/mobile/src/hooks/queries/lyrics/index.ts Outdated
Comment on lines +101 to +111
return ResultAsync.fromPromise(
fetch(downloadUrl, { signal }).then(
(res) => res.json() as Promise<KugouLyricDownloadResponse>,
),
(e) => new Error('Failed to download lyric from Kugou', { cause: e }),
).map((downloadRes) => {
// Decode Base64 content
const raw = downloadRes.content
const word = CryptoJS.enc.Base64.parse(raw)
return CryptoJS.enc.Utf8.stringify(word)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

下载歌词时缺少状态码校验

在解码 downloadRes.content 之前,未检查 downloadRes.status。若服务端返回错误状态或 content 为空,CryptoJS.enc.Base64.parse(raw) 可能产生意外结果。

🛡️ 建议添加状态校验
 return ResultAsync.fromPromise(
   fetch(downloadUrl, { signal }).then(
     (res) => res.json() as Promise<KugouLyricDownloadResponse>,
   ),
   (e) => new Error('Failed to download lyric from Kugou', { cause: e }),
 ).andThen((downloadRes) => {
+  if (downloadRes.status !== 200 || !downloadRes.content) {
+    return errAsync(new Error('Invalid lyric download response from Kugou'))
+  }
   // Decode Base64 content
   const raw = downloadRes.content
   const word = CryptoJS.enc.Base64.parse(raw)
-  return CryptoJS.enc.Utf8.stringify(word)
+  return okAsync(CryptoJS.enc.Utf8.stringify(word))
 })
🤖 Prompt for AI Agents
In `@apps/mobile/src/lib/api/kugou/api.ts` around lines 101 - 111, Before decoding
downloadRes.content, validate the response status and presence of content:
inside the ResultAsync.fromPromise().map callback that receives downloadRes
(from the fetch -> KugouLyricDownloadResponse flow), check downloadRes.status
for success (e.g. 200) and ensure downloadRes.content is non-empty; if invalid,
throw a descriptive Error so the ResultAsync rejects rather than calling
CryptoJS.enc.Base64.parse on an empty/invalid value. Update the branch that
currently decodes using CryptoJS.enc.Base64.parse(raw) /
CryptoJS.enc.Utf8.stringify(word) to run only after these checks.

Comment on lines +173 to +203
/**
* Get playlist by id
* @param id
* @returns
*/
public getPlaylist(id: string): ResultAsync<QQMusicPlaylistResponse, Error> {
const params = new URLSearchParams({
id,
format: 'json',
newsong: '1',
platform: 'jqspaframe.json',
})

const url = `https://c.y.qq.com/v8/fcg-bin/fcg_v8_playlist_cp.fcg?${params.toString()}`

return ResultAsync.fromPromise(
fetch(url, {
headers: {
Referer: 'http://y.qq.com',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
},
}).then((res) => {
if (!res.ok) {
throw new Error(`QQ Music API error: ${res.statusText}`)
}
return res.json() as Promise<QQMusicPlaylistResponse>
}),
(e) => new Error('Failed to fetch playlist from QQ Music', { cause: e }),
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find the file and examine its structure
fd api.ts apps/mobile/src/lib/api/qqmusic/

Repository: bbplayer-app/BBPlayer

Length of output: 103


🏁 Script executed:

# Let's examine the API class to see all methods and their signatures
cat -n apps/mobile/src/lib/api/qqmusic/api.ts | head -250

Repository: bbplayer-app/BBPlayer

Length of output: 6801


🏁 Script executed:

# Check if there's a type definition file for QQMusicPlaylistResponse
fd -i "qqmusic\|types\|interface" apps/mobile/src/lib/api/

Repository: bbplayer-app/BBPlayer

Length of output: 47


🏁 Script executed:

# Search for AbortSignal usage in the codebase to understand the pattern
rg "AbortSignal" apps/mobile/src/lib/api/

Repository: bbplayer-app/BBPlayer

Length of output: 870


🏁 Script executed:

# Find the types file for QQMusic APIs
fd qqmusic types/

Repository: bbplayer-app/BBPlayer

Length of output: 158


🏁 Script executed:

# Look at the type definitions
cat -n apps/mobile/src/types/apis/qqmusic.ts 2>/dev/null || cat -n apps/mobile/src/types/apis/qqmusic/index.ts 2>/dev/null

Repository: bbplayer-app/BBPlayer

Length of output: 2037


🏁 Script executed:

# Search for QQMusicPlaylistResponse definition
rg "QQMusicPlaylistResponse" apps/mobile/src/

Repository: bbplayer-app/BBPlayer

Length of output: 425


getPlaylist() 中补齐 AbortSignal 参数以保持一致性。

search()getLyrics()searchBestMatchedLyrics() 都有 signal?: AbortSignal 参数,但 getPlaylist() 缺失。建议添加该参数并传递给 fetch 以与其他方法保持一致。

🔧 建议改动(补齐 signal 参数)
-	public getPlaylist(id: string): ResultAsync<QQMusicPlaylistResponse, Error> {
+	public getPlaylist(
+		id: string,
+		signal?: AbortSignal,
+	): ResultAsync<QQMusicPlaylistResponse, Error> {
 		const params = new URLSearchParams({
 			id,
 			format: 'json',
 			newsong: '1',
 			platform: 'jqspaframe.json',
 		})

 		const url = `https://c.y.qq.com/v8/fcg-bin/fcg_v8_playlist_cp.fcg?${params.toString()}`

 		return ResultAsync.fromPromise(
 			fetch(url, {
 				headers: {
 					Referer: 'http://y.qq.com',
 					'User-Agent':
 						'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
 				},
+				signal,
 			}).then((res) => {
 				if (!res.ok) {
 					throw new Error(`QQ Music API error: ${res.statusText}`)
 				}
 				return res.json() as Promise<QQMusicPlaylistResponse>
 			}),
 			(e) => new Error('Failed to fetch playlist from QQ Music', { cause: e }),
 		)
 	}
🤖 Prompt for AI Agents
In `@apps/mobile/src/lib/api/qqmusic/api.ts` around lines 173 - 203, The
getPlaylist method is missing the optional AbortSignal parameter used elsewhere;
update the public getPlaylist(id: string) signature to accept signal?:
AbortSignal and pass that signal into the fetch call options so it behaves like
search(), getLyrics(), and searchBestMatchedLyrics(); keep the same error
handling and ResultAsync.fromPromise wrapper but include the signal in the fetch
options to enable request cancellation.

Comment on lines +63 to +85
return qqMusicApi.getPlaylist(playlistId).map((response) => {
const playlist = response.data.cdlist[0]
if (!playlist)
return {
playlist: {
id: playlistId,
title: 'Unknown',
coverUrl: '',
description: '',
trackCount: 0,
author: { name: 'Unknown' },
},
tracks: [],
}

const tracks = playlist.songlist.map((track) => ({
title: track.name,
artists: track.singer.map((s) => s.name),
album: track.album.name,
duration: track.interval * 1000,
coverUrl: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${track.album.mid}.jpg`,
translatedTitle: track.subtitle,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

QQ 返回字段缺失时可能直接抛异常。
songlist / singer / album 任一为 undefined 就会在 map 或字段访问时崩溃。建议加默认值与可选链,保证外部 API 异常数据下也能降级运行。

🔧 建议修复(增加防御性字段处理)
-        const tracks = playlist.songlist.map((track) => ({
-          title: track.name,
-          artists: track.singer.map((s) => s.name),
-          album: track.album.name,
-          duration: track.interval * 1000,
-          coverUrl: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${track.album.mid}.jpg`,
-          translatedTitle: track.subtitle,
-        }))
+        const tracks = (playlist.songlist ?? []).map((track) => ({
+          title: track.name ?? '',
+          artists: (track.singer ?? []).map((s) => s.name).filter(Boolean),
+          album: track.album?.name ?? '',
+          duration: (track.interval ?? 0) * 1000,
+          coverUrl: track.album?.mid
+            ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${track.album.mid}.jpg`
+            : '',
+          translatedTitle: track.subtitle ?? '',
+        }))
🤖 Prompt for AI Agents
In `@apps/mobile/src/lib/services/externalPlaylistService.ts` around lines 63 -
85, The mapping in the qqMusicApi.getPlaylist response (inside the .map callback
that builds playlist and tracks) assumes playlist.songlist, each track.singer,
and track.album exist and will throw if any are undefined; update the code to
defensively access these fields using optional chaining and defaults: treat
playlist.songlist as an empty array if falsy, treat track.singer as an empty
array, use track.album?.name and a safe album object fallback, coerce duration
with a numeric fallback, and only build coverUrl when track.album?.mid exists
(or use a default image); ensure translatedTitle falls back to an empty string
and that tracks mapping always returns a valid object structure so the function
degrades gracefully on missing QQ fields.

Comment thread apps/mobile/src/lib/services/externalPlaylistService.ts Outdated
Comment thread apps/mobile/src/lib/services/lyricService.ts Outdated
@roitium roitium marked this pull request as draft February 1, 2026 10:47
@roitium roitium marked this pull request as ready for review February 1, 2026 11:28
@roitium roitium merged commit 48fd7cf into dev Feb 1, 2026
3 of 5 checks passed
@roitium roitium deleted the feat/sync-external-playlist branch February 1, 2026 11:29
roitium added a commit that referenced this pull request Feb 8, 2026
* fix(mobile): fix version code getter logic

* Merge pull request #193 from bbplayer-app/feat/nitro-fetch

* fix(orpheus, mobile): disable usesCleartextTraffic, only allow hdslb.com use http

* fix(mobile): disable r8

* fix(mobile): update proguard rules and enable R8

* chore(mobile): disable r8

* fix(mobile): r8 made @nandrorjo/galeria raise error

* feat: sync external playlist (#194)

* feat(mobile): e2e (#195)

* feat(mobile): 1 (#196)

* fix(mobile): add more current state check

I don't know why, but I hope it will work.

* feat(mobile): some improvements (#198)

* feat(orpheus): enhance backend retention capabilities

* fix(mobile): player page setpage

* fix(orpheus): allow androidx.media3.player wrapping in REPEAT_MODE_ONE (#199)

* fix(orpheus): allow androidx.media3.player wrapping in REPEAT_MODE_ONE

* chore(mobile): update changelog

---------

Co-authored-by: roitium <65794453+roitium@users.noreply.github.com>

* feat(mobile): improve playlist UI (#200)

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): verbatim lyrics (#201)

* feat(mobile, splash): improve verbatim lyrics

* perf(mobile): improve verbatim lyrics perf

* fix(mobile): player slider cannot slide

* fix(mobile): react compiler in PlayerLyrics

* chore(mobile): remove reanimated logger config

* chore(mobile): edit changelog

* fix(mobile): desktop lyrics not work

* feat(mobile): add firebase analytics integration (#203)

* fix(mobile): improve UI on small screen devices

* perf(mobile): some perf improvements

* feat(mobile): firebase

* feat(mobile): add firebase analytics track

* chore(mobile): update privacy

* chore(mobile): use firebase analytics new api

* chore(root): move some libraries

* fix(mobile): type

* chore(root, mobile): update package.json

* chore(docs): update docs

* chore(docs): update docs

* chore(docs): update docs

* chore(root): wiki

* chore(root): wiki

* chore(orpheus): wiki

* feat(mobile): danmaku (#205)

* feat(mobile): implement bilibili danmaku api

* fix(mobile): use `useWindowDimensions` instead of `Dimensions.get`

* feat(mobile): implement

* fix(mobile): 1

* chore(mobile): bump version

* chore(mobile): update changelog

* ci(mobile): wrong secret key

* fix(root): ci not work

* chore(root): just make sure

* chore(root): remove unnecessary easignore

* chore(root): remove unnecessary easignore

* chore(root): update update json

---------

Co-authored-by: Deyu Wang <kvtoDev@outlook.com>
roitium added a commit that referenced this pull request Feb 9, 2026
* fix(mobile): fix version code getter logic

* Merge pull request #193 from bbplayer-app/feat/nitro-fetch

* fix(orpheus, mobile): disable usesCleartextTraffic, only allow hdslb.com use http

* fix(mobile): disable r8

* fix(mobile): update proguard rules and enable R8

* chore(mobile): disable r8

* fix(mobile): r8 made @nandrorjo/galeria raise error

* feat: sync external playlist (#194)

* feat(mobile): e2e (#195)

* feat(mobile): 1 (#196)

* fix(mobile): add more current state check

I don't know why, but I hope it will work.

* feat(mobile): some improvements (#198)

* feat(orpheus): enhance backend retention capabilities

* fix(mobile): player page setpage

* fix(orpheus): allow androidx.media3.player wrapping in REPEAT_MODE_ONE (#199)

* fix(orpheus): allow androidx.media3.player wrapping in REPEAT_MODE_ONE

* chore(mobile): update changelog

---------

Co-authored-by: roitium <65794453+roitium@users.noreply.github.com>

* feat(mobile): improve playlist UI (#200)

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): 1

* feat(mobile): verbatim lyrics (#201)

* feat(mobile, splash): improve verbatim lyrics

* perf(mobile): improve verbatim lyrics perf

* fix(mobile): player slider cannot slide

* fix(mobile): react compiler in PlayerLyrics

* chore(mobile): remove reanimated logger config

* chore(mobile): edit changelog

* fix(mobile): desktop lyrics not work

* feat(mobile): add firebase analytics integration (#203)

* fix(mobile): improve UI on small screen devices

* perf(mobile): some perf improvements

* feat(mobile): firebase

* feat(mobile): add firebase analytics track

* chore(mobile): update privacy

* chore(mobile): use firebase analytics new api

* chore(root): move some libraries

* fix(mobile): type

* chore(root, mobile): update package.json

* chore(docs): update docs

* chore(docs): update docs

* chore(docs): update docs

* chore(root): wiki

* chore(root): wiki

* chore(orpheus): wiki

* feat(mobile): danmaku (#205)

* feat(mobile): implement bilibili danmaku api

* fix(mobile): use `useWindowDimensions` instead of `Dimensions.get`

* feat(mobile): implement

* fix(mobile): 1

* chore(mobile): bump version

* chore(mobile): update changelog

* ci(mobile): wrong secret key

* fix(root): ci not work

* chore(root): just make sure

* chore(root): remove unnecessary easignore

* chore(root): remove unnecessary easignore

* chore(root): update update json

* fix(mobile): auto compile protobuf when prepare

* chore(root): update app screenshots

* fix(mobile): player controls button not work (#208)

* fix(mobile): remove player page animation (#212)

* fix(mobile): remove player page animation

* chore(mobile): bump version

* fix(mobile): cover

* chore(mobile): Bump version to 2.3.1 and update release URL

Updated version and URL for the release notes.

---------

Co-authored-by: Deyu Wang <kvtoDev@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 能不能加一个导入其他音乐软件歌单功能

1 participant