终端里看 QQ 音乐歌词,摸鱼神器
起因
作为一名当代大学生,写代码的时候没 BGM 简直写不出 Bug(误)。但是,每次想看歌词还得切屏到 QQ 音乐,这也太打断思路了。而且在实验室或者课堂上,频繁切屏显得我很不专心。
反正天天盯着终端,干脆把歌词塞进去得了。要能实时滚动、高亮当前行,最好还能自动识别正在放什么歌,不用手动输入。
思路
说白了就三件事:
- 读播放状态:用
winsdk读 Windows 的 SMTC(系统媒体控制),QQ 音乐在放什么歌、放到哪了,系统都知道 - 拉歌词:拿歌名去
api.vkeys.cn查,把 LRC 歌词搞下来 - 终端里画:ANSI 转义序列控制光标和颜色,不用清屏也能滚动刷新
WARNING
只能在 Windows 10/11 上跑,Mac 和 Linux 的媒体接口不一样,懒得适配了
装依赖
winsdk 不是自带的,得装一下:
bash
1pip install winsdk requests
python
1import asyncio2import winsdk.windows.media.control as media_control3import requests4import json5import time6import re7import os8import sys9 10class SmoothLyrics:11 def __init__(self):12 self.api_base = "[https://api.vkeys.cn/v2/music/tencent](https://api.vkeys.cn/v2/music/tencent)"13 self.current_song = None14 self.lyrics = []15 self.current_lyric_index = -116 self.last_position = -117 self.loading = False18 self.last_status = -1 # 缓存上次状态19 self.status_line = 17 # 状态显示行号20 21 # Windows 控制台优化,开启 VT100 模式22 if sys.platform == "win32":23 import ctypes24 kernel32 = ctypes.windll.kernel3225 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)26 27 def move_cursor(self, x, y):28 """移动光标到指定位置,指哪打哪"""29 print(f"\033[{y};{x}H", end='')30 31 def clear_line(self):32 """清除当前行,保持界面干净"""33 print("\033[K", end='')34 35 def search_song(self, title, artist):36 """去 API 搜一下这首歌的 ID"""37 url = f"{self.api_base}/search/song"38 params = {"word": f"{title} {artist}"}39 40 try:41 response = requests.get(url, params=params, timeout=5)42 data = response.json()43 44 if data["code"] == 200 and data["data"]:45 for song in data["data"]:46 singers = " ".join([s["name"] for s in song.get("singer_list", [])])47 # 简单的模糊匹配48 if song["song"] == title and artist in singers:49 return song["id"]50 return data["data"][0]["id"]51 except:52 pass53 return None54 55 def get_lyrics(self, song_id):56 """拿着 ID 换歌词"""57 url = f"{self.api_base}/lyric"58 params = {"id": song_id}59 60 try:61 response = requests.get(url, params=params, timeout=5)62 data = response.json()63 64 if data["code"] == 200 and data["data"]:65 return self.parse_lrc(data["data"].get("lrc", ""))66 except:67 pass68 return []69 70 def parse_lrc(self, lrc_text):71 """解析 LRC 格式,正则表达式立大功"""72 lines = lrc_text.split('\n')73 lyrics = []74 75 for line in lines:76 if any(line.startswith(x) for x in ['[ti:', '[ar:', '[al:', '[by:']):77 continue78 79 pattern = r'\[(\d{2}):(\d{2}\.\d{2})\](.*)'80 match = re.match(pattern, line)81 82 if match:83 minutes = int(match.group(1))84 seconds = float(match.group(2))85 text = match.group(3).strip()86 total_seconds = minutes * 60 + seconds87 88 if text:89 lyrics.append({'time': total_seconds, 'text': text})90 91 return sorted(lyrics, key=lambda x: x['time'])92 93 async def get_current_media_info(self):94 """核心黑科技:从 Windows 获取当前播放信息"""95 try:96 sessions = await media_control.GlobalSystemMediaTransportControlsSessionManager.request_async()97 current_session = sessions.get_current_session()98 99 # 这里锁定了 QQMusic.exe,其他播放器改改也能用100 if current_session and "QQMusic" in current_session.source_app_user_model_id:101 info = await current_session.try_get_media_properties_async()102 playback = current_session.get_playback_info()103 timeline = current_session.get_timeline_properties()104 position = timeline.position.total_seconds() if timeline else 0105 106 return {107 "title": info.title,108 "artist": info.artist,109 "album": info.album_title,110 "status": playback.playback_status,111 "position": position112 }113 except:114 pass115 return None116 117 def find_current_lyric(self, position):118 """二分太麻烦,直接倒序遍历找当前歌词"""119 if not self.lyrics: return -1120 for i in range(len(self.lyrics) - 1, -1, -1):121 if self.lyrics[i]['time'] <= position:122 return i123 return 0124 125 def update_lyrics_display(self, current_index):126 """渲染 UI,高亮当前行"""127 if not self.lyrics:128 self.move_cursor(1, 10)129 self.clear_line()130 print("正在加载歌词..." if self.loading else "暂无歌词")131 return132 133 # 显示7行歌词(当前行前后各3行)134 display_lines = 7135 center = display_lines // 2136 start_index = max(0, current_index - center)137 end_index = min(len(self.lyrics), start_index + display_lines)138 139 if end_index - start_index < display_lines:140 start_index = max(0, end_index - display_lines)141 142 for i, line_idx in enumerate(range(start_index, end_index)):143 self.move_cursor(1, 9 + i)144 self.clear_line()145 146 if line_idx < len(self.lyrics):147 text = self.lyrics[line_idx]['text']148 if line_idx == current_index:149 # 高亮绿色,加箭头150 print(f"\033[1;32m▶ {text} ◀\033[0m")151 else:152 # 其他行根据距离变灰153 distance = abs(line_idx - current_index)154 color = "\033[0;37m" if distance == 1 else "\033[0;90m"155 print(f"{color} {text}\033[0m")156 157 async def run(self):158 """主循环"""159 self.init_display()160 lyrics_task = None161 162 while True:163 try:164 media_info = await self.get_current_media_info()165 166 if media_info and media_info["status"] == 4: # 4 代表 Playing167 # 切歌检测168 if (not self.current_song or 169 self.current_song["title"] != media_info["title"]):170 171 self.current_song = media_info172 self.lyrics = []173 self.update_song_info(media_info)174 175 # 异步去抓歌词,不阻塞 UI176 if lyrics_task: lyrics_task.cancel()177 lyrics_task = asyncio.create_task(178 self.load_lyrics_async(media_info["title"], media_info["artist"])179 )180 181 # 更新进度条时间182 if abs(media_info["position"] - self.last_position) >= 1:183 self.update_position(media_info["position"])184 self.last_position = media_info["position"]185 186 # 刷新歌词187 if self.lyrics:188 new_index = self.find_current_lyric(media_info["position"])189 if new_index != self.current_lyric_index:190 self.current_lyric_index = new_index191 self.update_lyrics_display(self.current_lyric_index)192 193 else:194 self.move_cursor(1, 10)195 print("等待 QQ音乐 播放...")196 197 await asyncio.sleep(0.1) # 0.1秒刷新率,极低占用198 199 except KeyboardInterrupt:200 print("\033[20;1H\n已退出,继续搬砖吧")201 break202 except Exception:203 await asyncio.sleep(1)204 205 def init_display(self):206 print("\033[2J\033[H", end='') # 清屏207 print("♪ QQ音乐实时歌词 - 摸鱼版 ♪")208 print("=" * 60)209 print("\n" * 15)210 211 def update_song_info(self, media_info):212 self.move_cursor(1, 4); self.clear_line()213 print(f"歌曲: {media_info['title']} - {media_info['artist']}")214 self.move_cursor(1, 5); self.clear_line()215 print(f"专辑: {media_info['album']}")216 217 def update_position(self, position):218 self.move_cursor(1, 6); self.clear_line()219 mins, secs = divmod(position, 60)220 print(f"时间: {int(mins):02d}:{int(secs):02d}")221 222 async def load_lyrics_async(self, title, artist):223 self.loading = True224 song_id = self.search_song(title, artist)225 if song_id: self.lyrics = self.get_lyrics(song_id)226 self.loading = False227 228if __name__ == "__main__":229 print("\033[?25l", end='') # 隐藏光标230 try:231 player = SmoothLyrics()232 asyncio.run(player.run())233 finally:234 print("\033[?25h", end='') # 恢复光标