// ==UserScript== // @name Acfun 视频下载 // @description 视频下载 // @author girl-dream // @version 1.0.0 // @license The Unlicense // @namespace https://github.com/girl-dream/ // @match https://www.acfun.cn/v/ac* // @icon https://cdn.aixifan.com/ico/favicon.ico // ==/UserScript== (async () => { const title = videoInfo.title const video_info = JSON.parse(videoInfo.currentVideoInfo.ksPlayJson) const data_list = video_info.adaptationSet[0].representation const pop = document.createElement('div') pop.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 999999;` const content = document.createElement('div') content.style.cssText = ` padding: 20px; border-radius: 8px; width: 200px; max-width: 90%; max-height: 80vh; overflow-y: auto; background: #fff;` content.innerHTML = `
选择分辨率
` const select = content.querySelector('select') data_list.forEach(e => { const op = document.createElement('option') op.textContent = e.qualityLabel op.value = e.url select.append(op) }) pop.append(content) const div = document.createElement('div') div.title = '下载音频' div.innerHTML = '' div.style.cssText = ` width: 35px; height: 35px;` div.onclick = () => { document.body.append(pop) } content.querySelector('#cancel-btn').onclick = () => { pop.remove() } content.querySelector('#confirm-btn').onclick = () => { const url = select.options[select.selectedIndex].value pop.remove() download(url) } const progressDiv = document.createElement('div') progressDiv.style.cursor = 'default' progressDiv.style.fontSize = '1rem' document.querySelector('.right-area').prepend(progressDiv, div) async function download(url) { async function parseTsUrls(m3u8Url) { const content = await fetch(m3u8Url).then(e => e.text()) const lines = content.split('\n') let currentBase = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1) const tsList = [] // 检测是否是主播放列表(多码率) const isMasterPlaylist = lines.some(line => line.includes('CODECS') || line.includes('RESOLUTION') ) if (isMasterPlaylist) { console.log('检测到主播放列表,正在选择最高画质...') let bestBandwidth = 0 let bestUrl = '' for (let i = 0; i < lines.length; i++) { if (lines[i].includes('BANDWIDTH')) { const bwMatch = lines[i].match(/BANDWIDTH=(\d+)/) const bandwidth = bwMatch ? parseInt(bwMatch[1]) : 0 if (bandwidth > bestBandwidth && lines[i + 1]) { bestBandwidth = bandwidth let subUrl = lines[i + 1].trim() if (!subUrl.startsWith('http')) { subUrl = currentBase + subUrl } bestUrl = subUrl } } } if (bestUrl) { return parseTsUrls(bestUrl) // 递归解析子播放列表 } } // 解析当前M3U8中的TS文件和嵌套M3U8 for (let line of lines) { line = line.trim() if (line && !line.startsWith('#')) { if (line.endsWith('.ts') || line.includes('.ts?')) { let tsUrl = line if (!tsUrl.startsWith('http')) { tsUrl = currentBase + tsUrl } tsList.push(tsUrl) } else if (line.endsWith('.m3u8') || line.includes('.m3u8?')) { let subUrl = line if (!subUrl.startsWith('http')) { subUrl = currentBase + subUrl } const subTsList = await parseTsUrls(subUrl) tsList.push(...subTsList) } } } return tsList } // 步骤3:下载TS文件(并发控制) async function downloadTsFiles(tsUrls, concurrency = 5) { const chunks = new Array(tsUrls.length) let downloaded = 0 async function downloadOne(index) { try { const response = await fetch(tsUrls[index]) const blob = await response.blob() chunks[index] = blob downloaded++ progressDiv.textContent = `下载进度: ${downloaded}/${tsUrls.length} (${Math.round(downloaded / tsUrls.length * 100)}%)` return true } catch (error) { alert(`TS ${index} 下载失败:`, error) chunks[index] = null downloaded++ return false } } // 并发下载控制 let index = 0 const workers = [] for (let i = 0; i < Math.min(concurrency, tsUrls.length); i++) { workers.push((async () => { while (index < tsUrls.length) { const currentIndex = index++ await downloadOne(currentIndex) // 添加小延迟避免请求过快 await new Promise(r => setTimeout(r, 50)) } })()) } await Promise.all(workers) // 过滤失败的文件 const validChunks = chunks.filter(chunk => chunk !== null) if (validChunks.length === 0) throw new Error('所有TS文件下载失败') if (validChunks.length !== tsUrls.length) { console.warn(`成功下载 ${validChunks.length}/${tsUrls.length} 个TS文件`) } return validChunks } progressDiv.textContent = '下载进度: 0%' progressDiv.style.visibility = 'visible' const tsUrls = await parseTsUrls(url) if (tsUrls.length === 0) { throw new Error('未找到任何TS文件') } const mergedBlob = new Blob(await downloadTsFiles(tsUrls), { type: 'video/mp2t' }) const temp_url = URL.createObjectURL(mergedBlob) const a = document.createElement('a') a.href = temp_url a.download = title + '.ts' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(temp_url) progressDiv.style.visibility = 'hidden' } })();