网易云音乐显示完整歌单

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

目前為 2020-06-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         网易云音乐显示完整歌单
// @namespace    https://github.com/nondanee
// @version      1.2.3
// @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 escapeHTML = string =>
		string.replace(
			/[&<>'"]/g,
			word =>
			({
				'&': '&amp;',
				'<': '&lt;',
				'>': '&gt;',
				"'": '&#39;',
				'"': '&quot;',
			})[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 skipEventListener = (element, type, skip) => {
		const entry = Array(skip).fill(null)
			.reduce(pointer => pointer.parentNode || {}, element)

		element.addEventListener(type, event => {
			event.stopImmediatePropagation()
			const target = event.target.cloneNode(false)
			target.style.display = 'none'
			entry.parentNode.appendChild(target)
			target.click()
			entry.parentNode.removeChild(target)
		})
	}

	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)

		const 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 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 => 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 `
				<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 ">&nbsp;</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-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
				waitChange(replace, document.querySelector('.g-mn3.f-pr.j-flag .f-pr'))
		}

		if (playlistId) action()
	}

	const waitChange = (action, element) => {
		let observer = null
		const handler = () => {
			action()
			observer && observer.disconnect()
		}
		observer = new MutationObserver(handler)
		observer.observe(element, { childList: true, attributes: true, subtree: 'true' })
	}

	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') return waitChange(completePlaylist, document.querySelector('.g-mn3.f-pr.j-flag .f-pr'))
			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 tableBody = document.querySelector('table tbody')
		tableBody && tableBody.addEventListener('click', event => handler(event, 'song'))

		const operationElement = document.querySelector('#content-operation') || document.querySelector('#flag_play_addto_btn_wrapper')
		const contentPlay = operationElement && operationElement.querySelector('.u-btni-addply')
		const contentAdd = operationElement && operationElement.querySelector('.u-btni-add')

		contentPlay && contentPlay.addEventListener('click', event => handler(event, 'playlist'))
		contentAdd && contentAdd.addEventListener('click', event => handler(event, 'playlist'))

		const tableWrap = document.querySelector('table')
		tableWrap && skipEventListener(tableWrap, 'click', 3) // default listener throw an error
	}

	hijackRequest()

	window.addEventListener('load', completePlaylist, false)
	window.addEventListener('hashchange', completePlaylist, false)
})()

QingJ © 2025

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