您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
提取抖音直播地址
当前为
// ==UserScript== // @name Extract Douyin Live Stream URLs // @name:zh-CN 抖音直播流提取 // @namespace Cassandre // @version 1.1 // @description Extract stream URLs from Douyin live streams // @description:zh-CN 提取抖音直播地址 // @author Cassandre Cora // @license MIT // @icon https://p3-pc-weboff.byteimg.com/tos-cn-i-9r5gewecjs/logo-horizontal-small.svg // @match https://live.douyin.com/* // @run-at document-end // @grant GM_addStyle // @grant GM_setClipboard // ==/UserScript== const QUALITY_MAP = { 'FULL_HD1': '原画', 'HD1': '超清', 'SD2': '高清', 'SD1': '标清' }; const QUALITY_LEVELS = ['FULL_HD1', 'HD1', 'SD2', 'SD1']; const STYLES = ` .douyin-stream-url-side-button { position: fixed; z-index: 19998; top: 50%; right: 0; width: 40px; height: 40px; border: none; outline: none; cursor: pointer; color: white; text-align: center; background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%); border-radius: 50%; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; } .douyin-stream-url-side-button:hover { transform: scale(1.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%); color: #f0f0f0; font-weight: bold; } .douyin-stream-url-side-button::before { content: "⚡"; font-size: 20px; } .douyin-stream-url-close-button { position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; border: 2px solid #FE2C55; border-radius: 50%; background: white; cursor: pointer; outline: none; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .douyin-stream-url-close-button:hover { transform: scale(1.1); background: #FE2C55; color: white; font-weight: bold; } #douyin-stream-url-app { position: fixed; right: 20px; top: 110px; width: 320px; height: auto; opacity: 0; background-color: rgba(24, 24, 24, 0.95); color: #e0e0e0; padding: 15px; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; z-index: 9999; border-radius: 16px; transform: translateX(110%); transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); backdrop-filter: blur(10px); } .douyin-stream-url-list-container { background-color: rgba(31, 31, 31, 0.8); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); margin-bottom: 12px; overflow: hidden; } .douyin-stream-url-list-container:last-child { margin-bottom: 0; } .douyin-stream-url-list-header { background-color: rgba(45, 45, 45, 0.8); padding: 10px 12px; font-weight: 600; font-size: 14px; color: #ffffff; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .douyin-hls-stream-url-list-content, .douyin-flv-stream-url-list-content { padding: 8px; overflow-x: auto; white-space: nowrap; background-color: rgba(38, 38, 38, 0.8); font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13px; line-height: 1.6; color: #d1d1d1; max-height: 150px; overflow-y: auto; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .douyin-stream-url-button-container { display: flex; gap: 4px; padding: 4px; } .douyin-hls-stream-url-copy-button, .douyin-flv-stream-url-copy-button { display: flex; align-items: center; justify-content: center; width: 50%; padding: 10px; background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%); color: white; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-size: 14px; font-weight: 500; } .douyin-hls-stream-url-copy-button:hover, .douyin-flv-stream-url-copy-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(254, 44, 85, 0.3); background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%); color: #ffffff; font-weight: 600; letter-spacing: 0.5px; } .douyin-stream-url-download-all-button { display: flex; align-items: center; justify-content: center; width: 100%; padding: 12px; background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%); color: white; border: none; cursor: pointer; transition: all 0.3s ease; font-size: 14px; font-weight: 500; border-radius: 10px; margin-top: 8px; } .douyin-stream-url-download-all-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(254, 44, 85, 0.3); background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%); color: #ffffff; font-weight: 600; letter-spacing: 0.5px; } `; // Get stream ID from URL let streamId = window.location.href.split('/root/live/')[1]?.split('?')[0]; // Inject styles GM_addStyle(STYLES); class DouyinStreamExtractor { constructor() { this.pageHTML = document.documentElement.outerHTML; this.init(); } init() { const streamData = this.getStreamData(); if (streamData?.status) { this.createUI(streamData); } } onUrlChange(callback) { // Listen for popstate events window.addEventListener('popstate', () => callback({ type: 'popstate', url: window.location.href })); // Override pushState and replaceState const originalPushState = history.pushState; history.pushState = function (...args) { originalPushState.apply(this, args); callback({ type: 'pushState', url: window.location.href }); }; const originalReplaceState = history.replaceState; history.replaceState = function (...args) { originalReplaceState.apply(this, args); callback({ type: 'replaceState', url: window.location.href }); }; // Listen for hash changes window.addEventListener('hashchange', () => callback({ type: 'hashchange', url: window.location.href })); } extractJSON(pattern) { const match = this.pageHTML?.match(pattern); return match ? match[1].replace(/\\/g, '').replace(/u0026/g, '&') : null; } getStreamData() { try { const jsonStr = this.extractJSON(/(\{\\"state\\":.*?)]\\n"]\)/) || this.extractJSON(/(\{\\"common\\":.*?)]\\n"]\)<\/script><div hidden/); if (!jsonStr) { console.warn("Page JSON data not found"); return null; } const roomStoreMatch = jsonStr.match(/"roomStore":(.*?),"linkmicStore"/); if (!roomStoreMatch) { console.warn("Room data not found"); return null; } const roomStore = `${roomStoreMatch[1].split(',"has_commerce_goods"')[0]}}}}`; const roomData = JSON.parse(roomStore)?.roomInfo?.room; if (!roomData) { console.warn("Invalid room data structure"); return null; } const anchorNameMatch = roomStore.match(/"nickname":"(.*?)","avatar_thumb/); return { id: roomData.id_str, status: roomData.status === 2, anchor_name: anchorNameMatch?.[1] || '', hls_stream_url: roomData.stream_url?.hls_pull_url_map, flv_stream_url: roomData.stream_url?.flv_pull_url, title: roomData.title, avatar_thumb: roomData.owner?.avatar_thumb }; } catch (error) { console.error("Error parsing room data:", error); return null; } } createStreamUrlList(data) { return QUALITY_LEVELS.reduce((acc, quality) => { acc[quality] = { hls_stream_url: data.hls_stream_url?.[quality] || null, flv_stream_url: data.flv_stream_url?.[quality] || null }; return acc; }, {}); } async copyToClipboard(text, button, type) { try { if (GM_setClipboard) { await GM_setClipboard(text); } else { await navigator.clipboard.writeText(text); } button.textContent = '已复制!'; setTimeout(() => { button.textContent = `复制 ${type}`; }, 1000); } catch (err) { console.error('复制失败:', err); button.textContent = '复制失败!'; } } downloadM3U8(content, filename) { const blob = new Blob([content], { type: 'application/x-mpegURL' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } createUI(data) { const app = document.createElement('div'); app.id = 'douyin-stream-url-app'; const urlList = this.createStreamUrlList(data); // Create quality list Object.entries(urlList).forEach(([quality, urls]) => { if (!urls.hls_stream_url && !urls.flv_stream_url) return; const container = this.createQualityContainer(quality, urls); app.appendChild(container); }); // Create download all button const downloadAllButton = this.createDownloadAllButton(data, urlList); app.appendChild(downloadAllButton); // Create side button and close button const { sideButton, closeButton } = this.createControlButtons(app); document.body.appendChild(app); document.body.appendChild(sideButton); app.appendChild(closeButton); } createQualityContainer(quality, urls) { const container = document.createElement('div'); container.className = 'douyin-stream-url-list-container'; const header = document.createElement('div'); header.className = 'douyin-stream-url-list-header'; header.textContent = `${QUALITY_MAP[quality]} ${quality}`; container.appendChild(header); /* if (urls.hls_stream_url) { const hlsContent = document.createElement('pre'); hlsContent.className = 'douyin-hls-stream-url-list-content'; hlsContent.textContent = urls.hls_stream_url; container.appendChild(hlsContent); } if (urls.flv_stream_url) { const flvContent = document.createElement('pre'); flvContent.className = 'douyin-flv-stream-url-list-content'; flvContent.textContent = urls.flv_stream_url; container.appendChild(flvContent); } */ const buttonContainer = this.createButtonContainer(quality, urls); container.appendChild(buttonContainer); return container; } createButtonContainer(quality, urls) { const container = document.createElement('div'); container.className = 'douyin-stream-url-button-container'; if (urls.hls_stream_url) { const hlsButton = document.createElement('button'); hlsButton.className = 'douyin-hls-stream-url-copy-button'; hlsButton.textContent = '复制 HLS'; hlsButton.onclick = () => this.copyToClipboard(urls.hls_stream_url, hlsButton, 'HLS'); container.appendChild(hlsButton); } if (urls.flv_stream_url) { const flvButton = document.createElement('button'); flvButton.className = 'douyin-flv-stream-url-copy-button'; flvButton.textContent = '复制 FLV'; flvButton.onclick = () => this.copyToClipboard(urls.flv_stream_url, flvButton, 'FLV'); container.appendChild(flvButton); } return container; } createDownloadAllButton(data, urlList) { const button = document.createElement('button'); button.className = 'douyin-stream-url-download-all-button'; button.textContent = 'M3U8文件下载'; button.onclick = () => { let m3u8Content = '#EXTM3U\n'; Object.entries(urlList).forEach(([quality, urls]) => { if (urls.hls_stream_url) { m3u8Content += `#EXTINF:-1 tvg-name="${QUALITY_MAP[quality]} ${quality} hls" tvg-logo="${data.avatar_thumb.url_list[0]}"\n${urls.hls_stream_url}\n`; } if (urls.flv_stream_url) { m3u8Content += `#EXTINF:-1 tvg-name="${QUALITY_MAP[quality]} ${quality} flv" tvg-logo="${data.avatar_thumb.url_list[0]}"\n${urls.flv_stream_url}\n`; } }); const filename = `抖音直播_${data.anchor_name}_${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/[\/\s:]/g, '').replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1$2$3_$4:$5')}.m3u8`; this.downloadM3U8(m3u8Content, filename); button.textContent = 'M3U8文件成功生成!'; setTimeout(() => { button.textContent = 'M3U8文件下载'; }, 1000); }; return button; } createControlButtons(app) { const sideButton = document.createElement('button'); sideButton.className = 'douyin-stream-url-side-button'; const closeButton = document.createElement('button'); closeButton.className = 'douyin-stream-url-close-button'; closeButton.innerHTML = '<span>X</span>'; sideButton.onclick = () => { sideButton.style.display = 'none'; app.style.transform = 'translateX(0)'; app.style.opacity = '1'; }; closeButton.onclick = () => { app.style.transform = 'translateX(110%)'; app.style.opacity = '0'; sideButton.style.display = 'block'; }; return { sideButton, closeButton }; } } // Initialize new DouyinStreamExtractor();