// ==UserScript== // @name 网易云音乐显示完整歌单 // @namespace https://github.com/nondanee // @version 1.2.6 // @description 解除歌单歌曲展示数量限制 & 播放列表 1000 首上限 // @author nondanee // @match https://music.163.com/* // @grant none // @downloadURL none // ==/UserScript== (() => { if (window.top === window.self) return const search = (object, pattern) => { let result = null Object.keys(object) .some(key => { if (!object[key]) return else if (typeof object[key] === 'function') { result = String(object[key]).match(pattern) ? [key] : null } else if (typeof object[key] === 'object') { const chain = search(object[key], pattern) result = chain ? [key].concat(chain) : null } return !!result }) return result } const escapeHTML = string => string.replace( /[&<>'"]/g, word => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', })[word] || word ) const attach = (object, path, property) => { path = (path || []).slice() let poiner = object const last = path.pop() path.forEach(key => { if (!(key in poiner)) throw new Error('KeyError') poiner = poiner[key] }) return property ? poiner[last] = property : poiner[last] } const originConfirm = attach(window.nm, search(window.nm, /okstyle\s*\|\|\s*""/)) const skipEventListener = (element, type, skip) => { const entry = Array(skip).fill(null) .reduce(pointer => pointer.parentNode || {}, element) element.addEventListener(type, event => { event.stopImmediatePropagation() const clone = new event.constructor(event.type, event) Object.defineProperty(clone, 'target', { value: event.target }) entry.dispatchEvent(clone) }) } 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 showDuration = time => { const pad = number => number < 10 ? '0' + number : number time = parseInt(time / 1000) const minute = parseInt(time / 60) const second = time % 60 return [pad(minute), pad(second)].join(':') } const hijackRequest = () => { const location = search(window.nej || {}, '\\.replace\\("api","weapi') const originRequest = attach(window.nej || {}, location) window.simpleRequest = (url, options = {}) => new Promise((resolve, reject) => originRequest(url, { ...options, cookie: true, method: 'GET', onerror: reject, onload: resolve, type: 'json' }) ) const mapify = list => list.reduce((output, item) => ({ ...output, [item.id]: item }), {}) window.scriptCache = { playlist: {}, song: {}, } window.playlistDetail = async (url, id, origin) => { const capacity = 1000 const data = await window.simpleRequest(url, { data: `id=${id}&n=${origin ? 1000 : 0}` }) const trackIds = (data.playlist || {}).trackIds || [] const tracks = (data.playlist || {}).tracks || [] if (!trackIds.length || trackIds.length === tracks.length) return data if (origin) return data const batch = Math.ceil(trackIds.length / capacity) const result = await Promise.all( Array.from(Array(batch).keys()) .map(index => trackIds.slice(index * capacity).slice(0, capacity).map(({ id }) => ({ id }))) .map(part => window.simpleRequest('/api/v3/song/detail', { data: `c=${JSON.stringify(part)}` })) ) const songMap = mapify(Array.prototype.concat.apply([], result.map(({ songs }) => songs))) const privilegeMap = mapify(Array.prototype.concat.apply([], result.map(({ privileges }) => privileges))) data.playlist.tracks = trackIds .map(({ id }) => songMap[id] ? { ...songMap[id], privilege: privilegeMap[id] } : null) .filter(song => song) data.privileges = data.playlist.tracks .map(({ id }) => privilegeMap[id]) window.scriptCache.playlist[id] = data.playlist.tracks .map(song => window.scriptCache.song[song.id] = normalize(song)) return data } const overrideRequest = (url, options) => { if (url.includes('/playlist/detail')) { const data = new URLSearchParams(options.data) const { onload, onerror } = options window.playlistDetail(url, data.get('id'), true).then(onload).catch(onerror) } else { originRequest(url, options) } } attach(window.nej, location, overrideRequest) } const completePlaylist = () => { const render = (song, index, playlist) => { const { album, artists, status, duration } = song const deletable = playlist.creator.userId === window.GUser.userId const durationText = showDuration(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 `