分类
代码

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

0. 需求来源

网易云音乐的 Mac 客户端,无法将播放记录同步到 Last.fm, 而且也不太想下载客户端。所以想听歌的时候一般都用网页版,还可以配合 Web Scrobbler 追踪播放记录。 (使用过程中还顺便提交了 两个 PR. )

但是网页版歌词无法在菜单栏上显示,只能在当前网页显示。所以搜了一圈找到一个 SwiftBar 开源工具,可以自定义菜单栏。 (后来发现也可以在播放条右边找到画中画歌词按钮,可以开启一个画中画功能。搜了一下原理大概是不断将歌词绘制到一个 cavans 上,然后新建一个 video 元素,可以不 append 到页面中,直接通过 requestPictureInPicture 打开画中画。此为后话,有兴趣的可以深入了解。)

下面记录一下具体步骤。

1. 安装菜单栏自定义工具 SwiftBar

brew install swiftbar

下载慢?试试设置镜像:https://mirrors.tuna.tsinghua.edu.cn/help/homebrew/

2. 启动歌词推送服务器

// 先新建一个文件 main.go 放在哪里都行
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...)
}
# 然后启动它
go run main.go

3. 安装油猴插件将网页版歌词实时推送

点击 这里 安装。 或者打开 这个页面 再点击右上角的 Raw 也可(这种方式是最新脚本)。 `

4. 打开 SwiftBar 加载插件

先设置好插件目录,然后在插件目录中新建文件 163lyrics.sh, 内容如下:

#!/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

效果

打开 https://music.163.com/ 播放,应该就能在菜单栏上看到歌词了。

如果没有登录,想要登录发现播放列表会遮挡登录组件,可以先暂停油猴脚本,因为脚本需要确保歌词界面打开,才能读取到当前歌词。 或者在控制台执行:

document.querySelector('#g_playlist .listbd').style.height='60px'
document.querySelector('#g_playlist').style.height='100px'

使播放列表高度变矮一些。

效果-菜单栏显示歌词
效果-菜单栏显示歌词

注:代码可以在这里找到
https://gist.github.com/youthlin/be34fa9bb50b37ac39aa0ce59265632b


发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

[/鼓掌] [/难过] [/调皮] [/白眼] [/疑问] [/流泪] [/流汗] [/撇嘴] [/抠鼻] [/惊讶] [/微笑] [/得意] [/大兵] [/坏笑] [/呲牙] [/吓到] [/可爱] [/发怒] [/发呆] [/偷笑] [/亲亲]