🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

网易云音乐显示完整歌单

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         网易云音乐显示完整歌单
// @namespace    https://github.com/nondanee
// @version      1.4.13
// @description  解除歌单歌曲展示数量限制 & 播放列表 1000 首上限
// @author       nondanee
// @match        *://music.163.com/*
// @icon         https://s1.music.126.net/style/favicon.ico
// @grant        none
// @run-at       document-start
// ==/UserScript==

(() => {
	if (window.top === window.self) {
		const observe = () => {
			try {
				const callback = () => document.contentFrame.dispatchEvent(new Event('songchange'))
				const observer = new MutationObserver(callback)
				observer.observe(document.querySelector('.m-playbar .words'), { childList: true })
			} catch (_) {}
		}
		window.addEventListener('load', observe, false)
		return
	}

	const locate = (object, pattern) => {
		for (const key in object) {
			const value = object[key]
			if (!Object.prototype.hasOwnProperty.call(object, key) || !value) continue
			switch (typeof value) {
				case 'function': {
					if (String(value).match(pattern)) return [key]
					break
				}
				case 'object': {
					const path = locate(value, pattern)
					if (path) return [key].concat(path)
					break
				}
			}
		}
	}

	const findMethod = (object, pattern) => {
		const path = locate(object, pattern)
		if (!path) throw new Error('MethodNotFound')
		let poiner = object
		const last = path.pop()
		path.forEach(key => poiner = poiner[key])
		const origin = poiner[last]
		return {
			origin,
			override: (value) => {
				value.toString = () => origin.toString()
				poiner[last] = value
			}
		}
	}

	const cloneEvent = (event) => {
		const copy = new event.constructor(event.type, event)
		// copy.target = event.target // 有问题
		Object.defineProperty(copy, 'target', { value: event.target })
		return copy
	}

	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 zFill = (string = '', length = 2) => {
		string = String(string)
		while (string.length < length) string = '0' + string
		return string
	}

	const formatDuration = duration => {
		const oneSecond = 1e3
		const oneMinute = 60 * oneSecond
		const result = []

		Array(oneMinute, oneSecond)
			.reduce((remain, unit) => {
				const value = Math.floor(remain / unit)
				result.push(value)
				return remain - value * unit
			}, duration || 0)

		return result
			.map(value => zFill(value, 2))
			.join(':')
	}

	const TYPE = {
		SONG: '18',
		PLAYLIST: '13',
	}

	const CACHE = window.COMPLETE_PLAYLIST_CACHE = {
		[TYPE.SONG]: {},
		[TYPE.PLAYLIST]: {}
	}

	const interceptRequest = () => {
		if (window.getPlaylistDetail) return

		const request = findMethod(window.nej, '\\.replace\\("api","weapi')

		const Fetch = (url, options) => (
			new Promise((resolve, reject) =>
				request.origin(url, {
					...options,
					cookie: true,
					method: 'GET',
					onerror: reject,
					onload: resolve,
					type: 'json'
				})
			)
		)

		window.getPlaylistDetail = async (url, options) => {
			// const search = new URLSearchParams(options.data)
			// search.set('n', 0)
			// options.data = search.toString()

			const data = await Fetch(url, options)
			const slice = 1000

			const trackIds = (data.playlist || {}).trackIds || []
			const tracks = (data.playlist || {}).tracks || []

			if (!trackIds.length || trackIds.length === tracks.length) return data

			const missingTrackIds = trackIds.slice(tracks.length)
			const round = Math.ceil(missingTrackIds.length / slice)

			const result = await Promise.all(
				Array(round).fill().map((_, index) => {
					const part = missingTrackIds.slice(index * slice).slice(0, slice).map(({ id }) => ({ id }))
					return Fetch('/api/v3/song/detail', { data: `c=${JSON.stringify(part)}` })
				})
			)

			const songMap = {}
			const privilegeMap = {}

			result.forEach(({ songs, privileges }) => {
				songs.forEach(_ => songMap[_.id] = _)
				privileges.forEach(_ => privilegeMap[_.id] = _)
			})

			const missingTracks = missingTrackIds
				.map(({ id }) => ({ ...songMap[id], privilege: privilegeMap[id] }))

			const missPrivileges = missingTracks
				.map(({ id }) => privilegeMap[id])

			data.playlist.tracks = tracks.concat(missingTracks)
			data.privileges = (data.privileges || []).concat(missPrivileges)

			CACHE[TYPE.PLAYLIST][data.playlist.id] = data.playlist.tracks
				.map(song => CACHE[TYPE.SONG][song.id] = normalize(song))

			return data
		}

		const overrideRequest = async (url, options) => {
			if (/\/playlist\/detail/.test(url)) {
				const { onload, onerror } = options
				return window.getPlaylistDetail(url, options).then(onload).catch(onerror)
			}
			return request.origin(url, options)
		}

		request.override(overrideRequest)
	}

	const handleSongChange = () => {
		try {
			const { track } = window.top.player.getPlaying()
			const { id, source, program } = track
			if (program) return

			const base = 'span.ply'
			const attrs = `[data-res-id="${id}"][data-res-type="${TYPE.SONG}"]`

			// player.addTo() 相同 id 不同 source 会被过滤
			// const { fid, fdata } = source
			// if (String(fid) !== TYPE.PLAYLIST) return
			// const attrs = `[data-res-id="${id}"][data-res-from="${fid}"][data-res-data="${fdata}"]`

			document.querySelectorAll(base).forEach(node => {
				node.classList.remove('ply-z-slt')
			})

			document.querySelectorAll(base + attrs).forEach(node => {
				node.classList.add('ply-z-slt')
			})
		} catch (_) {}
	}

	const escapeHTML = string => (
		string.replace(
			/[&<>'"]/g,
			word =>
			({
				'&': '&amp;',
				'<': '&lt;',
				'>': '&gt;',
				"'": '&#39;',
				'"': '&quot;',
			})[word] || word
		)
	)

	const bindEvent = () => {
		const ACTIONS = new Set(['play', 'addto'])

		const onClick = (event) => {
			const {
				resAction,
				resId,
				resType,
				resData,
			} = event.target.dataset

			const data = (CACHE[resType] || {})[resId]
			if (!data) return

			event.stopPropagation()

			if (!ACTIONS.has(resAction)) {
				// 没有 privilege 冒泡后会报错
				document.body.dispatchEvent(cloneEvent(event))
				return
			}

			const playlistId = Number(resType === TYPE.PLAYLIST ? resId : resData)

			const list = (Array.isArray(data) ? data : [data])
				.map(song => ({
					...song,
					source: {
						fdata: playlistId,
						fid: TYPE.PLAYLIST,
						link: `/playlist?id=${playlistId}&_hash=songlist-${song.id}`,
						title: '歌单',
					},
				}))

			window.top.player.addTo(
				list,
				resAction === 'play' && resType === TYPE.PLAYLIST,
				resAction === 'play'
			)
		}

		const body = document.querySelector('table tbody')
		const play = document.querySelector('#content-operation .u-btni-addply')
		const add = document.querySelector('#content-operation .u-btni-add')

		if (play) play.addEventListener('click', onClick)
		if (add) add.addEventListener('click', onClick)
		if (body) body.addEventListener('click', onClick)
	}

	const completePlaylist = async (id) => {
		const render = (song, index, playlist) => {
			const { album, artists, status, duration } = song
			const deletable = playlist.creator.userId === window.GUser.userId
			const durationText = formatDuration(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-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 seeMore = document.querySelector('.m-playlist-see-more')
		if (seeMore) seeMore.innerHTML = '<div class="text">更多内容加载中...</div>'

		const data = await window.getPlaylistDetail(
			'/api/v6/playlist/detail/',
			{ data: `id=${id}&offset=0&total=true&limit=1000&n=1000` }
		)
		const { playlist } = data
		const content = playlist.tracks
			.map((song, index) => render(normalize(song), index, playlist))
			.join('')

		const body = document.querySelector('table tbody')
		if (body) body.innerHTML = content
		bindEvent()
		handleSongChange()

		if (seeMore) seeMore.parentNode.removeChild(seeMore)
	}

	const handleRoute = () => {
		interceptRequest()
		const { href, search } = location
		if (/\/my\//.test(href)) return

		const id = new URLSearchParams(search).get('id')
		if (/playlist[/?]/.test(href) && id) completePlaylist(id)
	}

	window.addEventListener('songchange', handleSongChange)
	window.addEventListener('load', handleRoute, false)
	window.addEventListener('hashchange', handleRoute, false)
})()