Greasy Fork

Greasy Fork is available in English.

网易云音乐显示完整歌单

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

当前为 2020-06-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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)
})()