网易云音乐显示完整歌单

解除歌单歌曲展示数量限制 & 播放列表 1000 首上限

目前为 2020-06-28 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 网易云音乐显示完整歌单
  3. // @namespace https://github.com/nondanee
  4. // @version 1.2.3
  5. // @description 解除歌单歌曲展示数量限制 & 播放列表 1000 首上限
  6. // @author nondanee
  7. // @match https://music.163.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (() => {
  12. if (window.top === window.self) return
  13.  
  14. const search = (object, pattern) => {
  15. let result = null
  16. Object.keys(object)
  17. .some(key => {
  18. if (!object[key]) return
  19. else if (typeof object[key] === 'function') {
  20. result = String(object[key]).match(pattern) ? [key] : null
  21. }
  22. else if (typeof object[key] === 'object') {
  23. const chain = search(object[key], pattern)
  24. result = chain ? [key].concat(chain) : null
  25. }
  26. return !!result
  27. })
  28. return result
  29. }
  30.  
  31. const escapeHTML = string =>
  32. string.replace(
  33. /[&<>'"]/g,
  34. word =>
  35. ({
  36. '&': '&amp;',
  37. '<': '&lt;',
  38. '>': '&gt;',
  39. "'": '&#39;',
  40. '"': '&quot;',
  41. })[word] || word
  42. )
  43.  
  44. const attach = (object, path, property) => {
  45. path = (path || []).slice()
  46. let poiner = object
  47. const last = path.pop()
  48. path.forEach(key => {
  49. if (!(key in poiner)) throw new Error('KeyError')
  50. poiner = poiner[key]
  51. })
  52. return property ? poiner[last] = property : poiner[last]
  53. }
  54.  
  55. const skipEventListener = (element, type, skip) => {
  56. const entry = Array(skip).fill(null)
  57. .reduce(pointer => pointer.parentNode || {}, element)
  58.  
  59. element.addEventListener(type, event => {
  60. event.stopImmediatePropagation()
  61. const target = event.target.cloneNode(false)
  62. target.style.display = 'none'
  63. entry.parentNode.appendChild(target)
  64. target.click()
  65. entry.parentNode.removeChild(target)
  66. })
  67. }
  68.  
  69. const normalize = song => {
  70. song = { ...song, ...song.privilege }
  71. return {
  72. ...song,
  73. album: song.al,
  74. alias: song.alia || song.ala || [],
  75. artists: song.ar || [],
  76. commentThreadId: `R_SO_4_${song.id}`,
  77. copyrightId: song.cp,
  78. duration: song.dt,
  79. mvid: song.mv,
  80. position: song.no,
  81. ringtone: song.rt,
  82. status: song.st,
  83. pstatus: song.pst,
  84. version: song.v,
  85. songType: song.t,
  86. score: song.pop,
  87. transNames: song.tns || [],
  88. privilege: song.privilege,
  89. lyrics: song.lyrics
  90. }
  91. }
  92.  
  93. const showDuration = time => {
  94. const pad = number => number < 10 ? '0' + number : number
  95. time = parseInt(time / 1000)
  96. const minute = parseInt(time / 60)
  97. const second = time % 60
  98. return [pad(minute), pad(second)].join(':')
  99. }
  100.  
  101. const hijackRequest = () => {
  102. const location = search(window.nej || {}, '\\.replace\\("api","weapi')
  103. const originRequest = attach(window.nej || {}, location)
  104.  
  105. const simpleRequest = (url, options = {}) =>
  106. new Promise((resolve, reject) =>
  107. originRequest(url, {
  108. ...options,
  109. cookie: true,
  110. method: 'GET',
  111. onerror: reject,
  112. onload: resolve,
  113. type: 'json'
  114. })
  115. )
  116.  
  117. const mapify = list => list.reduce((output, item) => ({ ...output, [item.id]: item }), {})
  118.  
  119. window.scriptCache = {
  120. playlist: {},
  121. song: {},
  122. }
  123.  
  124. window.playlistDetail = async (url, id, origin) => {
  125. const capacity = 1000
  126.  
  127. const data = await simpleRequest(url, { data: `id=${id}&n=${origin ? 1000 : 0}` })
  128. const trackIds = (data.playlist || {}).trackIds || []
  129. const tracks = (data.playlist || {}).tracks || []
  130.  
  131. if (!trackIds.length || trackIds.length === tracks.length) return data
  132. if (origin) return data
  133.  
  134. const batch = Math.ceil(trackIds.length / capacity)
  135.  
  136. const result = await Promise.all(
  137. Array.from(Array(batch).keys())
  138. .map(index => trackIds.slice(index * capacity).slice(0, capacity).map(({ id }) => ({ id })))
  139. .map(part => simpleRequest('/api/v3/song/detail', { data: `c=${JSON.stringify(part)}` }))
  140. )
  141.  
  142. const songMap = mapify(Array.prototype.concat.apply([], result.map(({ songs }) => songs)))
  143. const privilegeMap = mapify(Array.prototype.concat.apply([], result.map(({ privileges }) => privileges)))
  144.  
  145. data.playlist.tracks = trackIds
  146. .map(({ id }) => songMap[id] ? { ...songMap[id], privilege: privilegeMap[id] } : null)
  147. .filter(song => song)
  148. data.privileges = data.playlist.tracks
  149. .map(({ id }) => privilegeMap[id])
  150.  
  151. window.scriptCache.playlist[id] = data.playlist.tracks
  152. .map(song => window.scriptCache.song[song.id] = normalize(song))
  153. return data
  154. }
  155.  
  156. const overrideRequest = (url, options) => {
  157. if (url.includes('/playlist/detail')) {
  158. const data = new URLSearchParams(options.data)
  159. const { onload, onerror } = options
  160. window.playlistDetail(url, data.get('id'), true).then(onload).catch(onerror)
  161. }
  162. else {
  163. originRequest(url, options)
  164. }
  165. }
  166.  
  167. attach(window.nej, location, overrideRequest)
  168. }
  169.  
  170. const completePlaylist = () => {
  171.  
  172. const render = (song, index, playlist) => {
  173. const { album, artists, status, duration } = song
  174. const deletable = playlist.creator.userId === window.GUser.userId
  175. const durationText = showDuration(duration)
  176. const artistText = artists.map(({ name }) => escapeHTML(name)).join('/')
  177. const annotation = escapeHTML(song.transNames[0] || song.alias[0] || '')
  178. const albumName = escapeHTML(album.name)
  179. const songName = escapeHTML(song.name)
  180.  
  181. return `
  182. <tr id="${song.id}${Date.now()}" class="${index % 2 ? '' : 'even'} ${status ? 'js-dis' : ''}">
  183. <td class="left">
  184. <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 ">&nbsp;</span><span class="num">${index + 1}</span></div>
  185. </td>
  186. <td>
  187. <div class="f-cb">
  188. <div class="tt">
  189. <div class="ttc">
  190. <span class="txt">
  191. <a href="/song?id=${song.id}"><b title="${songName}${annotation ? ` - (${annotation})` : ''}">${songName}</b></a>
  192. ${annotation ? `<span title="${annotation}" class="s-fc8">${annotation ? ` - (${annotation})` : ''}</span>` : ''}
  193. ${song.mvid ? `<a href="/mv?id=${song.mvid}" title="播放mv" class="mv">MV</a>` : ''}
  194. </span>
  195. </div>
  196. </div>
  197. </div>
  198. </td>
  199. <td class=" s-fc3">
  200. <span class="u-dur candel">${durationText}</span>
  201. <div class="opt hshow">
  202. <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>
  203. <span data-res-id="${song.id}" data-res-type="18" data-res-action="fav" class="icn icn-fav" title="收藏"></span>
  204. <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>
  205. <span data-res-id="${song.id}" data-res-type="18" data-res-action="download" class="icn icn-dl" title="下载"></span>
  206. ${deletable ? `<span data-res-id="${song.id}" data-res-type="18" data-res-action="delete" class="icn icn-del" title="删除">删除</span>` : ''}
  207. </div>
  208. </td>
  209. <td>
  210. <div class="text" title="${artistText}">
  211. <span title="${artistText}">
  212. ${artists.map(({ id, name }) => `<a href="/artist?id=${id}" hidefocus="true">${escapeHTML(name)}</a>`).join('/')}
  213. </span>
  214. </div>
  215. </td>
  216. <td>
  217. <div class="text">
  218. <a href="/album?id=${album.id}" title="${albumName}">${albumName}</a>
  219. </div>
  220. </td>
  221. </tr>
  222. `
  223. }
  224.  
  225. const playlistId = (window.location.href.match(/playlist\?id=(\d+)/) || [])[1]
  226.  
  227. const action = async () => {
  228. const seeMore = document.querySelector('.m-playlist-see-more')
  229. if (seeMore) seeMore.innerHTML = '<div class="text">更多内容加载中...</div>'
  230. const data = await window.playlistDetail('/api/v6/playlist/detail', playlistId)
  231. const { playlist } = data
  232. const content = playlist.tracks.map((song, index) => render(normalize(song), index, playlist)).join('')
  233.  
  234. const replace = () => {
  235. document.querySelector('table tbody').innerHTML = content
  236. proxyAction()
  237. seeMore && seeMore.parentNode.removeChild(seeMore)
  238. }
  239.  
  240. if (document.querySelector('table'))
  241. replace()
  242. else
  243. waitChange(replace, document.querySelector('.g-mn3.f-pr.j-flag .f-pr'))
  244. }
  245.  
  246. if (playlistId) action()
  247. }
  248.  
  249. const waitChange = (action, element) => {
  250. let observer = null
  251. const handler = () => {
  252. action()
  253. observer && observer.disconnect()
  254. }
  255. observer = new MutationObserver(handler)
  256. observer.observe(element, { childList: true, attributes: true, subtree: 'true' })
  257. }
  258.  
  259. const proxyAction = (table) => {
  260. const targetAction = new Set(['play', 'addto'])
  261. const typeMap = { song: '18', playlist: '13' }
  262.  
  263. const handler = (event, type) => {
  264. const { resType, resAction, resId, resFrom, resData } = event.target.dataset
  265. if (resAction === 'delete') return waitChange(completePlaylist, document.querySelector('.g-mn3.f-pr.j-flag .f-pr'))
  266. if (typeMap[type] !== resType || !targetAction.has(resAction)) return
  267.  
  268. const list = ((window.scriptCache || {})[type] || {})[resId]
  269. if (!list) return
  270.  
  271. event.stopPropagation()
  272. window.top.player.addTo(
  273. Array.isArray(list) ? list : [list],
  274. resAction === 'play' && type === 'playlist',
  275. resAction === 'play'
  276. )
  277. }
  278.  
  279. const tableBody = document.querySelector('table tbody')
  280. tableBody && tableBody.addEventListener('click', event => handler(event, 'song'))
  281.  
  282. const operationElement = document.querySelector('#content-operation') || document.querySelector('#flag_play_addto_btn_wrapper')
  283. const contentPlay = operationElement && operationElement.querySelector('.u-btni-addply')
  284. const contentAdd = operationElement && operationElement.querySelector('.u-btni-add')
  285.  
  286. contentPlay && contentPlay.addEventListener('click', event => handler(event, 'playlist'))
  287. contentAdd && contentAdd.addEventListener('click', event => handler(event, 'playlist'))
  288.  
  289. const tableWrap = document.querySelector('table')
  290. tableWrap && skipEventListener(tableWrap, 'click', 3) // default listener throw an error
  291. }
  292.  
  293. hijackRequest()
  294.  
  295. window.addEventListener('load', completePlaylist, false)
  296. window.addEventListener('hashchange', completePlaylist, false)
  297. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址