Greasy Fork is available in English.
为知乎的视频播放器添加下载功能
- // ==UserScript==
- // @name 下载知乎视频
- // @version 2.1
- // @description 为知乎的视频播放器添加下载功能
- // @author 王超
- // @license MIT
- // @match https://www.zhihu.com/*
- // @match https://video.zhihu.com/video/*
- // @match https://v.vzuu.com/video/*
- // @connect zhihu.com
- // @connect video.zhihu.com
- // @connect vzuu.com
- // @grant GM_info
- // @grant GM_download
- // @grant unsafeWindow
- // @namespace https://greasyfork.org/users/38953
- // ==/UserScript==
- /* jshint esversion: 8 */
- (async () => {
- console.log('知乎视频下载')
- async function downloadUrl(url, name = (new Date()).valueOf() + '.mp4') {
- // Greasemonkey 需要把 url 转为 blobUrl
- if (GM_info.scriptHandler === 'Greasemonkey') {
- const res = await fetch(url)
- const blob = await res.blob()
- url = URL.createObjectURL(blob)
- }
- // Chrome 可以使用 Tampermonkey 的 GM_download 函数绕过 CSP(Content Security Policy) 的限制
- if (window.GM_download) {
- GM_download({ url, name })
- }
- else {
- // firefox 需要禁用 CSP, about:config -> security.csp.enable => false
- let a = document.createElement('a')
- a.href = url
- a.download = name
- a.style.display = 'none'
- // a.target = '_blank';
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- setTimeout(() => URL.revokeObjectURL(url), 100)
- }
- }
- async function getVideoInfo(videoId) {
- const playlistUrl = `https://lens.zhihu.com/api/v4/videos/${videoId}`
- const videoInfo = await (await fetch(playlistUrl, { credentials: 'include' })).json()
- let videos = []
- // 不同分辨率视频的信息
- for (const [key, video] of Object.entries(videoInfo.playlist_v2 || videoInfo.playlist)) {
- video.resolution_ename = key
- video.resolution_cname = resolutionsName.find(v => v.ename === video.resolution_ename)?.cname
- if (!videos.find(v => v.size === video.size)) {
- videos.push(video)
- }
- }
- // 按大小排序
- videos = videos.sort(function (v1, v2) {
- const v1Index = resolutionsName.findIndex(v => v.ename === v1.resolution_ename)
- const v2Index = resolutionsName.findIndex(v => v.ename === v2.resolution_ename)
- return v1Index === v2Index ? 0 : (v1Index > v2Index ? 1 : -1)
- })
- return videos
- }
- // 处理单张卡片(问题/文章)
- function processCard(domCard) {
- const data = JSON.parse(domCard.dataset.zaExtraModule)
- // 视频卡片
- if (data.card?.content?.video_id) {
- processVideo(domCard)
- }
- }
- // 处理详细内容页面
- function processContent(domArticle) {
- const data = JSON.parse(domArticle.dataset.zaExtraModule)
- if (data.card?.content?.video_id) {
- processVideo(domArticle)
- }
- }
- // 处理视频
- function processVideo(dom) {
- const domData = JSON.parse(dom?.dataset?.zaExtraModule || null)
- const itemData = JSON.parse(dom.querySelector('div[data-zop]')?.dataset?.zop || null)
- const videoId = domData ? domData.card.content.video_id : window.location.pathname.split('/').pop()
- let observer = new MutationObserver(async mutationRecords => {
- for (const mutationRecord of mutationRecords) {
- if (mutationRecord.addedNodes.length && mutationRecord.addedNodes.item(0).innerText.includes('倍速')) {
- observer.disconnect()
- observer = null
- const curVideoUrl = mutationRecord.target.parentElement.children[0].querySelector('video').getAttribute('src')
- const toolbar = mutationRecord.addedNodes.item(0).children.item(0).children.item(1).children.item(1)
- // 克隆全屏按钮并修改图标作为下载按钮
- const domDownload = toolbar.children.item(toolbar.children.length - 3).cloneNode(true)
- domDownload.dataset.videoUrl = curVideoUrl
- domDownload.querySelector('svg').setAttribute('viewBox', '0 0 24 24')
- domDownload.querySelector('svg').innerHTML = svgDownload
- domDownload.classList.add('download')
- domDownload.children.item(0).setAttribute('aria-label', '下载')
- domDownload.children.item(1).innerText = '下载'
- domDownload.addEventListener('click', (event) => {
- event.stopPropagation()
- downloadUrl(domDownload.dataset.videoUrl)
- })
- domDownload.addEventListener('pointerenter', () => {
- const domMenu = domDownload.children.item(1)
- domMenu.style.opacity = 1
- domMenu.style.visibility = 'visible'
- })
- domDownload.addEventListener('pointerleave', () => {
- const domMenu = domDownload.children.item(1)
- domMenu.style.opacity = 0
- domMenu.style.visibility = 'hidden'
- })
- toolbar.appendChild(domDownload)
- // 获取视频信息
- const videos = await getVideoInfo(videoId)
- // 如果有不同清晰度的视频,添加下载弹出菜单
- if (videos.length > 1) {
- const curResolute = toolbar.children.item(1).children.item(0).innerText
- // 克隆倍速菜单为下载菜单
- const menu = toolbar.children.item(0).children.item(1).cloneNode(true)
- const menuItemContainer = menu.children.item(0)
- const menuItemTemplate = menuItemContainer.children.item(0).cloneNode(true)
- let menuItem
- //menu.style.left = 'auto'
- menuItemContainer.innerHTML = ''
- for (const video of videos) {
- menuItem = menuItemTemplate.cloneNode(true)
- menuItem.dataset.videoUrl = video.play_url
- menuItem.innerText = video.resolution_cname
- menuItem.addEventListener('click', (event) => {
- event.stopPropagation()
- downloadUrl(event.srcElement.dataset.videoUrl)
- })
- menuItemContainer.appendChild(menuItem)
- }
- domDownload.removeChild(domDownload.children.item(1))
- domDownload.appendChild(menu)
- }
- }
- }
- })
- observer.observe(dom, {
- childList: true, // 观察直接子节点
- subtree: true // 观察更低的后代节点
- })
- }
- const svgDownload = '<path d="M9.5,4 H14.5 V10 H17.8 L12,15.8 L6.2,10 H9.5 Z M6.2,18 H17.8 V20 H6.2 Z"></path>'
- const resolutionsName = [
- { ename: 'FHD', cname: '超清' },
- { ename: 'HD', cname: '高清' },
- { ename: 'SD', cname: '清晰' },
- { ename: 'LD', cname: '普清' }
- ]
- if (['video.zhihu.com', 'v.vzuu.com'].includes(window.location.host)) {
- processVideo(document.getElementById('player'))
- }
- else {
- const observer = new MutationObserver(mutationRecords => {
- for (const mutationRecord of mutationRecords) {
- if (!mutationRecord.oldValue) {
- if (mutationRecord.target?.dataset?.zaDetailViewPathModule === 'FeedItem') {
- processCard(mutationRecord.target)
- }
- else if (mutationRecord.target?.dataset?.zaDetailViewPathModule === 'Content' && mutationRecord.target.tagName === 'ARTICLE') {
- processContent(mutationRecord.target)
- }
- }
- }
- })
- observer.observe(document.body, {
- attributeFilter: ['data-za-detail-view-path-module'], // 只观察指定特性的变化
- attributeOldValue: true, // 是否将特性的旧值传递给回调
- attributes: true, // 观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化)
- childList: false, // 观察直接子节点
- subtree: true // 观察更低的后代节点
- })
- }
- })()