哔哩哔哩自动画质

自动解锁并更改哔哩哔哩视频的画质和音质及直播画质,实现自动选择最高画质、无损音频、杜比全景声。

  1. // ==UserScript==
  2. // @name 哔哩哔哩自动画质
  3. // @namespace https://github.com/AHCorn/Bilibili-Auto-Quality/
  4. // @version 4.7-Beta
  5. // @license GPL-3.0
  6. // @description 自动解锁并更改哔哩哔哩视频的画质和音质及直播画质,实现自动选择最高画质、无损音频、杜比全景声。
  7. // @author 安和(AHCorn)
  8. // @icon https://www.bilibili.com/favicon.ico
  9. // @match *://www.bilibili.com/video/*
  10. // @match *://www.bilibili.com/list/*
  11. // @match *://www.bilibili.com/blackboard/*
  12. // @match *://www.bilibili.com/watchlater/*
  13. // @match *://www.bilibili.com/bangumi/*
  14. // @match *://www.bilibili.com/watchroom/*
  15. // @match *://www.bilibili.com/medialist/*
  16. // @match *://bangumi.bilibili.com/*
  17. // @exclude *://live.bilibili.com/
  18. // @exclude *://live.bilibili.com/*/*
  19. // @include *://live.bilibili.com/blanc/*
  20. // @match *://live.bilibili.com/*
  21. // @grant GM_addStyle
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_registerMenuCommand
  25. // ==/UserScript==
  26.  
  27. (async function () {
  28. "use strict";
  29. if (typeof unsafeWindow === "undefined") { unsafeWindow = window; }
  30. window.localStorage.bilibili_player_force_hdr = 1;
  31. const state = {
  32. hiResAudioEnabled: GM_getValue("hiResAudio", false),
  33. dolbyAtmosEnabled: GM_getValue("dolbyAtmos", false),
  34. userQualitySetting: GM_getValue("qualitySetting", "最高画质"),
  35. userBackupQualitySetting: GM_getValue("backupQualitySetting", "最高画质"),
  36. useHighestQualityFallback: GM_getValue("useHighestQualityFallback", true),
  37. activeQualityTab: GM_getValue("activeQualityTab", "primary"),
  38. userHasChangedQuality: false,
  39. takeOverQualityControl: GM_getValue("takeOverQualityControl", false),
  40. isVipUser: false,
  41. vipStatusChecked: false,
  42. isLoading: true,
  43. isLivePage: false,
  44. userLiveQualitySetting: GM_getValue("liveQualitySetting", "原画"),
  45. devModeEnabled: GM_getValue("devModeEnabled", false),
  46. devModeVipStatus: GM_getValue("devModeVipStatus", false),
  47. devModeNoLoginStatus: GM_getValue("devModeNoLoginStatus", false),
  48. devModeDisableUA: GM_getValue("devModeDisableUA", false),
  49. devModeAudioRetries: GM_getValue("devModeAudioRetries", 2),
  50. devModeAudioDelay: GM_getValue("devModeAudioDelay", 4000),
  51. devDoubleCheckDelay: GM_getValue("devDoubleCheckDelay", 5000),
  52. injectQualityButton: GM_getValue("injectQualityButton", true),
  53. qualityDoubleCheck: GM_getValue("qualityDoubleCheck", true),
  54. liveQualityDoubleCheck: GM_getValue("liveQualityDoubleCheck", true),
  55. disableHDROption: GM_getValue("disableHDR", false),
  56. sessionCache: {
  57. vipStatus: null,
  58. vipChecked: false
  59. }
  60. };
  61. try {
  62. if (!state.devModeDisableUA || !state.devModeEnabled) {
  63. Object.defineProperty(navigator, 'userAgent', {
  64. value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15",
  65. configurable: true
  66. });
  67. Object.defineProperty(navigator, 'platform', {
  68. value: "MacIntel",
  69. configurable: true
  70. });
  71. console.log("[系统设置] UA 和平台标识修改成功");
  72. } else {
  73. console.log("[开发者模式] 已禁用 UA 修改");
  74. }
  75. } catch (error) {
  76. console.error("[系统设置] 修改 UserAgent 失败,解锁功能可能失效:", error);
  77. }
  78. GM_addStyle(`
  79. #bilibili-quality-selector, #bilibili-live-quality-selector, #bilibili-dev-settings {
  80. position: fixed;
  81. top: 50%;
  82. left: 50%;
  83. transform: translate(-50%, -50%);
  84. background: linear-gradient(135deg, #f6f8fa, #e9ecef);
  85. border-radius: 24px;
  86. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1), 0 1px 8px rgba(0, 0, 0, 0.06);
  87. padding: 30px;
  88. width: 90%;
  89. max-width: 400px;
  90. max-height: 85vh;
  91. overflow-y: auto;
  92. overflow-x: hidden;
  93. display: none;
  94. z-index: 10000;
  95. font-family: 'Segoe UI', 'Roboto', sans-serif;
  96. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  97. scrollbar-width: thin;
  98. scrollbar-color: rgba(0, 161, 214, 0.3) transparent;
  99. }
  100. .quality-tabs {
  101. display: flex;
  102. margin-bottom: 20px;
  103. border-radius: 12px;
  104. background: #e8eaed;
  105. padding: 4px;
  106. }
  107. .quality-tab {
  108. flex: 1;
  109. padding: 8px;
  110. text-align: center;
  111. cursor: pointer;
  112. border-radius: 8px;
  113. transition: all 0.3s ease;
  114. color: #666;
  115. font-weight: 600;
  116. }
  117. .quality-tab.active {
  118. background: #00a1d6;
  119. color: white;
  120. }
  121. .quality-section {
  122. display: none;
  123. }
  124. .quality-section.active {
  125. display: block;
  126. }
  127. .quality-button-hidden {
  128. display: none !important;
  129. }
  130. .toggle-switch.hide {
  131. display: none;
  132. }
  133. .toggle-switch.show {
  134. display: flex;
  135. }
  136. #bilibili-quality-selector h2, #bilibili-live-quality-selector h2,
  137. #bilibili-live-quality-selector h3 {
  138. margin: 0 0 20px;
  139. color: #00a1d6;
  140. font-size: 28px;
  141. text-align: center;
  142. font-weight: 700;
  143. }
  144. #bilibili-live-quality-selector h3 {
  145. font-size: 24px;
  146. margin-top: 20px;
  147. }
  148. #bilibili-quality-selector p, #bilibili-live-quality-selector p {
  149. margin: 0 0 25px;
  150. color: #5f6368;
  151. font-size: 14px;
  152. text-align: center;
  153. }
  154. .quality-group {
  155. display: grid;
  156. grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  157. gap: 12px;
  158. margin-bottom: 25px;
  159. }
  160. .line-group {
  161. display: grid;
  162. grid-template-columns: repeat(4, 1fr);
  163. gap: 12px;
  164. margin-bottom: 25px;
  165. }
  166. .quality-button, .line-button {
  167. background-color: #ffffff;
  168. border: 2px solid #dadce0;
  169. border-radius: 12px;
  170. padding: 10px 8px;
  171. font-size: 14px;
  172. color: #3c4043;
  173. cursor: pointer;
  174. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  175. font-weight: 600;
  176. text-align: center;
  177. }
  178. .line-button {
  179. font-size: 12px;
  180. padding: 8px 4px;
  181. }
  182. .quality-button:hover, .line-button:hover {
  183. background-color: #f1f3f4;
  184. transform: translateY(-2px);
  185. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  186. }
  187. .quality-button.active, .line-button.active {
  188. background-color: #00a1d6;
  189. color: white;
  190. border-color: #00a1d6;
  191. box-shadow: 0 6px 12px rgba(0, 161, 214, 0.3);
  192. }
  193. .quality-button.active.vip-quality {
  194. background-color: #f25d8e;
  195. color: white;
  196. border-color: #f25d8e;
  197. box-shadow: 0 6px 12px rgba(242, 93, 142, 0.3);
  198. }
  199. .quality-button.unavailable {
  200. opacity: 0.5;
  201. cursor: not-allowed;
  202. }
  203. .toggle-switch {
  204. display: flex;
  205. align-items: center;
  206. justify-content: space-between;
  207. margin-bottom: 12px;
  208. padding: 10px 15px;
  209. border-radius: 12px;
  210. transition: all 0.3s ease;
  211. }
  212. .toggle-switch:hover {
  213. background-color: #e8eaed;
  214. }
  215. .toggle-switch label {
  216. font-size: 16px;
  217. color: #3c4043;
  218. font-weight: 600;
  219. }
  220. .switch {
  221. position: relative;
  222. display: inline-block;
  223. width: 52px;
  224. height: 28px;
  225. }
  226. .switch input {
  227. opacity: 0;
  228. width: 0;
  229. height: 0;
  230. }
  231. .slider {
  232. position: absolute;
  233. cursor: pointer;
  234. top: 0;
  235. left: 0;
  236. right: 0;
  237. bottom: 0;
  238. background-color: #ccc;
  239. transition: .4s;
  240. border-radius: 34px;
  241. }
  242. .slider:before {
  243. position: absolute;
  244. content: "";
  245. height: 20px;
  246. width: 20px;
  247. left: 4px;
  248. bottom: 4px;
  249. background-color: white;
  250. transition: .4s;
  251. border-radius: 50%;
  252. }
  253. input:checked + .slider {
  254. background-color: #00a1d6;
  255. }
  256. input:checked + .slider.vip-audio {
  257. background-color: #f25d8e;
  258. }
  259. input:checked + .slider:before {
  260. transform: translateX(24px);
  261. }
  262. @keyframes fadeIn {
  263. from { opacity: 0; }
  264. to { opacity: 1; }
  265. }
  266. @keyframes slideIn {
  267. from { transform: translate(-50%, -60%); }
  268. to { transform: translate(-50%, -50%); }
  269. }
  270. #bilibili-quality-selector.show, #bilibili-live-quality-selector.show {
  271. display: block;
  272. animation: fadeIn 0.3s ease-out, slideIn 0.3s ease-out;
  273. }
  274. @media (max-width: 480px) {
  275. #bilibili-quality-selector, #bilibili-live-quality-selector, #bilibili-dev-settings {
  276. width: 95%;
  277. padding: 20px;
  278. max-height: 80vh;
  279. }
  280. .quality-group {
  281. grid-template-columns: repeat(2, 1fr);
  282. gap: 8px;
  283. }
  284. .line-group {
  285. grid-template-columns: repeat(2, 1fr);
  286. gap: 8px;
  287. }
  288. .quality-button, .line-button {
  289. padding: 8px 6px;
  290. font-size: 13px;
  291. }
  292. .live-quality-button {
  293. padding: 10px 6px;
  294. font-size: 14px;
  295. }
  296. .toggle-switch {
  297. padding: 8px 12px;
  298. margin-bottom: 8px;
  299. }
  300. .toggle-switch label {
  301. font-size: 14px;
  302. }
  303. .toggle-switch .description {
  304. font-size: 12px;
  305. }
  306. .input-group {
  307. padding: 12px;
  308. margin-bottom: 12px;
  309. }
  310. .input-group label {
  311. font-size: 14px;
  312. }
  313. .input-group .description {
  314. font-size: 12px;
  315. }
  316. .input-group input[type="number"] {
  317. width: 70px;
  318. padding: 6px;
  319. font-size: 13px;
  320. }
  321. .github-link {
  322. top: 20px;
  323. right: 20px;
  324. width: 20px;
  325. height: 20px;
  326. }
  327. h2 {
  328. font-size: 24px !important;
  329. margin-bottom: 15px !important;
  330. }
  331. .dev-warning {
  332. font-size: 13px;
  333. padding: 12px;
  334. margin-bottom: 15px;
  335. }
  336. .warning {
  337. font-size: 13px;
  338. padding: 8px;
  339. margin: 8px 0;
  340. }
  341. .status-bar {
  342. font-size: 13px;
  343. padding: 8px;
  344. margin-bottom: 12px;
  345. }
  346. .quality-section-title {
  347. font-size: 15px;
  348. margin: 15px 0 12px;
  349. }
  350. .quality-section-description {
  351. font-size: 12px;
  352. margin: -3px 0 12px;
  353. }
  354. }
  355. @media (max-height: 600px) {
  356. #bilibili-quality-selector, #bilibili-live-quality-selector, #bilibili-dev-settings {
  357. max-height: 90vh;
  358. padding: 15px;
  359. }
  360. .quality-group, .line-group {
  361. margin-bottom: 15px;
  362. }
  363. .toggle-switch {
  364. margin-bottom: 6px;
  365. }
  366. .input-group {
  367. margin-bottom: 10px;
  368. }
  369. }
  370. .status-bar {
  371. padding: 10px;
  372. border-radius: 8px;
  373. margin-bottom: 15px;
  374. text-align: center;
  375. font-weight: bold;
  376. transition: all 0.5s ease;
  377. }
  378. .status-bar.non-vip {
  379. background-color: #f0f0f0;
  380. color: #666666;
  381. }
  382. .status-bar.vip {
  383. background-color: #fff1f5;
  384. color: #f25d8e;
  385. }
  386. .warning {
  387. background-color: #fce8e6;
  388. color: #d93025;
  389. padding: 10px;
  390. border-radius: 8px;
  391. margin-top: 12px;
  392. margin-bottom: 12px;
  393. text-align: center;
  394. font-weight: bold;
  395. transition: all 0.3s ease;
  396. }
  397. .warning::before {
  398. content: "";
  399. margin-right: 10px;
  400. }
  401. #bilibili-dev-settings {
  402. position: fixed;
  403. top: 50%;
  404. left: 50%;
  405. transform: translate(-50%, -50%);
  406. background: linear-gradient(135deg, #ffffff, #f8f9fa);
  407. border-radius: 24px;
  408. box-shadow: 0 12px 36px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
  409. padding: 32px;
  410. width: 90%;
  411. max-width: 420px;
  412. max-height: 85vh;
  413. overflow-y: auto;
  414. overflow-x: hidden;
  415. display: none;
  416. z-index: 10000;
  417. font-family: 'Segoe UI', 'Roboto', sans-serif;
  418. scrollbar-width: thin;
  419. scrollbar-color: rgba(0, 161, 214, 0.3) transparent;
  420. }
  421. #bilibili-dev-settings.show {
  422. display: block;
  423. animation: fadeIn 0.3s ease-out, slideIn 0.3s ease-out;
  424. }
  425. #bilibili-dev-settings h2 {
  426. margin: 0 0 24px;
  427. color: #f25d8e;
  428. font-size: 28px;
  429. text-align: center;
  430. font-weight: 700;
  431. letter-spacing: -0.5px;
  432. text-shadow: 0 2px 4px rgba(242, 93, 142, 0.1);
  433. }
  434. #bilibili-dev-settings .dev-warning {
  435. background: linear-gradient(135deg, #fff1f5, #fce8e6);
  436. color: #d93025;
  437. padding: 14px 18px;
  438. border-radius: 16px;
  439. margin-bottom: 24px;
  440. text-align: center;
  441. font-weight: 600;
  442. font-size: 14px;
  443. border: 2px solid rgba(217, 48, 37, 0.1);
  444. box-shadow: 0 4px 12px rgba(217, 48, 37, 0.05);
  445. }
  446. #bilibili-dev-settings .toggle-switch {
  447. display: flex;
  448. align-items: center;
  449. justify-content: space-between;
  450. margin-bottom: 12px;
  451. padding: 10px 15px;
  452. background-color: #f1f3f4;
  453. border-radius: 12px;
  454. transition: all 0.3s ease;
  455. }
  456. #bilibili-dev-settings .toggle-switch .description {
  457. font-size: 13px;
  458. color: #666;
  459. margin-top: 4px;
  460. }
  461. #bilibili-dev-settings .toggle-switch label {
  462. display: flex;
  463. flex-direction: column;
  464. font-size: 16px;
  465. color: #3c4043;
  466. font-weight: 600;
  467. }
  468. #bilibili-dev-settings .input-group {
  469. background: #f8f9fa;
  470. border-radius: 16px;
  471. padding: 15px;
  472. margin-bottom: 16px;
  473. border: 2px solid transparent;
  474. transition: all 0.3s ease;
  475. display: flex;
  476. align-items: center;
  477. gap: 12px;
  478. }
  479. #bilibili-dev-settings .input-group.disabled {
  480. opacity: 0.6;
  481. cursor: not-allowed;
  482. }
  483. #bilibili-dev-settings .input-group:hover {
  484. background: #f1f3f4;
  485. border-color: rgba(242, 93, 142, 0.1);
  486. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  487. }
  488. #bilibili-dev-settings .input-group label {
  489. flex: 1;
  490. display: flex;
  491. flex-direction: column;
  492. color: #3c4043;
  493. font-weight: 600;
  494. font-size: 15px;
  495. }
  496. #bilibili-dev-settings .input-group .description {
  497. font-size: 13px;
  498. color: #666;
  499. margin-top: 4px;
  500. font-weight: normal;
  501. }
  502. #bilibili-dev-settings .input-group input[type="number"] {
  503. width: 80px;
  504. padding: 8px;
  505. border: 2px solid #dadce0;
  506. border-radius: 8px;
  507. font-size: 14px;
  508. font-weight: 500;
  509. color: #3c4043;
  510. transition: all 0.3s ease;
  511. background: #ffffff;
  512. -moz-appearance: textfield;
  513. }
  514. #bilibili-dev-settings .input-group .unit {
  515. color: #666;
  516. font-size: 14px;
  517. font-weight: normal;
  518. margin-left: 4px;
  519. }
  520. #bilibili-dev-settings .refresh-button {
  521. width: 100%;
  522. padding: 12px;
  523. background: #f25d8e;
  524. color: white;
  525. border: none;
  526. border-radius: 12px;
  527. font-size: 15px;
  528. font-weight: 600;
  529. cursor: pointer;
  530. transition: all 0.3s ease;
  531. margin-top: 20px;
  532. }
  533. #bilibili-dev-settings .refresh-button:hover {
  534. background: #e74d7b;
  535. transform: translateY(-2px);
  536. box-shadow: 0 4px 12px rgba(242, 93, 142, 0.2);
  537. }
  538. #bilibili-dev-settings .refresh-button:disabled {
  539. background: #ccc;
  540. cursor: not-allowed;
  541. transform: none;
  542. box-shadow: none;
  543. }
  544. #bilibili-dev-settings input:checked + .slider {
  545. background-color: #f25d8e;
  546. }
  547. #bilibili-dev-settings input:checked + .slider:before {
  548. transform: translateX(26px);
  549. box-shadow: 0 2px 4px rgba(242, 93, 142, 0.2);
  550. }
  551. #bilibili-dev-settings input:disabled + .slider {
  552. opacity: 0.5;
  553. cursor: not-allowed;
  554. }
  555. #bilibili-dev-settings input:disabled + .slider:before {
  556. box-shadow: none;
  557. }
  558. @media (max-width: 480px) {
  559. #bilibili-dev-settings {
  560. width: 95%;
  561. padding: 24px;
  562. }
  563. #bilibili-dev-settings .toggle-switch,
  564. #bilibili-dev-settings .input-group {
  565. padding: 14px 16px;
  566. }
  567. }
  568. .bpx-player-ctrl-quality.quality-button-hidden {
  569. display: none !important;
  570. }
  571. .quality-section {
  572. margin-bottom: 20px;
  573. }
  574. .quality-label {
  575. font-size: 14px;
  576. color: #666;
  577. margin-bottom: 10px;
  578. text-align: left;
  579. }
  580. .quality-group.backup-quality .quality-button {
  581. font-size: 12px;
  582. padding: 8px 6px;
  583. }
  584. .quality-settings-btn {
  585. display: flex;
  586. align-items: center;
  587. justify-content: center;
  588. cursor: pointer;
  589. width: 40px;
  590. height: 100%;
  591. opacity: 0.9;
  592. transition: opacity 0.3s ease;
  593. position: relative;
  594. }
  595. .quality-settings-btn:hover {
  596. opacity: 1;
  597. }
  598. .quality-settings-btn .bpx-player-ctrl-btn-icon {
  599. width: 22px;
  600. height: 22px;
  601. margin-bottom: 12px;
  602. display: flex;
  603. align-items: center;
  604. justify-content: center;
  605. }
  606. .quality-settings-btn svg {
  607. width: 100%;
  608. height: 100%;
  609. stroke: #ffffff;
  610. }
  611. .quality-settings-btn::after {
  612. content: "自动画质面板";
  613. position: absolute;
  614. bottom: 100%;
  615. left: 50%;
  616. transform: translateX(-50%);
  617. background-color: rgba(21, 21, 21, 0.9);
  618. color: #fff;
  619. padding: 5px 8px;
  620. border-radius: 4px;
  621. font-size: 12px;
  622. white-space: nowrap;
  623. pointer-events: none;
  624. opacity: 0;
  625. transition: opacity 0.2s ease;
  626. margin-bottom: 5px;
  627. }
  628. .quality-settings-btn:hover::after {
  629. opacity: 1;
  630. }
  631. .github-link {
  632. position: absolute;
  633. top: 30px;
  634. right: 30px;
  635. width: 24px;
  636. height: 24px;
  637. cursor: pointer;
  638. transition: transform 0.3s ease;
  639. }
  640. .github-link:hover {
  641. transform: scale(1.1);
  642. }
  643. .github-link svg {
  644. width: 100%;
  645. height: 100%;
  646. fill: #00a1d6;
  647. }
  648. .quality-section-title {
  649. font-size: 16px;
  650. color: #00a1d6;
  651. font-weight: 600;
  652. margin: 20px 0 15px;
  653. padding-bottom: 8px;
  654. border-bottom: 2px solid rgba(0, 161, 214, 0.1);
  655. }
  656. .quality-section-description {
  657. font-size: 13px;
  658. color: #666;
  659. margin: -5px 0 15px;
  660. line-height: 1.4;
  661. }
  662. .live-quality-group {
  663. display: grid;
  664. grid-template-columns: repeat(2, 1fr);
  665. gap: 12px;
  666. margin-bottom: 25px;
  667. }
  668. .live-quality-button {
  669. background-color: #ffffff;
  670. border: 2px solid #dadce0;
  671. border-radius: 12px;
  672. padding: 12px 8px;
  673. font-size: 15px;
  674. color: #3c4043;
  675. cursor: pointer;
  676. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  677. font-weight: 600;
  678. text-align: center;
  679. }
  680. .live-quality-button:hover {
  681. background-color: #f1f3f4;
  682. transform: translateY(-2px);
  683. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  684. }
  685. .live-quality-button.active {
  686. background-color: #00a1d6;
  687. color: white;
  688. border-color: #00a1d6;
  689. box-shadow: 0 6px 12px rgba(0, 161, 214, 0.3);
  690. }
  691. #bilibili-quality-selector,
  692. #bilibili-live-quality-selector,
  693. #bilibili-dev-settings {
  694. -ms-overflow-style: none;
  695. scrollbar-width: none;
  696. }
  697. `);
  698. const Utils = {
  699. debounce(fn, wait) {
  700. let timeout;
  701. return function (...args) {
  702. clearTimeout(timeout);
  703. timeout = setTimeout(() => fn.apply(this, args), wait);
  704. };
  705. },
  706. delay(ms) {
  707. return new Promise(resolve => setTimeout(resolve, ms));
  708. },
  709. query(selector, parent = document) {
  710. return parent.querySelector(selector);
  711. },
  712. queryAll(selector, parent = document) {
  713. return Array.from(parent.querySelectorAll(selector));
  714. }
  715. };
  716. function checkIfLivePage() {
  717. state.isLivePage = window.location.href.includes("live.bilibili.com");
  718. }
  719. function checkVipStatus() {
  720. if (state.devModeEnabled) {
  721. state.isVipUser = state.devModeVipStatus;
  722. state.vipStatusChecked = true;
  723. // 缓存会员状态
  724. state.sessionCache.vipStatus = state.isVipUser;
  725. state.sessionCache.vipChecked = true;
  726. console.log("[开发者模式] 用户会员状态:", state.isVipUser ? "是" : "否");
  727. return;
  728. }
  729.  
  730. const vipElement = document.querySelector(".bili-avatar-icon.bili-avatar-right-icon.bili-avatar-icon-big-vip");
  731. const currentQualityEl = document.querySelector(".bpx-player-ctrl-quality-menu-item.bpx-state-active .bpx-player-ctrl-quality-text");
  732.  
  733. state.isVipUser = vipElement !== null || (currentQualityEl && currentQualityEl.textContent.includes("大会员"));
  734. state.vipStatusChecked = true;
  735. // 缓存会员状态
  736. state.sessionCache.vipStatus = state.isVipUser;
  737. state.sessionCache.vipChecked = true;
  738.  
  739. console.log("[会员状态] 用户会员状态:", state.isVipUser ? "是" : "否");
  740. if (state.isVipUser) {
  741. console.log("[会员状态] 判定依据:", vipElement ? "发现会员图标" : "当前使用会员画质");
  742. }
  743. }
  744. function updateWarnings(panel) {
  745. if (!panel || state.isLoading || !state.vipStatusChecked) return;
  746. const nonVipWarning = panel.querySelector("#non-vip-warning");
  747. const audioWarning = panel.querySelector("#audio-warning");
  748. if (!state.isVipUser && ["8K", "杜比视界", "HDR", "4K", "1080P 高码率", "1080P 60帧"].includes(state.userQualitySetting)) {
  749. nonVipWarning.textContent = "无法使用此会员画质。已自动选择最高可用画质。";
  750. nonVipWarning.style.display = "block";
  751. } else {
  752. nonVipWarning.style.display = "none";
  753. }
  754. if (!state.isVipUser && (state.hiResAudioEnabled || state.dolbyAtmosEnabled)) {
  755. audioWarning.textContent = "非大会员用户不能使用高级音频选项。";
  756. audioWarning.style.display = "block";
  757. } else {
  758. audioWarning.style.display = "none";
  759. }
  760. }
  761. function updateQualityButtons(panel) {
  762. if (!panel) return;
  763. const statusBar = panel.querySelector(".status-bar");
  764. if (state.isLoading) {
  765. statusBar.textContent = "加载中,请稍候...";
  766. statusBar.className = "status-bar";
  767. Utils.queryAll(".quality-button, .toggle-switch", panel).forEach(el => {
  768. el.style.opacity = "0.5";
  769. });
  770. } else {
  771. Utils.queryAll(".quality-button, .toggle-switch", panel).forEach(el => {
  772. el.style.opacity = "1";
  773. });
  774. if (state.vipStatusChecked) {
  775. statusBar.textContent = state.isVipUser
  776. ? "您是大会员用户,可正常使用所有选项。"
  777. : "您不是大会员用户,部分会员选项不可用。";
  778. statusBar.className = "status-bar " + (state.isVipUser ? "vip" : "non-vip");
  779. }
  780. }
  781. Utils.queryAll(".quality-tab", panel).forEach(tab => {
  782. tab.classList.toggle("active", tab.getAttribute("data-tab") === state.activeQualityTab);
  783. });
  784. Utils.queryAll(".quality-section", panel).forEach(section => {
  785. section.classList.toggle("active", section.getAttribute("data-section") === state.activeQualityTab);
  786. });
  787. Utils.queryAll(".quality-button", panel).forEach(button => {
  788. const section = button.closest(".quality-section");
  789. button.classList.remove("active", "vip-quality");
  790. if (
  791. (section.getAttribute("data-section") === "primary" && button.getAttribute("data-quality") === state.userQualitySetting) ||
  792. (section.getAttribute("data-section") === "backup" && button.getAttribute("data-quality") === state.userBackupQualitySetting)
  793. ) {
  794. button.classList.add("active");
  795. if (["8K", "杜比视界", "HDR", "4K", "1080P 高码率", "1080P 60帧"].includes(button.getAttribute("data-quality"))) {
  796. button.classList.add("vip-quality");
  797. }
  798. }
  799. });
  800. const fallbackContainer = panel.querySelector("#highest-quality-fallback-container");
  801. if (fallbackContainer) {
  802. fallbackContainer.classList.toggle("show", state.userBackupQualitySetting !== "最高画质");
  803. fallbackContainer.classList.toggle("hide", state.userBackupQualitySetting === "最高画质");
  804. }
  805. const hiResAudioSwitch = panel.querySelector("#hi-res-audio");
  806. if (hiResAudioSwitch) hiResAudioSwitch.checked = state.hiResAudioEnabled;
  807. const dolbyAtmosSwitch = panel.querySelector("#dolby-atmos");
  808. if (dolbyAtmosSwitch) dolbyAtmosSwitch.checked = state.dolbyAtmosEnabled;
  809. const fallbackCheckbox = panel.querySelector("#highest-quality-fallback");
  810. if (fallbackCheckbox) fallbackCheckbox.checked = state.useHighestQualityFallback;
  811. updateWarnings(panel);
  812. }
  813. function createSettingsPanel() {
  814. const panel = document.createElement("div");
  815. panel.id = "bilibili-quality-selector";
  816. const QUALITIES = ["最高画质", "8K", "杜比视界", "HDR", "4K", "1080P 高码率", "1080P 60帧", "1080P 高清", "720P", "480P", "360P", "默认"];
  817. panel.innerHTML = `
  818. <h2>画质设置</h2>
  819. <a href="https://github.com/AHCorn/Bilibili-Auto-Quality/" target="_blank" class="github-link">
  820. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  821. <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
  822. </svg>
  823. </a>
  824. <div class="status-bar"></div>
  825. <div class="quality-tabs">
  826. <div class="quality-tab ${state.activeQualityTab === 'primary' ? 'active' : ''}" data-tab="primary">首选画质</div>
  827. <div class="quality-tab ${state.activeQualityTab === 'backup' ? 'active' : ''}" data-tab="backup">备选画质</div>
  828. </div>
  829. <div class="quality-section ${state.activeQualityTab === 'primary' ? 'active' : ''}" data-section="primary">
  830. <div class="quality-group">
  831. ${QUALITIES.map(q => `<button class="quality-button" data-quality="${q}">${q}</button>`).join('')}
  832. </div>
  833. </div>
  834. <div class="quality-section ${state.activeQualityTab === 'backup' ? 'active' : ''}" data-section="backup">
  835. <div class="quality-group">
  836. ${QUALITIES.map(q => `<button class="quality-button" data-quality="${q}">${q}</button>`).join('')}
  837. </div>
  838. </div>
  839. <div id="non-vip-warning" class="warning" style="display:none;"></div>
  840. <div id="quality-warning" class="warning" style="display:none;"></div>
  841. <div class="toggle-switch">
  842. <label for="hi-res-audio">Hi-Res 音质</label>
  843. <label class="switch">
  844. <input type="checkbox" id="hi-res-audio">
  845. <span class="slider vip-audio"></span>
  846. </label>
  847. </div>
  848. <div class="toggle-switch">
  849. <label for="dolby-atmos">杜比全景声</label>
  850. <label class="switch">
  851. <input type="checkbox" id="dolby-atmos">
  852. <span class="slider vip-audio"></span>
  853. </label>
  854. </div>
  855. <div id="audio-warning" class="warning" style="display:none;"></div>
  856. <div class="toggle-switch ${state.userBackupQualitySetting !== "最高画质" ? 'show' : 'hide'}" id="highest-quality-fallback-container">
  857. <label for="highest-quality-fallback">找不到备选画质时使用最高画质</label>
  858. <label class="switch">
  859. <input type="checkbox" id="highest-quality-fallback" ${state.useHighestQualityFallback ? 'checked' : ''}>
  860. <span class="slider"></span>
  861. </label>
  862. </div>
  863. <div class="toggle-switch">
  864. <label for="inject-quality-button">注入画质选项</label>
  865. <label class="switch">
  866. <input type="checkbox" id="inject-quality-button" ${state.injectQualityButton ? 'checked' : ''}>
  867. <span class="slider"></span>
  868. </label>
  869. </div>
  870. `;
  871. panel.addEventListener("click", function (e) {
  872. const target = e.target;
  873. if (target.classList.contains("quality-tab") && !state.isLoading) {
  874. const tabName = target.getAttribute("data-tab");
  875. state.activeQualityTab = tabName;
  876. GM_setValue("activeQualityTab", tabName);
  877. Utils.queryAll(".quality-tab", panel).forEach(tab =>
  878. tab.classList.toggle("active", tab.getAttribute("data-tab") === tabName)
  879. );
  880. Utils.queryAll(".quality-section", panel).forEach(section =>
  881. section.classList.toggle("active", section.getAttribute("data-section") === tabName)
  882. );
  883. } else if (target.classList.contains("quality-button") && !state.isLoading) {
  884. const section = target.closest(".quality-section");
  885. const quality = target.getAttribute("data-quality");
  886. if (section.getAttribute("data-section") === "primary") {
  887. state.userQualitySetting = quality;
  888. GM_setValue("qualitySetting", quality);
  889. } else {
  890. state.userBackupQualitySetting = quality;
  891. GM_setValue("backupQualitySetting", quality);
  892. const fallbackContainer = Utils.query("#highest-quality-fallback-container", panel);
  893. if (fallbackContainer) {
  894. fallbackContainer.classList.toggle("show", quality !== "最高画质");
  895. fallbackContainer.classList.toggle("hide", quality === "最高画质");
  896. }
  897. }
  898. updateQualityButtons(panel);
  899. selectQualityBasedOnSetting();
  900. }
  901. });
  902. panel.querySelector("#highest-quality-fallback").addEventListener("change", function (e) {
  903. if (!state.isLoading) {
  904. state.useHighestQualityFallback = e.target.checked;
  905. GM_setValue("useHighestQualityFallback", state.useHighestQualityFallback);
  906. selectQualityBasedOnSetting();
  907. }
  908. });
  909. panel.querySelector("#hi-res-audio").addEventListener("change", function (e) {
  910. if (!state.isLoading) {
  911. state.hiResAudioEnabled = e.target.checked;
  912. GM_setValue("hiResAudio", state.hiResAudioEnabled);
  913. updateQualityButtons(panel);
  914. selectQualityBasedOnSetting();
  915. }
  916. });
  917. panel.querySelector("#dolby-atmos").addEventListener("change", function (e) {
  918. if (!state.isLoading) {
  919. state.dolbyAtmosEnabled = e.target.checked;
  920. GM_setValue("dolbyAtmos", state.dolbyAtmosEnabled);
  921. updateQualityButtons(panel);
  922. selectQualityBasedOnSetting();
  923. }
  924. });
  925. panel.querySelector("#inject-quality-button").addEventListener("change", function (e) {
  926. if (!state.isLoading) {
  927. state.injectQualityButton = e.target.checked;
  928. GM_setValue("injectQualityButton", state.injectQualityButton);
  929. const qualityControlElement = document.querySelector(".bpx-player-ctrl-quality");
  930. if (qualityControlElement) {
  931. if (state.injectQualityButton) {
  932. let settingsButton = qualityControlElement.previousElementSibling;
  933. if (!settingsButton || !settingsButton.classList.contains("quality-settings-btn")) {
  934. settingsButton = document.createElement("div");
  935. settingsButton.className = "bpx-player-ctrl-btn quality-settings-btn";
  936. settingsButton.innerHTML = '<div class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="15" rx="2" ry="2"></rect><polyline points="8 20 12 20 16 20"></polyline></svg></span></div>';
  937. settingsButton.addEventListener("click", toggleSettingsPanel);
  938. qualityControlElement.parentElement.insertBefore(settingsButton, qualityControlElement);
  939. }
  940. } else {
  941. const existingButton = qualityControlElement.previousElementSibling;
  942. if (existingButton && existingButton.classList.contains("quality-settings-btn")) {
  943. existingButton.remove();
  944. }
  945. }
  946. }
  947. }
  948. });
  949. document.body.appendChild(panel);
  950. updateQualityButtons(panel);
  951. }
  952. function selectQualityBasedOnSetting() {
  953. if (state.isLivePage) {
  954. selectLiveQuality();
  955. } else {
  956. selectVideoQuality();
  957. }
  958. }
  959. class TaskQueue {
  960. constructor() {
  961. this.currentTaskId = 0;
  962. this.activeTimeouts = new Map();
  963. this.activeTask = null;
  964. }
  965.  
  966. generateTaskId() {
  967. return ++this.currentTaskId;
  968. }
  969.  
  970. clearPreviousTasks() {
  971. this.activeTimeouts.forEach((timeout, taskId) => {
  972. clearTimeout(timeout);
  973. console.log(`[任务管理] 取消等待任务 #${taskId}`);
  974. });
  975. this.activeTimeouts.clear();
  976.  
  977. if (this.activeTask) {
  978. console.log(`[任务管理] 标记运行中任务 #${this.activeTask} 为已取消`);
  979. this.activeTask = null;
  980. }
  981. }
  982.  
  983. isTaskCancelled(taskId) {
  984. // 只检查是否是当前最新任务
  985. if (taskId !== this.currentTaskId) {
  986. console.log(`[任务管理] 任务 #${taskId} 已过期,当前任务 #${this.currentTaskId}`);
  987. return true;
  988. }
  989. return false;
  990. }
  991.  
  992. async scheduleTask(taskId, task, delay = 0) {
  993. if (this.isTaskCancelled(taskId)) return;
  994.  
  995. // 设置当前活动任务
  996. this.activeTask = taskId;
  997.  
  998. try {
  999. if (delay > 0) {
  1000. await new Promise((resolve, reject) => {
  1001. const timeout = setTimeout(async () => {
  1002. this.activeTimeouts.delete(taskId);
  1003. if (!this.isTaskCancelled(taskId)) {
  1004. try {
  1005. await task();
  1006. resolve();
  1007. } catch (error) {
  1008. reject(error);
  1009. }
  1010. } else {
  1011. console.log(`[任务管理] 延迟任务 #${taskId} 已取消`);
  1012. resolve();
  1013. }
  1014. }, delay);
  1015. this.activeTimeouts.set(taskId, timeout);
  1016. });
  1017. } else {
  1018. await task();
  1019. }
  1020. } finally {
  1021. if (this.activeTask === taskId) {
  1022. this.activeTask = null;
  1023. }
  1024. }
  1025. }
  1026. }
  1027.  
  1028. const taskQueue = new TaskQueue();
  1029.  
  1030. async function setAudioQuality(retryCount = 0) {
  1031. const taskId = taskQueue.currentTaskId;
  1032. if (taskQueue.isTaskCancelled(taskId)) return;
  1033.  
  1034. if (!state.isVipUser) {
  1035. console.log("[音质设置] 非会员用户,略过音质设置");
  1036. return;
  1037. }
  1038.  
  1039. const maxRetries = state.devModeEnabled ? state.devModeAudioRetries : 2;
  1040. const baseDelay = state.devModeEnabled ? state.devModeAudioDelay : 4000;
  1041. const retryInterval = baseDelay * Math.pow(2, retryCount);
  1042.  
  1043. function tryToggle(buttonSelector, shouldEnable, label) {
  1044. if (taskQueue.isTaskCancelled(taskId)) return false;
  1045.  
  1046. const button = document.querySelector(buttonSelector);
  1047. if (!button) return false;
  1048. const isActive = button.classList.contains("bpx-state-active");
  1049. if (shouldEnable && !isActive) {
  1050. console.log(`[音质设置] 检测到需开启${label} (第${retryCount + 1}次尝试)`);
  1051. button.click();
  1052. return true;
  1053. } else if (!shouldEnable && isActive) {
  1054. console.log(`[音质设置] 检测到需关闭${label} (第${retryCount + 1}次尝试)`);
  1055. button.click();
  1056. return true;
  1057. }
  1058. return false;
  1059. }
  1060.  
  1061. console.log(`[音质设置] 开始第${retryCount + 1}次设置`);
  1062. const hiResChanged = tryToggle(".bpx-player-ctrl-flac", state.hiResAudioEnabled, "Hi-Res音质");
  1063. const dolbyChanged = tryToggle(".bpx-player-ctrl-dolby", state.dolbyAtmosEnabled, "杜比全景声");
  1064.  
  1065. if ((hiResChanged || dolbyChanged) && retryCount < maxRetries) {
  1066. console.log(`[音质设置] 等待 ${retryInterval / 1000} 秒后验证设置`);
  1067. await taskQueue.scheduleTask(taskId, async () => {
  1068. if (!taskQueue.isTaskCancelled(taskId)) {
  1069. await setAudioQuality(retryCount + 1);
  1070. }
  1071. }, retryInterval);
  1072. } else {
  1073. console.log("[音质设置] 设置完成或达到最大重试次数");
  1074. }
  1075. }
  1076.  
  1077. async function selectVideoQuality() {
  1078. const currentQualityEl = document.querySelector(".bpx-player-ctrl-quality-menu-item.bpx-state-active .bpx-player-ctrl-quality-text");
  1079. const currentQuality = currentQualityEl ? currentQualityEl.textContent : "";
  1080. console.log("[画质设置] 当前画质:", currentQuality);
  1081. console.log("[画质设置] 目标画质:", state.userQualitySetting);
  1082.  
  1083. // 确保会员状态已检查
  1084. if (!state.vipStatusChecked) {
  1085. console.log("[画质设置] 等待会员状态检查完成");
  1086. return;
  1087. }
  1088.  
  1089. const qualityItems = document.querySelectorAll(".bpx-player-ctrl-quality-menu .bpx-player-ctrl-quality-menu-item");
  1090. let availableQualities = Array.from(qualityItems).map(item => ({
  1091. name: item.textContent.trim(),
  1092. element: item,
  1093. isVipOnly: !!item.querySelector(".bpx-player-ctrl-quality-badge-bigvip"),
  1094. isFreeNow: !!(item.querySelector(".bpx-player-ctrl-quality-badge-bigvip") &&
  1095. item.querySelector(".bpx-player-ctrl-quality-badge-bigvip").textContent.includes("限免中"))
  1096. }));
  1097.  
  1098. if (state.disableHDROption) {
  1099. availableQualities = availableQualities.filter(q => q.name.indexOf("HDR") === -1);
  1100. }
  1101. // 未登录(不可用)模式下过滤掉高于1080P的画质
  1102. if (state.devModeEnabled && state.devModeNoLoginStatus) {
  1103. availableQualities = availableQualities.filter(q => {
  1104. const name = q.name.trim();
  1105. return !name.includes("8K") && !name.includes("杜比视界") && !name.includes("HDR") && !name.includes("4K");
  1106. });
  1107. console.log("[未登录(不可用)模式] 已过滤高于1080P的画质,可用画质:", availableQualities.map(q => q.name));
  1108. }
  1109.  
  1110. console.log("[画质设置] 可用画质:", availableQualities.map(q => q.name));
  1111. console.log("[画质设置] 会员状态:", state.isVipUser ? "是" : "否");
  1112.  
  1113. const qualityPreferences = ["8K", "杜比视界", "HDR", "4K", "1080P 高码率", "1080P 60帧", "1080P 高清", "720P 60帧", "720P", "480P", "360P", "默认"];
  1114. availableQualities.sort((a, b) => {
  1115. function getQualityIndex(name) {
  1116. for (let i = 0; i < qualityPreferences.length; i++) {
  1117. if (name.includes(qualityPreferences[i])) return i;
  1118. }
  1119. return qualityPreferences.length;
  1120. }
  1121. return getQualityIndex(a.name) - getQualityIndex(b.name);
  1122. });
  1123. let targetQuality;
  1124. function cleanQuality(q) { return q ? q.replace(/大会员|限免中/g, '').trim() : ""; }
  1125. if (state.userQualitySetting === "最高画质") {
  1126. const hasFreeVip = availableQualities.some(q => q.isFreeNow);
  1127. if (state.isVipUser || hasFreeVip) {
  1128. targetQuality = availableQualities[0];
  1129. } else {
  1130. targetQuality = availableQualities.find(q => cleanQuality(q.name).includes(state.userBackupQualitySetting));
  1131. if (!targetQuality && state.useHighestQualityFallback)
  1132. targetQuality = availableQualities.find(q => !q.isVipOnly);
  1133. if (!targetQuality && !state.useHighestQualityFallback) {
  1134. console.log("[画质设置] 未找到备选画质,保持当前画质");
  1135. await setAudioQuality();
  1136. return;
  1137. }
  1138. }
  1139. } else if (state.userQualitySetting === "默认") {
  1140. console.log("[画质设置] 使用默认画质");
  1141. await setAudioQuality();
  1142. return;
  1143. } else {
  1144. targetQuality = availableQualities.find(q => cleanQuality(q.name).includes(state.userQualitySetting));
  1145. if (!targetQuality) {
  1146. console.log("[画质设置] 未找到目标画质 " + state.userQualitySetting + ", 尝试使用备选画质");
  1147. targetQuality = availableQualities.find(q => cleanQuality(q.name).includes(state.userBackupQualitySetting));
  1148. if (!targetQuality && state.useHighestQualityFallback)
  1149. targetQuality = state.isVipUser ? availableQualities[0] : availableQualities.find(q => !q.isVipOnly);
  1150. if (!targetQuality && !state.useHighestQualityFallback) {
  1151. console.log("[画质设置] 未找到备选画质,保持当前画质");
  1152. await setAudioQuality();
  1153. return;
  1154. }
  1155. }
  1156. }
  1157. console.log("[画质设置] 实际目标画质: " + targetQuality.name);
  1158. targetQuality.element.click();
  1159. if (state.qualityDoubleCheck) {
  1160. await Utils.delay(state.devDoubleCheckDelay);
  1161. const currentQualityAfterSwitchEl = document.querySelector(".bpx-player-ctrl-quality-menu-item.bpx-state-active .bpx-player-ctrl-quality-text");
  1162. const currentQualityAfterSwitch = currentQualityAfterSwitchEl ? currentQualityAfterSwitchEl.textContent : "";
  1163. if (currentQualityAfterSwitch && cleanQuality(currentQualityAfterSwitch) !== cleanQuality(targetQuality.name)) {
  1164. console.log("[画质设置] 画质切换未成功,执行二次切换...");
  1165. targetQuality.element.click();
  1166. } else {
  1167. console.log("[画质设置] 画质切换验证成功,当前画质: " + currentQualityAfterSwitch);
  1168. }
  1169. await setAudioQuality();
  1170. }
  1171. }
  1172. function createLiveSettingsPanel() {
  1173. const panel = document.createElement("div");
  1174. panel.id = "bilibili-live-quality-selector";
  1175. function updatePanel() {
  1176. const qualityCandidates = unsafeWindow.livePlayer.getPlayerInfo().qualityCandidates;
  1177. const LIVE_QUALITIES = ["原画", "蓝光", "超清", "高清"];
  1178. const lineSelector = document.querySelector(".YccudlUCmLKcUTg_yzKN");
  1179. const lines = lineSelector ? Array.from(lineSelector.children).map(li => li.textContent) : ["加载中..."];
  1180. const currentLineIndex = lineSelector ? Array.from(lineSelector.children).findIndex(li => li.classList.contains("fG2r2piYghHTQKQZF8bl")) : 0;
  1181. panel.innerHTML = `
  1182. <h2>直播设置</h2>
  1183. <a href="https://github.com/AHCorn/Bilibili-Auto-Quality/" target="_blank" class="github-link">
  1184. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  1185. <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
  1186. </svg>
  1187. </a>
  1188. <div class="quality-section-title">线路选择</div>
  1189. <div class="line-group">
  1190. ${lines.map((line, index) => `<button class="line-button ${index === currentLineIndex ? 'active' : ''}" data-line="${index}">${line}</button>`).join('')}
  1191. </div>
  1192. <div class="quality-section-title">画质选择</div>
  1193. <div class="live-quality-group">
  1194. ${LIVE_QUALITIES.map(quality => `<button class="live-quality-button ${quality === state.userLiveQualitySetting ? 'active' : ''}" data-quality="${quality}">${quality}</button>`).join('')}
  1195. </div>
  1196. `;
  1197. panel.querySelectorAll(".line-button").forEach(button => {
  1198. button.addEventListener("click", () => {
  1199. const lineIndex = parseInt(button.getAttribute("data-line"), 10);
  1200. changeLine(lineIndex);
  1201. });
  1202. });
  1203. panel.querySelectorAll(".live-quality-button").forEach(button => {
  1204. button.addEventListener("click", () => {
  1205. state.userLiveQualitySetting = button.getAttribute("data-quality");
  1206. GM_setValue("liveQualitySetting", state.userLiveQualitySetting);
  1207. updatePanel();
  1208. selectLiveQuality();
  1209. });
  1210. });
  1211. }
  1212. document.body.appendChild(panel);
  1213. panel.updatePanel = updatePanel;
  1214. updatePanel();
  1215. }
  1216. async function selectLiveQuality() {
  1217. await new Promise(resolve => {
  1218. const timer = setInterval(() => {
  1219. if (
  1220. unsafeWindow.livePlayer &&
  1221. unsafeWindow.livePlayer.getPlayerInfo &&
  1222. unsafeWindow.livePlayer.getPlayerInfo().playurl &&
  1223. unsafeWindow.livePlayer.switchQuality
  1224. ) {
  1225. clearInterval(timer);
  1226. resolve();
  1227. }
  1228. }, 1000);
  1229. });
  1230. const qualityCandidates = unsafeWindow.livePlayer.getPlayerInfo().qualityCandidates;
  1231. console.log("[直播画质] 可用画质选项:", qualityCandidates.map((q, i) => `${i + 1}. ${q.desc} (qn: ${q.qn})`));
  1232. console.log("[直播画质] 选择的画质:", state.userLiveQualitySetting);
  1233. let targetQuality = qualityCandidates.find(q => q.desc === state.userLiveQualitySetting);
  1234. if (!targetQuality) {
  1235. const qualityPriority = ["原画", "蓝光", "超清", "高清"];
  1236. for (let quality of qualityPriority) {
  1237. targetQuality = qualityCandidates.find(q => q.desc === quality);
  1238. if (targetQuality) break;
  1239. }
  1240. }
  1241. if (!targetQuality) targetQuality = qualityCandidates[0];
  1242. console.log("[直播画质] 目标画质:", targetQuality.desc, "(qn:", targetQuality.qn, ")");
  1243. function switchQuality() {
  1244. const currentQualityNumber = unsafeWindow.livePlayer.getPlayerInfo().quality;
  1245. if (currentQualityNumber !== targetQuality.qn) {
  1246. unsafeWindow.livePlayer.switchQuality(targetQuality.qn);
  1247. console.log("[直播画质] 已切换到目标画质:", targetQuality.desc);
  1248. state.userLiveQualitySetting = targetQuality.desc;
  1249. GM_setValue("liveQualitySetting", state.userLiveQualitySetting);
  1250. updateLiveSettingsPanel();
  1251. if (state.liveQualityDoubleCheck) {
  1252. setTimeout(() => {
  1253. const currentQualityAfterSwitch = unsafeWindow.livePlayer.getPlayerInfo().quality;
  1254. if (currentQualityAfterSwitch !== targetQuality.qn) {
  1255. console.log("[直播画质] 画质切换可能未成功,执行二次切换...");
  1256. unsafeWindow.livePlayer.switchQuality(targetQuality.qn);
  1257. } else {
  1258. console.log("[直播画质] 画质切换验证成功,当前画质:", targetQuality.desc);
  1259. }
  1260. }, 5000);
  1261. }
  1262. } else {
  1263. console.log("[直播画质] 已经是目标画质:", targetQuality.desc);
  1264. }
  1265. }
  1266. switchQuality();
  1267. }
  1268. function changeLine(lineIndex) {
  1269. const lineSelector = document.querySelector(".YccudlUCmLKcUTg_yzKN");
  1270. if (lineSelector && lineSelector.children[lineIndex]) {
  1271. lineSelector.children[lineIndex].click();
  1272. console.log("[直播线路] 已切换到线路:", lineSelector.children[lineIndex].textContent);
  1273. const panel = document.getElementById("bilibili-live-quality-selector");
  1274. if (panel) {
  1275. Utils.queryAll(".line-button", panel).forEach((button, index) => {
  1276. button.classList.toggle("active", index === lineIndex);
  1277. });
  1278. }
  1279. } else {
  1280. console.log("[直播线路] 无法切换线路");
  1281. }
  1282. }
  1283. function observeLineChanges() {
  1284. const lineSelector = document.querySelector(".YccudlUCmLKcUTg_yzKN");
  1285. if (lineSelector) {
  1286. const observer = new MutationObserver(Utils.debounce(mutations => {
  1287. mutations.forEach(mutation => {
  1288. if (mutation.type === "attributes" && mutation.attributeName === "class") {
  1289. Array.from(lineSelector.children).forEach(li => {
  1290. if (li.classList.contains("fG2r2piYghHTQKQZF8bl")) updateLiveSettingsPanel();
  1291. });
  1292. }
  1293. });
  1294. }, 300));
  1295. observer.observe(lineSelector, { attributes: true, subtree: true, attributeFilter: ["class"] });
  1296. }
  1297. }
  1298. function updateLiveSettingsPanel() {
  1299. const panel = document.getElementById("bilibili-live-quality-selector");
  1300. if (panel && typeof panel.updatePanel === "function") panel.updatePanel();
  1301. }
  1302. function createDevSettingsPanel() {
  1303. const panel = document.createElement("div");
  1304. panel.id = "bilibili-dev-settings";
  1305. panel.innerHTML = `
  1306. <h2>开发者设置</h2>
  1307. <a href="https://github.com/AHCorn/Bilibili-Auto-Quality/" target="_blank" class="github-link">
  1308. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  1309. <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
  1310. </svg>
  1311. </a>
  1312. <div class="dev-warning">以下选项的错误配置可能会影响脚本正常工作</div>
  1313. <div class="toggle-switch">
  1314. <label for="dev-mode">
  1315. 开发者模式
  1316. <div class="description">启用后可以使用开发者选项</div>
  1317. </label>
  1318. <label class="switch">
  1319. <input type="checkbox" id="dev-mode" ${state.devModeEnabled ? 'checked' : ''}>
  1320. <span class="slider"></span>
  1321. </label>
  1322. </div>
  1323. <div class="toggle-switch">
  1324. <label for="quality-double-check">
  1325. 视频画质二次验证
  1326. <div class="description">启用后将在视频画质切换后等待指定时间后进行验证</div>
  1327. </label>
  1328. <label class="switch">
  1329. <input type="checkbox" id="quality-double-check" ${state.qualityDoubleCheck ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1330. <span class="slider"></span>
  1331. </label>
  1332. </div>
  1333. <div class="toggle-switch">
  1334. <label for="live-quality-double-check">
  1335. 直播画质二次验证
  1336. <div class="description">启用后将在直播画质切换后等待指定时间后进行验证</div>
  1337. </label>
  1338. <label class="switch">
  1339. <input type="checkbox" id="live-quality-double-check" ${state.liveQualityDoubleCheck ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1340. <span class="slider"></span>
  1341. </label>
  1342. </div>
  1343. <div class="toggle-switch">
  1344. <label for="dev-vip">
  1345. 模拟大会员状态
  1346. <div class="description">模拟脚本所识别到的大会员状态,<b>并非破解</b></div>
  1347. </label>
  1348. <label class="switch">
  1349. <input type="checkbox" id="dev-vip" ${state.devModeVipStatus ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1350. <span class="slider"></span>
  1351. </label>
  1352. </div>
  1353. <div class="toggle-switch">
  1354. <label for="dev-no-login">
  1355. 未登录(不可用)模式
  1356. <div class="description">启用后不等待头像加载,默认最高画质1080P</div>
  1357. </label>
  1358. <label class="switch">
  1359. <input type="checkbox" id="dev-no-login" ${state.devModeNoLoginStatus ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1360. <span class="slider"></span>
  1361. </label>
  1362. </div>
  1363. <div class="toggle-switch">
  1364. <label for="dev-ua">
  1365. 禁用 UA 修改
  1366. <div class="description">禁用后部分旧版本浏览器可能无法解锁画质</div>
  1367. </label>
  1368. <label class="switch">
  1369. <input type="checkbox" id="dev-ua" ${state.devModeDisableUA ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1370. <span class="slider"></span>
  1371. </label>
  1372. </div>
  1373. <div class="toggle-switch">
  1374. <label for="disable-hdr">
  1375. 禁用 HDR 选项
  1376. <div class="description">为没有 HDR 设备的用户屏蔽该画质</div>
  1377. </label>
  1378. <label class="switch">
  1379. <input type="checkbox" id="disable-hdr" ${state.disableHDROption ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1380. <span class="slider"></span>
  1381. </label>
  1382. </div>
  1383. <div class="toggle-switch">
  1384. <label for="remove-quality-button">
  1385. 移除清晰度按钮
  1386. <div class="description">启用后将隐藏播放器的清晰度按钮</div>
  1387. </label>
  1388. <label class="switch">
  1389. <input type="checkbox" id="remove-quality-button" ${state.takeOverQualityControl ? 'checked' : ''} ${!state.devModeEnabled ? 'disabled' : ''}>
  1390. <span class="slider"></span>
  1391. </label>
  1392. </div>
  1393. <div class="input-group ${!state.devModeEnabled ? 'disabled' : ''}">
  1394. <label for="dev-double-check-delay">
  1395. 二次验证等待时间
  1396. <div class="description">画质切换后等待验证的时间</div>
  1397. </label>
  1398. <input type="number" id="dev-double-check-delay" value="${state.devDoubleCheckDelay}" min="0" max="20000" step="100" ${!state.devModeEnabled ? 'disabled' : ''}>
  1399. <span class="unit">毫秒</span>
  1400. </div>
  1401. <div class="input-group ${!state.devModeEnabled ? 'disabled' : ''}">
  1402. <label for="dev-audio-delay">
  1403. 音质切换初始延迟
  1404. <div class="description">音质切换重试的初始等待时间,后续等待时间将以此为基数进行指数退避</div>
  1405. </label>
  1406. <input type="number" id="dev-audio-delay" value="${state.devModeAudioDelay}" min="0" max="10000" step="100" ${!state.devModeEnabled ? 'disabled' : ''}>
  1407. <span class="unit">毫秒</span>
  1408. </div>
  1409. <div class="input-group ${!state.devModeEnabled ? 'disabled' : ''}">
  1410. <label for="dev-audio-retries">
  1411. 音质切换重试次数
  1412. <div class="description">音质切换失败后的重试次数</div>
  1413. </label>
  1414. <input type="number" id="dev-audio-retries" value="${state.devModeAudioRetries}" min="0" max="5" step="1" ${!state.devModeEnabled ? 'disabled' : ''}>
  1415. <span class="unit" style="margin-left: 15px;">次</span>
  1416. </div>
  1417. <div id="dev-warning" class="warning" style="display: none;"></div>
  1418. <button class="refresh-button">确认并刷新页面</button>
  1419. `;
  1420. document.body.appendChild(panel);
  1421. panel.querySelector('#dev-mode').addEventListener('change', function (e) {
  1422. state.devModeEnabled = e.target.checked;
  1423. GM_setValue("devModeEnabled", state.devModeEnabled);
  1424. const devOptions = panel.querySelectorAll('input[type="checkbox"]:not(#dev-mode), input[type="number"]');
  1425. devOptions.forEach(option => {
  1426. option.disabled = !state.devModeEnabled;
  1427. });
  1428. const inputGroups = panel.querySelectorAll('.input-group');
  1429. inputGroups.forEach(group => {
  1430. if (state.devModeEnabled) {
  1431. group.classList.remove('disabled');
  1432. } else {
  1433. group.classList.add('disabled');
  1434. }
  1435. });
  1436. });
  1437. panel.querySelector('#quality-double-check').addEventListener('change', function (e) {
  1438. state.qualityDoubleCheck = e.target.checked;
  1439. GM_setValue("qualityDoubleCheck", state.qualityDoubleCheck);
  1440. });
  1441. panel.querySelector('#live-quality-double-check').addEventListener('change', function (e) {
  1442. state.liveQualityDoubleCheck = e.target.checked;
  1443. GM_setValue("liveQualityDoubleCheck", state.liveQualityDoubleCheck);
  1444. });
  1445. panel.querySelector('#dev-vip').addEventListener('change', function (e) {
  1446. state.devModeVipStatus = e.target.checked;
  1447. GM_setValue("devModeVipStatus", state.devModeVipStatus);
  1448. });
  1449. panel.querySelector('#dev-no-login').addEventListener('change', function (e) {
  1450. state.devModeNoLoginStatus = e.target.checked;
  1451. GM_setValue("devModeNoLoginStatus", state.devModeNoLoginStatus);
  1452. });
  1453. panel.querySelector('#dev-ua').addEventListener('change', function (e) {
  1454. state.devModeDisableUA = e.target.checked;
  1455. GM_setValue("devModeDisableUA", state.devModeDisableUA);
  1456. });
  1457. panel.querySelector('#disable-hdr').addEventListener('change', function (e) {
  1458. state.disableHDROption = e.target.checked;
  1459. GM_setValue("disableHDR", state.disableHDROption);
  1460. });
  1461. panel.querySelector('#remove-quality-button').addEventListener('change', function (e) {
  1462. state.takeOverQualityControl = e.target.checked;
  1463. GM_setValue("takeOverQualityControl", state.takeOverQualityControl);
  1464. });
  1465. panel.querySelector('.refresh-button').addEventListener('click', function () {
  1466. location.reload();
  1467. });
  1468. return panel;
  1469. }
  1470. function togglePanel(panelId, createPanelFunc, updateFunc) {
  1471. let panel = document.getElementById(panelId);
  1472. if (!panel) {
  1473. createPanelFunc();
  1474. panel = document.getElementById(panelId);
  1475. }
  1476. function handleOutsideClick(event) {
  1477. if (panel && !panel.contains(event.target)) {
  1478. panel.classList.remove("show");
  1479. document.removeEventListener("mousedown", handleOutsideClick);
  1480. }
  1481. }
  1482. if (!panel.classList.contains("show")) {
  1483. ["bilibili-quality-selector", "bilibili-live-quality-selector", "bilibili-dev-settings"].forEach(id => {
  1484. if (id !== panelId) {
  1485. const otherPanel = document.getElementById(id);
  1486. if (otherPanel && otherPanel.classList.contains("show")) otherPanel.classList.remove("show");
  1487. }
  1488. });
  1489. panel.classList.add("show");
  1490. document.addEventListener("mousedown", handleOutsideClick);
  1491. } else {
  1492. panel.classList.remove("show");
  1493. document.removeEventListener("mousedown", handleOutsideClick);
  1494. }
  1495. if (updateFunc) updateFunc(panel);
  1496. }
  1497. function toggleSettingsPanel() {
  1498. togglePanel("bilibili-quality-selector", createSettingsPanel, updateQualityButtons);
  1499. }
  1500. function toggleLiveSettingsPanel() {
  1501. togglePanel("bilibili-live-quality-selector", createLiveSettingsPanel, updateLiveSettingsPanel);
  1502. }
  1503. function toggleDevSettingsPanel() {
  1504. togglePanel("bilibili-dev-settings", createDevSettingsPanel, function (panel) {
  1505. const removeQualityButton = panel.querySelector('#remove-quality-button');
  1506. if (removeQualityButton) removeQualityButton.checked = state.takeOverQualityControl;
  1507. });
  1508. }
  1509. GM_registerMenuCommand("设置面板", function () {
  1510. checkIfLivePage();
  1511. if (state.isLivePage) toggleLiveSettingsPanel();
  1512. else toggleSettingsPanel();
  1513. });
  1514. GM_registerMenuCommand("开发者选项", toggleDevSettingsPanel);
  1515. window.addEventListener("load", function () {
  1516. if (state.isLivePage) {
  1517. observeLineChanges();
  1518. }
  1519. });
  1520. window.onload = function () {
  1521. checkIfLivePage();
  1522. if (state.isLivePage) {
  1523. selectLiveQuality().then(() => { createLiveSettingsPanel(); });
  1524. } else {
  1525. const DOM = {
  1526. selectors: {
  1527. qualityControl: '.bpx-player-ctrl-quality',
  1528. qualityButton: '.bpx-player-ctrl-btn.bpx-player-ctrl-quality',
  1529. playerControls: '.bpx-player-control-wrap',
  1530. headerAvatar: '.v-popover-wrap.header-avatar-wrap',
  1531. vipIcon: '.bili-avatar-icon.bili-avatar-right-icon.bili-avatar-icon-big-vip',
  1532. qualityMenu: '.bpx-player-ctrl-quality-menu',
  1533. qualityMenuItem: '.bpx-player-ctrl-quality-menu-item',
  1534. activeQuality: '.bpx-player-ctrl-quality-menu-item.bpx-state-active .bpx-player-ctrl-quality-text',
  1535. controlBottomRight: '.bpx-player-control-bottom-right'
  1536. },
  1537. elements: {},
  1538. get(key) {
  1539. if (!this.elements[key]) this.elements[key] = document.querySelector(this.selectors[key]);
  1540. return this.elements[key];
  1541. },
  1542. refresh(key) {
  1543. this.elements[key] = document.querySelector(this.selectors[key]);
  1544. return this.elements[key];
  1545. },
  1546. clear() { this.elements = {}; }
  1547. };
  1548.  
  1549. function hideQualityButton() {
  1550. const qualityControl = DOM.get('qualityButton');
  1551. if (qualityControl && state.takeOverQualityControl) qualityControl.classList.add('quality-button-hidden');
  1552. }
  1553.  
  1554. function initQualitySettingsButton() {
  1555. const controlBottomRight = DOM.get('controlBottomRight');
  1556. const qualityControl = DOM.get('qualityControl');
  1557. if (controlBottomRight && qualityControl && state.injectQualityButton) {
  1558. let existingSettingsBtn = controlBottomRight.querySelector('.quality-settings-btn');
  1559. if (!existingSettingsBtn) {
  1560. const settingsButton = document.createElement('div');
  1561. settingsButton.className = 'bpx-player-ctrl-btn quality-settings-btn';
  1562. settingsButton.innerHTML = '<div class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="15" rx="2" ry="2"></rect><polyline points="8 20 12 20 16 20"></polyline></svg></span></div>';
  1563. settingsButton.addEventListener("click", toggleSettingsPanel);
  1564. qualityControl.parentElement.insertBefore(settingsButton, qualityControl);
  1565. }
  1566. }
  1567. }
  1568.  
  1569. hideQualityButton();
  1570. initQualitySettingsButton();
  1571.  
  1572. window.playerControlsObserver = new MutationObserver(function () {
  1573. const qualityControl = DOM.refresh('qualityControl');
  1574. if (qualityControl) {
  1575. hideQualityButton();
  1576. initQualitySettingsButton();
  1577. }
  1578. });
  1579.  
  1580. const playerControls = DOM.get('playerControls');
  1581. if (playerControls) {
  1582. window.playerControlsObserver.observe(playerControls, { childList: true, subtree: true });
  1583. }
  1584.  
  1585. const vipCheckObserver = new MutationObserver((mutations, observer) => {
  1586. // 未登录(不可用)模式不等待头像元素
  1587. if (state.devModeEnabled && state.devModeNoLoginStatus) {
  1588. observer.disconnect();
  1589. console.log("[未登录(不可用)模式] 跳过等待头像元素,直接执行画质设置");
  1590. waitForPlayerWithBackoff(async () => {
  1591. state.isLoading = false;
  1592. await checkVipStatusAsync();
  1593. await selectVideoQuality();
  1594. updateQualityButtons(Utils.query("#bilibili-quality-selector"));
  1595. }, 5, 1000, 0);
  1596. return;
  1597. }
  1598. const headerAvatar = document.querySelector(".v-popover-wrap.header-avatar-wrap");
  1599. if (headerAvatar) {
  1600. observer.disconnect();
  1601.  
  1602. let timeoutId = null;
  1603. let hasExecuted = false;
  1604.  
  1605. const executeQualitySettings = () => {
  1606. if (hasExecuted) return;
  1607. hasExecuted = true;
  1608. if (timeoutId) {
  1609. clearTimeout(timeoutId);
  1610. timeoutId = null;
  1611. }
  1612. waitForPlayerWithBackoff(async () => {
  1613. state.isLoading = false;
  1614. await checkVipStatusAsync();
  1615. await selectVideoQuality();
  1616. updateQualityButtons(Utils.query("#bilibili-quality-selector"));
  1617. }, 5, 1000, 0);
  1618. };
  1619.  
  1620. // 等待会员图标加载完成
  1621. const vipIconObserver = new MutationObserver((mutations, observer) => {
  1622. const vipElement = document.querySelector(".bili-avatar-icon.bili-avatar-right-icon.bili-avatar-icon-big-vip");
  1623. if (vipElement || mutations.some(m => m.target.classList.contains('bili-avatar-icon-big-vip'))) {
  1624. observer.disconnect();
  1625. console.log("[会员状态] 会员图标已加载,开始执行画质设置");
  1626. executeQualitySettings();
  1627. }
  1628. });
  1629.  
  1630. vipIconObserver.observe(headerAvatar, {
  1631. childList: true,
  1632. subtree: true,
  1633. attributes: true,
  1634. attributeFilter: ['class']
  1635. });
  1636.  
  1637. // 优先保证会员切换速度,因为非会员用户 1080P 的切换频率并不高,并且 3.5 秒其实与之前版本体验一致。
  1638. timeoutId = setTimeout(() => {
  1639. vipIconObserver.disconnect();
  1640. console.log("[会员状态] 会员图标检测超时,继续执行画质设置");
  1641. executeQualitySettings();
  1642. }, 3500);
  1643. }
  1644. });
  1645.  
  1646. vipCheckObserver.observe(document.body, { childList: true, subtree: true });
  1647.  
  1648. window.addEventListener('popstate', () => { DOM.clear(); });
  1649. window.addEventListener('beforeunload', () => { DOM.clear(); });
  1650. }
  1651. };
  1652. function isPlayerReady() {
  1653. const qualityMenu = document.querySelector('.bpx-player-ctrl-quality-menu');
  1654. const qualityItems = qualityMenu ? qualityMenu.querySelectorAll('.bpx-player-ctrl-quality-menu-item') : null;
  1655. const headerAvatar = document.querySelector(".v-popover-wrap.header-avatar-wrap");
  1656. // 未登录(不可用)模式下不检查头像元素
  1657. const isReady = qualityItems && qualityItems.length > 0 && (state.devModeEnabled && state.devModeNoLoginStatus ? true : headerAvatar);
  1658.  
  1659. console.log(`[播放器状态]
  1660. - 画质菜单: ${qualityMenu ? '已加载' : '未加载'}
  1661. - 画质选项: ${qualityItems ? `${qualityItems.length}个选项` : '未加载'}
  1662. - 用户头像: ${headerAvatar ? '已加载' : (state.devModeEnabled && state.devModeNoLoginStatus ? '未登录(不可用)模式,跳过检查' : '未加载')}`);
  1663.  
  1664. return isReady;
  1665. }
  1666.  
  1667. async function waitForPlayerWithBackoff(callback, maxRetries = 5, initialDelay = 1000, retryCount = 0) {
  1668. const taskId = taskQueue.currentTaskId;
  1669.  
  1670. if (taskQueue.isTaskCancelled(taskId)) {
  1671. console.log(`[任务管理] 任务 #${taskId} 已取消,停止等待播放器`);
  1672. return;
  1673. }
  1674.  
  1675. if (isPlayerReady()) {
  1676. console.log(`[任务管理] 任务 #${taskId}: 播放器和用户信息已就绪`);
  1677. await callback();
  1678. return;
  1679. }
  1680.  
  1681. if (retryCount >= maxRetries) {
  1682. console.log(`[任务管理] 任务 #${taskId}: 达到最大重试次数(${maxRetries}),强制执行回调`);
  1683. await callback();
  1684. return;
  1685. }
  1686.  
  1687. const delayTime = initialDelay * Math.pow(2, retryCount);
  1688. console.log(`[任务管理] 任务 #${taskId}: 等待播放器加载中 (第${retryCount + 1}次尝试,等待${delayTime}ms)`);
  1689.  
  1690. await taskQueue.scheduleTask(taskId, async () => {
  1691. if (!taskQueue.isTaskCancelled(taskId)) {
  1692. await waitForPlayerWithBackoff(callback, maxRetries, initialDelay, retryCount + 1);
  1693. }
  1694. }, delayTime);
  1695. }
  1696.  
  1697. async function checkVipStatusAsync() {
  1698. // 直接使用缓存的结果
  1699. if (state.sessionCache.vipChecked) {
  1700. state.isVipUser = state.sessionCache.vipStatus;
  1701. state.vipStatusChecked = true;
  1702. console.log("[会员状态] 使用缓存状态:", state.isVipUser ? "是" : "否");
  1703. return;
  1704. }
  1705.  
  1706. if (state.devModeEnabled) {
  1707. // 未登录(不可用)模式下直接返回非会员状态
  1708. if (state.devModeNoLoginStatus) {
  1709. state.isVipUser = false;
  1710. state.vipStatusChecked = true;
  1711. state.sessionCache.vipStatus = false;
  1712. state.sessionCache.vipChecked = true;
  1713. console.log("[开发者模式] 未登录(不可用)模式,用户会员状态: 否");
  1714. return;
  1715. }
  1716. // 模拟大会员状态
  1717. state.isVipUser = state.devModeVipStatus;
  1718. state.vipStatusChecked = true;
  1719. state.sessionCache.vipStatus = state.isVipUser;
  1720. state.sessionCache.vipChecked = true;
  1721. console.log("[开发者模式] 用户会员状态:", state.isVipUser ? "是" : "否");
  1722. return;
  1723. }
  1724.  
  1725. try {
  1726. const [vipElement, currentQualityEl] = await Promise.all([
  1727. waitForElement(() => document.querySelector(".bili-avatar-icon.bili-avatar-right-icon.bili-avatar-icon-big-vip"), 3000),
  1728. waitForElement(() => document.querySelector(".bpx-player-ctrl-quality-menu-item.bpx-state-active .bpx-player-ctrl-quality-text"), 3000)
  1729. ]);
  1730.  
  1731. state.isVipUser = vipElement !== null || (currentQualityEl && currentQualityEl.textContent.includes("大会员"));
  1732. state.vipStatusChecked = true;
  1733. // 缓存结果
  1734. state.sessionCache.vipStatus = state.isVipUser;
  1735. state.sessionCache.vipChecked = true;
  1736.  
  1737. console.log("[会员状态] 用户会员状态:", state.isVipUser ? "是" : "否");
  1738. if (state.isVipUser) {
  1739. console.log("[会员状态] 判定依据:", vipElement ? "发现会员图标" : "当前使用会员画质");
  1740. }
  1741. } catch (error) {
  1742. console.log("[会员状态] 检查超时,默认为非会员用户");
  1743. state.isVipUser = false;
  1744. state.vipStatusChecked = true;
  1745. // 缓存结果
  1746. state.sessionCache.vipStatus = state.isVipUser;
  1747. state.sessionCache.vipChecked = true;
  1748. }
  1749. }
  1750.  
  1751. function waitForElement(selector, timeout = 5000) {
  1752. return new Promise((resolve, reject) => {
  1753. const element = selector();
  1754. if (element) {
  1755. resolve(element);
  1756. return;
  1757. }
  1758.  
  1759. const observer = new MutationObserver((mutations, obs) => {
  1760. const element = selector();
  1761. if (element) {
  1762. obs.disconnect();
  1763. resolve(element);
  1764. }
  1765. });
  1766.  
  1767. observer.observe(document.body, {
  1768. childList: true,
  1769. subtree: true,
  1770. attributes: true,
  1771. attributeFilter: ['class']
  1772. });
  1773.  
  1774. if (timeout) {
  1775. setTimeout(() => {
  1776. observer.disconnect();
  1777. resolve(null);
  1778. }, timeout);
  1779. }
  1780. });
  1781. }
  1782.  
  1783. function createCleanupFunction() {
  1784. const observers = new Set();
  1785. const eventListeners = new Set();
  1786. const timeouts = new Set();
  1787. const intervals = new Set();
  1788.  
  1789. return {
  1790. addObserver: (observer) => observers.add(observer),
  1791. addEventListener: (element, type, listener) => {
  1792. element.addEventListener(type, listener);
  1793. eventListeners.add({ element, type, listener });
  1794. },
  1795. setTimeout: (callback, delay) => {
  1796. const id = setTimeout(callback, delay);
  1797. timeouts.add(id);
  1798. return id;
  1799. },
  1800. setInterval: (callback, delay) => {
  1801. const id = setInterval(callback, delay);
  1802. intervals.add(id);
  1803. return id;
  1804. },
  1805. cleanup: () => {
  1806. observers.forEach(observer => observer.disconnect());
  1807. observers.clear();
  1808.  
  1809. eventListeners.forEach(({ element, type, listener }) => {
  1810. element.removeEventListener(type, listener);
  1811. });
  1812. eventListeners.clear();
  1813.  
  1814. timeouts.forEach(clearTimeout);
  1815. timeouts.clear();
  1816.  
  1817. intervals.forEach(clearInterval);
  1818. intervals.clear();
  1819. }
  1820. };
  1821. }
  1822.  
  1823. const cleanup = createCleanupFunction();
  1824.  
  1825. function initQualitySettingsButton() {
  1826. const controlBottomRight = DOM.get('controlBottomRight');
  1827. const qualityControl = DOM.get('qualityControl');
  1828.  
  1829. if (controlBottomRight && qualityControl && state.injectQualityButton) {
  1830. const existingSettingsBtn = controlBottomRight.querySelector('.quality-settings-btn');
  1831. if (!existingSettingsBtn) {
  1832. const settingsButton = document.createElement('div');
  1833. settingsButton.className = 'bpx-player-ctrl-btn quality-settings-btn';
  1834. settingsButton.innerHTML = '<div class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="15" rx="2" ry="2"></rect><polyline points="8 20 12 20 16 20"></polyline></svg></span></div>';
  1835.  
  1836. const handleClick = () => toggleSettingsPanel();
  1837. settingsButton.addEventListener('click', handleClick);
  1838. cleanup.addEventListener(settingsButton, 'click', handleClick);
  1839.  
  1840. qualityControl.parentElement.insertBefore(settingsButton, qualityControl);
  1841. }
  1842. }
  1843. }
  1844.  
  1845. function observePlayerControls() {
  1846. const playerControls = DOM.get('playerControls');
  1847. if (playerControls) {
  1848. const observer = new MutationObserver(() => {
  1849. const qualityControl = DOM.refresh('qualityControl');
  1850. if (qualityControl) {
  1851. hideQualityButton();
  1852. initQualitySettingsButton();
  1853. }
  1854. });
  1855.  
  1856. observer.observe(playerControls, {
  1857. childList: true,
  1858. subtree: true,
  1859. attributes: false
  1860. });
  1861.  
  1862. cleanup.addObserver(observer);
  1863. }
  1864. }
  1865.  
  1866. // 清理资源
  1867. window.addEventListener('beforeunload', () => {
  1868. cleanup.cleanup();
  1869. });
  1870.  
  1871. let currentUrlChangeTaskId = 0;
  1872. function canonicalUrl(rawUrl) {
  1873. try {
  1874. const urlObj = new URL(rawUrl);
  1875. urlObj.pathname = urlObj.pathname.replace(/\/+$/, '');
  1876. const p = urlObj.searchParams.get("p");
  1877. urlObj.search = "";
  1878. if (p !== null) urlObj.searchParams.set("p", p);
  1879. return urlObj.toString();
  1880. } catch (e) { return rawUrl; }
  1881. }
  1882. let lastCanonicalUrl = canonicalUrl(location.href);
  1883. (function (history) {
  1884. const pushState = history.pushState;
  1885. const replaceState = history.replaceState;
  1886. history.pushState = function () {
  1887. const result = pushState.apply(history, arguments);
  1888. window.dispatchEvent(new Event('locationchange'));
  1889. return result;
  1890. };
  1891. history.replaceState = function () {
  1892. const result = replaceState.apply(history, arguments);
  1893. window.dispatchEvent(new Event('locationchange'));
  1894. return result;
  1895. };
  1896. })(window.history);
  1897. async function onUrlChange() {
  1898. const newUrl = canonicalUrl(location.href);
  1899. if (newUrl === lastCanonicalUrl) return;
  1900.  
  1901. lastCanonicalUrl = newUrl;
  1902. const taskId = taskQueue.generateTaskId();
  1903. console.log(`[URL变更] 开始新任务 #${taskId}, URL: ${newUrl}`);
  1904.  
  1905. taskQueue.clearPreviousTasks();
  1906. state.isLoading = true;
  1907.  
  1908. const panel = document.getElementById("bilibili-quality-selector");
  1909. if (panel) updateQualityButtons(panel);
  1910.  
  1911. try {
  1912. await taskQueue.scheduleTask(taskId, async () => {
  1913. if (!taskQueue.isTaskCancelled(taskId)) {
  1914. console.log(`[任务管理] 任务 #${taskId}: 开始检查播放器状态`);
  1915. await waitForPlayerWithBackoff(async () => {
  1916. if (!taskQueue.isTaskCancelled(taskId)) {
  1917. console.log(`[任务管理] 任务 #${taskId}: 播放器就绪,开始初始化画质设置`);
  1918. state.isLoading = false;
  1919.  
  1920. if (state.devModeEnabled && state.devModeNoLoginStatus) {
  1921. state.isVipUser = false;
  1922. state.vipStatusChecked = true;
  1923. state.sessionCache.vipStatus = false;
  1924. state.sessionCache.vipChecked = true;
  1925. console.log("[未登录(不可用)模式] 非会员状态");
  1926. }
  1927. // 第二次切换就用缓存
  1928. else if (!state.sessionCache.vipChecked) {
  1929. console.log("[会员状态] 首次检查会员状态");
  1930. await checkVipStatusAsync();
  1931. } else {
  1932. state.isVipUser = state.sessionCache.vipStatus;
  1933. state.vipStatusChecked = true;
  1934. console.log("[会员状态] 使用缓存状态:", state.isVipUser ? "是" : "否");
  1935. }
  1936.  
  1937. checkIfLivePage();
  1938. if (state.isLivePage) {
  1939. await selectLiveQuality();
  1940. } else {
  1941. await selectVideoQuality();
  1942. }
  1943. if (panel) updateQualityButtons(panel);
  1944. console.log(`[任务管理] 任务 #${taskId}: 画质设置完成`);
  1945. }
  1946. });
  1947. }
  1948. }, 1000);
  1949. } catch (error) {
  1950. console.error(`[任务管理] 任务 #${taskId}: 执行出错:`, error);
  1951. }
  1952. }
  1953. const urlChangeEvents = ['popstate', 'hashchange', 'locationchange'];
  1954. urlChangeEvents.forEach(eventName => {
  1955. window.addEventListener(eventName, onUrlChange);
  1956. });
  1957. window.addEventListener('beforeunload', () => {
  1958. urlChangeEvents.forEach(eventName => {
  1959. window.removeEventListener(eventName, onUrlChange);
  1960. });
  1961. });
  1962. })();

QingJ © 2025

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