AutoPlayNextEpisode

自動撥放下一集&紀錄觀看的動畫集數

  1. // ==UserScript==
  2. // @name AutoPlayNextEpisode
  3. // @version 1.0.2
  4. // @description 自動撥放下一集&紀錄觀看的動畫集數
  5. // @author Jay.Huang
  6. // @match https://v.myself-bbs.com/player/*
  7. // @match https://myself-bbs.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=myself-bbs.com
  9. // @grant none
  10. // @license MIT
  11. // @namespace https://github.com/2jo4u4/MySelfRecorder.git
  12. // ==/UserScript==
  13. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  14. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  15. return new (P || (P = Promise))(function (resolve, reject) {
  16. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  17. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  18. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  19. step((generator = generator.apply(thisArg, _arguments || [])).next());
  20. });
  21. };
  22. const storeKeyWord = "recorder";
  23. const storeFavorite = "favorite";
  24. const favoriteIconHref = "https://cdn-icons-png.flaticon.com/512/2107/2107845.png";
  25. const favoriteAddIconHref = "https://cdn-icons-png.flaticon.com/512/2001/2001314.png";
  26. const favoriteRemoveIconHref = "https://cdn-icons-png.flaticon.com/512/2001/2001316.png";
  27. const notFoundCoverImg = "https://cdn-icons-png.flaticon.com/512/7214/7214281.png";
  28. const clearLogImg = "https://cdn-icons-png.flaticon.com/512/3602/3602056.png";
  29. const PIPmode = true
  30.  
  31. var FavoriteBtnStatus;
  32. (function (FavoriteBtnStatus) {
  33. FavoriteBtnStatus[FavoriteBtnStatus["\u672A\u52A0\u5165\u6700\u611B"] = 0] = "\u672A\u52A0\u5165\u6700\u611B";
  34. FavoriteBtnStatus[FavoriteBtnStatus["\u5DF2\u52A0\u5165\u6700\u611B"] = 1] = "\u5DF2\u52A0\u5165\u6700\u611B";
  35. })(FavoriteBtnStatus || (FavoriteBtnStatus = {}));
  36. function dalay() {
  37. return __awaiter(this, arguments, void 0, function* (timeout = 500) {
  38. return new Promise(resolve => {
  39. setTimeout(resolve, timeout);
  40. });
  41. });
  42. }
  43. class VideoPlayManager {
  44. constructor() {
  45. // https://v.myself-bbs.com/player/AgADjw0AAmr7uVQ?totalEpisode=11&0=AgADjw0AAmr7uVQ&1=AgADuQ8AAhENCFU
  46. this.url = new URL(window.location.href);
  47. this.totalEpisode = Number(this.url.searchParams.get("totalEpisode") ?? "NaN");
  48. this.currEpisode = Number(this.url.searchParams.get("currEpisode") ?? "NaN");
  49. if(!isNaN(this.totalEpisode)) {
  50. this.from = this.url.searchParams.get('from') ?? null
  51. this.addCrtlBtn();
  52. document.body.onload = () => {
  53. this.getVideoPlay().then(videoEl => {
  54. if (videoEl) {
  55. console.log(videoEl, { PIPmode, requestPictureInPicture: Boolean(videoEl.requestPictureInPicture) })
  56. if(PIPmode && videoEl.requestPictureInPicture) {
  57. videoEl.addEventListener("play", ()=>{
  58. videoEl.requestPictureInPicture().then(()=>{
  59. console.log("auto open pip mode");
  60. })
  61. })
  62. }
  63. videoEl.addEventListener("ended", () => {
  64. this.changeEpisode(true);
  65. });
  66. }
  67. });
  68. }
  69. }
  70. }
  71. changeEpisode(next = true){
  72. const targetNumber = next ? this.currEpisode + 1 : this.currEpisode - 1
  73. const videoUrl = this.url.searchParams.get(targetNumber.toString()) ?? null
  74. if(videoUrl !== null) {
  75. window.location.href = this.getNextURL(videoUrl, targetNumber);
  76. } else {
  77. if(next) {
  78. alert("沒有下一集了,將返回列表。");
  79. if(this.from) {
  80. window.location.href = this.from
  81. }
  82. }
  83. else {
  84. alert("找不到上一集。");
  85. }
  86. }
  87. }
  88. addCrtlBtn(){
  89. const prevBtn = document.createElement("div");
  90. prevBtn.onclick = () => {
  91. this.changeEpisode(false);
  92. }
  93.  
  94. const nextBtn = document.createElement("div");
  95. nextBtn.onclick = () => {
  96. this.changeEpisode(true);
  97. }
  98.  
  99. function addStyle(btn, type = "left"){
  100. btn.style.position = "fixed";
  101. btn.style.zIndex = "998";
  102. btn.style.top = "50%";
  103. btn.style[type] = "0px";
  104. btn.style.width = "24px";
  105. btn.style.height = "64px";
  106. btn.style.borderRadius = "12px";
  107. btn.style.border = "1px black solid";
  108. btn.style.backgroundColor = "white";
  109. btn.style.opacity = "0.3";
  110. btn.style.transition = "opacity 0.3s"
  111. btn.onmouseenter = () => {
  112. btn.style.opacity = "1";
  113. }
  114. btn.onmouseleave = () => {
  115. btn.style.opacity = "0.3";
  116. }
  117. }
  118.  
  119. addStyle(prevBtn, "left");
  120. addStyle(nextBtn, "right");
  121. document.body.append(prevBtn, nextBtn)
  122. }
  123. getVideoPlay() {
  124. return __awaiter(this, arguments, void 0, function* (times = 5) {
  125. let video;
  126. let retry = 0;
  127. while (!Boolean(video) && retry <= times) {
  128. video = document.querySelector("video");
  129. yield dalay(1000);
  130. retry += 1;
  131. }
  132. return video;
  133. });
  134. }
  135. getNextURL(videoUrl, episodeNumber) {
  136. const nextUrl = new URL(videoUrl)
  137. nextUrl.search = this.url.search
  138. nextUrl.searchParams.set("currEpisode", episodeNumber)
  139. return nextUrl.toString();
  140. }
  141. }
  142. class AnimeManager {
  143. constructor() {
  144. this.favoriteList = Tools.getFavorite();
  145. this.createPositionEl();
  146. this.renderFavoriteListUI();
  147. // thread-47934-1-1.html
  148. if (/^\/thread/.test(window.location.pathname)) {
  149. this.page = "episode";
  150. this.animeCode = window.location.pathname.split("-")[1];
  151. this.recordList = Tools.getRecorder();
  152. this.episodeUrls = [];
  153. this.getElement().then(main => {
  154. if (main) {
  155. this.mainEl = main;
  156. this.animeName = this.getAnimeName();
  157. this.anchorEls = Array.from(main.getElementsByClassName("various"));
  158. this.anchorEls.forEach((tagA, index) => {
  159. const url = this.getEpisodeUrlByElement(tagA);
  160. this.enhanceAnchorEl(tagA, index);
  161. if (url) {
  162. this.episodeUrls.push(url);
  163. this.addAutoBtnEachEpisode(tagA, url);
  164. }
  165. });
  166. this.rerenderAnchorElHighight("render");
  167. this.renderEpisodeUI();
  168. this.renderCtrlFavoriteUI();
  169. }
  170. else {
  171. console.warn("找不到動畫集數資訊");
  172. }
  173. });
  174. }
  175. else {
  176. this.page = "overview";
  177. }
  178. document.body.append(this.positionEl);
  179. }
  180. get currAnimeRecord() {
  181. return this.recordList[this.animeCode];
  182. }
  183. renderEpisodeUI() {
  184. const animeCode = this.animeCode;
  185. const container = document.querySelector(".fr.vodlist_index").children[0];
  186. container.style.position = "relative";
  187. // 添加清除按鈕
  188. container.appendChild(UIComponent.cleanWatchLogBtn(() => {
  189. const recorder = Tools.getRecorder();
  190. Tools.setRecorder(Object.assign(Object.assign({}, recorder), { [animeCode]: [] }));
  191. this.rerenderAnchorElHighight("clearAll");
  192. }));
  193. }
  194. // 添加觀看紀錄的高亮提示
  195. rerenderAnchorElHighight(type) {
  196. if (type === "clearAll") {
  197. this.anchorEls.forEach(el => {
  198. el.parentElement.parentElement.parentElement.style.backgroundColor = "unset";
  199. });
  200. }
  201. else if (type === "render" && this.currAnimeRecord && this.currAnimeRecord.length > 1) {
  202. const [recently, ...log] = this.currAnimeRecord;
  203. this.anchorEls[recently].parentElement.parentElement.parentElement.style.backgroundColor = "#ff000080";
  204. log.forEach(episodeIndex => {
  205. this.anchorEls[episodeIndex].parentElement.parentElement.parentElement.style.backgroundColor = "#ff000030";
  206. });
  207. }
  208. }
  209. getEpisodeUrlByElement(el) {
  210. return el.dataset.href || null;
  211. }
  212. addAutoBtnEachEpisode(el, url) {
  213. const span = document.createElement("span");
  214. span.innerText = "自動接續下集";
  215. span.style.cursor = "pointer";
  216. span.style.marginLeft = "4px";
  217. span.onclick = () => {
  218. this.gotoPlayPage(url);
  219. };
  220. el.parentElement.appendChild(span);
  221. }
  222. gotoPlayPage(targetUrl) {
  223. const totalEpisode = `totalEpisode=${this.episodeUrls.length}`;
  224. let keyValue = ""
  225. let currEpisode = 0
  226. this.episodeUrls.forEach((url, index) => {
  227. const episode = index + 1;
  228. if(targetUrl === url) {
  229. currEpisode = episode
  230. }
  231. keyValue += `&${episode}=${url}`;
  232. });
  233. const from = window.location.href;
  234. const url = `${targetUrl}?currEpisode=${currEpisode}&${totalEpisode}${keyValue}&from=${from}`;
  235. window.location.href = url;
  236. }
  237. getElement() {
  238. return __awaiter(this, arguments, void 0, function* (times = 5) {
  239. let main;
  240. let retry = 0;
  241. while (main === undefined && retry <= times) {
  242. main = document.getElementsByClassName("main_list")[0];
  243. yield dalay();
  244. retry += 1;
  245. }
  246. return main;
  247. });
  248. }
  249. enhanceAnchorEl(el, index) {
  250. const animeCode = this.animeCode;
  251. el.onclick = () => {
  252. const recorder = Tools.getRecorder();
  253. const newNumberList = Array.from(new Set(recorder[animeCode] ? [index, ...recorder[animeCode]] : [index]));
  254. Tools.setRecorder(Object.assign(Object.assign({}, recorder), { [animeCode]: newNumberList }));
  255. this.recordList = Tools.getRecorder();
  256. this.rerenderAnchorElHighight("render");
  257. };
  258. }
  259. getAnimeName() {
  260. var _a;
  261. const block = (((_a = document.querySelector("#pt .z")) === null || _a === void 0 ? void 0 : _a.lastElementChild) || null);
  262. if (block) {
  263. return block.innerText.replace(/【(\S|\s|0-9)*/, "");
  264. }
  265. else {
  266. return "";
  267. }
  268. }
  269. /** 建立主畫面定位按鈕元素 */
  270. createPositionEl() {
  271. // 定位主畫面的按鈕
  272. this.positionEl = document.createElement("div");
  273. this.positionEl.id = "positionEl";
  274. this.positionEl.style.position = "fixed";
  275. this.positionEl.style.display = "flex";
  276. this.positionEl.style.flexDirection = "row";
  277. this.positionEl.style.top = "20px";
  278. this.positionEl.style.left = "20px";
  279. }
  280. /** 我的最愛列表 */
  281. renderFavoriteListUI() {
  282. let favoritListFlag = false;
  283. // 父元素
  284. const container = document.createElement("div");
  285. container.id = "container";
  286. container.style.display = "flex";
  287. container.style.flexDirection = "row";
  288. container.style.marginRight = "4px";
  289. // 用於打開最愛列表的按鈕
  290. const favorite_btn = document.createElement("img");
  291. this.ctrlBtnStyle(favorite_btn);
  292. favorite_btn.src = favoriteIconHref;
  293. favorite_btn.title = "打開/關閉最愛列表";
  294. // 被打開的列表
  295. const favorite_list = document.createElement("div");
  296. favorite_list.style.maxHeight = "50vh";
  297. favorite_list.style.display = favoritListFlag ? "flex" : "none";
  298. favorite_list.style.flexDirection = "column";
  299. favorite_list.style.marginTop = "12px";
  300. favorite_list.style.padding = "12px";
  301. favorite_list.style.backdropFilter = "blur(20px)";
  302. favorite_list.style.borderRadius = "12px";
  303. favorite_list.style.overflow = "auto";
  304. favorite_list.style.position = "absolute";
  305. favorite_list.style.width = "max-content";
  306. favorite_list.style.top = "48px";
  307. // 用於顯示開動畫的 Cover 圖定位
  308. const coverImage = document.createElement("div");
  309. coverImage.style.marginTop = "12px";
  310. coverImage.style.position = "relative";
  311. const span = document.createElement("span");
  312. span.innerText = "暫無最愛";
  313. span.style.color = "darkorange";
  314. favorite_btn.onclick = () => {
  315. favoritListFlag = !favoritListFlag;
  316. favorite_list.style.display = favoritListFlag ? "flex" : "none";
  317. span.style.display = this.favoriteList.length === 0 ? "block" : "none";
  318. };
  319. this.favoriteList.forEach((item, index) => {
  320. const card = this.favoriteAnimeItem(coverImage, item, index !== 0 ? 12 : 0);
  321. favorite_list.append(card);
  322. });
  323. favorite_list.append(span);
  324. container.append(favorite_btn, coverImage, favorite_list);
  325. this.positionEl.append(container);
  326. }
  327. favoriteAnimeItem(coverImagePosition, v, marginTop = 0) {
  328. const { animecode, image, name, href } = v;
  329. const card = document.createElement("div");
  330. card.id = animecode;
  331. card.style.marginTop = `${marginTop}px`;
  332. card.style.maxWidth = "300px";
  333. const coverImage = document.createElement("img");
  334. coverImage.src = image;
  335. coverImage.style.position = "absolute";
  336. coverImage.style.top = "0";
  337. coverImage.style.left = "310px";
  338. coverImage.style.border = "6px solid white";
  339. coverImage.style.borderRadius = "12px";
  340. coverImage.style.boxShadow = "4px 6px 8px 6px #60606073";
  341. const link = document.createElement("a");
  342. link.href = href;
  343. link.style.color = "#fff";
  344. link.style.textShadow = "#000 0.1em 0.1em 0.2em";
  345. link.innerText = name;
  346. link.style.display = "flex";
  347. link.style.flexDirection = "column";
  348. link.onmouseenter = () => {
  349. coverImagePosition.append(coverImage);
  350. };
  351. link.onmouseleave = () => {
  352. coverImage.remove();
  353. };
  354. card.append(link);
  355. return card;
  356. }
  357. /** 加入最愛 / 移除最愛 */
  358. renderCtrlFavoriteUI() {
  359. let index = this.favoriteList.findIndex(({ animecode }) => animecode === this.animeCode);
  360. let isFavorite = index !== -1;
  361. const coverPicture = document.querySelector(".info_con .info_img_box img");
  362. const info = {
  363. name: this.animeName,
  364. image: (coverPicture === null || coverPicture === void 0 ? void 0 : coverPicture.src) || notFoundCoverImg,
  365. href: window.location.pathname,
  366. animecode: this.animeCode,
  367. };
  368. const ctrl_btn = document.createElement("img");
  369. this.ctrlBtnStyle(ctrl_btn);
  370. if (isFavorite) {
  371. ctrl_btn.src = favoriteRemoveIconHref;
  372. ctrl_btn.title = "移除最愛";
  373. }
  374. else {
  375. ctrl_btn.src = favoriteAddIconHref;
  376. ctrl_btn.title = "加入最愛";
  377. }
  378. ctrl_btn.addEventListener("click", () => {
  379. if (isFavorite) {
  380. // remove
  381. this.favoriteList.splice(index, 1);
  382. Tools.setFavorite(this.favoriteList);
  383. ctrl_btn.title = "加入最愛";
  384. ctrl_btn.src = favoriteAddIconHref;
  385. }
  386. else {
  387. // add
  388. this.favoriteList = [...this.favoriteList, info];
  389. Tools.setFavorite(this.favoriteList);
  390. ctrl_btn.title = "移除最愛";
  391. ctrl_btn.src = favoriteRemoveIconHref;
  392. }
  393. this.rerenderFavoriteListUI();
  394. isFavorite = !isFavorite;
  395. });
  396. this.positionEl.append(ctrl_btn);
  397. }
  398. rerenderFavoriteListUI() {
  399. const listContainer = this.positionEl.children[0];
  400. const ctrlBtn = this.positionEl.children[1];
  401. listContainer.remove();
  402. ctrlBtn.remove();
  403. this.renderFavoriteListUI();
  404. this.positionEl.append(ctrlBtn);
  405. }
  406. /** 主畫面顯示的按鈕樣式 */
  407. ctrlBtnStyle(el) {
  408. el.style.width = "24px";
  409. el.style.height = "24px";
  410. el.style.padding = "12px";
  411. el.style.borderRadius = "12px";
  412. el.style.backdropFilter = "blur(20px)";
  413. el.style.cursor = "pointer";
  414. el.style.border = "3px solid rgb(130, 130, 130)";
  415. }
  416. }
  417. class Tools {
  418. static setFavorite(data) {
  419. window.localStorage.setItem(storeFavorite, JSON.stringify(data));
  420. }
  421. static getFavorite() {
  422. const str = window.localStorage.getItem(storeFavorite) || "[]";
  423. return JSON.parse(str);
  424. }
  425. static setRecorder(data) {
  426. window.localStorage.setItem(storeKeyWord, JSON.stringify(data));
  427. }
  428. static getRecorder() {
  429. const str = window.localStorage.getItem(storeKeyWord) || "{}";
  430. return JSON.parse(str);
  431. }
  432. }
  433. class NoticeUIChange {
  434. }
  435. class UIComponent {
  436. static cleanWatchLogBtn(clickCB) {
  437. const clearBtn = document.createElement("img");
  438. clearBtn.src = clearLogImg;
  439. clearBtn.style.position = "absolute";
  440. clearBtn.style.top = "6px";
  441. clearBtn.style.right = "6px";
  442. clearBtn.style.cursor = "pointer";
  443. clearBtn.style.width = "24px";
  444. clearBtn.style.height = "24px";
  445. clearBtn.alt = "清除觀看紀錄";
  446. clearBtn.title = "清除觀看紀錄";
  447. clearBtn.onclick = clickCB;
  448. // clearBtn.onclick = function () {
  449. // const recorder = Tools.getRecorder();
  450. // Tools.setRecorder({ ...recorder, [animeCode]: [] });
  451. // mark([]);
  452. // };
  453. return clearBtn;
  454. }
  455. static ctrlFavoriteBtn(initType) {
  456. const btn = document.createElement("img");
  457. let isAlreadyAdd = initType === FavoriteBtnStatus.已加入最愛;
  458. const changeIcon_Remove = () => {
  459. btn.src = favoriteRemoveIconHref;
  460. btn.title = "移除最愛";
  461. };
  462. const changeIcon_Add = () => {
  463. btn.src = favoriteAddIconHref;
  464. btn.title = "加入最愛";
  465. };
  466. btn.style.marginRight = "4px";
  467. btn.style.width = "24px";
  468. btn.style.height = "24px";
  469. btn.style.padding = "12px";
  470. btn.style.borderRadius = "12px";
  471. btn.style.backdropFilter = "blur(20px)";
  472. btn.style.cursor = "pointer";
  473. btn.style.border = "3px solid rgb(130, 130, 130)";
  474. if (isAlreadyAdd) {
  475. changeIcon_Remove();
  476. }
  477. else {
  478. changeIcon_Add();
  479. }
  480. btn.addEventListener("click", () => {
  481. isAlreadyAdd = !isAlreadyAdd;
  482. if (isAlreadyAdd) {
  483. changeIcon_Remove();
  484. }
  485. else {
  486. changeIcon_Add();
  487. }
  488. });
  489. }
  490. }
  491. (function () {
  492. "use strict";
  493. const isVideoPlay = window.location.origin === "https://v.myself-bbs.com";
  494. if (isVideoPlay) {
  495. new VideoPlayManager();
  496. }
  497. else {
  498. new AnimeManager();
  499. }
  500. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址