1063 字
5 分钟
终端里看 QQ 音乐歌词,摸鱼神器
写代码的时候终于能看歌词了
2025-12-29
-
-

起因#

作为一名当代大学生,写代码的时候没 BGM 简直写不出 Bug(误)。但是,每次想看歌词还得切屏到 QQ 音乐,这也太打断思路了。而且在实验室或者课堂上,频繁切屏显得我很不专心。

反正天天盯着终端,干脆把歌词塞进去得了。要能实时滚动高亮当前行,最好还能自动识别正在放什么歌,不用手动输入。

思路#

说白了就三件事:

  1. 读播放状态:用 winsdk 读 Windows 的 SMTC(系统媒体控制),QQ 音乐在放什么歌、放到哪了,系统都知道
  2. 拉歌词:拿歌名去 api.vkeys.cn 查,把 LRC 歌词搞下来
  3. 终端里画:ANSI 转义序列控制光标和颜色,不用清屏也能滚动刷新
WARNING

只能在 Windows 10/11 上跑,Mac 和 Linux 的媒体接口不一样,懒得适配了

装依赖#

winsdk 不是自带的,得装一下:

Terminal window
pip install winsdk requests
import asyncio
import winsdk.windows.media.control as media_control
import requests
import json
import time
import re
import os
import sys
class SmoothLyrics:
def __init__(self):
self.api_base = "[https://api.vkeys.cn/v2/music/tencent](https://api.vkeys.cn/v2/music/tencent)"
self.current_song = None
self.lyrics = []
self.current_lyric_index = -1
self.last_position = -1
self.loading = False
self.last_status = -1 # 缓存上次状态
self.status_line = 17 # 状态显示行号
# Windows 控制台优化,开启 VT100 模式
if sys.platform == "win32":
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
def move_cursor(self, x, y):
"""移动光标到指定位置,指哪打哪"""
print(f"\033[{y};{x}H", end='')
def clear_line(self):
"""清除当前行,保持界面干净"""
print("\033[K", end='')
def search_song(self, title, artist):
"""去 API 搜一下这首歌的 ID"""
url = f"{self.api_base}/search/song"
params = {"word": f"{title} {artist}"}
try:
response = requests.get(url, params=params, timeout=5)
data = response.json()
if data["code"] == 200 and data["data"]:
for song in data["data"]:
singers = " ".join([s["name"] for s in song.get("singer_list", [])])
# 简单的模糊匹配
if song["song"] == title and artist in singers:
return song["id"]
return data["data"][0]["id"]
except:
pass
return None
def get_lyrics(self, song_id):
"""拿着 ID 换歌词"""
url = f"{self.api_base}/lyric"
params = {"id": song_id}
try:
response = requests.get(url, params=params, timeout=5)
data = response.json()
if data["code"] == 200 and data["data"]:
return self.parse_lrc(data["data"].get("lrc", ""))
except:
pass
return []
def parse_lrc(self, lrc_text):
"""解析 LRC 格式,正则表达式立大功"""
lines = lrc_text.split('\n')
lyrics = []
for line in lines:
if any(line.startswith(x) for x in ['[ti:', '[ar:', '[al:', '[by:']):
continue
pattern = r'\[(\d{2}):(\d{2}\.\d{2})\](.*)'
match = re.match(pattern, line)
if match:
minutes = int(match.group(1))
seconds = float(match.group(2))
text = match.group(3).strip()
total_seconds = minutes * 60 + seconds
if text:
lyrics.append({'time': total_seconds, 'text': text})
return sorted(lyrics, key=lambda x: x['time'])
async def get_current_media_info(self):
"""核心黑科技:从 Windows 获取当前播放信息"""
try:
sessions = await media_control.GlobalSystemMediaTransportControlsSessionManager.request_async()
current_session = sessions.get_current_session()
# 这里锁定了 QQMusic.exe,其他播放器改改也能用
if current_session and "QQMusic" in current_session.source_app_user_model_id:
info = await current_session.try_get_media_properties_async()
playback = current_session.get_playback_info()
timeline = current_session.get_timeline_properties()
position = timeline.position.total_seconds() if timeline else 0
return {
"title": info.title,
"artist": info.artist,
"album": info.album_title,
"status": playback.playback_status,
"position": position
}
except:
pass
return None
def find_current_lyric(self, position):
"""二分太麻烦,直接倒序遍历找当前歌词"""
if not self.lyrics: return -1
for i in range(len(self.lyrics) - 1, -1, -1):
if self.lyrics[i]['time'] <= position:
return i
return 0
def update_lyrics_display(self, current_index):
"""渲染 UI,高亮当前行"""
if not self.lyrics:
self.move_cursor(1, 10)
self.clear_line()
print("正在加载歌词..." if self.loading else "暂无歌词")
return
# 显示7行歌词(当前行前后各3行)
display_lines = 7
center = display_lines // 2
start_index = max(0, current_index - center)
end_index = min(len(self.lyrics), start_index + display_lines)
if end_index - start_index < display_lines:
start_index = max(0, end_index - display_lines)
for i, line_idx in enumerate(range(start_index, end_index)):
self.move_cursor(1, 9 + i)
self.clear_line()
if line_idx < len(self.lyrics):
text = self.lyrics[line_idx]['text']
if line_idx == current_index:
# 高亮绿色,加箭头
print(f"\033[1;32m▶ {text}\033[0m")
else:
# 其他行根据距离变灰
distance = abs(line_idx - current_index)
color = "\033[0;37m" if distance == 1 else "\033[0;90m"
print(f"{color} {text}\033[0m")
async def run(self):
"""主循环"""
self.init_display()
lyrics_task = None
while True:
try:
media_info = await self.get_current_media_info()
if media_info and media_info["status"] == 4: # 4 代表 Playing
# 切歌检测
if (not self.current_song or
self.current_song["title"] != media_info["title"]):
self.current_song = media_info
self.lyrics = []
self.update_song_info(media_info)
# 异步去抓歌词,不阻塞 UI
if lyrics_task: lyrics_task.cancel()
lyrics_task = asyncio.create_task(
self.load_lyrics_async(media_info["title"], media_info["artist"])
)
# 更新进度条时间
if abs(media_info["position"] - self.last_position) >= 1:
self.update_position(media_info["position"])
self.last_position = media_info["position"]
# 刷新歌词
if self.lyrics:
new_index = self.find_current_lyric(media_info["position"])
if new_index != self.current_lyric_index:
self.current_lyric_index = new_index
self.update_lyrics_display(self.current_lyric_index)
else:
self.move_cursor(1, 10)
print("等待 QQ音乐 播放...")
await asyncio.sleep(0.1) # 0.1秒刷新率,极低占用
except KeyboardInterrupt:
print("\033[20;1H\n已退出,继续搬砖吧")
break
except Exception:
await asyncio.sleep(1)
def init_display(self):
print("\033[2J\033[H", end='') # 清屏
print("♪ QQ音乐实时歌词 - 摸鱼版 ♪")
print("=" * 60)
print("\n" * 15)
def update_song_info(self, media_info):
self.move_cursor(1, 4); self.clear_line()
print(f"歌曲: {media_info['title']} - {media_info['artist']}")
self.move_cursor(1, 5); self.clear_line()
print(f"专辑: {media_info['album']}")
def update_position(self, position):
self.move_cursor(1, 6); self.clear_line()
mins, secs = divmod(position, 60)
print(f"时间: {int(mins):02d}:{int(secs):02d}")
async def load_lyrics_async(self, title, artist):
self.loading = True
song_id = self.search_song(title, artist)
if song_id: self.lyrics = self.get_lyrics(song_id)
self.loading = False
if __name__ == "__main__":
print("\033[?25l", end='') # 隐藏光标
try:
player = SmoothLyrics()
asyncio.run(player.run())
finally:
print("\033[?25h", end='') # 恢复光标

这篇文章是否对你有帮助?

发现错误或想要改进这篇文章?

在 GitHub 上编辑此页
终端里看 QQ 音乐歌词,摸鱼神器
作者
lansonsam
发布于
2025-12-29
许可协议
CC BY-NC-SA 4.0