// ==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 // @downloadURL https://update.greasyfork.icu/scripts/395376/163%20Music%20MediaMetadata.user.js // @updateURL https://update.greasyfork.icu/scripts/395376/163%20Music%20MediaMetadata.meta.js // ==/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(); })();