- // ==UserScript==
- // @name U-NEXT Skip Intro
- // @name:zh-CN U-NEXT 跳过片头
- // @name:ja U-NEXT イントロスキップ
- // @namespace http://tampermonkey.net/
- // @match https://*.unext.jp/*
- // @run-at document-start
- // @grant unsafeWindow
- // @version 1.2
- // @author DiruSec
- // @license MIT
- // @icon https://www.google.com/s2/favicons?sz=64&domain=unext.jp
- // @description Add missing skip intro/credit to U-NEXT player
- // @description:zh-CN 给 U-NEXT 添加跳过片头/演职人员表的功能
- // @description:ja U-NEXT に「イントロ/クレジットをスキップ」機能を追加
- // ==/UserScript==
-
- (function () {
- 'use strict';
- // define default variables
- let introObject = {
- startDuration: null,
- endDuration: null
- }
- let creditObject = {
- startDuration: null,
- endDuration: null
- }
- let moviePartsPositionList = []
- let episodeDuration = null
- let lastPlayTimeThrottle = null
- let playerPanelNode = null
- let hideSkipButtonWithPanel = false
- let moviePartsObjectInitialized = false
- let nextEpisodeObject = {
- titleCode: null,
- episodeCode: null,
- displayNo: null,
- episodeName: null,
- thumbnail: null,
- getPlayUrl() { return this.titleCode && this.episodeCode ? `https://video.unext.jp/play/${this.titleCode}/${this.episodeCode}` : null},
- getDisplayTitle() {return `${this.displayNo}\n${this.episodeName}`},
- }
-
- function initializeGlobalVar() {
- introObject = {
- startDuration: null,
- endDuration: null
- }
- creditObject = {
- startDuration: null,
- endDuration: null
- }
- moviePartsPositionList = []
- episodeDuration = null
- lastPlayTimeThrottle = null
- playerPanelNode = null
- hideSkipButtonWithPanel = false
- moviePartsObjectInitialized = false
- nextEpisodeObject = {
- titleCode: null,
- episodeCode: null,
- displayNo: null,
- episodeName: null,
- thumbnail: null,
- getPlayUrl() { return this.titleCode && this.episodeCode ? `https://video.unext.jp/play/${this.titleCode}/${this.episodeCode}` : null},
- getDisplayTitle() {return `${this.displayNo}\n${this.episodeName}`},
- }
- }
-
- function listenReactUrlChange() {
- // Save references to the original methods
- const originalPushState = history.pushState;
- const originalReplaceState = history.replaceState;
-
- // Utility function to handle URL changes
- function onUrlChange() {
- console.log('React Router URL changed:', window.location.href);
-
- // You can trigger a custom event or callback here
- const urlChangeEvent = new Event('reactRouterUrlChange');
- window.dispatchEvent(urlChangeEvent);
- }
-
- // Override pushState
- history.pushState = function(...args) {
- originalPushState.apply(this, args);
- onUrlChange(); // Trigger the function on URL change
- };
-
- // Override replaceState
- history.replaceState = function(...args) {
- originalReplaceState.apply(this, args);
- onUrlChange(); // Trigger the function on URL change
- };
- }
-
- function preProcessRequest(requestOptions) {
- // condition checks
- if (!(requestOptions?.method === 'POST' && requestOptions?.headers['content-type'] === 'application/json' && requestOptions.body)) {
- return requestOptions;
- }
-
- let requestBody;
- try {
- requestBody = JSON.parse(requestOptions.body);
- } catch (e) {
- console.error('[U-NEXT Skip Intro] invaild graphql request body found');
- return requestOptions;
- }
-
- return requestOptions
-
- }
-
- function replaceGraphql(requestBody) {
- // replaces graphql to add intro/credit parts query
- const searchString = 'commodityCode\n movieAudioList {\n audioType\n __typename\n }\n ';
- if (!requestBody.query || !requestBody.query.includes(searchString)) {
- return requestBody;
- }
-
- const replaceString = `${searchString}moviePartsPositionList {\n hasRemainingPart\n to\n from\n __typename\n }\n `;
- requestBody.query = requestBody.query.replace(searchString, replaceString);
- return requestBody
- }
-
- async function handleGetNextEpisode(response) {
- try {
- const jsonData = await response.json();
- const data = jsonData.data?.webfront_postPlay;
-
- if (!data || !data.nextEpisode) {
- console.warn('[U-NEXT Skip Intro] No next episode information found.');
- return null;
- }
-
- const { titleCode, episodeCode, displayNo, episodeName, thumbnail } = data.nextEpisode;
-
- return {
- titleCode,
- episodeCode,
- displayNo,
- episodeName,
- thumbnail: thumbnail.standard,
- };
- } catch (e) {
- console.error('[U-NEXT Skip Intro] Error parsing response:', e);
- return null;
- }
- }
-
-
- async function handleGetSkipDuration(response) {
- try {
- const jsonData = await response.json();
- const data = jsonData.data?.webfront_playlistUrl?.urlInfo && jsonData.data?.webfront_playlistUrl?.urlInfo[0];
-
- if (!data || !data.moviePartsPositionList) {
- console.warn('[U-NEXT Skip Intro] No moviePartsPositionList information found.');
- return null;
- }
-
- return data.moviePartsPositionList || [];
- } catch (e) {
- console.error('[U-NEXT Skip Intro] Error parsing response:', e);
- return [];
- }
- }
-
- function handleParseSkipDuration() {
- console.log('moviePartsPositionList', moviePartsPositionList)
- if (moviePartsPositionList.length === 0) return;
-
- // If there's only one part, compare 'from' with video duration/2
- if (moviePartsPositionList.length === 1) {
- const part = moviePartsPositionList[0];
- part.startDuration = Number(part.fromSeconds);
- part.endDuration = Number(part.endSeconds);
- part.duration = part.endDuration - part.startDuration;
-
- if (part.type === 'OPENING') {
- introObject.startDuration = part.startDuration
- introObject.endDuration = part.endDuration
- part.label = 'Intro';
- } else {
- creditObject.startDuration = part.startDuration
- creditObject.endDuration = part.endDuration
- part.label = 'Credits';
-
- part.hasRemainingPart === false && (creditObject.hasRemainingPart = false);
- }
- } else {
- // Logic for more than one part
- let introPart = moviePartsPositionList[0];
- let creditsPart = moviePartsPositionList[0];
-
- moviePartsPositionList.forEach(part => {
- part.startDuration = Number(part.fromSeconds);
- part.endDuration = Number(part.endSeconds);
- part.duration = part.endDuration - part.startDuration;
-
- // Find the earliest 'from' value for the intro
- if (part.startDuration < introPart.startDuration) {
- introPart = part;
- }
-
- // Find the latest 'to' value for the credits
- if (part.endDuration > creditsPart.endDuration) {
- creditsPart = part;
- }
- });
-
- introObject.startDuration = introPart.startDuration
- creditObject.startDuration = creditsPart.startDuration
- introObject.endDuration = introPart.endDuration
- creditObject.endDuration = creditsPart.endDuration
- creditObject.hasRemainingPart = creditsPart.hasRemainingPart
- // Assign labels
- introPart.label = 'Intro';
- creditsPart.label = 'Credits';
- }
- }
-
- // Save the original fetch function
- const originalFetch = window.fetch;
-
- // Override the fetch function
- const newFetch = async function (...args) {
- const url = args[0];
-
- // Check if the URL matches the pattern
- const regex = /^https:\/\/cc\.unext\.jp\/\?/;
- const getPlaylistUrlStr = 'operationName=cosmo_getPlaylistUrl';
- const getPostPlayStr = 'operationName=cosmo_getPostPlay';
-
- if (regex.test(url)) {
-
- //let requestOptions = args[1];
- //args[1] = preProcessRequest(requestOptions)
-
- // need to get something from response
- const response = await originalFetch(...args);
- const responseClone = response.clone()
-
- try {
- //const requestBody = JSON.parse(requestOptions.body);
-
- if (url.indexOf(getPlaylistUrlStr) !== -1) {
- let skipDuration = await handleGetSkipDuration(responseClone);
-
- moviePartsPositionList = skipDuration
- moviePartsObjectInitialized = true
- } else if (url.indexOf(getPostPlayStr) !== -1) {
- let nextEpisode = await handleGetNextEpisode(responseClone);
- nextEpisode && (
- nextEpisodeObject.titleCode = nextEpisode.titleCode,
- nextEpisodeObject.episodeCode = nextEpisode.episodeCode,
- nextEpisodeObject.displayNo = nextEpisode.displayNo,
- nextEpisodeObject.episodeName = nextEpisode.episodeName,
- nextEpisodeObject.thumbnail = nextEpisode.thumbnail.standard
- )
- }
- } catch (e) {
- console.error('[U-NEXT Skip Intro] Error handling operationName:', e);
- }
-
- // Return original Response object with no modification
- return response;
- }
-
- // If the URL doesn't match, return the original fetch call
- return originalFetch(...args);
- };
-
- Object.defineProperty(unsafeWindow, 'fetch', { value: newFetch, enumerable: false, writable: true });
-
- // Function to create a button dynamically
- function createSkipButton(text, onClick) {
- const isPanelDisplayed = window.getComputedStyle(document.querySelector('button[data-testid="player-header-back"]').parentElement.parentElement, null).getPropertyValue('opacity') === '1'
- const button = document.createElement('button');
- button.id = 'introskip-btn-skip';
- button.innerText = text;
- button.style.position = 'absolute';
- button.style.bottom = isPanelDisplayed? '9.6rem': '3rem';
- button.style.right = '2rem';
- button.style.zIndex = '1000';
- button.addEventListener('click', onClick);
- createButtonStyle();
- return button;
- }
-
- function createButtonStyle() {
- const style = document.createElement('style');
- style.innerHTML = `
- #introskip-btn-skip {
- background-color: #0F0F0FFF;
- color: #EEE;
- border: solid;
- border-color: #666;
- border-width: .1rem;
- border-radius: .2rem;
- cursor: pointer;
- padding: 1rem 2rem;
- opacity: 1;
- transition: all 0.2s ease;
- }
-
- #introskip-btn-skip:hover {
- background-color: #0F0F0F99;
- transform: scale(1.05);
- }
-
- #introskip-btn-skip.hide {
- opacity: 0;
- display: none;
- }
- `
- document.head.appendChild(style)
- }
-
- function removeButtonStyle() {
- const styleSheets = document.head.querySelectorAll('style');
-
- styleSheets.forEach(styleSheet => {
- if (styleSheet.innerHTML.includes('#introskip-btn-skip')) {
- styleSheet.remove();
- }
- });
- }
-
- function setHideSkipButtonWithPanel() {
- hideSkipButtonWithPanel = true;
- playerPanelNode = document.querySelector('button[data-testid="player-header-back"]').parentElement.parentElement;
- let isDisplayed = window.getComputedStyle(playerPanelNode, null).getPropertyValue('opacity') === '1'
- document.querySelector('#introskip-btn-skip').className = hideSkipButtonWithPanel&&!isDisplayed?'hide':''
- }
-
- // Function to add event listeners to the video
- function addSkipButtonsToVideo(video) {
- let skipIntroButton = null;
- let skipCreditsButton = null;
-
- const callback = (mutationsList, observer) => {
- mutationsList.forEach((mutationObj) => {
- if (mutationObj.attributeName === 'class') {
- // for mutationsObserver, when opacity starts change, value will be the last moment before changes.
- let isDisplayed = window.getComputedStyle(playerPanelNode, null).getPropertyValue('opacity') === '0'
- let skipBtnDom = document.querySelector('#introskip-btn-skip')
- skipBtnDom && (skipBtnDom.className = hideSkipButtonWithPanel&&!isDisplayed?'hide':'')
- skipBtnDom && (skipBtnDom.style.bottom = isDisplayed?'9.6rem':'3rem')
- }
- })
- };
-
- const observer = new MutationObserver(callback);
-
- const config = { attributes: true, childList: false, subtree: false };
-
- const skipIntroPress = event => {
- event.code === 'KeyS' && (video.currentTime = introObject.endDuration);
- }
- const skipCreditPress = event => {
- event.code === 'KeyS' && (video.currentTime = creditObject.endDuration);
- }
- const nextEpisodePress = event => {
- event.code === 'KeyS' && (window.location.href = nextEpisodeObject.getPlayUrl());
- }
- // Get the episode duration
- video.ondurationchange = function () {
- episodeDuration = video.duration;
- console.log(`Episode Duration: ${episodeDuration}`);
- };
-
- // Listen to ontimeupdate event
- video.ontimeupdate = function () {
- const currentTime = video.currentTime;
-
- // Skip Intro Button
- if (currentTime >= introObject.startDuration && currentTime <= introObject.endDuration) {
- if (introObject.endDuration - currentTime >= 5 && !lastPlayTimeThrottle) {
- lastPlayTimeThrottle = setTimeout(setHideSkipButtonWithPanel, 5000)
- }
- if (!skipIntroButton) {
- playerPanelNode = document.querySelector('button[data-testid="player-header-back"]').parentElement.parentElement;
-
- skipIntroButton = createSkipButton('SKIP INTRO', ()=> {
- video.currentTime = introObject.endDuration;
- });
- window.addEventListener('keyup', skipIntroPress)
-
- document.querySelector('#videoFullScreenWrapper').appendChild(skipIntroButton);
-
- if (playerPanelNode) {
- observer.observe(playerPanelNode, config);
- } else {
- console.error("Target node not found.");
- }
- }
- } else if (skipIntroButton) {
- try {
- document.querySelector('#videoFullScreenWrapper').removeChild(skipIntroButton);
- } catch (e) {
- console.error('[U-NEXT Skip Intro] Cannot remove skip button. Page content maybe changed?', e);
- }
- observer.disconnect()
- window.removeEventListener('keyup', skipIntroPress)
- removeButtonStyle();
- clearTimeout(lastPlayTimeThrottle);
- lastPlayTimeThrottle = null;
- skipIntroButton = null;
- }
-
- // Skip Credits or Next Episode Button
- if (currentTime >= creditObject.startDuration && currentTime <= creditObject.endDuration) {
- const timeDifference = episodeDuration - creditObject.endDuration;
- playerPanelNode = document.querySelector('button[data-testid="player-header-back"]').parentElement.parentElement;
-
- if (creditObject.endDuration - currentTime >= 5 && !lastPlayTimeThrottle) {
- lastPlayTimeThrottle = setTimeout(setHideSkipButtonWithPanel, 5000)
- }
-
- // Show "Next Episode" if the time difference is <= 10 seconds
-
- if (creditObject.hasRemainingPart === false) {
- if (!skipCreditsButton || skipCreditsButton.innerText !== 'NEXT EPISODE') {
- if (skipCreditsButton) document.querySelector('#videoFullScreenWrapper').removeChild(skipCreditsButton);
- skipCreditsButton = createSkipButton('NEXT EPISODE', () => {
- window.location.href = nextEpisodeObject.getPlayUrl();
- });
- document.querySelector('#videoFullScreenWrapper').appendChild(skipCreditsButton);
- window.addEventListener('keyup', nextEpisodePress)
-
- if (playerPanelNode) {
- observer.observe(playerPanelNode, config);
- } else {
- console.error("Target node not found.");
- }
- }
- }
- // Otherwise, show "Skip Credits"
- else {
- if (!skipCreditsButton || skipCreditsButton.innerText !== 'SKIP CREDITS') {
- if (skipCreditsButton) document.querySelector('#videoFullScreenWrapper').removeChild(skipCreditsButton);
- skipCreditsButton = createSkipButton('SKIP CREDITS', () => {
- video.currentTime = creditObject.endDuration;
- });
- document.querySelector('#videoFullScreenWrapper').appendChild(skipCreditsButton);
- window.addEventListener('keyup', skipCreditPress)
-
- if (playerPanelNode) {
- observer.observe(playerPanelNode, config);
- } else {
- console.error("Target node not found.");
- }
- }
- }
- } else if (skipCreditsButton) {
- try {
- document.querySelector('#videoFullScreenWrapper').removeChild(skipCreditsButton);
- } catch (e) {
- console.error('[U-NEXT Skip Intro] Cannot remove skip button. Page content maybe changed?', e);
- }
- observer.disconnect();
- window.removeEventListener('keyup', skipCreditPress)
- window.removeEventListener('keyup', nextEpisodePress)
- removeButtonStyle();
- clearTimeout(lastPlayTimeThrottle);
- lastPlayTimeThrottle = null;
- skipCreditsButton = null;
- }
- };
- }
-
- window.addEventListener('reactRouterUrlChange', () => {
- document.querySelector('#introskip-btn-skip')?.remove();
- removeButtonStyle();
- clearTimeout();
- initializeGlobalVar();
- setTimeout(waitForVideoElement, 1000);
- });
-
- // Function to wait until the video element is available
- function waitForVideoElement() {
- const video = document.getElementsByTagName("video")[0];
- if (video && moviePartsObjectInitialized) {
- handleParseSkipDuration()
- console.log(introObject, creditObject)
- addSkipButtonsToVideo(video);
- } else {
- // Retry after 500ms if video element is not found
- setTimeout(waitForVideoElement, 500);
- }
- }
-
- listenReactUrlChange();
- waitForVideoElement();
-
- })();