2025年10月17日星期五

AboutCG任务脚本

1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址

1.购买服务器

阿里云:

服务器购买地址

https://t.aliyun.com/U/Bg6shY

若失效,可用地址

https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d

腾讯云:

https://curl.qcloud.com/wJpWmSfU

若失效,可用地址

https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console

华为云

https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=201905

2.部署教程

2024年最新青龙面板跑脚本教程(一)持续更新中

3.代码如下

(function () {  'use strict';
  const SELECTORS = {player: {      container"#players",      coverPlayBtn".pv-cover .pv-icon-btn-play",      playPauseBtn".pv-playpause",      timeCurrent".pv-time-current",      timeDuration".pv-time-duration",playBtnClass"pv-icon-btn-play",      pauseBtnClass"pv-icon-btn-pause"    },course: {      menu".cg-player-menu",      chapterSection".cg-play-chapter-section",      chapterIndex".cg-play-chapter-index",      chapterTitle".cg-play-chapter-title .cg-player-text2",      chapterDuration".cg-play-chapter-title .cg-player-text1",      lessonNode".cg-play-node",      lessonName".cg-player-text2",      lessonDuration".cg-f-fe-c span",      lessonClickable".cg-text-row1",      playingIndicator".cg-textp"    }  };  const CONFIG = {timeout: {      element15e3,      playButton1e4    },stability: {      checks3},autoPlay: {      afterEnd3e3,afterClick500,lessonChangeDelay2e3},playEnd: {      tolerance1,resetThreshold5},monitor: {      rebindInterval3e3}  };  class DOMUtils {static async waitForElement(selector, timeout = CONFIG.timeout.element) {      return new Promise((resolve) => {        let foundElement = null;        let stableCount = 0;        const checkElement = () => {          const element = document.querySelector(selector);          if (element) {            if (foundElement === element) {              stableCount++;              if (stableCount >= CONFIG.stability.checks) {                observer.disconnect();                clearTimeout(timeoutId);                resolve(element);              }            } else {              foundElement = element;              stableCount = 1;            }          } else {            foundElement = null;            stableCount = 0;          }        };        const observer = new MutationObserver(checkElement);        observer.observe(document.body, { childListtruesubtreetrue });        const timeoutId = setTimeout(() => {          observer.disconnect();          resolve(foundElement);        }, timeout);        checkElement();      });    }static isElementVisible(element) {      return !!element && document.body.contains(element) && element.style.display !== "none";    }static safeClick(element) {      if (!element || !this.isElementVisible(element)) return false;      element.click();      return true;    }static getTextContent(element) {      return element?.textContent?.trim() || "";    }static queryText(parent, selector) {      return this.getTextContent(parent.querySelector(selector));    }  }  const PREFIX = "[AboutCG]";  class Logger {static info(message, ...args) {      console.log(`${PREFIX} ${message}`, ...args);    }static success(message, ...args) {      console.log(`${PREFIX} ✅ ${message}`, ...args);    }static error(message, ...args) {      console.error(`${PREFIX} ❌ ${message}`, ...args);    }static warn(message, ...args) {      console.warn(`${PREFIX} ⚠️ ${message}`, ...args);    }static debug(message, ...args) {      console.debug(`${PREFIX} 🔍 ${message}`, ...args);    }static special(emoji, message, ...args) {      console.log(`${PREFIX} ${emoji} ${message}`, ...args);    }  }  class CourseDataProvider {    cachedCourseData = null;parse(forceRefresh = false) {      if (this.cachedCourseData && !forceRefresh) {        return this.cachedCourseData;      }      const chapters = [];      let currentLesson;      const chapterElements = document.querySelectorAll(        SELECTORS.course.chapterSection      );      chapterElements.forEach((chapterEl) => {        const chapterIndex = parseInt(          DOMUtils.queryText(chapterEl, SELECTORS.course.chapterIndex) || "0"        );        const chapterName = DOMUtils.queryText(chapterEl, SELECTORS.course.chapterTitle);        const chapterDuration = DOMUtils.queryText(chapterEl, SELECTORS.course.chapterDuration);        if (!chapterName) return;        const lessons = [];        const lessonElements = chapterEl.querySelectorAll(          SELECTORS.course.lessonNode        );        lessonElements.forEach((lessonEl, lessonIdx) => {          const lessonName = DOMUtils.queryText(lessonEl, SELECTORS.course.lessonName);          const lessonDuration = DOMUtils.queryText(lessonEl, SELECTORS.course.lessonDuration);          const playingEl = lessonEl.querySelector(SELECTORS.course.playingIndicator);          const isPlaying = DOMUtils.getTextContent(playingEl).includes("正在播放");          if (!lessonName) return;          if (isPlaying) {            currentLesson = {              chapterIndex,              lessonIndex: lessonIdx,              lessonName            };          }          lessons.push({            name: lessonName,            duration: lessonDuration,            isPlaying,            index: lessonIdx,            element: lessonEl          });        });        chapters.push({          index: chapterIndex,          name: chapterName,          duration: chapterDuration,          lessons        });      });      this.cachedCourseData = { chapters, currentLesson };      return this.cachedCourseData;    }clearCache() {      this.cachedCourseData = null;    }getCurrentPosition(courseData) {      if (!courseData.currentLessonreturn null;      const { chapterIndex, lessonIndex } = courseData.currentLesson;      const currentChapterIdx = courseData.chapters.findIndex(        (ch) => ch.index === chapterIndex      );      if (currentChapterIdx === -1return null;      const currentChapter = courseData.chapters[currentChapterIdx];      const currentLesson = currentChapter.lessons[lessonIndex];      if (!currentLesson) return null;      const result = {        current: {          chapter: currentChapter,          lesson: currentLesson,          chapterIndex: currentChapterIdx,          lessonIndex        }      };      if (lessonIndex > 0) {        result.previous = {          chapter: currentChapter,          lesson: currentChapter.lessons[lessonIndex - 1],          chapterIndex: currentChapterIdx,          lessonIndex: lessonIndex - 1        };      } else if (currentChapterIdx > 0) {        const prevChapter = courseData.chapters[currentChapterIdx - 1];        const prevLessonIdx = prevChapter.lessons.length - 1;        if (prevLessonIdx >= 0) {          result.previous = {            chapter: prevChapter,            lesson: prevChapter.lessons[prevLessonIdx],            chapterIndex: currentChapterIdx - 1,            lessonIndex: prevLessonIdx          };        }      }      if (lessonIndex < currentChapter.lessons.length - 1) {        result.next = {          chapter: currentChapter,          lesson: currentChapter.lessons[lessonIndex + 1],          chapterIndex: currentChapterIdx,          lessonIndex: lessonIndex + 1        };      } else if (currentChapterIdx < courseData.chapters.length - 1) {        const nextChapter = courseData.chapters[currentChapterIdx + 1];        if (nextChapter.lessons.length > 0) {          result.next = {            chapter: nextChapter,            lesson: nextChapter.lessons[0],            chapterIndex: currentChapterIdx + 1,            lessonIndex0          };        }      }      return result;    }getNextLesson(courseData) {      const position = this.getCurrentPosition(courseData);      return position?.next?.lesson || null;    }getPreviousLesson(courseData) {      const position = this.getCurrentPosition(courseData);      return position?.previous?.lesson || null;    }logCourseData(courseData) {      Logger.info("课程数据解析完成");      Logger.info(`章节总数: ${courseData.chapters.length}`);      courseData.chapters.forEach((chapter) => {        Logger.info(`第${chapter.index}章: ${chapter.name} (${chapter.duration})`);        Logger.info(`  - 小节数: ${chapter.lessons.length}`);        chapter.lessons.forEach((lesson, idx) => {          const playingMark = lesson.isPlaying ? "▶️ " : "";          Logger.info(`  ${idx + 1}${playingMark}${lesson.name} (${lesson.duration})`);        });      });      if (courseData.currentLesson) {        Logger.info(          `当前播放: 第${courseData.currentLesson.chapterIndex}章 - ${courseData.currentLesson.lessonName}`        );        const position = this.getCurrentPosition(courseData);        if (position) {          Logger.info("小节定位信息:");          Logger.info(            `  当前: 第${position.current.chapter.index}章 第${position.current.lessonIndex + 1}节 - ${position.current.lesson.name}`          );          if (position.previous) {            Logger.info(              `  上一节: 第${position.previous.chapter.index}章 第${position.previous.lessonIndex + 1}节 - ${position.previous.lesson.name}`            );          } else {            Logger.info("  上一节: 无(已是第一节)");          }          if (position.next) {            Logger.info(              `  下一节: 第${position.next.chapter.index}章 第${position.next.lessonIndex + 1}节 - ${position.next.lesson.name}`            );          } else {            Logger.info("  下一节: 无(已是最后一节)");          }        }      }    }  }  class CourseObserverService {    observer = null;    callbacks = {};    courseDataProvider;    currentPlayingLesson = null;    constructor(courseDataProvider) {      this.courseDataProvider = courseDataProvider;    }setCallbacks(callbacks) {      this.callbacks = { ...this.callbacks, ...callbacks };    }start() {      const courseMenu = document.querySelector(SELECTORS.course.menu);      if (!courseMenu) return;      this.currentPlayingLesson = this.findCurrentPlayingLesson(this.courseDataProvider.parse());      this.observer = new MutationObserver(() => this.handleCourseMenuChange());      this.observer.observe(courseMenu, {        childListtrue,        subtreetrue,        attributestrue,        attributeFilter: ["class"]      });    }stop() {      this.observer?.disconnect();      this.observer = null;    }handleCourseMenuChange() {      const newPlayingLesson = this.findCurrentPlayingLesson(this.courseDataProvider.parse(true));      if (newPlayingLesson && newPlayingLesson.element !== this.currentPlayingLesson?.element) {        const oldLesson = this.currentPlayingLesson;        this.currentPlayingLesson = newPlayingLesson;        this.callbacks.onLessonChange?.(newPlayingLesson, oldLesson || void 0);      }    }findCurrentPlayingLesson(courseData) {      for (const chapter of courseData.chapters) {        const playing = chapter.lessons.find((lesson) => lesson.isPlaying);        if (playing) return playing;      }      return null;    }getCurrentLesson() {      return this.currentPlayingLesson;    }  }  class PlayButtonService {async smartClick() {      const coverBtn = await DOMUtils.waitForElement(SELECTORS.player.coverPlayBtnCONFIG.timeout.element);      if (coverBtn && DOMUtils.isElementVisible(coverBtn)) {        const freshBtn = document.querySelector(SELECTORS.player.coverPlayBtn);        if (freshBtn && DOMUtils.safeClick(freshBtn)) {          Logger.success("已点击封面播放按钮");          return true;        }      }      await new Promise((resolve) => setTimeout(resolve, 1e3));      const playBtn = document.querySelector(SELECTORS.player.playPauseBtn);      if (playBtn?.classList.contains(SELECTORS.player.playBtnClass) && DOMUtils.safeClick(playBtn)) {        Logger.success("已点击控制栏播放按钮");        return true;      }      Logger.error("自动播放失败");      return false;    }isPlaying() {      const playPauseBtn = document.querySelector(        SELECTORS.player.playPauseBtn      );      return playPauseBtn?.classList.contains(SELECTORS.player.pauseBtnClass) || false;    }  }  class TimeUtils {static parseTimeToSeconds(timeStr) {      const parts = timeStr.split(":").map(Number);      if (parts.length === 3) {        return parts[0] * 3600 + parts[1] * 60 + parts[2];      } else if (parts.length === 2) {        return parts[0] * 60 + parts[1];      }      return 0;    }static formatSeconds(seconds) {      const hours = Math.floor(seconds / 3600);      const minutes = Math.floor(seconds % 3600 / 60);      const secs = Math.floor(seconds % 60);      const pad = (num) => num.toString().padStart(2"0");      if (hours > 0) {        return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;      }      return `${pad(minutes)}:${pad(secs)}`;    }static calculatePercentage(current, total) {      if (total <= 0return "0.00";      return (current / total * 100).toFixed(2);    }static isTimeClose(time1, time2, tolerance) {      return Math.abs(time1 - time2) <= tolerance;    }  }  class PlayerMonitorService {    observers = [];    callbacks = {};    lastTime = "";    hasEnded = false;    boundTimeElement = null;    checkInterval = null;setCallbacks(callbacks) {      this.callbacks = { ...this.callbacks, ...callbacks };    }start() {      this.bindObservers();      this.startContainerObserver();      this.startPeriodicCheck();    }stop() {      this.cleanup();      if (this.checkInterval) {        clearInterval(this.checkInterval);        this.checkInterval = null;      }    }rebind() {      this.boundTimeElement = null;      this.bindObservers();    }bindObservers() {      const currentTimeEl = document.querySelector(SELECTORS.player.timeCurrent);      const durationEl = document.querySelector(SELECTORS.player.timeDuration);      const playPauseBtn = document.querySelector(SELECTORS.player.playPauseBtn);      if (!currentTimeEl || !durationEl || this.boundTimeElement === currentTimeEl) {        return;      }      this.cleanup();      this.boundTimeElement = currentTimeEl;      this.hasEnded = false;      this.lastTime = "";      const timeObserver = new MutationObserver(() => this.handleTimeUpdate(currentTimeEl, durationEl));      timeObserver.observe(currentTimeEl, { childListtruecharacterDatatruesubtreetrue });      this.observers.push(timeObserver);      if (playPauseBtn) {        const btnObserver = new MutationObserver(() => this.handleStateChange(playPauseBtn));        btnObserver.observe(playPauseBtn, { attributestrueattributeFilter: ["class"] });        this.observers.push(btnObserver);      }    }handleTimeUpdate(currentTimeEl, durationEl) {      const currentTime = currentTimeEl.textContent?.trim() || "00:00";      const duration = durationEl.textContent?.trim() || "00:00";      if (currentTime === this.lastTimereturn;      this.lastTime = currentTime;      const currentSeconds = TimeUtils.parseTimeToSeconds(currentTime);      const durationSeconds = TimeUtils.parseTimeToSeconds(duration);      const percentage = parseFloat(TimeUtils.calculatePercentage(currentSeconds, durationSeconds));      Logger.info(`播放时间: ${currentTime} / ${duration} (${percentage.toFixed(2)}%)`);      this.callbacks.onTimeUpdate?.(currentTime, duration, percentage);      if (!this.hasEnded && durationSeconds > 0 && TimeUtils.isTimeClose(currentSeconds, durationSeconds, CONFIG.playEnd.tolerance)) {        this.hasEnded = true;        Logger.special("🎉""视频播放完成!");        this.callbacks.onPlayEnd?.();      }      if (this.hasEnded && currentSeconds < durationSeconds - CONFIG.playEnd.resetThreshold) {        this.hasEnded = false;      }    }handleStateChange(playPauseBtn) {      if (playPauseBtn.classList.contains(SELECTORS.player.playBtnClass)) {        Logger.info("视频已暂停");        this.callbacks.onStateChange?.("paused");      } else if (playPauseBtn.classList.contains(SELECTORS.player.pauseBtnClass)) {        this.callbacks.onStateChange?.("playing");      }    }startContainerObserver() {      const playerContainer = document.querySelector(SELECTORS.player.container);      if (!playerContainer) return;      const containerObserver = new MutationObserver(() => {        const currentTimeEl = document.querySelector(SELECTORS.player.timeCurrent);        if (currentTimeEl && this.boundTimeElement !== currentTimeEl) {          this.bindObservers();        }      });      containerObserver.observe(playerContainer, { childListtruesubtreetrue });      this.observers.push(containerObserver);    }startPeriodicCheck() {      this.checkInterval = setInterval(() => {        const currentTimeEl = document.querySelector(SELECTORS.player.timeCurrent);        if (currentTimeEl && !this.boundTimeElement) {          this.bindObservers();        }      }, CONFIG.monitor.rebindInterval);    }cleanup() {      this.observers.forEach((observer) => observer.disconnect());      this.observers = [];      this.boundTimeElement = null;    }  }  class AutoPlayController {    courseDataProvider;    playerMonitor;    courseObserver;    playButtonService;    constructor() {      this.courseDataProvider = new CourseDataProvider();      this.playerMonitor = new PlayerMonitorService();      this.courseObserver = new CourseObserverService(this.courseDataProvider);      this.playButtonService = new PlayButtonService();    }async initialize() {      Logger.info("AboutCG Auto Video 已加载");      if (await DOMUtils.waitForElement(SELECTORS.course.menu)) {        const courseData = this.courseDataProvider.parse();        this.courseDataProvider.logCourseData(courseData);      }      if (!await DOMUtils.waitForElement(SELECTORS.player.timeCurrent)) {        Logger.error("播放器控件加载超时");        return;      }      this.playerMonitor.setCallbacks({ onPlayEnd() => this.handlePlayEnd() });      this.courseObserver.setCallbacks({ onLessonChange(newLesson, oldLesson) => this.handleLessonChange(newLesson, oldLesson) });      this.courseObserver.start();      if (!await DOMUtils.waitForElement(`${SELECTORS.player.coverPlayBtn}${SELECTORS.player.playPauseBtn}`CONFIG.timeout.playButton)) {        Logger.error("播放按钮加载超时");        return;      }      setTimeout(() => this.startAutoPlay(), CONFIG.autoPlay.afterClick);    }async startAutoPlay() {      this.playerMonitor.start();      if (!this.playButtonService.isPlaying()) {        await this.playButtonService.smartClick();      }    }handlePlayEnd() {      setTimeout(() => this.playNextLesson(), CONFIG.autoPlay.afterEnd);    }async handleLessonChange(newLesson, oldLesson) {      Logger.info(`小节切换: ${oldLesson?.name || "无"} -> ${newLesson.name}`);      await new Promise((resolve) => setTimeout(resolve, CONFIG.autoPlay.lessonChangeDelay));      this.playerMonitor.rebind();      await this.playButtonService.smartClick();    }playNextLesson() {      const position = this.courseDataProvider.getCurrentPosition(this.courseDataProvider.parse());      if (!position?.next) {        Logger.info("所有课程已播放完毕");        return;      }      const { chapter, lessonIndex, lesson } = position.next;      Logger.info(`播放下一节: 第${chapter.index}章 第${lessonIndex + 1}节 - ${lesson.name}`);      const nameElement = lesson.element.querySelector(SELECTORS.course.lessonClickable);      if (nameElement && DOMUtils.safeClick(nameElement)) {        Logger.success(`已点击小节: ${lesson.name}`);      } else {        Logger.error("点击小节失败");      }    }  }  const controller = new AutoPlayController();  async function bootstrap() {    try {      await controller.initialize();    } catch (error) {      Logger.error("插件初始化失败", error);    }  }  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", bootstrap);  } else {    bootstrap();  }
})();
解析

该脚本作用

  • 在 AboutCG 播放页(/play*自动播放视频,并在当前小节播放完后自动切到下一小节继续播放。

  • 监听课程目录变化与播放器状态,稳健地重绑定监听器,避免因 DOM 变动导致失效。

  • 输出清晰的日志,便于观察"当前/下一节"等播放状态。

关键设计与流程

  1. 选择器与配置

    • SELECTORS:集中定义播放器控件与课程目录的 CSS 选择器(播放/暂停按钮、时间、章节/小节节点等)。

    • CONFIG:集中管理时间阈值与容错策略(元素等待超时、重绑定间隔、播放结束容差、课节切换延迟等)。

  2. 启动流程

    • 等待目录与播放器控件出现;

    • 解析并打印课程结构;

    • 注册两个回调:播放结束课节切换

    • 尝试点击封面播放/控制栏按钮开始播放(smartClick())。

    • bootstrap():在 DOMContentLoaded 后启动。

    • AutoPlayController.initialize()

  3. 播放与切课

    • 发现"正在播放"标记从一个小节转移到另一个小节 → 触发 onLessonChange 回调;

    • 回调中延迟重绑定监听再次点击播放

    • 当 current ≈ duration(容差1秒) → 认定播放结束 → 延迟后调用 playNextLesson()

    • PlayerMonitorService 监听当前时间/总时长与按钮状态:

    • CourseObserverService 监听目录 DOM 变化:

主要方法

DOMUtils(DOM 工具)

  • waitForElement(selector, timeout)稳态等待元素出现(需要连续检测到同一个节点 CONFIG.stability.checks 次),并带总超时。

  • isElementVisible(element):判定可见。

  • safeClick(element):"可见才点"的安全点击。

  • getTextContent()/queryText():获取去空白文本。

Logger(日志)

  • info/success/error/warn/debug/special():带统一前缀与表情的日志输出。

CourseDataProvider(课程数据解析)

  • parse(forceRefresh):扫描目录,产出结构体:{  chapters: [    { index, name, duration, lessons: [      { name, duration, isPlaying, index, element } ...    ] }  ],  currentLesson: { chapterIndex, lessonIndex, lessonName } // 若能识别}

  • getCurrentPosition(courseData):基于当前小节定位前/当前/后位置,方便切换。

  • getNextLesson()/getPreviousLesson():便捷获取下一/上一节。

  • logCourseData():打印章节与小节明细及当前定位。

CourseObserverService(目录观察)

  • start()/stop():以 MutationObserver 监听目录区域(class/节点变动)。

  • handleCourseMenuChange():若"正在播放"小节发生变化 → 调 callbacks.onLessonChange(new, old)

  • findCurrentPlayingLesson():在解析数据中找带"正在播放"标识的小节。

PlayButtonService(播放点击策略)

  • smartClick()

    1. 优先点封面大播放按钮

    2. 再点控制栏播放按钮(仅当按钮是"播放"态类名)。

  • isPlaying():判断控制键是否处于"暂停按钮"态(即正在播放)。

TimeUtils(时间工具)

  • parseTimeToSeconds("mm:ss" | "hh:mm:ss"):文本转秒。

  • formatSeconds(sec):秒转标准时间字符串。

  • calculatePercentage(cur, total):进度百分比(字符串保留两位)。

  • isTimeClose(a,b,tolerance):容差判断。

PlayerMonitorService(播放器观察)

  • start()/stop():绑定/解绑所有观察器,并开启周期性重绑定检查

  • bindObservers()

    • 监听 当前时间节点 的文本变化(时间推进);

    • 监听 播放键 class 变化(播放/暂停切换)。

  • handleTimeUpdate()

    • 记录并打印 current / duration 与百分比;

    • 若接近结束(容差 CONFIG.playEnd.tolerance 秒) → 触发 onPlayEnd()

    • 若之后时间被重置回头(小于 duration - resetThreshold)→ 清除"已结束"标记。

  • handleStateChange():输出暂停/播放状态变化。

  • startContainerObserver():若播放器内部重新渲染导致时间节点替换 → 触发 bindObservers()

  • startPeriodicCheck():每隔 CONFIG.monitor.rebindInterval ms 检测是否需要重绑定。

  • rebind()/cleanup():重置并重新挂钩观察器。

AutoPlayController(总控)

  • initialize():如上启动逻辑。

  • startAutoPlay():启动 PlayerMonitorService,必要时 smartClick()

  • handlePlayEnd():延迟后 playNextLesson()

  • handleLessonChange(new, old):延迟、重绑、smartClick()

  • playNextLesson():用 CourseDataProvider.getCurrentPosition() 找 下一节 并点击其可点击元素(lessonClickable),失败则报错。

稳健细节

  • 稳态元素检测waitForElement 要求同一元素出现多次才认定"稳定",减少误判。

  • 多重监听:时间文本、播放键 class、播放器容器与目录区 DOM 全量覆盖,DOM 重绘也能自愈

  • 容差与阈值:播放结束容差 1s、重置阈值 5s、切课/点击延时参数可统一调参。

  • 日志详尽,便于排错与观测行为。


注意

本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。


历史脚本txt文件获取>>
服务器搭建,人工服务咨询>>

没有评论:

发表评论

2025曼谷AWA大会折扣链接来啦!

大家好呀,行业最佳展会Affiliate World Asia Conference(简称AWA)在曼谷的大会 大家好呀,行业最佳展会Affiliate World Asia Conference(简称AWA)在曼谷的大会又即将到来了,本次大会举办时间为 2025年12月3号...