Skip to content

增加对云盘音乐内封装歌词的支持 - #82

Open
LLLingYu wants to merge 1 commit into
tomakino:masterfrom
LLLingYu:master
Open

LLLingYu wants to merge 1 commit into
tomakino:masterfrom
LLLingYu:master

Conversation

@LLLingYu

Copy link
Copy Markdown

使用deepseek V4 Pro,对网易云音乐云盘中上传文件内封装的歌词提供了支持
现在会hook网易云歌词并提供
类名基于网易云音乐9.3.35反编译获取,不确保在其他版本可用性
在9.3.35版本已测试可用

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a LyricInterceptor to capture lyrics directly from the application's internal processes as a fallback mechanism. It includes cross-process synchronization of song metadata via shared files and improves the handling of empty lyric responses and non-numeric media IDs. Feedback identifies critical thread-safety issues in the lyric accumulator and logic errors in the cache flushing sequence. Additionally, there are concerns regarding performance due to frequent file I/O and overly broad method hooking, as well as bugs in the regex pattern and re-hooking logic that could lead to duplicate hooks or missed lyrics.

private var lyricInterceptor: LyricInterceptor? = null

/** 累积内部 Hook 拦截到的单行 LRC 文本,key 为 songId */
private val lrcLineAccumulator = mutableMapOf<Long, MutableSet<String>>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

lrcLineAccumulator 在多个线程(MediaSession 钩子线程和 Xposed 方法钩子线程)中被访问,mutableMapOf 不是线程安全的。建议使用 ConcurrentHashMap,并确保内部的 Set 也是线程安全的。

Suggested change
private val lrcLineAccumulator = mutableMapOf<Long, MutableSet<String>>()
private val lrcLineAccumulator = java.util.concurrent.ConcurrentHashMap<Long, MutableSet<String>>()

Log.i(TAG, "Song changed: id=$newMusicId, title=$metadata.title, artist=$metadata.artist")

// 清空上一首歌的 LRC 行累积器,并写入缓存
flushLrcAccumulator(currentMusicId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

逻辑错误:在 hookMediaSession 中,currentMusicId 在调用 onSongChanged 之前已经被更新为新歌曲的 ID。因此这里调用 flushLrcAccumulator(currentMusicId) 实际上是在刷新新歌曲的累积器,而不是上一首歌曲的。这会导致上一首歌的歌词无法正确持久化到缓存。

Comment on lines +270 to +308
val realSongId = readSharedCurrentSongId()
if (realSongId != null && realSongId != currentMusicId) {
val oldId = currentMusicId
currentMusicId = realSongId
// 把 fallback ID 下累积的行转移到真实 ID 下
val transferred = lrcLineAccumulator.remove(oldId)
if (transferred != null && transferred.isNotEmpty()) {
lrcLineAccumulator.getOrPut(currentMusicId) { LinkedHashSet() }.addAll(transferred)
Log.i(TAG, "Transferred ${transferred.size} LRC lines from fallback $oldId to real $currentMusicId")
} else {
lrcLineAccumulator.remove(currentMusicId)
}
lastCacheWriteLineCount = 0
if (MediaMetadataCache.get(currentMusicId) == null) {
MediaMetadataCache.savePlaceholder(currentMusicId)
}
Log.i(TAG, "Switched to PLAY process songId: $currentMusicId")
}

// 如果仍为 0(PLAY 进程尚未 setMetadata),用 fallback ID
if (currentMusicId == 0L) {
currentMusicId = lyricLine.hashCode().toLong().let { if (it < 0) -it else it }
MediaMetadataCache.savePlaceholder(currentMusicId)
Log.i(TAG, "Using fallback ID: $currentMusicId")
}

val metadata = MediaMetadataCache.get(currentMusicId) ?: return

// 累积 LRC 行
val lines = lrcLineAccumulator.getOrPut(currentMusicId) { LinkedHashSet() }
lines.add(lyricLine.trim())
val fullLrc = lines.joinToString("\n")

val cacheEntry = LocalLyricCache(musicId = metadata.id, lrc = fullLrc)

try {
val song = cacheEntry.toSong()
// 优先用本地 metadata,没有则从 PLAY 进程共享文件读取
val (sharedTitle, sharedArtist) = readSharedSongMeta()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

在 onInternalLyricReceived 中频繁读取和解析共享文件(每行歌词触发一次)会带来严重的 I/O 性能问题。建议在内存中缓存这些元数据,或者合并 readSharedCurrentSongId 和 readSharedSongMeta 的读取逻辑以减少文件访问次数。

Comment on lines +74 to +84
fun rehook(classLoader: ClassLoader) {
this.classLoader = classLoader
hookedMethods.clear()
try {
dexKitBridge.findClass {
searchPackages("com.netease.cloudmusic.module.lyric")
}.forEach { cls ->
tryHookClass(cls.name)
}
} catch (_: Exception) { }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

rehook 逻辑在 Tinker 加载后会清除 hookedMethods 记录并重新执行 Hook。由于 Xposed 钩子在进程生命周期内是持久的,这会导致同一个方法被注册多个回调,从而在热更新后导致歌词被重复处理。建议在 Hook 前检查方法是否已被当前类加载器处理过。

Comment on lines +99 to +117
for (method in clazz.declaredMethods) {
val hasStringParam = method.parameterTypes.any { it == String::class.java }
if (!hasStringParam) continue

val key = "$className.${method.name}"
if (key in hookedMethods) continue

try {
XposedBridge.hookMethod(method, object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
handleArgs(param.args, "$className.${method.name}")
}
})
hookedMethods.add(key)
count++
} catch (e: Exception) {
Log.d(TAG, " Hook FAILED: $key - ${e.message}")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

遍历并 Hook 所有带有 String 参数的方法过于激进,在网易云音乐这样的大型应用中会产生巨大的性能开销,并可能导致应用不稳定。建议利用 DexKit 寻找特定的歌词处理方法(如特定的混淆类名或方法签名),或者至少通过方法名白名单进行过滤。

)

/** LRC 行首时间标签正则,兼容 [mm:ss.xx] [mm:ss.xxx] [mm:ss] [mm:ss:xx] 等 */
private val lrcLinePattern = Regex("""\[\d{1,3}[ :.]\d{2}(?:[ :.]\d{1,3})?].{1,}""")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

正则表达式中的 .{1,} 要求时间戳后必须至少有一个字符,这会导致无法匹配有效的空行歌词(例如用于表示间奏或停顿的行)。建议改为 .*。

Suggested change
private val lrcLinePattern = Regex("""\[\d{1,3}[ :.]\d{2}(?:[ :.]\d{1,3})?].{1,}""")
private val lrcLinePattern = Regex("""\[\d{1,3}[ :.]\d{2}(?:[ :.]\d{1,3})?].*""")

Log.i(TAG, "*** LRC HIT! source=$source, lyricLen=${lyric.length}, hasTrans=${trans != null}")
Log.i(TAG, "*** LRC preview: ${lyric.take(150)}")

if (lyric.length > 20) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

20 个字符的长度过滤阈值过高,许多有效的短歌词行(如 [00:01.00]Hello)会被忽略。建议降低阈值或仅检查字符串是否为空。

Suggested change
if (lyric.length > 20) {
if (lyric.isNotBlank()) {

@563012289

Copy link
Copy Markdown

希望能支持咪咕音乐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants