Conversation
There was a problem hiding this comment.
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>>() |
There was a problem hiding this comment.
lrcLineAccumulator 在多个线程(MediaSession 钩子线程和 Xposed 方法钩子线程)中被访问,mutableMapOf 不是线程安全的。建议使用 ConcurrentHashMap,并确保内部的 Set 也是线程安全的。
| 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) |
| 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() |
| 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) { } | ||
| } |
| 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}") | ||
| } | ||
| } |
| ) | ||
|
|
||
| /** 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,}""") |
| 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) { |
|
希望能支持咪咕音乐 |
使用deepseek V4 Pro,对网易云音乐云盘中上传文件内封装的歌词提供了支持
现在会hook网易云歌词并提供
类名基于网易云音乐9.3.35反编译获取,不确保在其他版本可用性
在9.3.35版本已测试可用