163 Music MediaMetadata

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

// ==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();
})();