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

Greasy fork 爱吃馍镜像

网易云音乐显示完整歌单

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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

公众号二维码

扫码关注【爱吃馍】

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

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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

公众号二维码

扫码关注【爱吃馍】

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

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