Fuck Chaoxing

解除超星自动暂停播放的限制并添加自动播放下一集的功能

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name			Fuck Chaoxing
// @namespace		xuyiming.open@outlook.com
// @description		解除超星自动暂停播放的限制并添加自动播放下一集的功能
// @author			依然独特
// @version			1.2.4
// @grant			none
// @run-at			document-start
// @require			https://greasyfork.org/scripts/18715-hooks/code/Hooks.js?version=661566
// @require			https://greasyfork.org/scripts/29782-docsready/code/docsReady.js?version=603417
// @include			*://*.chaoxing.com/mycourse/studentstudy*
// @include			*://*.chaoxing.com/ananas/modules/video/index.html*
// @match			*://*.chaoxing.com/mycourse/studentstudy*
// @match			*://*.chaoxing.com/ananas/modules/video/index.html*
// @include			*://*.chaoxing.com/ananas/modules/work/index.html
// @include			*://*.chaoxing.com/ananas/modules/work/index.html*
// @match			*://*.chaoxing.com/ananas/modules/work/index.html
// @match			*://*.chaoxing.com/ananas/modules/work/index.html*
// @license			BSD 2-Clause
// @homepageURL		https://gist.github.com/xymopen/eb65046644ff5cb7c0668e5d4f9607d1
// ==/UserScript==

// TODO: CX update with an HTML5 Player. Need some time digging with it
// NOTE: I no longer have any lesson on CX. development may delayed.

( function () {
	"use strict";

	// So, let's first clarify the structure of Chaoxing Student Study Page

	// A course is made up of mulitple chapters
	// A chapter is made up of mulitple cards, saying a multi-media card and a unit test card
	// A card is made up of multiple jobs, saying two video jobs and a ppt job

	// When a video job is finished, finishJob() would be called on MoocPlayer,
	// which calls proxy_completed(),
	// whick calls ed_complete(),
	// which calls JC.completed on card iframe,
	// which emits an completed event,
	// which trigger unlock(),
	// which calls onReadComplete() on the top window

	// When a ppt job is loaded, uParse() would be called
	// which calls unlock()

	// onReadComplete() then calls onReadComplete1() to pull updated chapter list from the server
	// and calls jobflag() to figure out how many jobs remaining to finish

	// jobflag() looks in card iframe for .ans-job-icon as total jobs and .ans-job-finished as unfinished ones.
	// ppt jobs doesn't count for they don't have .ans-job-icon or .ans-job-finished

	// Generally speaking we only need to handle video jobs
	// However Chrome blocks Flash. :facepalm:

	/**
	 * @param {(config: any, createCXPlayer: Function) => any} onPlayerInit
	 * @param {Window} [contextWindow]
	 */
	function hookCXPlayer ( onPlayerInit, contextWindow ) {
		if ( undefined === contextWindow ) {
			contextWindow = window;
		}

		// CXPlayer and pauseMovie() loaded as jQuery plug-ins
		// so hook jQuery to access it.
		Hooks.set( contextWindow, "jQuery", function ( target, propertyName, ignored, jQuery ) {
			Hooks.method( jQuery.fn, "cxplayer", function ( target, methodName, method, thisArg, args ) {
				var replyArgs = arguments, $globalPlayer, $player,
					globalConfig = args[ 0 ];

				function createCXPlayer ( config ) {
					if ( undefined !== config ) {
						globalConfig = config;
						args[ 0 ] = config;
					}

					$globalPlayer = Hooks.Reply.method( replyArgs );

					return $globalPlayer;
				}

				$player = onPlayerInit( globalConfig, createCXPlayer );

				if ( undefined !== $player ) {
					$globalPlayer = $player;
				}

				return $globalPlayer;
			} );

			return Hooks.Reply.set( arguments );
		} );
	};

	var globalVideoJs;

	/**
	 * @param {Window} [contextWindow]
	 * @see {@link [videojs-ext.min.js](https://mooc1-2.chaoxing.com/ananas/videojs-ext/videojs-ext.min.js)}
	 */
	function videoJsStudyUncontrolAndTimelineNull ( contextWindow ) {
		if ( undefined === contextWindow ) {
			contextWindow = window;
		}

		Hooks.set( contextWindow, "videojs", function ( target, propertyName, ignored, videojs ) {
			globalVideoJs = videojs;

			Hooks.method( videojs, "registerPlugin", function ( target, methodName, method, thisArg, args ) {
				if ( "studyControl" === args[ 0 ] ) {
					method.call( thisArg, "studyControl", function () { } );
					return args[ 1 ]
				} else if ( "timelineObjects" === args[ 0 ] ) {
					method.call( thisArg, "timelineObjects", function () { } );
					return args[ 1 ]
				} else {
					return Hooks.Reply.method( arguments );
				}
			} );

			return Hooks.Reply.set( arguments );
		} );
	}

	/**
	 * @param {(config: any, createPlayer: Function) => any} onPlayerInit
	 * @param {Window} [contextWindow]
	 */
	function hookVideojs ( onPlayerInit, contextWindow ) {
		if ( undefined === contextWindow ) {
			contextWindow = window;
		}

		Hooks.set( contextWindow, "ans", function ( target, propertyName, ignored, ans ) {
			Hooks.method( ans, "VideoJs", function ( target, methodName, method, thisArg, args ) {
				var replyArgs = arguments, $globalPlayer, $player,
					globalConfig = args[ 0 ].params;

				function createPlayer ( config ) {
					var player;

					if ( undefined !== config ) {
						globalConfig = config;
						args[ 0 ].params = config;
					}

					// CX didn't return player instance to us
					// nail it
					Hooks.Reply.method( replyArgs );

					return globalVideoJs( args[ 0 ].videojs );
				}

				$player = onPlayerInit( globalConfig, createPlayer );

				if ( undefined !== $player ) {
					$globalPlayer = $player;
				}

				return $globalPlayer;
			} );

			return Hooks.Reply.set( arguments );
		} );
	};

	/**
	 * @param {NodeList} list
	 * @returns {number}
	 */
	function findCurIdx ( list ) {
		return Array.prototype.findIndex.call( list, function ( chapter ) {
			return chapter.classList.contains( "currents" );
		} );
	};

	function canNextCard () {
		var contextDocument = window.top.document.querySelector( "iframe" ).contentDocument;

		return Array.prototype.filter.call( contextDocument.querySelectorAll( ".ans-job-icon" ), function ( jobContainer ) {
			return !jobContainer.parentNode.classList.contains( "ans-job-finished" );
		} ).length === 0;
	}

	function nextCard () {
		var cards, nextSectionIndex;

		cards = document.querySelectorAll( "#mainid .tabtags span" );
		nextSectionIndex = findCurIdx( cards ) + 1;

		if ( nextSectionIndex < cards.length ) {
			cards[ nextSectionIndex ].click();

			return true;
		} else {
			return false;
		}
	}

	function nextChapter () {
		var document = window.top.document,
			chapters = document.querySelectorAll(
				"#coursetree .ncells h1," +
				"#coursetree .ncells h2," +
				"#coursetree .ncells h3," +
				"#coursetree .ncells h4," +
				"#coursetree .ncells h5," +
				"#coursetree .ncells h6"
			),
			curChapterIdx = findCurIdx( chapters ),
			nextChapter = Array.prototype.slice.call( chapters, curChapterIdx + 1 ).find( function ( chapter ) {
				// finished chapters are classified as blue
				// and locked chapters are classified as lock
				return !chapter.querySelector( ".blue" ) && !chapter.querySelector( ".lock" );
			} );


		// Go to the first unfinished and unlocked chapter
		if ( nextChapter ) {
			nextChapter.click();

			return true;
		} else {
			// or wait for next call when one locked chapter may be unlocked
			return false;
		}
	}

	if ( "/ananas/modules/video/index.html" === window.location.pathname ) {
		// Video Job iframe
		hookCXPlayer( function ( config, createCXPlayer ) {
			var $player;

			// https://mooc1-1.chaoxing.com/ananas/modules/video/cxplayer/moocplayer_4.0.11.js
			config.datas.enableFastForward = true;
			config.datas.enableSwitchWindow = 1;
			config.datas.errorBackTime = false;
			config.datas.isAutoPlayNext = true;
			config.datas.isDefaultPlay = true;
			config.datas.pauseAdvertList = [];
			config.datas.preAdvertList = [];

			// if ( config.events &&
			// 	config.events.onAnswerRight &&
			// 	!config.events.onAnswerRight.toString()
			// 		.replace( /(function .*?\(.*?\))/g, "" ).trim()		// remove function signifigure
			// 		.replace( /^\{|\}$/g, "" )
			// 		.replace( /\/\/.*(\r|\n|(\r\n))/g, "" )				// remove single line comment
			// 		.replace( /\/\*.*\*\//mg, "" )						// remove multiple line comment
			// 		.match( /^\s*$/ )
			// ) {
			// 	window.alert( "onAnswerRight() is not empty. It's unsafe to block the resource URL." );
			// }

			$player = createCXPlayer();

			// Remove native `onPause` listener
			// prevent pause the movie from JS side
			$player.unbind( "onPause" );

			// Unpausable playback
			// TODO: find better way handling multiple players playing at the same time
			$player.pauseMovie = function () { };
			$player.bind( "onPause", function () {
				$player.playMovie();
			} );

			$player.bind( "onError", function () {
				if ( 4 === $player.getPlayState() ) {
					window.location.reload();
				}
			} );

			window.MoocPlayer.prototype.switchWindow = function () { return this; };
			window.jQuery.fn.pauseMovie = function () { };

			// Object.keys( config.events ).forEach( e => $player.bind( e, () => {
			// 	const id = $player.find( 'object[type="application/x-shockwave-flash"]' ).attr( 'id' );
			// 	const state = [ "error", "playing", "paused", "hanging", "stop" ][ $player.getPlayState() ];

			// 	console.log( `[Fuck Chaoxing]${ e } is triggered. Player#${ id } is ${ state }.` );
			// } ) );
		} );
		videoJsStudyUncontrolAndTimelineNull();
		hookVideojs( function ( config, createPlayer ) {
			var $player;

			config.enableFastForward = 1;
			config.enableSwitchWindow = 1;

			$player = createPlayer();

			$player.on( "ready", function () {
				// immediate play video may cause DOMException
				setTimeout( function () {
					$player.play();
				}, 5000 );
			} );
		} );
	} else if ( "/mycourse/studentstudy" === window.location.pathname ) {
		// Card iframe
		domReady().then( function () {

			var hasNextCard = true,
				jobflagApplied = false,
				ajaxesPending = 0;

			function onReadComplete () {
				if ( jobflagApplied && ajaxesPending && !hasNextCard ) {
					nextChapter();
					jobflagApplied = false;
				}
			};

			window.jQuery( document ).ajaxComplete( function () {
				ajaxesPending -= 1;

				if ( ajaxesPending === 0 ) {
					onReadComplete();
				}
			} );

			Hooks.method( window.jQuery, "ajax", function ( target, methodName, method, thisArg, args ) {
				ajaxesPending += 1;

				return Hooks.Reply.method( arguments );
			} );

			Hooks.method( window, "onReadComplete1", function ( target, methodName, method, thisArg, args ) {
				var returns = Hooks.Reply.method( arguments );

				onReadComplete();

				return returns;
			} );

			Hooks.method( window, "jobflag", function ( target, methodName, method, thisArg, args ) {
				if ( canNextCard() ) {
					hasNextCard = nextCard();
				}

				jobflagApplied = true;
				onReadComplete();

				return Hooks.Reply.method( arguments );
			} );
		} );
	}
} )();