解除歌单歌曲展示数量限制 & 播放列表 1000 首上限
// ==UserScript== // @name 网易云音乐显示完整歌单 // @namespace https://github.com/nondanee // @version 1.4.13 // @description 解除歌单歌曲展示数量限制 & 播放列表 1000 首上限 // @author nondanee // @match *://music.163.com/* // @icon https://s1.music.126.net/style/favicon.ico // @grant none // @run-at document-start // ==/UserScript== (() => { if (window.top === window.self) { const observe = () => { try { const callback = () => document.contentFrame.dispatchEvent(new Event('songchange')) const observer = new MutationObserver(callback) observer.observe(document.querySelector('.m-playbar .words'), { childList: true }) } catch (_) {} } window.addEventListener('load', observe, false) return } const locate = (object, pattern) => { for (const key in object) { const value = object[key] if (!Object.prototype.hasOwnProperty.call(object, key) || !value) continue switch (typeof value) { case 'function': { if (String(value).match(pattern)) return [key] break } case 'object': { const path = locate(value, pattern) if (path) return [key].concat(path) break } } } } const findMethod = (object, pattern) => { const path = locate(object, pattern) if (!path) throw new Error('MethodNotFound') let poiner = object const last = path.pop() path.forEach(key => poiner = poiner[key]) const origin = poiner[last] return { origin, override: (value) => { value.toString = () => origin.toString() poiner[last] = value } } } const cloneEvent = (event) => { const copy = new event.constructor(event.type, event) // copy.target = event.target // 有问题 Object.defineProperty(copy, 'target', { value: event.target }) return copy } const normalize = song => { song = { ...song, ...song.privilege } return { ...song, album: song.al, alias: song.alia || song.ala || [], artists: song.ar || [], commentThreadId: `R_SO_4_${song.id}`, copyrightId: song.cp, duration: song.dt, mvid: song.mv, position: song.no, ringtone: song.rt, status: song.st, pstatus: song.pst, version: song.v, songType: song.t, score: song.pop, transNames: song.tns || [], privilege: song.privilege, lyrics: song.lyrics } } const zFill = (string = '', length = 2) => { string = String(string) while (string.length < length) string = '0' + string return string } const formatDuration = duration => { const oneSecond = 1e3 const oneMinute = 60 * oneSecond const r###lt = [] Array(oneMinute, oneSecond) .reduce((remain, unit) => { const value = Math.floor(remain / unit) r###lt.push(value) return remain - value * unit }, duration || 0) return r###lt .map(value => zFill(value, 2)) .join(':') } const TYPE = { SONG: '18', PLAYLIST: '13', } const CACHE = window.COMPLETE_PLAYLIST_CACHE = { [TYPE.SONG]: {}, [TYPE.PLAYLIST]: {} } const interceptRequest = () => { if (window.getPlaylistDetail) return const request = findMethod(window.nej, '\\.replace\\("api","weapi') const Fetch = (url, options) => ( new Promise((resolve, reject) => request.origin(url, { ...options, cookie: true, method: 'GET', onerror: reject, onload: resolve, type: 'json' }) ) ) window.getPlaylistDetail = async (url, options) => { // const search = new URLSearchParams(options.data) // search.set('n', 0) // options.data = search.toString() const data = await Fetch(url, options) const slice = 1000 const trackIds = (data.playlist || {}).trackIds || [] const tracks = (data.playlist || {}).tracks || [] if (!trackIds.length || trackIds.length === tracks.length) return data const missingTrackIds = trackIds.slice(tracks.length) const round = Math.ceil(missingTrackIds.length / slice) const r###lt = await Promise.all( Array(round).fill().map((_, index) => { const part = missingTrackIds.slice(index * slice).slice(0, slice).map(({ id }) => ({ id })) return Fetch('/api/v3/song/detail', { data: `c=${JSON.stringify(part)}` }) }) ) const songMap = {} const privilegeMap = {} r###lt.forEach(({ songs, privileges }) => { songs.forEach(_ => songMap[_.id] = _) privileges.forEach(_ => privilegeMap[_.id] = _) }) const missingTracks = missingTrackIds .map(({ id }) => ({ ...songMap[id], privilege: privilegeMap[id] })) const missPrivileges = missingTracks .map(({ id }) => privilegeMap[id]) data.playlist.tracks = tracks.concat(missingTracks) data.privileges = (data.privileges || []).concat(missPrivileges) CACHE[TYPE.PLAYLIST][data.playlist.id] = data.playlist.tracks .map(song => CACHE[TYPE.SONG][song.id] = normalize(song)) return data } const overrideRequest = async (url, options) => { if (/\/playlist\/detail/.test(url)) { const { onload, onerror } = options return window.getPlaylistDetail(url, options).then(onload).catch(onerror) } return request.origin(url, options) } request.override(overrideRequest) } const handleSongChange = () => { try { const { track } = window.top.player.getPlaying() const { id, source, program } = track if (program) return const base = 'span.ply' const attrs = `[data-res-id="${id}"][data-res-type="${TYPE.SONG}"]` // player.addTo() 相同 id 不同 source 会被过滤 // const { fid, fdata } = source // if (String(fid) !== TYPE.PLAYLIST) return // const attrs = `[data-res-id="${id}"][data-res-from="${fid}"][data-res-data="${fdata}"]` document.querySelectorAll(base).forEach(node => { node.classList.remove('ply-z-slt') }) document.querySelectorAll(base + attrs).forEach(node => { node.classList.add('ply-z-slt') }) } catch (_) {} } const escapeHTML = string => ( string.replace( /[&<>'"]/g, word => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', })[word] || word ) ) const bindEvent = () => { const ACTIONS = new Set(['play', 'addto']) const onClick = (event) => { const { resAction, resId, resType, resData, } = event.target.dataset const data = (CACHE[resType] || {})[resId] if (!data) return event.stopPropagation() if (!ACTIONS.has(resAction)) { // 没有 privilege 冒泡后会报错 document.body.dispatchEvent(cloneEvent(event)) return } const playlistId = Number(resType === TYPE.PLAYLIST ? resId : resData) const list = (Array.isArray(data) ? data : [data]) .map(song => ({ ...song, source: { fdata: playlistId, fid: TYPE.PLAYLIST, link: `/playlist?id=${playlistId}&_hash=songlist-${song.id}`, title: '歌单', }, })) window.top.player.addTo( list, resAction === 'play' && resType === TYPE.PLAYLIST, resAction === 'play' ) } const body = document.querySelector('table tbody') const play = document.querySelector('#content-operation .u-btni-addply') const add = document.querySelector('#content-operation .u-btni-add') if (play) play.addEventListener('click', onClick) if (add) add.addEventListener('click', onClick) if (body) body.addEventListener('click', onClick) } const completePlaylist = async (id) => { const render = (song, index, playlist) => { const { album, artists, status, duration } = song const deletable = playlist.creator.userId === window.GUser.userId const durationText = formatDuration(duration) const artistText = artists.map(({ name }) => escapeHTML(name)).join('/') const annotation = escapeHTML(song.transNames[0] || song.alias[0] || '') const albumName = escapeHTML(album.name) const songName = escapeHTML(song.name) return ` <tr id="${song.id}${Date.now()}" class="${index % 2 ? '' : 'even'} ${status ? 'js-dis' : ''}"> <td class="left"> <div class="hd "><span data-res-id="${song.id}" data-res-type="18" data-res-action="play" data-res-from="13" data-res-data="${playlist.id}" class="ply "> </span><span class="num">${index + 1}</span></div> </td> <td> <div class="f-cb"> <div class="tt"> <div class="ttc"> <span class="txt"> <a href="#/song?id=${song.id}"><b title="${songName}${annotation ? ` - (${annotation})` : ''}">${songName}</b></a> ${annotation ? `<span title="${annotation}" class="s-fc8">${annotation ? ` - (${annotation})` : ''}</span>` : ''} ${song.mvid ? `<a href="#/mv?id=${song.mvid}" title="播放mv" class="mv">MV</a>` : ''} </span> </div> </div> </div> </td> <td class=" s-fc3"> <span class="u-dur candel">${durationText}</span> <div class="opt hshow"> <a class="u-icn u-icn-81 icn-add" href="javascript:;" title="添加到播放列表" hidefocus="true" data-res-type="18" data-res-id="${song.id}" data-res-action="addto" data-res-from="13" data-res-data="${playlist.id}"></a> <span data-res-id="${song.id}" data-res-type="18" data-res-action="fav" class="icn icn-fav" title="收藏"></span> <span data-res-id="${song.id}" data-res-type="18" data-res-action="share" data-res-name="${albumName}" data-res-author="${artistText}" data-res-pic="${album.picUrl}" class="icn icn-share" title="分享">分享</span> <span data-res-id="${song.id}" data-res-type="18" data-res-action="download" class="icn icn-dl" title="下载"></span> ${deletable ? `<span data-res-id="${song.id}" data-res-type="18" data-res-from="13" data-res-data="${playlist.id}" data-res-action="delete" class="icn icn-del" title="删除">删除</span>` : ''} </div> </td> <td> <div class="text" title="${artistText}"> <span title="${artistText}"> ${artists.map(({ id, name }) => `<a href="#/artist?id=${id}" hidefocus="true">${escapeHTML(name)}</a>`).join('/')} </span> </div> </td> <td> <div class="text"> <a href="#/album?id=${album.id}" title="${albumName}">${albumName}</a> </div> </td> </tr> ` } const seeMore = document.querySelector('.m-playlist-see-more') if (seeMore) seeMore.innerHTML = '<div class="text">更多内容加载中...</div>' const data = await window.getPlaylistDetail( '/api/v6/playlist/detail/', { data: `id=${id}&offset=0&total=true&limit=1000&n=1000` } ) const { playlist } = data const content = playlist.tracks .map((song, index) => render(normalize(song), index, playlist)) .join('') const body = document.querySelector('table tbody') if (body) body.innerHTML = content bindEvent() handleSongChange() if (seeMore) seeMore.parentNode.removeChild(seeMore) } const handleRoute = () => { interceptRequest() const { href, search } = location if (/\/my\//.test(href)) return const id = new URLSearchParams(search).get('id') if (/playlist[/?]/.test(href) && id) completePlaylist(id) } window.addEventListener('songchange', handleSongChange) window.addEventListener('load', handleRoute, false) window.addEventListener('hashchange', handleRoute, false) })()