Greasy Fork

来自缓存

Greasy Fork is available in English.

B站自动最高画质

自动选择B站视频/直播的最高可用分辨率

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         B站自动最高画质
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  自动选择B站视频/直播的最高可用分辨率
// @author       Benjia Zou
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/bangumi/*
// @match        https://live.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @license      MIT
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_log
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  const CONFIG = {
    enabled: true,
    autoSelectAudio: true,  // 自动选择最高音质(杜比、Hi-Res)
    showNotification: true,  // 显示操作提示
    debug: false,            // 调试模式
  };

  // 日志系统
  const Logger = {
    log: (msg) => CONFIG.debug && console.log('[B站最高画质]', msg),
    info: (msg) => console.info('[B站最高画质]', msg),
    warn: (msg) => console.warn('[B站最高画质]', msg),
    error: (msg) => console.error('[B站最高画质]', msg),
  };

  // 页面类型检测
  const PAGE_TYPE = (() => {
    const url = window.location.href;
    if (url.includes('live.bilibili.com')) return 'LIVE';
    if (url.includes('/bangumi/')) return 'BANGUMI';
    if (url.includes('/video/')) return 'VIDEO';
    return 'UNKNOWN';
  })();

  // 通知系统
  const Notifier = {
    show: (msg, duration = 3000) => {
      if (!CONFIG.showNotification) return;

      const style = `
        position: fixed;
        top: 20px;
        right: 20px;
        background: rgba(0, 0, 0, 0.8);
        color: #fff;
        padding: 12px 20px;
        border-radius: 4px;
        font-size: 14px;
        z-index: 10000;
        max-width: 300px;
        border-left: 3px solid #23aaff;
        animation: slideIn 0.3s ease-out;
      `;

      const notif = document.createElement('div');
      notif.innerHTML = msg;
      notif.style.cssText = style;

      const keyframes = `
        @keyframes slideIn {
          from {
            transform: translateX(400px);
            opacity: 0;
          }
          to {
            transform: translateX(0);
            opacity: 1;
          }
        }
      `;

      if (!document.querySelector('#bilibili-auto-quality-styles')) {
        const styleSheet = document.createElement('style');
        styleSheet.id = 'bilibili-auto-quality-styles';
        styleSheet.textContent = keyframes;
        document.head.appendChild(styleSheet);
      }

      document.body.appendChild(notif);

      setTimeout(() => {
        notif.style.animation = 'slideIn 0.3s ease-out reverse';
        setTimeout(() => notif.remove(), 300);
      }, duration);
    },
  };

  // ============ 视频页面逻辑 ============
  const VideoQuality = {
    qualityMap: {
      16: '360P',
      32: '480P',
      64: '720P',
      74: '720P60',
      80: '1080P',
      112: '1080P+',
      116: '1080P60',
      120: '4K',
      125: 'HDR',
      126: '杜比视界',
      127: '8K',
    },

    // 获取品质标签文本
    getQualityName: (element) => {
      const badge = element.querySelector('.bpx-player-ctrl-quality-badge');
      const text = element.textContent.trim();
      return text || '未知';
    },

    // 检查是否为VIP限制
    isVIPOnly: (element) => {
      return element.querySelector('.bpx-player-ctrl-quality-badge-bigvip') !== null;
    },

    // 检查用户VIP状态
    isUserVIP: () => {
      // 方式1:检查VIP头像
      if (document.querySelector('.bili-avatar-icon-big-vip')) return true;

      // 方式2:检查是否能访问1080P+
      const qualityItems = document.querySelectorAll('.bpx-player-ctrl-quality-menu-item');
      for (const item of qualityItems) {
        if (item.textContent.includes('1080P+') &&
            !VideoQuality.isVIPOnly(item)) {
          return true;
        }
      }

      return false;
    },

    // 选择最高画质
    selectHighest: () => {
      const qualityMenu = document.querySelector('.bpx-player-ctrl-quality-menu');
      if (!qualityMenu) {
        Logger.log('未找到画质菜单');
        return false;
      }

      const qualityItems = Array.from(
        qualityMenu.querySelectorAll('.bpx-player-ctrl-quality-menu-item')
      );

      if (qualityItems.length === 0) {
        Logger.log('未找到画质选项');
        return false;
      }

      // 找到第一个可用的(最高的)画质
      let targetQuality = null;
      const isVIP = VideoQuality.isUserVIP();

      for (const item of qualityItems) {
        const isVIPOnly = VideoQuality.isVIPOnly(item);

        // 跳过VIP限制的项(非VIP用户)
        if (isVIPOnly && !isVIP) {
          Logger.log(`跳过VIP限制: ${VideoQuality.getQualityName(item)}`);
          continue;
        }

        targetQuality = item;
        break; // 第一个可用的就是最高的
      }

      if (!targetQuality) {
        Logger.log('没有可用的画质');
        return false;
      }

      const qualityName = VideoQuality.getQualityName(targetQuality);
      const isActive = targetQuality.classList.contains('bpx-state-active');

      if (!isActive) {
        Logger.log(`切换到: ${qualityName}`);
        targetQuality.click();
        Notifier.show(`✅ 已切换到 <strong>${qualityName}</strong> 画质`);
        return true;
      } else {
        Logger.log(`已在最高画质: ${qualityName}`);
        return true;
      }
    },

    // 选择最高音质
    selectHighestAudio: () => {
      // 杜比视界
      const dolbyBtn = document.querySelector('.bpx-player-ctrl-dolby');
      if (dolbyBtn && !dolbyBtn.classList.contains('bpx-state-active')) {
        dolbyBtn.click();
        Logger.log('已启用杜比视界');
        return true;
      }

      // Hi-Res
      const hiResBtn = document.querySelector('.bpx-player-ctrl-flac');
      if (hiResBtn && !hiResBtn.classList.contains('bpx-state-active')) {
        hiResBtn.click();
        Logger.log('已启用Hi-Res');
        return true;
      }

      return false;
    },

    // 自动选择逻辑
    auto: () => {
      if (!CONFIG.enabled) return false;

      let success = VideoQuality.selectHighest();

      if (CONFIG.autoSelectAudio) {
        const audioSuccess = VideoQuality.selectHighestAudio();
        success = success || audioSuccess;
      }

      return success;
    },
  };

  // ============ 直播页面逻辑 ============
  const LiveQuality = {
    qualityMap: {
      80: '流畅',
      150: '高清',
      250: '超清',
      400: '蓝光',
      10000: '原画',
      20000: '4K',
      30000: '杜比',
    },

    // 获取最高可用画质
    getHighestQuality: () => {
      try {
        const livePlayer = unsafeWindow.livePlayer;
        if (!livePlayer) {
          Logger.log('未找到直播播放器对象');
          return null;
        }

        const playerInfo = livePlayer.getPlayerInfo?.();
        if (!playerInfo || !playerInfo.qualityCandidates) {
          Logger.log('无法获取画质信息');
          return null;
        }

        // 找最高的qn值
        const qualities = playerInfo.qualityCandidates || [];
        if (qualities.length === 0) return null;

        const highest = Math.max(...qualities.map(q => q.qn));
        return highest;
      } catch (e) {
        Logger.error(`获取画质失败: ${e.message}`);
        return null;
      }
    },

    // 切换画质
    switchQuality: (qn) => {
      try {
        const livePlayer = unsafeWindow.livePlayer;
        if (!livePlayer) return false;

        livePlayer.switchQuality?.(qn);
        const qualityName = LiveQuality.qualityMap[qn] || `QN:${qn}`;
        Logger.log(`已切换直播画质到: ${qualityName}`);
        Notifier.show(`✅ 已切换到 <strong>${qualityName}</strong> 画质`);
        return true;
      } catch (e) {
        Logger.error(`切换画质失败: ${e.message}`);
        return false;
      }
    },

    // 自动选择逻辑
    auto: () => {
      if (!CONFIG.enabled) return false;

      const currentInfo = unsafeWindow.livePlayer?.getPlayerInfo?.();
      const highestQn = LiveQuality.getHighestQuality();

      if (!highestQn) {
        Logger.log('无法获取直播最高画质');
        return false;
      }

      // 已经是最高画质
      if (currentInfo?.quality === highestQn) {
        Logger.log(`已在最高直播画质: ${LiveQuality.qualityMap[highestQn]}`);
        return true;
      }

      // 切换到最高画质
      return LiveQuality.switchQuality(highestQn);
    },
  };

  // ============ 主控制器 ============
  const Controller = {
    // 重试策略:指数退避
    retrySelect: (selectFn, maxAttempts = 15) => {
      let attempts = 0;

      function attempt() {
        if (attempts >= maxAttempts) {
          Logger.warn(`在${maxAttempts}次尝试后放弃`);
          return;
        }

        try {
          const success = selectFn();
          if (success !== false) {
            Logger.log(`第${attempts + 1}次尝试成功`);
            return;
          }
        } catch (e) {
          Logger.error(`尝试${attempts + 1}失败: ${e.message}`);
        }

        // 指数退避
        const delay = Math.min(
          Math.pow(1.5, attempts) * 200,
          5000
        );

        attempts++;
        setTimeout(attempt, delay);
      }

      attempt();
    },

    // 初始化
    init: () => {
      Logger.info(`页面类型: ${PAGE_TYPE}`);

      if (PAGE_TYPE === 'LIVE') {
        // 直播页面:等待更久
        setTimeout(() => {
          Controller.retrySelect(() => LiveQuality.auto(), 15);
        }, 2000);
      } else if (PAGE_TYPE === 'VIDEO' || PAGE_TYPE === 'BANGUMI') {
        // 视频/番剧页面
        Controller.retrySelect(() => VideoQuality.auto(), 15);
      }
    },

    // 处理SPA导航
    setupNavigation: () => {
      let lastUrl = location.href;

      // 监听URL变化
      const checkUrlChange = () => {
        if (location.href !== lastUrl) {
          lastUrl = location.href;
          Logger.info('页面已切换,重新初始化...');

          // 小延迟后重新初始化
          setTimeout(() => Controller.init(), 1000);
        }
      };

      // 监听路由变化
      window.addEventListener('popstate', checkUrlChange);
      setInterval(checkUrlChange, 1000);

      // 监听hash变化(番剧)
      window.addEventListener('hashchange', () => {
        Logger.info('Hash已变化,重新初始化...');
        setTimeout(() => Controller.init(), 1000);
      });
    },
  };

  // ============ 启动 ============
  (function startup() {
    Logger.info('脚本已加载');

    // 加载配置
    if (typeof GM_getValue !== 'undefined') {
      CONFIG.enabled = GM_getValue('bilibili_auto_quality_enabled', true);
      CONFIG.autoSelectAudio = GM_getValue('bilibili_auto_quality_audio', true);
      CONFIG.showNotification = GM_getValue('bilibili_auto_quality_notify', true);
      CONFIG.debug = GM_getValue('bilibili_auto_quality_debug', false);
    }

    // 初始化
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => {
        setTimeout(() => Controller.init(), 500);
      });
    } else {
      setTimeout(() => Controller.init(), 500);
    }

    // 设置导航监听
    Controller.setupNavigation();

    // 暴露控制接口(开发者工具用)
    if (!unsafeWindow.BilibiliAutoQuality) {
      unsafeWindow.BilibiliAutoQuality = {
        CONFIG,
        selectVideo: () => VideoQuality.selectHighest(),
        selectLive: () => LiveQuality.auto(),
        setEnabled: (val) => { CONFIG.enabled = val; },
        setDebug: (val) => { CONFIG.debug = val; },
      };
      Logger.info('开发者接口已暴露: window.BilibiliAutoQuality');
    }
  })();
})();