- // ==UserScript==
- // @name 网易云音乐显示完整歌单
- // @namespace https://github.com/nondanee
- // @version 1.2.8
- // @description 解除歌单歌曲展示数量限制 & 播放列表 1000 首上限
- // @author nondanee
- // @match https://music.163.com/*
- // @grant 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 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, level) => {
- const entry = Array(level).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 interceptRequest = () => {
- const location = search(window.nej || {}, '\\.replace\\("api","weapi')
- const originRequest = attach(window.nej || {}, location)
- const originRequestFunction = originRequest.toString()
-
- 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 slice = 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 / slice)
-
- const result = await Promise.all(
- Array.from(Array(batch).keys())
- .map(index => trackIds.slice(index * slice).slice(0, slice).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)
- }
- }
- overrideRequest.toString = () => originRequestFunction // call String(function) function.toString() same as origin
-
- attach(window.nej, location, overrideRequest)
- }
-
- const escapeHTML = string => (
- string.replace(
- /[&<>'"]/g,
- word =>
- ({
- '&': '&',
- '<': '<',
- '>': '>',
- "'": ''',
- '"': '"',
- })[word] || word
- )
- )
-
- const onceMutate= (element) => new Promise((resolve) => {
- const observer = new MutationObserver(() => (observer.disconnect(), resolve()))
- observer.observe(element, { childList: true, attributes: true, subtree: 'true' })
- })
-
- 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 `
- <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 playlistId = (window.location.href.match(/playlist\?id=(\d+)/) || [])[1]
-
- const action = async () => {
- const seeMore = document.querySelector('.m-playlist-see-more')
- if (seeMore) seeMore.innerHTML = '<div class="text">更多内容加载中...</div>'
- const data = await window.playlistDetail('/api/v6/playlist/detail', playlistId)
- const { playlist } = data
- const content = playlist.tracks.map((song, index) => render(normalize(song), index, playlist)).join('')
-
- const replace = () => {
- document.querySelector('table tbody').innerHTML = content
- proxyAction()
- seeMore && seeMore.parentNode.removeChild(seeMore)
- }
-
- if (document.querySelector('table'))
- replace()
- else
- onceMutate(document.querySelector('.g-mn3.f-pr.j-flag .f-pr')).then(replace)
- }
-
- if (playlistId) action()
- }
-
- const proxyAction = (table) => {
- const targetAction = new Set(['play', 'addto'])
- const typeMap = { song: '18', playlist: '13' }
-
- const handler = (event, type) => {
- const { resType, resAction, resId, resFrom, resData } = event.target.dataset
- if (resAction === 'delete') {
- const action = value => value === 'ok' && window.simpleRequest(
- '/api/playlist/manipulate/tracks',
- { data: `op=del&pid=${resData}&trackIds=[${resId}]` }
- )
- .then(({ code }) => code === 200 && completePlaylist())
- originConfirm({ btnok: '确定', btncc:'取消', message:'确定删除歌曲?', action })
- }
- if (typeMap[type] !== resType || !targetAction.has(resAction)) return
-
- const list = ((window.scriptCache || {})[type] || {})[resId]
- if (!list) return
-
- event.stopPropagation()
- window.top.player.addTo(
- Array.isArray(list) ? list : [list],
- resAction === 'play' && type === 'playlist',
- resAction === 'play'
- )
- }
-
- const operationElement = document.querySelector('#content-operation') || document.querySelector('#flag_play_addto_btn_wrapper')
-
- if (operationElement) {
- const contentPlay = operationElement.querySelector('.u-btni-addply')
- const contentAdd = operationElement.querySelector('.u-btni-add')
-
- if (contentPlay) contentPlay.addEventListener('click', event => handler(event, 'playlist'))
- if (contentAdd) contentAdd.addEventListener('click', event => handler(event, 'playlist'))
- }
-
- const tableBody = document.querySelector('table tbody')
- if (tableBody) tableBody.addEventListener('click', event => handler(event, 'song'))
- const tableWrap = document.querySelector('table')
- if (tableWrap) skipEventListener(tableWrap, 'click', 3) // default listener throw an error
- }
-
- interceptRequest()
-
- window.addEventListener('load', completePlaylist, false)
- window.addEventListener('hashchange', completePlaylist, false)
- })()