Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save youthlin/be34fa9bb50b37ac39aa0ce59265632b to your computer and use it in GitHub Desktop.
Save youthlin/be34fa9bb50b37ac39aa0ce59265632b to your computer and use it in GitHub Desktop.

[Mac]网易云音乐网页版歌词显示到菜单栏

  1. 安装 https://github.com/swiftbar/SwiftBar

    brew install swiftbar
  2. 实现一个网页服务器,用来存当前歌词,并提供一个接口查询当前歌词。 见下方 main.go.

  3. 安装油猴插件推送当前歌词 点击下方 .user.js 脚本 Raw 按钮安装。

  4. 打开第一步安装的软件,设置好软件插件目录后,在这个目录新建一个脚本文件,将歌词显示到菜单栏 见下方 sh 脚本。

  5. 限制:需要保持播放列表打开状态,才能获取到歌词。


PS:Web Scrobbler 一个浏览器插件,支持将网页中正在播放的音乐提交到 Last.fm

package main
import (
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
func main() {
type Client struct {
ID string
Ch chan string
}
var (
nextID = atomic.Int64{}
title = "" // 标题
by = "" // 艺术家
clients = sync.Map{} // 读取端 string -> *Client
actionCh = make(chan string, 1)
)
// 动作指令
http.HandleFunc("/action/send", func(w http.ResponseWriter, r *http.Request) {
var q = r.URL.Query()
var action = q.Get("action")
fmt.Printf(">> action = %v\n", action)
select {
case actionCh <- action:
default:
}
})
http.HandleFunc("/action/get", func(w http.ResponseWriter, r *http.Request) {
var action = "nop"
select {
case action = <-actionCh:
case <-time.After(10 * time.Second): // 10s 超时
}
fmt.Fprintln(w, action)
})
// 接收推送的歌词
http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
var q = r.URL.Query()
if q.Has("title") { // 标题
title = q.Get("title")
by = q.Get("by")
fmt.Println()
fmt.Println()
fmt.Println(title + " - " + by)
fmt.Println()
}
if q.Has("current") { // 歌词
var lyrics = r.URL.Query().Get("current")
lyrics = strings.TrimSpace(lyrics)
fmt.Println(lyrics)
clients.Range(func(_, value any) bool { // 推送给所有的读取端
client := value.(*Client)
select {
case client.Ch <- lyrics:
default: // 推送失败忽略
}
return true
})
}
})
// 获取当前歌词
http.HandleFunc("/next", func(w http.ResponseWriter, r *http.Request) {
var (
lyrics = ""
client = &Client{
ID: fmt.Sprintf("%d", nextID.Add(1)),
Ch: make(chan string, 1),
}
)
clients.Store(client.ID, client)
defer func() {
clients.Delete(client.ID)
}()
select {
case lyrics = <-client.Ch: // 获取歌词
case <-time.After(10 * time.Second): // 10s 超时
}
fmt.Fprintln(w, "~~~") // 提醒 Swiftbar 刷新内容
if lyrics != "" { // 拿到了歌词就输出
fmt.Fprintln(w, lyrics)
if title != "" { // 如果有标题一起输出 点击歌词可以看到
fmt.Fprintln(w, "---")
fmt.Fprintln(w, title)
fmt.Fprintln(w, by)
}
} else { // 超时了
if title != "" {
fmt.Fprintln(w, title+" - "+by)
} else {
fmt.Fprintln(w, "未在播放")
}
}
})
fmt.Printf("服务已启动\n")
http.ListenAndServe(":51917", nil)
}
func log(format string, args ...any) {
fmt.Printf(format+"\n", args...)
}
// ==UserScript==
// @name 网易云音乐歌词推送
// @namespace https://youthlin.com/
// @match *://music.163.com/*
// @noframes true
// @grant GM.xmlHttpRequest
// @version 1.0
// @author Youth.霖
// @description 需要保持播放列表打开状态。创建于 2023/5/19 16:41:26
// ==/UserScript==
(function () {
'use strict';
// https://gist.github.com/youthlin/be34fa9bb50b37ac39aa0ce59265632b
setTimeout(() => {// 延迟 1s 启动
console.log('网易云音乐歌词推送启动中...')
function makeSureLyricsShow() {
if (!document.querySelector('#g_playlist')) {// 确保播放列表开启
document.querySelector('[data-action="panel"]')?.click();
}
}
setInterval(makeSureLyricsShow, 1000)
function send(data) {
if (data.lyrics) {// 推送当前歌词
GM.xmlHttpRequest({ // 需要 @grant GM.xmlHttpRequest 授权
method: "GET",
url: `http://localhost:51917/set?current=${encodeURI(data.lyrics)}`,
});
}
if (data.title && data.by) {// 推送当前播放曲目 - 艺术家
GM.xmlHttpRequest({ // 需要 @grant GM.xmlHttpRequest 授权
method: "GET",
url: `http://localhost:51917/set?title=${encodeURI(data.title)}&by=${encodeURI(data.by)}`,
});
}
}
function addTitleObserver() {// 监听播放曲目变动
// 当前播放
const title = document.querySelector('.words .name')?.innerText
const by = document.querySelector('.words .by')?.innerText
if (title && by) {
console.log('当前播放', title, by)
send({ title, by })
}
// 监听后续变动
new MutationObserver(records => {
for (let record of records) {
let title = ''
let by = ''
for (let node of record.addedNodes) {
if (node.classList?.contains('name')) {
title = node.innerText
}
if (node.classList?.contains('by')) {
by = node.innerText
}
}
console.log(`检测到切歌 ${title} - ${by}`)
send({ title, by })
}
}).observe(document.querySelector('.words'), {
childList: true,
characterData: true,// node 的文本内容变动
})
}
let foundLyricsNode = false
function addLyricsObserver() {// 监听歌词变动
if (foundLyricsNode) return
const node = document.querySelector('.listlyric');
if (node) {
let last = '';
new MutationObserver(records => {
for (let record of records) {
if (record.target.classList.contains('z-sel')) {
const lyrics = record.target.innerText
if (lyrics != last) {
last = lyrics
console.log(lyrics)
send({ lyrics })
}
break
}
}
}).observe(node, {
childList: true, // 直接子节点
subtree: true, // 所有后代
attributes: true, // 属性
attributeFilter: ['class'],
})
foundLyricsNode = true
addTitleObserver()
console.log('歌词监听节点:', node)
} else {
console.log('歌词监听节点未找到 1s 后重试')
setTimeout(addLyricsObserver, 1000)// 1s 后重试
}
}
addLyricsObserver()
// 切割控制
function handleAction() {
GM.xmlHttpRequest({
method: "GET",
url: `http://localhost:51917/action/get`,
onerror: r => {
setTimeout(handleAction, 1000)
},
onloadend: r => {
const action = r.response.trim()
console.log(action)
switch (action) {
case 'nop': break;
case 'prev':
document.querySelector('a[data-action="prev"]')?.click()
break;
case 'toggle':
let node = document.querySelector('a[data-action="play"]')
if (!node) {
node = document.querySelector('a[data-action="pause"]')
}
console.log('toggle node', node)
if (node) {
node.click()
}
break;
case 'next':
document.querySelector('a[data-action="next"]')?.click()
break;
}
setTimeout(handleAction, 0)
},
});
}
handleAction()
console.log('网易云音乐歌词推送已启动')
}, 1000);
})()
#!/usr/bin/env bash
# <bitbar.title>网易云音乐网页版歌词显示</bitbar.title>
# <bitbar.version>v1.1</bitbar.version>
# <bitbar.author>Youth.霖</bitbar.author>
# <bitbar.author.github>youthlin</bitbar.author.github>
# <bitbar.desc>网易云音乐网页版歌词显示</bitbar.desc>
# <bitbar.abouturl>https://gist.github.com/youthlin/be34fa9bb50b37ac39aa0ce59265632b/</bitbar.abouturl>
# <bitbar.droptypes>Supported UTI's for dropping things on menu bar</bitbar.droptypes>
# <swiftbar.runInBash>false</swiftbar.runInBash>
# <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
# <swiftbar.hideLastUpdated>true</swiftbar.hideLastUpdated>
# <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin>
# <swiftbar.type>streamable</swiftbar.type>
# 如果带参数 就执行动作 通过menu生成的菜单 点击时触发
if [[ "$1" = "action" ]]; then
# 发送控制指令
curl http://localhost:51917/action/send?action=$2 >/dev/null 2>&1
exit
fi
menu() { # 输出菜单
echo "上一曲 | terminal=false bash=$0 param0=action param1=prev"
echo "暂停/播放 | terminal=false bash=$0 param0=action param1=toggle"
echo "下一曲 | terminal=false bash=$0 param0=action param1=next"
}
refresh() { # 更新歌词
# 服务器总是以 ~~~ 开头 10s超时
curl http://localhost:51917/next 2>/dev/null && menu # 输出歌词、曲名、艺术家
# 如果输出歌词成功 补充菜单
}
fallback() { # 如果更新歌词失败
echo '~~~' # 刷新
echo '歌词推送服务器未启动'
sleep 1 # 1s后重试(外层调用时死循环)
}
echo '~~~'
echo '播放以显示歌词'
menu
while true; do
# streamable 表示该脚本会不断输出 遇到 ~~~ 表示刷新
refresh || fallback
done
@youthlin
Copy link
Author

@Bigxxi
Copy link

Bigxxi commented Dec 4, 2023

好东西,支持👍👍

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