网易云音乐显示完整歌单

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

目前为 2020-12-07 提交的版本。查看 最新版本

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

QingJ © 2025

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