BiliBackToBeginning (v0.2.0 - 修复切换)

打开或切换视频时,通过监听 video 元素的 loadstart 事件,精确地回到视频开头处

目前为 2025-04-23 提交的版本。查看 最新版本

// ==UserScript==
// @name         BiliBackToBeginning (v0.2.0 - 修复切换)
// @namespace    https://github.com/ImQQiaoO/BiliBackToBeginning
// @version      v0.2.0
// @description  打开或切换视频时,通过监听 video 元素的 loadstart 事件,精确地回到视频开头处
// @author       ImQQiaoO
// @match        *://*.bilibili.com/video/*
// @match        *://*.bilibili.com/list/*
// @match        *://*.bilibili.com/watchlater/*
// @match        *://*.bilibili.com/medialist/play/*
// @match        *://*.bilibili.com/bangumi/play/*
// @exclude      *://message.bilibili.com/*
// @exclude      *://data.bilibili.com/*
// @exclude      *://cm.bilibili.com/*
// @exclude      *://link.bilibili.com/*
// @exclude      *://passport.bilibili.com/*
// @exclude      *://api.bilibili.com/*
// @exclude      *://api.*.bilibili.com/*
// @exclude      *://*.chat.bilibili.com/*
// @exclude      *://member.bilibili.com/*
// @exclude      *://www.bilibili.com/tensou/*
// @exclude      *://www.bilibili.com/correspond/*
// @exclude      *://live.bilibili.com/* // 排除所有直播页面
// @exclude      *://www.bilibili.com/blackboard/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // --- 设置和获取 localStorage (保持不变) ---
  const STORAGE_KEY = "reset_bili_video_enabled";
  // 在setEnabled函数中增加更详细的日志
  function setEnabled(flag) {
    const previousValue = localStorage.getItem(STORAGE_KEY);
    localStorage.setItem(STORAGE_KEY, flag ? "1" : "0");
    console.log(
      `[B站重置进度脚本] 设置已 ${
        flag ? "启用" : "禁用"
      } (之前的值: ${previousValue})`
    );
  }

  // 在getEnabled函数中增加日志
  function getEnabled() {
    const storedValue = localStorage.getItem(STORAGE_KEY);
    const isEnabled = storedValue === null || storedValue === "1";
    console.log(
      `[B站重置进度脚本] 获取启用状态: ${isEnabled} (存储值: ${storedValue})`
    );
    return isEnabled;
  }

  // --- 创建设置面板 (保持不变) ---
  function createSettingsPanel() {
    // ... (面板创建和事件处理代码不变,此处省略) ...
    // (请确保你使用的是之前我们完善过的面板代码,包括检查 head/body 是否存在)
    const style = `
              #biliResetPanel {
                  position: fixed; bottom: 30px; right: 30px;
                  z-index: 99999; background: #fff; color: #333;
                  border: 1px solid #bbb; border-radius: 8px;
                  box-shadow: 0 6px 16px rgba(0,0,0,.1);
                  padding: 18px 26px 18px 18px; font-size: 16px;
                  display: none;
              }
              #biliResetPanel input[type=checkbox] { transform: scale(1.3); margin-right:8px; vertical-align: middle;}
              #biliResetPanelClose { cursor:pointer;color: #f66; float:right; font-size: 18px; line-height: 1;}
              #biliResetPanelBtn {
                  position: fixed; bottom: 30px; right: 30px;
                  z-index: 99998; background: #ffe2a0; color: #333;
                  border: 1px solid #bbb; border-radius: 50%;
                  width: 42px; height: 42px; text-align:center; line-height: 42px;
                  font-size: 24px; cursor: pointer; box-shadow: 0 3px 12px rgba(0,0,0,.08);
                  user-select: none; /* 防止意外选中文本 */
              }
          `;
    // 确保 head 存在
    let head = document.head;
    if (!head) {
      // 降级处理:如果 head 不存在,尝试创建或等待
      head = document.getElementsByTagName("head")[0];
      if (!head) {
        head = document.createElement("head");
        document.documentElement.insertBefore(head, document.body); // 尝试插入
      }
    }
    const styleEl = document.createElement("style");
    styleEl.textContent = style;
    if (head) {
      head.appendChild(styleEl);
    } else {
      // 如果 head 实在没有,作为最后手段延迟添加
      document.addEventListener("DOMContentLoaded", () =>
        document.head.appendChild(styleEl)
      );
      console.warn(
        "[B站重置进度脚本] Head element not found immediately, delaying style injection."
      );
    }

    // 确保 body 存在再添加面板/按钮
    const addPanelElements = () => {
      if (!document.body) {
        console.log(
          "[B站重置进度脚本] Body not ready, delaying panel creation..."
        );
        setTimeout(addPanelElements, 100); // 稍后重试
        return;
      }

      // 面板内容
      const panel = document.createElement("div");
      panel.id = "biliResetPanel";
      panel.innerHTML = `
                  <span id="biliResetPanelClose" title="关闭设置面板">&times;</span>
                  <label>
                      <input type="checkbox" id="biliResetSwitch">
                      启用自动重置进度到0秒
                  </label>
              `;
      document.body.appendChild(panel);

      // 显示/隐藏按钮
      const btn = document.createElement("div");
      btn.id = "biliResetPanelBtn";
      btn.title = "打开【重置到0秒】设置";
      btn.textContent = "↩₀";
      document.body.appendChild(btn);

      // 获取元素(在添加到 DOM 后)
      const switchCheckbox = document.getElementById("biliResetSwitch");
      if (switchCheckbox) {
        // 确保正确读取 localStorage 的值并设置复选框状态
        const isEnabled = getEnabled();
        switchCheckbox.checked = isEnabled;
        // 确保复选框的变化能正确保存到 localStorage
        switchCheckbox.onchange = (e) => {
          setEnabled(e.target.checked);
        };
        console.log(
          `[B站重置进度脚本] 复选框初始化完成,当前状态: ${
            isEnabled ? "已启用" : "已禁用"
          }`
        );
      } else {
        console.error(
          "[B站重置进度脚本] Checkbox 'biliResetSwitch' not found after creation."
        );
      }
      const closeButton = document.getElementById("biliResetPanelClose");

      // 绑定事件
      if (btn) {
        btn.onclick = (e) => {
          e.stopPropagation();
          panel.style.display =
            panel.style.display === "block" ? "none" : "block";
        };
      }
      if (closeButton) {
        closeButton.onclick = (e) => {
          e.stopPropagation();
          panel.style.display = "none";
        };
      }
      document.addEventListener("click", (e) => {
        if (
          panel &&
          btn && // 确保元素存在
          panel.style.display === "block" &&
          !panel.contains(e.target) &&
          !btn.contains(e.target)
        ) {
          panel.style.display = "none";
        }
      });

      if (switchCheckbox) {
        const isEnabled = getEnabled();
        switchCheckbox.checked = isEnabled;
        console.log(
          `[B站重置进度脚本] 初始化复选框状态: ${
            isEnabled ? "已启用" : "已禁用"
          }`
        );
      } else {
        console.error(
          "[B站重置进度脚本] Checkbox 'biliResetSwitch' not found after creation."
        );
      }
      console.log(
        "[B站重置进度脚本] Settings panel created and events attached."
      );
    };
    addPanelElements();
  }

  // --- 核心功能:为视频元素添加重置逻辑 (修改版) ---

  // 使用 WeakSet 来跟踪已经处理过的 video 元素,防止重复添加监听器,
  // 并且当 video 元素被 GC 时自动移除引用,避免内存泄漏。
  const processedVideoElements = new WeakSet();

  function setupVideoReset(videoElement) {
    // 如果元素无效或已处理过,则直接返回
    if (!videoElement || processedVideoElements.has(videoElement)) {
      return;
    }

    console.log(
      "[B站重置进度脚本] 开始为 video 元素设置重置监听器:",
      videoElement
    );
    processedVideoElements.add(videoElement); // 标记为已处理

    // 定义在元数据加载后执行的重置操作
    const onLoadedMetadata = () => {
      // 每次触发时都重新检查功能是否启用
      if (getEnabled()) {
        if (videoElement.currentTime > 0) {
          console.log(
            `[B站重置进度脚本] 'loadedmetadata' 触发 (当前时间: ${videoElement.currentTime}), 准备重置到 0 秒.`
          );
          videoElement.currentTime = 0;
          console.log("[B站重置进度脚本] 视频进度已重置到 0 秒.");
        }
      }
    };

    // 定义在每次视频开始加载时执行的操作
    const onLoadStart = () => {
      console.log(
        "[B站重置进度脚本] 'loadstart' 事件触发, 为本次加载添加一次性的 'loadedmetadata' 监听器."
      );
      // 移除上一次可能遗留的监听器 (以防万一)
      videoElement.removeEventListener("loadedmetadata", onLoadedMetadata);
      // 添加一次性的 'loadedmetadata' 监听器
      videoElement.addEventListener("loadedmetadata", onLoadedMetadata, {
        once: true,
      });
    };

    // 为 video 元素 **持续地** 监听 'loadstart' 事件
    videoElement.addEventListener("loadstart", onLoadStart);

    console.log(
      "[B站重置进度脚本] 已为 video 元素添加持续的 'loadstart' 监听器."
    );

    // --- 处理初始状态 ---
    // 有时候脚本运行时,视频可能已经开始加载甚至元数据已加载完毕
    // readyState: 0=HAVE_NOTHING, 1=HAVE_METADATA, 2=HAVE_CURRENT_DATA, ...
    // 如果 readyState >= 1,说明至少元数据有了,或者正在加载。
    // 并且 currentSrc 有值,说明确实有一个有效的视频源。
    if (videoElement.readyState >= 1 && videoElement.currentSrc) {
      console.log(
        "[B站重置进度脚本] 检测到脚本运行时 video 已有数据或在加载中 (readyState:",
        videoElement.readyState,
        "), 尝试立即触发一次重置检查."
      );
      // 直接调用一次 onLoadedMetadata,检查当前是否需要重置
      // 注意:这里不再手动调用 onLoadStart,因为如果 loadstart 事件还没触发,我们直接处理;如果已经触发了,onLoadStart 里的逻辑会处理。
      // 我们关心的是元数据加载完的那个时间点。
      onLoadedMetadata();
      // 如果此时元数据已经加载完毕,但 'loadedmetadata' 事件可能已经错过了,
      // 再次添加监听器以防万一 (如果上面 onLoadedMetadata 没重置,这里可以确保下次 loadstart 能触发)。
      // 实际上 onLoadStart 会处理这个,这里的冗余检查可以简化。
      // 简化:信任 loadstart 会在未来触发(如果视频源变了),或者信任上面的 onLoadedMetadata() 已经处理了当前状态。
    } else if (videoElement.currentSrc) {
      // 如果有 src 但 readyState 是 0,说明即将开始加载
      console.log(
        "[B站重置进度脚本] 检测到脚本运行时 video 有 src 但未加载 (readyState: 0), 等待 'loadstart'..."
      );
      // 这种情况 'loadstart' 事件会正常触发,无需额外操作。
    }

    // (可选) 错误处理
    videoElement.addEventListener("error", (e) => {
      console.warn("[B站重置进度脚本] Video 元素报告错误:", e);
      // 可以在这里移除监听器,但 'loadstart' 可能在尝试重新加载时仍需要
      // videoElement.removeEventListener('loadstart', onLoadStart);
    });
  }

  // --- 使用 MutationObserver 监听 DOM 变化 (逻辑微调) ---
  function observeDOMForVideo() {
    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type === "childList") {
          mutation.addedNodes.forEach((node) => {
            // 检查添加的节点本身或其子孙节点是否包含 <video>
            if (node.nodeType === Node.ELEMENT_NODE) {
              const videosToSetup = [];
              if (node.tagName === "VIDEO") {
                videosToSetup.push(node);
              } else if (typeof node.querySelectorAll === "function") {
                // 使用 querySelectorAll 查找所有后代 video 元素
                videosToSetup.push(...node.querySelectorAll("video"));
              }

              videosToSetup.forEach((video) => {
                // 对每个找到的 video 元素,如果之前未处理过,则设置监听器
                if (!processedVideoElements.has(video)) {
                  console.log(
                    "[B站重置进度脚本] MutationObserver 发现新的 video 元素."
                  );
                  setupVideoReset(video);
                }
              });
            }
          });
        }
      }
    });

    const config = { childList: true, subtree: true };

    // 确保 body 存在后再开始观察
    const startObserving = () => {
      if (document.body) {
        observer.observe(document.body, config);
        console.log(
          "[B站重置进度脚本] 已启动 DOM 变化监听 (MutationObserver)."
        );

        // --- 初始检查 ---
        // 检查页面加载时已经存在的 video 元素
        const existingVideos = document.querySelectorAll("video");
        console.log(
          `[B站重置进度脚本] 页面加载时发现 ${existingVideos.length} 个 video 元素.`
        );
        existingVideos.forEach((video) => {
          // 同样检查是否已处理
          if (!processedVideoElements.has(video)) {
            console.log("[B站重置进度脚本] 处理页面加载时已存在的 video 元素.");
            setupVideoReset(video);
          }
        });
      } else {
        console.log(
          "[B站重置进度脚本] Body not ready, delaying MutationObserver start..."
        );
        setTimeout(startObserving, 100); // 稍后重试
      }
    };
    startObserving();

    // 返回 observer 实例(虽然当前代码没用到,但保留是好习惯)
    return observer;
  }

  // --- 初始化 ---
  try {
    createSettingsPanel(); // 创建设置 UI
    observeDOMForVideo(); // 开始监听视频元素
    console.log("[B站重置进度脚本] 初始化完成.");
  } catch (error) {
    console.error("[B站重置进度脚本] 初始化过程中发生错误:", error);
  }
})();
window.addEventListener('load', () => {
  setTimeout(() => {
    const switchCheckbox = document.getElementById("biliResetSwitch");
    if (switchCheckbox) {
      switchCheckbox.checked = getEnabled();
      console.log(`[B站重置进度脚本] 页面加载完成后再次同步复选框状态: ${getEnabled() ? "已启用" : "已禁用"}`);
    }
  }, 500); // 给予足够的时间确保面板已创建
});

QingJ © 2025

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