yuntech tronclass util

一鍵觀看影片,一鍵完成每周影音教材觀看,一鍵觀看投影片

// ==UserScript==
// @name         yuntech tronclass util
// @namespace    no
// @version      0.3.4
// @description  一鍵觀看影片,一鍵完成每周影音教材觀看,一鍵觀看投影片
// @author       someone
// @match        https://eclass.yuntech.edu.tw/course/*
// @icon         https://cdn.discordapp.com/avatars/755351137577599016/5ccfb5d525fb3d304c7d61c8d51c6777.png?size=1024
// @grant        none
// @license MIT
// ==/UserScript==

;(function () {
	'use strict'
	let tglobal = {
		process: 0,
		processmax: 0,
		persent: 0,
		videoispress: false,
		courseispress: false,
		closeonfinish: false
	}

	function contains(selector, text) {
		var elements = document.querySelectorAll(selector)
		return Array.prototype.filter.call(elements, function (element) {
			return RegExp(text).test(element.textContent)
		})
	}

	function tempAlert(msg, duration) {
		var el = document.createElement('div')
		el.innerHTML = `<div class="lol_alert alert-box success radius" data-alert>
      ${msg}
    </div>
    <style>
      .lol_alert {
        position: absolute;
        top: 40%;
        left: 20%;
        z-index: 99;
      }
    </style>`
		setTimeout(function () {
			el.parentNode.removeChild(el)
		}, duration)
		document.body.appendChild(el)
	}

	function updateprocessbar() {
		try {
			let processbar = document.getElementById('watch-process')
			let persent = (tglobal.process / tglobal.processmax) * 100
			processbar.style = `width: ${persent}%`
		} catch (e) {}
	}

	function finishprocessbar() {
		try {
			let processbar = document.getElementById('watch-process-div')
			processbar.parentNode.removeChild(processbar)
		} catch (e) {}
	}

	function extractVideoId(url) {
		try {
			const parsedUrl = new URL(url)
			const videoId = parsedUrl.searchParams.get('v') || parsedUrl.pathname.split('/').pop()
			return videoId
		} catch (error) {
			console.error('獲取影片ID出錯:', error)
			return null
		}
	}

	async function get_youtube_length() {
		const iframe = document.querySelector('iframe[src*="youtube.com"]')
		if (!iframe) {
			reject('No YouTube iframe')
			return
		}
		const videoId = extractVideoId(iframe.src)
		if (!videoId) {
			reject('無法獲取影片ID')
			return
		}
		console.log('找到影片ID:', videoId)
		const apifetch = await fetch(
			'https://youtube_videotime_worker.phillychi3.workers.dev/api/video?url=' + videoId,
			{
				mode: 'no-cors'
			}
		)
		if (!apifetch.ok) {
			console.error('API錯誤:', apifetch.status)
			reject('API錯誤')
		} else {
			const apidata = await apifetch.json()
			return apidata.length
		}
	}

	async function circle_watch(fast = 1000) {
		const video = document.querySelector('video')
		//*[@id="player"]
		let max = 10000
		if (document.getElementById('player')) {
			max = await get_youtube_length()
		} else {
			max = video.duration
		}
		if (!video) {
			setTimeout(() => {
				circle_watch(fast)
			}, 1000)
			console.error('video not found')
			return
		}

		const maxrun = 60
		let lasttime = 0
		const thisvideoid = document.URL.split('/').splice(-1).toString()
		tglobal.processmax = max
		fetch(`https://eclass.yuntech.edu.tw/api/activities/${thisvideoid}`, {
			method: 'GET',
			headers: {
				'Content-Type': 'application/json'
			},
			cookie: document.cookie
		})
			.then((response) => response.json())
			.then((data) => {
				let ct = 0
				for (let i = 0; i < max; i = i + maxrun) {
					ct++
					setTimeout(() => {
						watchthevideo(i, i + maxrun, data)
						tglobal.process = i + maxrun
						updateprocessbar()
						if (max - i < maxrun) {
							console.log('last')
							watchthevideo(i, max, data)
							tglobal.process = max
							updateprocessbar()
							finishprocessbar()
							tglobal.videoispress = false
							tempAlert('aleardy watch the video', 2000)
							if (tglobal.closeonfinish) {
								window.close()
							}
						}
					}, fast * ct)
					lasttime = i + maxrun
				}
			})
	}

	function watchthevideo(start, end, videodata) {
		let student = globalData.user
		let course = globalData.course
		let dep = globalData.dept
		fetch(`https://eclass.yuntech.edu.tw/api/course/activities-read/${videodata.id}`, {
			method: 'POST',
			headers: {
				Origin: 'https://eclass.yuntech.edu.tw',
				Referer: `https://eclass.yuntech.edu.tw/course/${course.id}/learning-activity/full-screen`,
				'Content-Type': 'application/json;charset=UTF-8'
			},
			body: JSON.stringify({
				start: start,
				end: end
			}),
			cookie: document.cookie
		}).then((response) => response.json())
		fetch('https://eclass.yuntech.edu.tw/statistics/api/online-videos', {
			method: 'POST',
			headers: {
				Connection: 'keep-alive',
				Origin: 'https://eclass.yuntech.edu.tw',
				Referer: `https://eclass.yuntech.edu.tw/course/${course.id}/learning-activity/full-screen`,
				'Content-Type': 'Typetext/plain;charset=UTF-8'
			},
			cookie: document.cookie,

			body: JSON.stringify({
				user_id: student.id,
				org_id: 1,
				course_id: course.id,
				module_id: videodata.moduls_id,
				syllabus_id: videodata.syllabus_id,
				activity_id: videodata.id,
				upload_id: videodata.uploads[0].id,
				reply_id: null,
				comment_id: null,
				forum_type: '',
				action_type: 'play',
				is_teacher: false,
				is_student: true,
				ts: Date.now(),
				user_agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0',
				meeting_type: 'online_video',
				start_at: start,
				end_at: end,
				duration: end - start,
				master_course_id: 0,
				org_name: student.orgName,
				user_no: student.userNo,
				user_name: student.name,
				course_code: course.courseCode,
				course_name: course.name,
				dep_id: dep.id,
				dep_name: dep.name,
				dep_code: dep.code
			})
		}).then((response) => {
			if (response.ok) {
				console.log('success')
			} else {
				console.log(response.status)
			}
		})
	}

	function watchthefile() {
		const activity_id = document.URL.split('/').splice(-1).toString()
		fetch(`https://eclass.yuntech.edu.tw/api/activities/${activity_id}?sub_course_id=0`, {
			headers: {
				Origin: 'https://eclass.yuntech.edu.tw',
				Referer: `https://eclass.yuntech.edu.tw/course/${course.id}/learning-activity/full-screen`,
				'Content-Type': 'application/json;charset=UTF-8'
			},
			cookie: document.cookie
		})
			.then((response) => response.json())
			.then((data) => {
				tglobal.processmax = data.uploads.length
				tglobal.process = 0
				data.uploads.forEach((element) => {
					fetch(`https://eclass.yuntech.edu.tw/api/course/activities-read/${data.id}`, {
						method: 'POST',
						headers: {
							Origin: 'https://eclass.yuntech.edu.tw',
							Referer: `https://eclass.yuntech.edu.tw/course/${course.id}/learning-activity/full-screen`,
							'Content-Type': 'application/json;charset=UTF-8'
						},
						cookie: document.cookie,
						body: JSON.stringify({
							upload_id: element.reference_id
						})
					})
						// fetch(
						//   `https://eclass.yuntech.edu.tw/api/uploads/reference/document/${element.reference_id}/url?preview=true&refer_id=${data.id}}&refer_type=learning_activity`,
						//   {
						//     headers: {
						//       Origin: "https://eclass.yuntech.edu.tw",
						//       Referer: `https://eclass.yuntech.edu.tw/course/${course.id}/learning-activity/full-screen`,
						//       "Content-Type": "application/json;charset=UTF-8",
						//     },
						//     cookie: document.cookie,
						//   }
						// )
						.then((response) => response.json())
					tglobal.process = tglobal.process + 1
					updateprocessbar()
					finishprocessbar()
				})
			})
		updateprocessbar()
		finishprocessbar()
		tglobal.videoispress = false
	}

	// 影片頁按鈕
	function makevideopanel() {
		let panel = document.createElement('div')
		panel.style = 'padding: 20px;margin-top: -40px;'
		panel.innerHTML = `
    <div class="panel" id="eclassutilpanel">
          <div class="panel-heading">
            <h4>tronclass util</h4>
          </div>
          <div class="panel-body">
            <div class="panel-buttons" id="buttons">
              <button class="btn btn-default glow-button" id="btn1">Watch Video</button>
              <button class="btn btn-default glow-button" id="btn2">Watch Video Fast(useless)</button>
            </div>
          </div>
        </div>
        <style>
          .panel {
            margin-bottom: 23px;
            background: rgba(255, 255, 255, 0.9);
          }
          .panel-heading {
            padding: 0px;
          }

          .glow-button {
            position: relative;
            background: #428bca;
            color: white;
            border: none;
            box-shadow: 0 0 10px #428bca;
            animation: permanentGlow 2s ease-in-out infinite;
          }

          .glow-button:hover {
            transform: translateY(-2px);
            animation: permanentGlowHover 2s ease-in-out infinite;
            background: #3071a9;
            color: white;
          }
        </style>
    `
		document.querySelector('div.fullscreen-right').appendChild(panel)
		let btn1 = document.getElementById('btn1')
		btn1.addEventListener('click', function () {
			if (!tglobal.videoispress) {
				let processbar = document.createElement('div')
				processbar.innerHTML = `
        <div class="panel-progress" id="watch-process-div">
            <div class="progress-meter" id="watch-process" style="width: ${tglobal.persent}%"></div>
        </div>
        <style>
          .panel-progress {
            margin-top: 20px;
            padding: 6px;
            border-radius: 30px;
            background: rgba(0, 0, 0, 0.25);
            box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25),
              0 1px rgba(255, 255, 255, 0.08);
          }
          .progress-meter {
            animation: progressAnimation 6s;
            background-color: #ef416f;
            height: 10px;
            border-radius: 30px;
            transition: 0.4s ease-out;
          }
        `
				document.querySelector('div.panel-body').appendChild(processbar)
				tglobal.videoispress = true
				circle_watch()
			}
		})
		let btn2 = document.getElementById('btn2')
		btn2.addEventListener('click', function () {
			circle_watch(10)
		})
		let hash = window.location.hash.substring(1)
		let autowatch = false
		if (hash.includes('?')) {
			let hashParams = new URLSearchParams(hash.split('?')[1])
			autowatch = hashParams.get('autowatch')
		}
		if (autowatch === 'true') {
			tglobal.closeonfinish = true
			circle_watch()
		}
	}

	function makefilepanel() {
		let panel = document.createElement('div')
		panel.style = 'padding: 20px;margin-top: -40px;'
		panel.innerHTML = `
    <div class="panel" id="eclassutilpanel">
      <div class="panel-heading">
        <h4>tronclass util</h4>
      </div>
      <div class="panel-body">
        <div class="panel-buttons" id="buttons">
          <button class="btn btn-default" id="btn1">Read All Files</button>
        </div>
      </div>
    </div>
    <style>
      .panel {
        margin-bottom: 23px;
        background: rgba(255, 255, 255, 0.9);
      }
      .panel-heading {
        padding: 0px;
      }
    </style>
    `
		document.querySelector('div.fullscreen-right').appendChild(panel)
		let btn1 = document.getElementById('btn1')
		btn1.addEventListener('click', function () {
			if (!tglobal.videoispress) {
				let processbar = document.createElement('div')
				processbar.innerHTML = `
        <div class="panel-progress" id="watch-process-div">
            <div class="progress-meter" id="watch-process" style="width: ${tglobal.persent}%"></div>
        </div>
        <style>
          .panel-progress {
            margin-top: 20px;
            padding: 6px;
            border-radius: 30px;
            background: rgba(0, 0, 0, 0.25);
            box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25),
              0 1px rgba(255, 255, 255, 0.08);
          }
          .progress-meter {
            animation: progressAnimation 6s;
            background-color: #ef416f;
            height: 10px;
            border-radius: 30px;
            transition: 0.4s ease-out;
          }
        `
				document.querySelector('div.panel-body').appendChild(processbar)
				tglobal.videoispress = true
				watchthefile()
			}
		})
		let hash = window.location.hash.substring(1)
		let autowatch = false
		if (hash.includes('?')) {
			let hashParams = new URLSearchParams(hash.split('?')[1])
			autowatch = hashParams.get('autowatch')
		}
		if (autowatch === 'true') {
			tglobal.closeonfinish = true
			watchthefile()
		}
	}

	// 首頁按鈕
	function makecoursepanel() {
		let panel = document.createElement('div')
		panel.innerHTML = `
    <div class="panel panel-default">
        <div class="panel-heading">
            <h4 class="panel-title">
                <a class="collapsed" data-toggle="collapse" data-parent="#accordion" aria-expanded="false">tronclass util</a>
            </h4>
        </div>
        <div class="buttons" id="buttons">
            <button class="btn btn-default" id="btn1">watch all video(not finish</button>
            <button class="btn btn-default" id="btn2">wait...</button>
        </div>
    </div>
    <style>
        .panel {
            margin-bottom: 23px;
        }
        .panel-heading {
            padding: 0px;
        }
        .panel-title {
            padding: 10px;
        }
    `

		let target = document.querySelector('.collapse')
		target.parentNode.insertBefore(panel, target)

		let btn1 = document.getElementById('btn1')
		let btn2 = document.getElementById('btn2')
		btn1.addEventListener('click', function () {
			console.log('click1')
		})
		btn2.addEventListener('click', function () {
			console.log('click2')
		})
	}

	// 觀看這周 按鈕
	function makeweekvideopanel() {
		let syllabus = document.getElementsByClassName('syllabus-list')
		Array.from(syllabus).forEach((element) => {
			let titleElement = element.querySelector('.title.ng-binding')
			if (titleElement && titleElement.innerText == '影音教材') {
				let activities = element.parentElement.getElementsByClassName('learning-activity')

				let activityIds = Array.from(activities)
					.map((activity) => {
						let match = activity.id.match(/learning-activity-(\d+)/)
						return match ? match[1] : null
					})
					.filter((id) => id !== null)

				let button = document.createElement('button')
				button.className = 'button-green'
				button.innerText = '觀看這周'
				button.style.marginRight = '10px'

				button.addEventListener('click', (event) => {
					event.stopPropagation()
					if (tglobal.courseispress) {
						return
					}
					tglobal.courseispress = true
					let container = document.createElement('div')
					container.style.display = 'flex'
					container.style.alignItems = 'center'
					container.style.gap = '10px'
					button.parentNode.insertBefore(container, button)
					container.appendChild(button)
					let processbar = document.createElement('div')
					processbar.className = 'loader'
					processbar.style.display = 'none'
					processbar.innerHTML = `
          <style>
            .loader {
              width: 30px;
              height: 30px;
              border: 5px solid #0000;
              box-sizing: border-box;
              background:
                radial-gradient(farthest-side,#000 98%,#0000) 0    0/5px 5px,
                radial-gradient(farthest-side,#000 98%,#0000) 100% 0/5px 5px,
                radial-gradient(farthest-side,#000 98%,#0000) 100% 100%/5px 5px,
                radial-gradient(farthest-side,#000 98%,#0000) 0 100%/5px 5px,
                linear-gradient(#000 0 0) 50%/10px 10px,
                #fff;
              background-repeat: no-repeat;
              filter: blur(2px) contrast(10);
              animation: l12 0.8s infinite;
            }
            @keyframes l12 {
              100%  {background-position:100% 0,100% 100%,0 100%,0 0,center}
            }
          `
					container.appendChild(processbar)
					processbar.style.display = 'block'
					alert(
						'自動觀看即將執行 請勿觸碰頁面,第一次使用請手動同意跳出過多窗口(瀏覽器右上角會有警示,並且重新執行自動觀看),如有頁面長時間並無自動關閉請重新整理並手動按下觀看按鈕'
					)
					activityIds.forEach((id, index) => {
						setTimeout(() => {
							window.open(
								`https://eclass.yuntech.edu.tw/course/${globalData.course.id}/learning-activity/full-screen#/${id}?autowatch=true`
							)
							console.log(id)
						}, index * 6000)
					})
					setTimeout(() => {
						tglobal.courseispress = false
						processbar.style.display = 'none'
					}, activityIds.length * 6000)
				})

				titleElement.parentNode.appendChild(button)
			}
		})
	}

	var observer = new MutationObserver(resetTimer)
	var timer = setTimeout(action, 1000, observer)
	observer.observe(document, { childList: true, subtree: true })

	function resetTimer(changes, observer) {
		clearTimeout(timer)
		timer = setTimeout(action, 1000, observer)
	}

	function modifyLearningActivities() {
		const learningActivities = document.querySelectorAll('.learning-activity')

		learningActivities.forEach((activity) => {
			const activityId = activity.id.replace('learning-activity-', '')

			const clickableArea = activity.querySelector('.clickable-area')

			if (clickableArea) {
				const newClickableArea = clickableArea.cloneNode(true)
				clickableArea.parentNode.replaceChild(newClickableArea, clickableArea)

				const courseId = window.location.pathname.split('/')[2]

				newClickableArea.addEventListener('click', function (e) {
					e.preventDefault()
					e.stopPropagation()

					window.open(
						`https://eclass.yuntech.edu.tw/course/${courseId}/learning-activity#/${activityId}`,
						'_blank'
					)
				})

				newClickableArea.style.cursor = 'pointer'
			}
		})
	}

	function waitForElement(selector, text, maxAttempts = 5) {
		return new Promise((resolve) => {
			let attempts = 0

			const checkElement = () => {
				const elements = contains(selector, text)
				if (elements.length > 0) {
					resolve(true)
				} else if (attempts < maxAttempts) {
					attempts++
					setTimeout(checkElement, 100)
				} else {
					resolve(false)
				}
			}

			checkElement()
		})
	}

	async function action(observer) {
		observer.disconnect()
		if (document.URL.match(/https?:\/\/eclass.yuntech.edu.tw\/course\/[0-9]{1,6}\/content#\//)) {
			makecoursepanel()
			makeweekvideopanel()
			modifyLearningActivities()
		} else if (
			document.URL.match(
				/https?:\/\/eclass.yuntech.edu.tw\/course\/[0-9]{1,6}\/learning-activity\/full-screen/
			)
		) {
			const hasWatchRequirement = await waitForElement('span', '需累積觀看')
			if (hasWatchRequirement) {
				makevideopanel()
			}

			const hasDownloadOption = await waitForElement('span', '觀看或下載')
			if (hasDownloadOption) {
				makefilepanel()
			}
		}
	}

	console.log(
		'%c eclass Util %c https://github.com/phillychi3/loltronclass ',
		'color: white; background: #e9546b; padding:5px 0;',
		'padding:4px;border:1px solid #e9546b;'
	)
})()