163 Music MediaMetadata

在网易云音乐 Web 版上启用 MediaSession 支持

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         163 Music MediaMetadata
// @version      1.0
// @description  在网易云音乐 Web 版上启用 MediaSession 支持
// @match        *://music.163.com/*
// @include      *://music.163.com/*
// @author       864907600cc
// @icon         https://secure.gravatar.com/avatar/147834caf9ccb0a66b2505c753747867
// @run-at       document-end
// @grant        none
// @namespace    http://ext.ccloli.com
// ==/UserScript==

'use strict';

/* globals CryptoJS, MediaMetadata */

(() => {
	if (!navigator.mediaSession || typeof MediaMetadata === 'undefined') {
		console.log('The browser doesn\'t support MediaSession');
		return;
	}

	/**
	 * document.querySelector
	 *
	 * @param {string} selector - 选择器
	 * @returns {Node|null} 节点
	 */
	const $ = (selector) => document.querySelector(selector);

	/**
	 * 获取缩放后的图片 URL
	 *
	 * @param {string} url - 图片 URL
	 * @param {number} [size] - 图片缩放尺寸
	 * @returns {string} 缩放后的图片 URL
	 */
	const getResizedPicUrl = (url, size) => {
		return `${url}${size ? `?param=${size}y${size}` : ''}`;
	};

	/**
	 * 获取图片完整 URL
	 *
	 * @param {number|string} id - 文件 ID
	 * @returns {string} 图片 URL
	 */
	const getPicUrl = (id) => {
		const key = '3go8&$8*3*3h0k(2)2';
		const idStr = `${id}`;
		const idStrLen = idStr.length;
		const token = idStr.split('').map((e, i) => {
			return String.fromCharCode(e.charCodeAt(0) ^ key.charCodeAt(i % idStrLen));
		}).join('');
		const result = CryptoJS.MD5(token).toString(CryptoJS.enc.Base64)
			.replace(/\/|\+/g, match => ({
				'/': '_',
				'+': '-'
			})[match]);
		return `https://p1.music.126.net/${result}/${idStr}.jpg`;
	};

	/**
	 * 从播放列表中生成对应曲目的 metadata
	 *
	 * @param {number} id - 歌曲 ID
	 * @returns {object} metadata 数据内容
	 */
	const getSongMetadata = (id) => {
		let result;
		try {
			const playlist = JSON.parse(localStorage.getItem('track-queue'));
			const item = playlist.find(e => e.id === +id);
			if (item) {
				const album = item.album || {};
				result = {
					title: item.name,
					artist: (item.artists || []).map(e => e.name).join('/'),
					album: album.name,
					artwork: [{
						src: getResizedPicUrl(album.picUrl || getPicUrl(album.pic_str || album.pic), 160),
						sizes: '160x160',
						type: 'image/jpeg'
					}, {
						src: getResizedPicUrl(album.picUrl || getPicUrl(album.pic_str || album.pic), 320),
						sizes: '320x320',
						type: 'image/jpeg'
					}, {
						src: getResizedPicUrl(album.picUrl || getPicUrl(album.pic_str || album.pic), 480),
						sizes: '480x480',
						type: 'image/jpeg'
					}]
				};
			}
		} catch (err) {
			console.log(err);
		}

		return result;
	};

	/**
	 * 从页面内 DOM 生成 metadata(不含专辑名)
	 *
	 * @returns {object} metadata 数据内容
	 */
	const generateCurrentMetadata = () => {
		const coverUrl = $('.m-playbar .head > img').getAttribute('src').split('?').shift();
		return {
			title: $('.m-playbar .words .name').getAttribute('title'),
			artist: $('.m-playbar .words .by > span').getAttribute('title'),
			artwork: [{
				src: getResizedPicUrl(coverUrl),
				sizes: '160x160',
				type: 'image/jpeg'
			}, {
				src: getResizedPicUrl(coverUrl),
				sizes: '320x320',
				type: 'image/jpeg'
			}, {
				src: getResizedPicUrl(coverUrl),
				sizes: '480x480',
				type: 'image/jpeg'
			}]
		};
	};

	/**
	 * 设置 metadata 到 mediaSession 上
	 *
	 * @param {object} [data] - metadata 数据
	 */
	const setMetadata = (data) => {
		if (data) {
			navigator.mediaSession.metadata = new MediaMetadata(data);
		}
		else {
			navigator.mediaSession.metadata = null;
		}
	};

	/**
	 * 更新当前曲目的 media metadata
	 *
	 */
	const updateCurrent = () => {
		const id = ($('.m-playbar .words .name').getAttribute('href').match(/\?id=(\d+)/) || [])[1];
		if (id) {
			const data = getSongMetadata(id) || generateCurrentMetadata();
			setMetadata(data);
		}
	};

	/**
	 * 处理客户端播放面板操作回调
	 *
	 */
	const setActionHandler = () => {
		navigator.mediaSession.setActionHandler('previoustrack', () => {
			$('.m-playbar .btns .prv').click();
		});
		navigator.mediaSession.setActionHandler('nexttrack', () => {
			$('.m-playbar .btns .nxt').click();
		});
	};

	/**
	 * 初始化函数
	 *
	 */
	const init = () => {
		if ($('.m-playbar')) {
			// 使用 animation 事件监听可能会覆盖其他使用相同方法处理的脚本,改用 MutationObserver
			const observer = new MutationObserver((mutations) => {
				if (mutations && mutations[0]) {
					updateCurrent();
				}
			});

			observer.observe($('.m-playbar .words'), {
				attributes: true, childList: true, subtree: true
			});

			setActionHandler();
			updateCurrent();
		}
	};

	init();
})();