// ==UserScript== // @name  .mp4 Extractor & Player (動的追加対応, 右上表示) // @version 1.7 // @description f*.mp4 を抽出し順番再生。新規レスにも対応、右上に表示 // @match *://img.2chan.net/b/res/* // @run-at document-end // @grant none // ==/UserScript== (function() { 'use strict'; console.log("[MP4Player] スクリプト開始:", location.href); const re = /\bf[a-z0-9]+\.mp4\b/gi; const urls = []; const urlSet = new Set(); let total = 0; let shuffle = false; // === コンテナ & プレイヤー === const container = document.createElement("div"); container.style.position = "fixed"; container.style.top = "10px";// ← 上端に配置 container.style.right = "10px";// ← 右端に配置 container.style.zIndex = 999999; container.style.background = "rgba(0,0,0,0.6)"; container.style.padding = "6px"; container.style.border = "2px solid red"; container.style.borderRadius = "6px"; container.style.display = "flex"; container.style.flexDirection = "column"; container.style.alignItems = "center"; document.body.appendChild(container); const video = document.createElement("video"); video.controls = true; video.autoplay = true; video.style.backgroundColor = "black"; container.appendChild(video); const controls = document.createElement("div"); controls.style.display = "flex"; controls.style.alignItems = "center"; controls.style.gap = "6px"; controls.style.marginTop = "4px"; container.appendChild(controls); const info = document.createElement("span"); info.style.color = "white"; controls.appendChild(info); const prevBtn = document.createElement("button"); prevBtn.textContent = "◀ 前"; controls.appendChild(prevBtn); const nextBtn = document.createElement("button"); nextBtn.textContent = "次 ▶"; controls.appendChild(nextBtn); const jumpInput = document.createElement("input"); jumpInput.type = "number"; jumpInput.style.width = "50px"; controls.appendChild(jumpInput); const jumpBtn = document.createElement("button"); jumpBtn.textContent = "ジャンプ"; controls.appendChild(jumpBtn); const sizeBtn = document.createElement("button"); sizeBtn.textContent = "サイズ切替"; controls.appendChild(sizeBtn); const shuffleBtn = document.createElement("button"); shuffleBtn.textContent = "🔀"; controls.appendChild(shuffleBtn); shuffleBtn.addEventListener("click", () => { shuffle = !shuffle; shuffleBtn.style.background = shuffle ? "orange" : ""; console.log("[MP4Player] シャッフル:", shuffle); }); let index = 0; let large = false; let finished = false; const STORAGE_KEY = "MP4Player_currentIndex"; // 保存処理 function saveIndex(i) { localStorage.setItem(STORAGE_KEY, i.toString()); } // 復元処理 function loadIndex() { const val = localStorage.getItem(STORAGE_KEY); if (val !== null) { const num = parseInt(val, 10); if (!isNaN(num)) return num; } return 0; } function updateInfo() { info.textContent = `動画 ${index + 1} / ${total}`; jumpInput.min = 1; jumpInput.max = total; jumpInput.value = index + 1; } function adjustVideoSize() { const vw = video.videoWidth; const vh = video.videoHeight; if (!vw || !vh) return; const base = large ? 960 : 480; if (vw >= vh) { video.width = base; video.height = Math.round(base * (vh / vw)); } else { video.height = base; video.width = Math.round(base * (vw / vh)); } console.log(`[MP4Player] サイズ調整: ${video.width}x${video.height}`); } function playAt(i) { if (i < 0 || i >= total) return; index = i; saveIndex(index); const url = urls[index]; console.log("[MP4Player] 再生開始:", url); video.src = url; video.play().catch(err => console.error("[MP4Player] 再生エラー:", err)); updateInfo(); } function playNext() { if (shuffle) { finished = true; const nextIndex = Math.floor(Math.random() * total); playAt(nextIndex); } else { if (index + 1 < total) { playAt(index + 1); } else { console.log("[MP4Player] 全動画再生終了"); finished = true; } } } function playPrev() { if (index - 1 >= 0) playAt(index - 1); } video.addEventListener("ended", playNext); video.addEventListener("loadedmetadata", adjustVideoSize); prevBtn.addEventListener("click", playPrev); nextBtn.addEventListener("click", () => playNext()); jumpBtn.addEventListener("click", () => playAt(parseInt(jumpInput.value, 10) - 1)); sizeBtn.addEventListener("click", () => { large = !large; adjustVideoSize(); }); // === 初期走査 & 動的監視 === function addFromText(text) { const matches = text.match(re); if (!matches) return; let added = false; let pretotal = total; matches.forEach(name => { const url = /^fu/i.test(name) ? "https://dec.2chan.net/up2/src/" + name : "https://dec.2chan.net/up/src/" + name; if (!urlSet.has(url)) { urlSet.add(url); urls.push(url); total = urls.length; console.log("[MP4Player] 新規追加:", url); added = true; } }); updateInfo(); if (pretotal != total && finished) { finished = false; playAt(pretotal); // 新規追加分から再生 } } addFromText(document.body.innerText); const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType === Node.TEXT_NODE) { addFromText(node.textContent); } else if (node.nodeType === Node.ELEMENT_NODE) { addFromText(node.innerText || ""); } } } }); observer.observe(document.body, { childList: true, subtree: true }); // 少し遅らせて復元再生 setTimeout(() => { if (urls.length > 0) { let startIndex = loadIndex(); if (startIndex >= urls.length) { startIndex = urls.length - 1; // 範囲を超えていたら末尾に丸める } playAt(startIndex); } }, 500); // 0.5秒遅らせる })();