macOS 菜单栏歌词工具,读取波点音乐客户端本地歌曲缓存和日志,再请求歌词并显示当前歌词行。

📅 2026/7/24 12:54:51
macOS 菜单栏歌词工具,读取波点音乐客户端本地歌曲缓存和日志,再请求歌词并显示当前歌词行。
波点歌词macOS 菜单栏歌词工具读取波点音乐客户端本地歌曲缓存和日志再请求歌词并显示当前歌词行。构建要求 macOS、Xcode Command Line Tools以及已安装并运行的波点音乐客户端。./build.shopenbuild/波点歌词.app也可以直接使用发布包中的波点歌词.app。说明支持 Apple Silicon 和 Intel Mac。不截取屏幕不需要辅助功能权限。只读取当前用户的波点音乐缓存目录。波点客户端的本地文件格式或歌词接口变化后工具可能需要调整。本项目与波点音乐及酷我音乐无隶属关系。请遵守相关服务条款和适用的版权规定。mportCocoaimportFoundationstructSong{letid:Stringletname:Stringletartist:Stringletstarted:Date}structLyricLine{lettime:TimeIntervallettext:String}finalclassAppDelegate:NSObject,NSApplicationDelegate{privatevaritem:NSStatusItem!privatevartimer:Timer!privatevarsongIDprivatevarlines:[LyricLine][]privatevarstartedDate()privatevarcacheRoot:String{NSHomeDirectory()/Library/Caches/cn.wenyu.bodian.bodianPc}funcapplicationDidFinishLaunching(_notification:Notification){NSApp.setActivationPolicy(.accessory)itemNSStatusBar.system.statusItem(withLength:NSStatusItem.variableLength)item.length240setTitle(波点歌词)letmenuNSMenu()menu.addItem(NSMenuItem(title:刷新歌词,action:#selector(refresh),keyEquivalent:r))menu.addItem(.separator())menu.addItem(NSMenuItem(title:退出,action:#selector(quit),keyEquivalent:q))item.menumenu timerTimer.scheduledTimer(timeInterval:1,target:self,selector:#selector(refresh),userInfo:nil,repeats:true)refresh()}objcprivatefuncrefresh(){guardletsongcurrentSong()else{setTitle(波点歌词)return}ifsong.id!songID{songIDsong.id startedsong.started lines[]fetchLyrics(song)}letelapsedDate().timeIntervalSince(started)letcurrentlines.last(where:{$0.timeelapsed})?.text??song.namesetTitle(current.isEmpty?song.name:current)}privatefuncsetTitle(_value:String){lettextvalue.replacingOccurrences(of:\n,with: ).trimmingCharacters(in:.whitespacesAndNewlines)item.button?.imagenilitem.button?.titletext item.button?.attributedTitleNSAttributedString(string:text,attributes:[.font:NSFont.systemFont(ofSize:13)])item.button?.toolTiptext}privatefunccurrentSong()-Song?{letpathcacheRoot/database/songDB.dbletpProcess()letoutPipe()p.executableURLURL(fileURLWithPath:/usr/bin/sqlite3)p.arguments[path,select id || | || json_extract(json, $.name) || | || json_extract(json, $.artist) || | || time from hist_song order by ord desc limit 1;]p.standardOutputouttry?p.run()p.waitUntilExit()guardletvalueString(data:out.fileHandleForReading.readDataToEndOfFile(),encoding:.utf8)else{returnnil}letpartsvalue.trimmingCharacters(in:.whitespacesAndNewlines).split(separator:|,maxSplits:3).map(String.init)letformatterDateFormatter()formatter.localeLocale(identifier:en_US_POSIX)formatter.dateFormatyyyy-MM-dd HH:mm:ss.SSSSSSguardparts.count4,letdateformatter.date(from:parts[3])else{returnnil}returnSong(id:parts[0],name:parts[1],artist:parts[2],started:date)}privatefuncfetchLyrics(_song:Song){letlogPathcacheRoot/bdlog/log_1.logguardletlogtry?String(contentsOfFile:logPath,encoding:.utf8),letlinelog.split(separator:\n).last(where:{$0.contains(uri: http://mlyric.kuwo.cn/mobi.s)}),leturlTextline.split(separator:uri: ,maxSplits:1).last,letoldURLURL(string:String(urlText).trimmingCharacters(in:.whitespacesAndNewlines)),letoldQueryURLComponents(url:oldURL,resolvingAgainstBaseURL:false)?.queryItems,letuidoldQuery.first(where:{$0.nameuid})?.value,lettokenoldQuery.first(where:{$0.nametoken})?.valueelse{return}letrawtypelyricreq2lrcx1rid\(song.id)songname\(song.name)artist\(song.artist)corpkuwofromchannelbodianletqData(raw.utf8).base64EncodedString()varcomponentsURLComponents(string:https://mlyric.kuwo.cn/mobi.s)!components.queryItems[URLQueryItem(name:f,value:bodian),URLQueryItem(name:q,value:q),URLQueryItem(name:uid,value:uid),URLQueryItem(name:token,value:token)]guardleturlcomponents.urlelse{return}URLSession.shared.dataTask(with:url){[weakself]data,_,_inguardletdata,letjsontry?JSONSerialization.jsonObject(with:data)as?[String:Any],letpayloadjson[data]as?[String:Any],letencodedpayload[content]as?String,letlyricDataData(base64Encoded:encoded),letlyricString(data:lyricData,encoding:.utf8)else{return}letparsedlyric.split(separator:\n).compactMap{line-LyricLine?inletsString(line)guardletmatchs.range(of:#\[(\d):(\d(?:\.\d)?)\](.*)#,options:.regularExpression)else{returnnil}letpartss[match].dropFirst().split(separator:],maxSplits:1).map(String.init)guardparts.count2else{returnnil}letclockparts[0].split(separator::).map(String.init)guardclock.count2,letminDouble(clock[0]),letsecDouble(clock[1])else{returnnil}lettextparts[1].replacingOccurrences(of:#[-0-9],[-0-9]#,with:,options:.regularExpression).trimmingCharacters(in:.whitespaces)returnLyricLine(time:min*60sec,text:text)}DispatchQueue.main.async{self?.linesparsed}}.resume()}objcprivatefuncquit(){NSApp.terminate(nil)}}letappNSApplication.sharedletdelegateAppDelegate()app.delegatedelegate app.run()俺还不会贴app。。网盘分享了通过网盘分享的文件软件包相关.zip链接: https://pan.baidu.com/s/1ojUcjMpIeCyyZ77RxmsGFQ?pwdcu9x 提取码: cu9x 复制这段内容后打开百度网盘手机App操作更方便哦