您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Finds HLS playlists, decrypts, remuxes to MP4, and gets the video title from the page tab.
// ==UserScript== // @name Rou Video Downloader // @namespace http://tampermonkey.net/ // @version 2.5 // @description Finds HLS playlists, decrypts, remuxes to MP4, and gets the video title from the page tab. // @author Gemini // @match *://*/* // @resource CRYPTOJS https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js // @resource MUXJS https://cdn.jsdelivr.net/npm/[email protected]/dist/mux.min.js // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @connect * // @run-at document-start // @all-frames true // @license MIT // ==/UserScript== (function() { 'use strict'; let isCryptoLoaded = false; let isMuxLoaded = false; try { eval(GM_getResourceText('CRYPTOJS')); isCryptoLoaded = true; console.log('CryptoJS library loaded successfully.'); } catch (e) { console.error('FATAL: Could not load CryptoJS library. Decryption will fail.', e); } try { eval(GM_getResourceText('MUXJS')); isMuxLoaded = true; console.log('Mux.js library loaded successfully.'); } catch (e) { console.error('FATAL: Could not load Mux.js library. Remuxing will fail.', e); } console.log('Playlist Hunter Script v2.5 (with Remuxer) Loaded in frame:', window.location.href); let masterPlaylistUrl = null; let totalSegments = 0; let decryptionKey = null; let iv = null; const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = async function(...args) { const url = args[0] instanceof Request ? args[0].url : args[0]; checkForPlaylist(url); return originalFetch.apply(this, args); }; const originalXhrOpen = unsafeWindow.XMLHttpRequest.prototype.open; unsafeWindow.XMLHttpRequest.prototype.open = function(...args) { const url = args[1]; checkForPlaylist(url); return originalXhrOpen.apply(this, args); }; function checkForPlaylist(url) { if (masterPlaylistUrl) return; if (typeof url === 'string' && url.toLowerCase().includes('.m3u8')) { console.log(`SUCCESS: Playlist HLS ditemukan! URL: ${url}`); masterPlaylistUrl = url; if (downloadButton) { updateButtonState('found'); } } } let downloadButton = null; let statusText = null; function createButtons() { if (document.getElementById('playlist-download-container-v2-5')) return; const container = document.createElement('div'); container.id = 'playlist-download-container-v2-5'; Object.assign(container.style, { position: 'fixed', top: '15px', right: '15px', zIndex: '99999', display: 'flex', flexDirection: 'column', gap: '8px', backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: '10px', borderRadius: '8px' }); downloadButton = document.createElement('button'); Object.assign(downloadButton.style, { color: 'white', border: 'none', padding: '12px 22px', borderRadius: '8px', cursor: 'pointer', fontSize: '16px', fontWeight: 'bold', transition: 'background-color 0.3s' }); downloadButton.addEventListener('click', downloadFullStream); statusText = document.createElement('p'); Object.assign(statusText.style, { color: 'white', margin: '0', textAlign: 'center', fontSize: '12px', fontWeight: 'normal' }); container.appendChild(downloadButton); container.appendChild(statusText); document.body.appendChild(container); updateButtonState(masterPlaylistUrl ? 'found' : 'initial'); } function updateButtonState(state, progress = {}) { if (!downloadButton) return; switch (state) { case 'initial': downloadButton.innerHTML = '⏳ Mencari Playlist...'; downloadButton.style.backgroundColor = '#6c757d'; downloadButton.disabled = true; statusText.textContent = 'Putar video untuk mendeteksi.'; break; case 'found': downloadButton.innerHTML = '⬇️ Unduh Video Lengkap'; downloadButton.style.backgroundColor = '#28a745'; downloadButton.disabled = false; statusText.textContent = 'Playlist ditemukan! Siap unduh.'; break; case 'downloading': downloadButton.innerHTML = '📥 Mengunduh...'; downloadButton.style.backgroundColor = '#fd7e14'; downloadButton.disabled = true; statusText.textContent = `Segmen: ${progress.downloaded}/${progress.total}`; break; case 'decrypting': downloadButton.innerHTML = '🔑 Mendekripsi...'; downloadButton.style.backgroundColor = '#6f42c1'; downloadButton.disabled = true; statusText.textContent = `Segmen: ${progress.decrypted}/${progress.total}`; break; case 'remuxing': downloadButton.innerHTML = '⚙️ Remuxing...'; downloadButton.style.backgroundColor = '#ffc107'; downloadButton.disabled = true; statusText.textContent = `Memproses segmen ke MP4...`; break; case 'error': downloadButton.innerHTML = '❌ Gagal'; downloadButton.style.backgroundColor = '#dc3545'; downloadButton.disabled = false; statusText.textContent = 'Terjadi kesalahan. Coba lagi.'; break; } } async function downloadFullStream() { if (!masterPlaylistUrl) { alert('URL Playlist tidak ditemukan. Coba putar video sebentar.'); return; } updateButtonState('downloading', { downloaded: 0, total: '??' }); try { let mediaPlaylistUrl = masterPlaylistUrl; let playlistContent = await fetchUrl(masterPlaylistUrl, 'text'); if (playlistContent.includes('#EXT-X-STREAM-INF')) { console.log('Master playlist terdeteksi. Mencari playlist media...'); const mediaPlaylistPath = playlistContent.split('\n').find(line => line.trim() && !line.startsWith('#')); if (!mediaPlaylistPath) throw new Error('Master playlist tidak berisi link ke media playlist.'); mediaPlaylistUrl = new URL(mediaPlaylistPath, masterPlaylistUrl).toString(); console.log('Menggunakan media playlist:', mediaPlaylistUrl); playlistContent = await fetchUrl(mediaPlaylistUrl, 'text'); } const baseUrl = new URL(mediaPlaylistUrl); baseUrl.pathname = baseUrl.pathname.split('/').slice(0, -1).join('/'); const keyTag = playlistContent.split('\n').find(line => line.startsWith('#EXT-X-KEY')); if (keyTag) { if (!isCryptoLoaded) throw new Error('Gagal mendekripsi: Library kripto tidak dapat dimuat.'); const uriMatch = keyTag.match(/URI="([^"]+)"/); if (uriMatch) { const keyUrl = new URL(uriMatch[1], baseUrl.toString()).toString(); decryptionKey = await fetchUrl(keyUrl, 'arraybuffer'); } const ivMatch = keyTag.match(/IV=0x([0-9a-fA-F]+)/); if (ivMatch) iv = CryptoJS.enc.Hex.parse(ivMatch[1]); } const segmentUrls = playlistContent.split('\n').filter(line => line.trim() && !line.startsWith('#')); totalSegments = segmentUrls.length; if (totalSegments === 0) throw new Error('Playlist tidak berisi segmen.'); const fullSegmentUrls = segmentUrls.map(segment => new URL(segment, baseUrl.toString()).toString()); const allChunks = []; for (let i = 0; i < fullSegmentUrls.length; i++) { updateButtonState('downloading', { downloaded: i + 1, total: totalSegments }); let chunk = await fetchUrl(fullSegmentUrls[i], 'arraybuffer'); if (decryptionKey) { updateButtonState('decrypting', { decrypted: i + 1, total: totalSegments }); chunk = decryptChunk(chunk, decryptionKey, iv || i); } allChunks.push(new Uint8Array(chunk)); } updateButtonState('remuxing'); if (!isMuxLoaded) throw new Error('Gagal remuxing: Library Mux.js tidak dapat dimuat.'); const remuxedSegments = []; const transmuxer = new muxjs.mp4.Transmuxer(); transmuxer.on('data', segment => { remuxedSegments.push(segment.initSegment); remuxedSegments.push(segment.data); }); await new Promise((resolve, reject) => { transmuxer.on('done', resolve); transmuxer.on('error', reject); allChunks.forEach(chunk => transmuxer.push(chunk)); transmuxer.flush(); }); // --- PERUBAHAN KUNCI: Mengambil Judul Video dari Tag <title> --- let videoTitle = `video_lengkap_${new Date().getTime()}`; if (document.title) { // Ambil bagian pertama dari judul tab, sebelum tanda " - " const mainTitle = document.title.split(' - ')[0]; // Membersihkan judul dari karakter yang tidak valid untuk nama file videoTitle = mainTitle.trim().replace(/[\\?%*|:"<>]/g, '-'); console.log(`Judul video ditemukan: "${videoTitle}"`); } const mediaBlob = new Blob(remuxedSegments, { type: 'video/mp4' }); const anchor = document.createElement('a'); anchor.href = URL.createObjectURL(mediaBlob); anchor.download = `${videoTitle}.mp4`; anchor.click(); URL.revokeObjectURL(anchor.href); updateButtonState('found'); } catch (error) { console.error('Gagal mengunduh stream lengkap:', error); alert(`Terjadi kesalahan: ${error.message}`); updateButtonState('error'); } } function fetchUrl(url, responseType) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: responseType === 'arraybuffer' ? 'arraybuffer' : undefined, onload: (res) => { if (res.status === 200) resolve(responseType === 'arraybuffer' ? res.response : res.responseText); else reject(`Gagal mengambil: ${url} - Status: ${res.status}`); }, onerror: (err) => reject(`Kesalahan jaringan saat mengambil: ${url}`) }); }); } function decryptChunk(encryptedData, key, iv) { const encryptedWords = CryptoJS.lib.WordArray.create(encryptedData); const keyWords = CryptoJS.lib.WordArray.create(key); let ivWords = iv; if (typeof iv === 'number') { const ivArray = new Uint8Array(16); for (let i = 15; i >= 0; i--) { ivArray[i] = iv & 0xff; iv >>= 8; } ivWords = CryptoJS.lib.WordArray.create(ivArray.buffer); } const decrypted = CryptoJS.AES.decrypt({ ciphertext: encryptedWords }, keyWords, { iv: ivWords, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding }); const latin1String = CryptoJS.enc.Latin1.stringify(decrypted); const decryptedUint8Array = new Uint8Array(latin1String.length); for (let i = 0; i < latin1String.length; i++) { decryptedUint8Array[i] = latin1String.charCodeAt(i); } return decryptedUint8Array.buffer; } function setupObserver() { const observer = new MutationObserver((_, obs) => { if (document.querySelector('video')) { createButtons(); obs.disconnect(); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { new MutationObserver((_, obs) => { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); obs.disconnect(); } }).observe(document.documentElement, { childList: true }); } } setupObserver(); })();