// ==UserScript==
// @name SUKEBEI PLUS
// @namespace Violentmonkey Scripts
// @match *://sukebei.nyaa.si/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @version 1.0.5
// @author Chaewon
// @description Add original video preview.
// @license Unlicense
// @icon https://sukebei.nyaa.si/static/favicon.png
// ==/UserScript==
(function () {
"use strict";
const stylesheet = `
.overlay-video-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.overlay-video {
width: auto;
max-width: 90%;
min-height: 60%;
height: auto;
max-height: 90%;
border: 1px solid #919191;
background: #000;
}
.close-button {
position: absolute;
padding: 6px 14px;
top: 10px;
right: 10px;
background: none;
border: none;
color: white;
cursor: pointer;
border: 1px solid #333;
}
.settings-container {
position: fixed;
right: 1em;
top: 2em;
max-width: 400px;
width: 100%;
padding: 10px 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 9999;
}
.settings-container .settings-item:not(:last-of-type) {
margin-bottom: 20px;
}
.settings-container label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.settings-container input[type="text"],
.settings-container input[type="range"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.settings-container input[type="checkbox"] {
margin-right: 10px;
}
.settings-container .button {
display: inline-block;
padding: 10px 15px;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: center;
transition: background-color 0.3s ease;
}
.settings-container .button:hover {
background-color: #0056b3;
}
.settings-container range-label {
margin-bottom: 5px;
color: #333;
}
.settings-container #quality {
color: black;
}
.settings-container option {
color: black;
}`;
GM_addStyle(stylesheet);
GM_registerMenuCommand("Config", openSettings);
let userConfig;
const fetchConfig = GM_getValue("config");
if (fetchConfig) {
userConfig = fetchConfig;
} else {
console.log("[US:DEBUG] : ", "No local config found. Uses default.");
userConfig = {
darkTheme: false,
playerAutoplay: false,
playerVolume: 0.1,
playerLoop: true,
playerClickAnywhereToClose: false,
previewQuality: 5,
};
}
console.log("[US:DEBUG] : ", "CONFIG: ", userConfig);
// Manually added. Not all code added nor tested.
const studios = {
general: ["PFES"],
das: ["DASD", "DASS", "DAZD", "PLA", "PLB"],
sOne: ["SSIS", "SOE", "SNIS", "SSNI", "OFJE", "SONE", "ONSD"],
wanz: [
"WAAA",
"BMW",
"WANZ",
"WNZ",
"PPS",
"WNZS",
"WAB",
"WFS",
"SWF",
],
eBody: ["EBOD", "EBWH", "MKCK", "EYAN"],
oppai: ["PPPD", "PPPE", "PPBD", "PPMD", "PPSD"],
aurora: [
"APNS",
"APGH",
"APAK",
"APAA",
"APGG",
"APAE",
"APKH",
"APNH",
],
bibian: ["BBAN", "BBSS"],
dahlia: ["DLDSS"],
faleno: ["FSDSS", "FCDSS", "FTBLD", "FTKD", "FTHTD", "MGOLD"],
hunter: ["HUNTC", "HUBLK", "HHF", "HUNTA", "HUNBL", "HUNTB"],
kawaii: ["CAWD", "KAWD", "KWBD", "KANE", "KAPD"],
moodyz: [
"MIDV",
"MIMK",
"MIFD",
"MIAB",
"MIZD",
"MIRD",
"MIAA",
"MIDE",
"KMIDE",
"MIAE",
"MIGD",
"MIAD",
"MIBD",
"MIDD",
"MDED",
"MDLD",
"MDUD",
],
madonna: [
"JUQ",
"JUY",
"ROE",
"JUMS",
"ACHJ",
"URE",
"JUSD",
"JUL",
"OBA",
],
oneMore: ["ONEX"], //also has ONEZ - Prestige is bigger therefor priortized over this studio.
premium: ["PRED", "PBD", "PRWF", "PRST", "PGD", "PXD", "PJD", "PID"],
jetEizou: ["NGOD", "NKKD", "NDRA", "NBES", "LLAN"],
prestige: [
"ABF",
"ABP",
"YRK",
"FIG",
"SHF",
"HHE",
"AAS",
"THU",
"PPX",
"HEO",
"PXM",
"VPC",
"ONEZ",
"PPT",
"ICHK",
"ABW",
"AFS",
"DCX",
"CPDE",
"MGT",
"KUM",
"TRE",
//"MBMH",
"FIR",
"DNW",
"DOCP",
"TEN",
"KBI",
"GOAL",
"MFCC",
"GNAB",
//"MBM",
"GZAP",
"JNT",
"CHN",
"JAC",
"FIV",
"TUS",
"BGN",
"ASI",
"PXH",
"MTM",
//"DCV",
"GGEN",
"DTT",
"CPDE",
"KPB",
"DIC",
"AMA",
"YRH",
"CDC",
"SOUD",
"NAM",
"DCX",
"SIM",
],
attackers: [
"ADN",
"ATK",
"ATID",
"SAME",
"YUJ",
"SSPD",
"ATKD",
"JBD",
"ATAD",
"RBK",
"SHKD",
],
sodCreate: [
"STAR",
"STARS",
"START",
"STARTS",
"SDNM",
"SDMM",
"SDMF",
"SDDE",
"SDAM",
"SETM",
"SDMUA",
"SDJS",
"FTAV",
"SUWK",
"MOGI",
"KKBT",
"SDMU",
"KIRE",
"KMHRS",
"SDTH",
"KUSE",
"MSFH",
"SDNT",
"SDEN",
],
aliceJapan: ["DVAJ", "DV"],
ideaPocket: [
"IPX",
"IPZ",
"IPZZ",
"IPTD",
"IDBD",
"IPIT",
"SUPD",
"IPSE",
],
nagaeStyle: ["NSFS"],
naturalHigh: ["NHDTB", "NHDTA", "NHDT"],
tameikeiGoro: ["MEYD", "MDYD", "MBYD"],
sQute: ["SQTE", "KRAY"],
royal: ["ROYD"],
materiall: ["MTATA", "MTALL", "MTABS"],
honnaka: ["MIH", "HMN", "HNDB", "HNDS", "HND", "KRND"],
lunatics: ["LULU"],
nampaJapan: ["NPJS", "NNPJ", "NPJB", "TNB"],
serebuNoTomo: ["CEMD", "CEAD", "CESD", "CETD"],
barutan: [
"BASJ",
"BASB",
"BAGR",
"BACJ",
"BAHP",
"BAEM",
"BACN",
"BAVC",
"BADA",
],
guppi: ["GUPP"],
shiroutoKissa: ["HAZU"],
cineMagic: ["CNY", "CMC", "CMN", "CMA", "CMV", "CMF"],
globalMediaAnnex: ["GMA", "GARA", "GAJK", "GMAB"],
takaraEizou: [
"MOND",
"ALDN",
"NTRD",
"MGHT",
"SPRD",
"AVOP",
"RADC",
"CEMN",
"DTKM",
],
fairAndWay: ["FWAY"],
hsoda: ["HSODA", "PFES"],
ieEnergy: ["IESP", "IENF", "IESM"],
kmProduce: [
"SCOP",
"REAL",
"MKMP",
"MDTM",
"MDBK",
"BOKD",
"BAZX",
"UMSO",
"SCPX",
"OKAX",
"ENKI",
"BAZE",
"MDTE",
"NANX",
],
kaguyaHimePtMousozoku: ["KPIE", "KAGP", "MASM", "MKON", "BABM"],
hotEntertainment: [
"HEZ",
"SHE",
"HOC",
"HFF",
"HJT",
"HZM",
"HVY",
"HNU",
"HNK",
],
crystalEizou: ["NITR", "EKDV", "MADV", "CADV", "MADM", "GEKI"],
rocket: ["RCTD", "RCT"],
};
const studiosInDMM = [
...studios.general,
...studios.sOne,
...studios.ideaPocket,
...studios.wanz,
...studios.attackers,
...studios.kawaii,
...studios.madonna,
...studios.hunter,
...studios.jetEizou,
...studios.bibian,
...studios.das,
...studios.aurora,
...studios.eBody,
...studios.tameikeiGoro,
...studios.nagaeStyle,
...studios.moodyz,
...studios.oppai,
...studios.premium,
...studios.aliceJapan,
...studios.sQute,
...studios.royal,
...studios.honnaka,
...studios.lunatics,
...studios.nampaJapan,
...studios.serebuNoTomo,
...studios.barutan,
...studios.guppi,
...studios.shiroutoKissa,
...studios.cineMagic,
...studios.globalMediaAnnex,
...studios.takaraEizou,
...studios.fairAndWay,
...studios.hsoda,
...studios.kmProduce,
...studios.kaguyaHimePtMousozoku,
],
plusOne = [
...studios.faleno,
...studios.dahlia,
...studios.naturalHigh,
...studios.sodCreate,
...studios.materiall,
...studios.ieEnergy,
...studios.rocket,
],
prestigeStudios = [...studios.prestige];
const regexPatterns = {
dmm: new RegExp(`\\b(?:${studiosInDMM.join("|")})(?:-)?\\d{3}\\b`, "i"),
plusOne: new RegExp(`\\b(?:${plusOne.join("|")})(?:-)?\\d{3}\\b`, "i"),
prestige: new RegExp(
`\\b(?:${prestigeStudios.join("|")})(?:-)?\\d{3}\\b`,
"i"
),
oneMore: new RegExp(
`\\b(?:${studios.oneMore.join("|")})(?:-)?\\d{3}\\b`,
"i"
),
hotEntertainment: new RegExp(
`\\b(?:${studios.hotEntertainment.join("|")})(?:-)?\\d{3}\\b`,
"i"
),
//faleno: new RegExp(`\\b(?:${faleno.join('|')})(?:-)?\\d{3}\\b`, 'i'),
//dahlia: new RegExp(`\\b(?:${dahlia.join('|')})(?:-)?\\d{3}\\b`, 'i')
};
//SECTION - MAIN
if (window.location.pathname.startsWith("/view/")) {
console.log("[US:DEBUG] : ", "Torrent Detail Page");
const titlePanel = document.getElementsByClassName("panel-title");
if (
titlePanel[0].parentElement.nextElementSibling.classList.contains(
"panel-body"
) &&
titlePanel[0].parentElement.nextElementSibling.children[0].classList.contains(
"row"
)
) {
//Append Button
const code = detectCode(titlePanel[0].innerText, regexPatterns);
if (code && code.match) {
console.log("[US:DEBUG] : ", "DMM studios detected: ", code);
let codeId = code.match.toLowerCase();
const buttonDom = document.getElementsByClassName(
"panel-footer clearfix"
)[0];
const newButton = document.createElement("a");
newButton.setAttribute("href", "?preview=" + codeId);
newButton.setAttribute(
"title",
"Original DVD Preview (Not Torrent Preview!)"
);
newButton.innerHTML = `<i class="fa fa-video-camera"></i> Preview`;
const buttonDivider = document.createTextNode(" ・");
buttonDom.insertBefore(newButton, buttonDom.firstChild);
buttonDom.insertBefore(
buttonDivider,
buttonDom.firstChild.nextSibling
);
newButton.addEventListener("click", function (event) {
event.preventDefault();
openPreview(codeId, code.cdn);
});
} else {
return;
}
} else {
return;
}
} else {
console.log("[US:DEBUG] : ", "Non Torrent Detail Page");
const rows = document.querySelectorAll("tr");
//Append Button
//TODO - Only check if AV category?
rows.forEach((row, index) => {
if (!row || index === 0) return;
const categoryCell =
row.querySelector("td:nth-child(1) a")["title"];
const titleCell = row.querySelector("td:nth-child(2)");
const linkCell = row.querySelector("td:nth-child(3)");
if (categoryCell === "Real Life - Videos") {
const code = detectCode(titleCell.innerText, regexPatterns);
if (code && code.match) {
console.log(
"[US:DEBUG] : ",
"DMM studios detected: ",
code
);
let codeId = code.match.toLowerCase();
//const buttonDom =
// titleCell.parentElement.querySelector("td:nth-child(3)");
const newButton = document.createElement("a");
newButton.setAttribute("href", "?preview=" + codeId);
newButton.setAttribute(
"title",
"Original DVD Preview (Not Torrent Preview!)"
);
newButton.innerHTML = `<i class="fa fa-video-camera"></i>`;
linkCell.appendChild(newButton);
newButton.addEventListener("click", function (event) {
event.preventDefault();
openPreview(codeId, code.cdn);
});
} else {
return;
}
}
});
}
function detectCode(title, patterns) {
for (const [key, pattern] of Object.entries(patterns)) {
const match = title.match(pattern);
if (match) {
return {
match: match[0],
cdn: key,
};
}
}
return { match: null, cdn: null };
}
function fetchData(id, cdn, callback) {
//NOTE PER WEBSITE METHOD
// switch (cdn) {
// case 'dmm':
// GM_xmlhttpRequest({
// method: "GET",
// url: 'https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=' + id.toLowerCase(),
// onload: function (response) {
// if (response.status === 200) {
// let scriptData, scriptObj;
// let scripts = response.responseXML.scripts;
// for (let i = 0; i < scripts.length; i++) {
// const script = scripts[i];
// if (script.textContent.includes('dmm')) {
// scriptData = script.textContent;
// }
// }
// const regex = /const args = ({.*?});/s;
// const match = scriptData.match(regex);
// if (match) {
// const jsonString = match[1];
// try {
// scriptObj = JSON.parse(jsonString.replace(/\\/g, ''));
// } catch (error) {
// console.error('Error parsing JSON:', error);
// }
// callback(scriptObj.src)
// }
// } else {
// console.error("Request failed with status:", response.status);
// }
// }
// });
// break;
// case 'faleno':
// GM_xmlhttpRequest({
// method: "GET",
// url: 'https://faleno.jp/top/works/' + id.toLowerCase() + '/',
// onload: function (response) {
// if (response.status === 200) {
// const links = response.responseXML.links;
// const findPreview = (links) => {
// for (let i = 0; i < links.length; i++) {
// const link = links[i];
// if (
// link.classList.contains('pop_sample') &&
// link.href.toLowerCase().replace(/\s+/g, '').replace(/[^\w]/g, '').includes(id.toLowerCase()) &&
// link.href.endsWith('.mp4')
// ) {
// return link.href;
// }
// }
// return null;
// };
// const result = findPreview(links);
// callback(result)
// } else {
// console.error("Request failed with status:", response.status);
// }
// }
// });
// break;
// default:
// console.log("[US:DEBUG] : ", `No CDN found: ${expr}.`);
// }
//NOTE DMM ONLY METHOD
let dvdId = id;
//if (cdn === 'faleno' || cdn === 'dahlia') {
if (cdn === "plusOne") {
dvdId = "1" + dvdId;
}
if (cdn === "prestige") {
dvdId = "118" + dvdId;
}
if (cdn === "oneMore") {
dvdId = "h_1674" + dvdId;
}
if (cdn === "hotEntertainment") {
dvdId = "59" + dvdId;
}
GM_xmlhttpRequest({
method: "GET",
url:
"https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=" +
dvdId.toLowerCase(),
onload: function (response) {
if (response.status === 200) {
let scriptData, scriptObj;
let scripts = response.responseXML.scripts;
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
if (script.textContent.includes("dmm")) {
scriptData = script.textContent;
}
}
const regex = /const args = ({.*?});/s;
const match = scriptData.match(regex);
if (match) {
const jsonString = match[1];
try {
scriptObj = JSON.parse(
jsonString.replace(/\\/g, "")
);
} catch (error) {
console.error("Error parsing JSON:", error);
}
//callback(scriptObj.src);
callback(scriptObj.bitrates);
}
} else {
console.error(
"Request failed with status:",
response.status
);
}
},
});
}
function qualitySorter(items) {
const qualityTiers = {
5: { min: 1440, max: 2160 },
4: { min: 1080, max: 1439 },
3: { min: 720, max: 1079 },
2: { min: 480, max: 719 },
1: { min: 0, max: 479 },
};
const extractResolution = (bitrate) => {
const match = bitrate.match(/\((\d+)p\)/);
return match ? parseInt(match[1], 10) : null;
};
const getQualityTier = (resolution) => {
for (const [tier, { min, max }] of Object.entries(qualityTiers)) {
if (resolution >= min && resolution <= max) {
return tier;
}
}
return "Unknown";
};
const tierMap = new Map();
items.forEach((item) => {
const resolution = extractResolution(item.bitrate);
if (resolution) {
const qualityTier = getQualityTier(resolution);
if (!tierMap.has(qualityTier)) {
tierMap.set(qualityTier, {
quality: qualityTier,
bitrate: item.bitrate,
src: item.src,
});
}
}
});
return Array.from(tierMap.values());
}
function qualitySelector(desiredQuality, array) {
const sortedArray = array.slice().sort((a, b) => b.quality - a.quality);
for (const item of sortedArray) {
if (item.quality <= desiredQuality) {
return item.src;
}
}
return null;
}
function openPreview(code, cdn) {
code = code.replace(/\s+/g, "").replace(/[^\w]/g, "");
fetchData(code, cdn, (response) => {
//if (!response) return;
//Player
const video = document.createElement("video");
video.src = qualitySelector(userConfig.previewQuality, qualitySorter(response));
video.autoplay = userConfig.playerAutoplay;
video.volume = userConfig.playerVolume;
video.loop = userConfig.playerLoop;
video.controls = true;
video.classList.add("overlay-video");
video.addEventListener("error", function (event) {
console.error("Video failed to load:");
videoContainer.innerHTML = `<p style="color: white; margin: 0; text-align: center;">Preview Not Available.<br>This window will auto close in 3 seconds</p>`;
setTimeout(() => {
videoContainer.remove();
}, 3000);
});
const closeButton = document.createElement("button");
closeButton.textContent = "Close";
closeButton.classList.add("close-button");
closeButton.addEventListener("click", () => {
videoContainer.remove();
});
const videoContainer = document.createElement("div");
videoContainer.classList.add("overlay-video-container");
videoContainer.classList.add(`${code}`);
videoContainer.appendChild(video);
videoContainer.appendChild(closeButton);
document.body.appendChild(videoContainer);
if (userConfig.playerClickAnywhereToClose) {
videoContainer.addEventListener("click", (event) => {
if (event.target !== video) {
videoContainer.remove();
}
});
}
});
}
//!SECTION
// SECTION - Settings
//const footer = document.querySelector('footer');
const footerLink = document.querySelector("footer p");
const settingButton = document.createElement("a");
const buttonDivider = document.createTextNode("・");
settingButton.setAttribute("href", "?setting");
settingButton.setAttribute("title", "Settings");
settingButton.innerHTML = `<i class="fa fa-cogs"></i> Settings`;
footerLink.insertBefore(settingButton, footerLink.firstChild);
footerLink.insertBefore(buttonDivider, footerLink.firstChild.nextSibling);
settingButton.addEventListener("click", function (event) {
event.preventDefault();
openSettings();
});
function openSettings() {
const isExist = document.querySelector(".settings-container");
if (isExist) return;
const container = document.createElement("div");
container.classList.add("settings-container");
container.setAttribute("aria-label", "Settings");
// Parse config
const parsePlayerAutoplay = userConfig.playerAutoplay ? "checked" : "";
const parsePlayerLoop = userConfig.playerLoop ? "checked" : "";
const parsePlayerClickAnywhereToClose = userConfig.playerClickAnywhereToClose ? "checked" : "";
const parseDarkTheme = userConfig.darkTheme ? "checked" : "";
container.innerHTML = `
<div class="settings-item">
<!--
<label for="video-width">Video Player Width (px) <i>[Default: 320]</i></label>
<input type="text" id="video-width" name="video-width" placeholder="Enter width in px" value="${userConfig.playerWidth}">
</div>
-->
<div class="settings-item">
<label for="volume-slider" class="range-label">Volume:</label>
<input type="range" id="volume-slider" name="volume-slider" min="0" max="1" value="${userConfig.playerVolume}" step="0.01">
</div>
<div class="settings-item">
<label for="quality">Video Quality (if available):</label>
<select name="qualities" id="quality">
<option value="5">QHD (2K/1440p)</option>
<option value="4">FHD (1080p)</option>
<option value="3">HD (720p)</option>
<option value="2">Medium</option>
<option value="1">Low</option>
</select>
</div>
<div class="settings-item">
<label>
<input type="checkbox" id="autoplay" name="autoplay" ${parsePlayerAutoplay}>
Video Autoplay
</label>
</div>
<div class="settings-item">
<label>
<input type="checkbox" id="loop" name="loop" ${parsePlayerLoop}>
Video Loop
</label>
</div>
<div class="settings-item">
<label>
<input type="checkbox" id="clickanywhere" name="clickanywhere" ${parsePlayerClickAnywhereToClose}>
Click Anywhere To Close Player
</label>
</div>
<div class="settings-item last">
<label disabled>
<input type="checkbox" id="darktheme" name="darktheme" ${parseDarkTheme} disabled>
Auto Dark Theme
</label>
</div>
<div class="settings-item">
<button id="saveButton" class="button" type="button">Save Settings</button>
<button id="closeButton" class="button" type="button">Close</button>
</div>`;
// Append the container to the DOM
document.body.appendChild(container);
// Access the quality select element and set its value
const preQuality = document.getElementById("quality");
if (preQuality) {
preQuality.value = userConfig.previewQuality || 5;
}
// Add event listeners for buttons
let saveButton = document.getElementById("saveButton");
saveButton.addEventListener("click", saveConfigMenu);
let closeButton = document.getElementById("closeButton");
closeButton.addEventListener("click", () => {
const toRemove = document.querySelector(".settings-container");
if (toRemove) {
toRemove.remove();
} else {
console.log("[US:DEBUG] : ", "Bruh.");
}
});
function saveConfigMenu() {
const preQuality = document.getElementById("quality");
const newVolume = document.getElementById("volume-slider");
const autoplay = document.getElementById("autoplay");
const loop = document.getElementById("loop");
const darktheme = document.getElementById("darktheme");
const clickanywhere = document.getElementById("clickanywhere");
let finalVolume;
if (
newVolume.value !== null &&
!isNaN(newVolume.value) &&
newVolume.value >= 0 &&
newVolume.value <= 1
) {
finalVolume = parseFloat(newVolume.value);
}
const newUserConfig = {
darkTheme: darktheme.checked,
playerAutoplay: autoplay.checked,
playerVolume: finalVolume,
playerLoop: loop.checked,
playerClickAnywhereToClose: clickanywhere.checked,
previewQuality: preQuality.value,
};
GM_setValue("config", newUserConfig);
location.reload();
}
}
//!SECTION
})();