🏠 Home 

Bilibili Video Downloader 📥

Download Bilibili videos with one click, clean and easy-to-use interface


Install this script?
  1. // ==UserScript==
  2. // @name bilibili哔哩哔哩视频下载 📥
  3. // @name:zh-CN 哔哩哔哩视频下载助手📥
  4. // @name:zh-TW 嗶哩嗶哩視頻下載助手📥
  5. // @name:en Bilibili Video Downloader 📥
  6. // @name:ja ビリビリ動画ダウンローダー 📥
  7. // @name:ko 비리비리 비디오 다운로더 📥
  8. // @namespace http://tampermonkey.net/
  9. // @version 0.8
  10. // @description 在哔哩哔哩视频页面添加下载按钮,支持多种清晰度和格式
  11. // @description:zh-CN 一键下载哔哩哔哩视频,界面简洁易用
  12. // @description:zh-TW 一鍵下載嗶哩嗶哩視頻,界面簡潔易用
  13. // @description:en Download Bilibili videos with one click, clean and easy-to-use interface
  14. // @description:ja ビリビリ動画を1クリックでダウンロード、シンプルで使いやすいインターフェース
  15. // @description:ko 비리비리 동영상 원클릭 다운로드, 깔끔하고 사용하기 쉬운 인터페이스
  16. // @author youhou
  17. // @match https://www.bilibili.com/video/*
  18. // @grant none
  19. // @license MIT
  20. // @homepage https://saveany.cn
  21. // @supportURL https://saveany.cn
  22. // @keywords bilibili,哔哩哔哩,视频下载,B站下载,bilibili下载,哔哩哔哩视频下载,B站视频下载器,bilibili视频下载,B站,下载视频,下载,ビリビリ,ダウンロード,비리비리,다운로드
  23. // @icon https://www.bilibili.com/favicon.ico
  24. // ==/UserScript==
  25. (function() {
  26. 'use strict';
  27. const PARSE_APIS = [
  28. 'https://api.injahow.cn/bparse/',
  29. 'https://jx.jsonplayer.com/player/',
  30. 'https://jx.bozrc.com:4433/player/',
  31. 'https://jx.parwix.com:4433/player/'
  32. ];
  33. function createDownloadButton() {
  34. const downloadBtn = document.createElement('button');
  35. downloadBtn.textContent = '下载';
  36. downloadBtn.style.cssText = `
  37. margin-left: 10px;
  38. padding: 5px 12px;
  39. background: #00aeec;
  40. color: white;
  41. border: none;
  42. border-radius: 8px;
  43. cursor: pointer;
  44. font-size: 13px;
  45. height: 32px;
  46. line-height: 18px;
  47. min-width: 50px;
  48. `;
  49. downloadBtn.onclick = startDownload;
  50. return downloadBtn;
  51. }
  52. function getBiliVideoInfo() {
  53. try {
  54. let initialState = window.__INITIAL_STATE__;
  55. let videoData = initialState?.videoData;
  56. if (!videoData) {
  57. // 尝试从 window.__playinfo__ 获取
  58. const playInfo = window.__playinfo__;
  59. if (playInfo) {
  60. videoData = {
  61. bvid: document.querySelector('meta[itemprop="url"]')?.content?.split('/').pop(),
  62. aid: playInfo.aid,
  63. cid: playInfo.cid,
  64. title: document.querySelector('h1.video-title')?.textContent?.trim(),
  65. desc: document.querySelector('.desc-info-text')?.textContent?.trim(),
  66. pic: document.querySelector('meta[itemprop="image"]')?.content,
  67. owner: {
  68. name: document.querySelector('.up-name')?.textContent?.trim(),
  69. face: document.querySelector('.up-avatar img')?.src,
  70. mid: document.querySelector('.up-name')?.href?.match(/\d+/)?.[0]
  71. }
  72. };
  73. }
  74. }
  75. if (!videoData) {
  76. const bvid = location.pathname.match(/BV\w+/)?.[0];
  77. videoData = {
  78. bvid: bvid,
  79. title: document.title.replace(' - 哔哩哔哩', '').trim(),
  80. pic: document.querySelector('meta[property="og:image"]')?.content,
  81. desc: document.querySelector('meta[property="og:description"]')?.content,
  82. owner: {
  83. name: document.querySelector('.up-name')?.textContent?.trim(),
  84. face: document.querySelector('.up-avatar img')?.src,
  85. mid: document.querySelector('.up-name')?.href?.match(/\d+/)?.[0]
  86. }
  87. };
  88. }
  89. if (!videoData || !videoData.bvid) {
  90. throw new Error('无法获取视频信息');
  91. }
  92. return {
  93. bvid: videoData.bvid,
  94. pic: videoData.pic || '',
  95. title: videoData.title || document.title,
  96. pubdate: videoData.pubdate,
  97. desc: videoData.desc || '',
  98. duration: videoData.duration,
  99. owner: {
  100. mid: videoData.owner?.mid || '',
  101. name: videoData.owner?.name || '未知用户',
  102. face: videoData.owner?.face || ''
  103. },
  104. aid: videoData.aid,
  105. cid: videoData.cid || videoData.pages?.[0]?.cid
  106. };
  107. } catch (error) {
  108. console.error('获取视频信息失败:', error);
  109. // 添加更详细的错误信息
  110. console.log('当前页面URL:', location.href);
  111. console.log('window.__INITIAL_STATE__:', window.__INITIAL_STATE__);
  112. console.log('window.__playinfo__:', window.__playinfo__);
  113. throw error;
  114. }
  115. }
  116. async function getVideoUrl(aid, cid, quality) {
  117. const apiUrl = 'https://api.bilibili.com/x/player/playurl';
  118. const params = {
  119. otype: 'json',
  120. platform: 'html5',
  121. avid: aid,
  122. cid: cid,
  123. qn: quality || window.__playinfo__?.data?.accept_quality?.[0] || 80,
  124. fnver: 0,
  125. fnval: 4048,
  126. high_quality: window.__playinfo__?.data?.quality || 1
  127. };
  128. const queryString = Object.entries(params)
  129. .map(([key, value]) => `${key}=${value}`)
  130. .join('&');
  131. const response = await fetch(`${apiUrl}?${queryString}`, {
  132. credentials: 'include'
  133. });
  134. const data = await response.json();
  135. if (data.code !== 0) {
  136. throw new Error(data.message || '获取下载地址失败');
  137. }
  138. return data.data.durl[0].url;
  139. }
  140. async function parseVideoUrl(bvid, apiIndex = 0, usedQuality = null) {
  141. if (apiIndex >= PARSE_APIS.length) {
  142. throw new Error('所有解析接口都失败了');
  143. }
  144. try {
  145. const quality = usedQuality || window.__playinfo__?.data?.quality || 80;
  146. const apiUrl = `${PARSE_APIS[apiIndex]}?bv=${bvid}&q=${quality}`;
  147. const response = await fetch(apiUrl);
  148. const data = await response.json();
  149. if (!data.url && !data.data?.url) {
  150. if (quality !== 80) {
  151. return parseVideoUrl(bvid, apiIndex, 80);
  152. }
  153. throw new Error('解析接口返回数据格式错误');
  154. }
  155. return {
  156. url: data.url || data.data.url,
  157. quality: quality
  158. };
  159. } catch (error) {
  160. return parseVideoUrl(bvid, apiIndex + 1, usedQuality);
  161. }
  162. }
  163. async function constructDownloadInfo() {
  164. try {
  165. const videoInfo = getBiliVideoInfo();
  166. let downloadUrl;
  167. let usedQuality; // 添加变量记录使用的清晰度
  168. try {
  169. if (videoInfo.aid && videoInfo.cid) {
  170. const quality = window.__playinfo__?.data?.accept_quality?.[0] || 80;
  171. downloadUrl = await getVideoUrl(videoInfo.aid, videoInfo.cid, quality);
  172. usedQuality = quality;
  173. }
  174. } catch (error) {
  175. }
  176. if (!downloadUrl) {
  177. const r###lt = await parseVideoUrl(videoInfo.bvid, 0, window.__playinfo__?.data?.quality);
  178. downloadUrl = r###lt.url;
  179. usedQuality = r###lt.quality;
  180. }
  181. return {
  182. bvid: videoInfo.bvid,
  183. downloadUrl: downloadUrl,
  184. title: videoInfo.title,
  185. desc: videoInfo.desc,
  186. pic: videoInfo.pic,
  187. aid: videoInfo.aid,
  188. cid: videoInfo.cid,
  189. owner: videoInfo.owner,
  190. face: videoInfo.face,
  191. downloadUrl,
  192. usedQuality, // 将清晰度信息添加到返回对象中
  193. };
  194. } catch (error) {
  195. throw error;
  196. }
  197. }
  198. async function startDownload() {
  199. try {
  200. const downloadInfo = await constructDownloadInfo();
  201. // 在控制台打印下载信息
  202. console.group('视频下载信息');
  203. console.log('标题:', downloadInfo.title);
  204. console.log('描述:', downloadInfo.desc);
  205. console.log('封面:', downloadInfo.pic);
  206. console.log('下载地址:', downloadInfo.downloadUrl);
  207. console.log('UP主:', downloadInfo.owner?.name);
  208. console.log('UP主头像:', downloadInfo.owner?.face);
  209. console.log('BV号:', downloadInfo.bvid);
  210. console.log('AV号:', downloadInfo.aid);
  211. console.log('CID:', downloadInfo.cid);
  212. console.group('清晰度信息');
  213. console.log('支持的清晰度列表:', window.__playinfo__?.data?.accept_quality?.map(qn => ({
  214. qn,
  215. desc: {
  216. 120: '4K',
  217. 116: '1080P60帧',
  218. 112: '1080P+高码率',
  219. 80: '1080P',
  220. 64: '720P',
  221. 32: '480P',
  222. 16: '360P'
  223. }[qn] || `未知(${qn})`
  224. })));
  225. console.log('当前播放清晰度:', window.__playinfo__?.data?.quality);
  226. if (downloadInfo.isOfficialApi) {
  227. console.log('下载使用的清晰度:', `${downloadInfo.usedQuality} (${
  228. {
  229. 120: '4K',
  230. 116: '1080P60帧',
  231. 112: '1080P+高码率',
  232. 80: '1080P',
  233. 64: '720P',
  234. 32: '480P',
  235. 16: '360P'
  236. }[downloadInfo.usedQuality] || '未知清晰度'
  237. })`);
  238. console.log('使用接口: 官方API');
  239. } else {
  240. console.log('下载使用的清晰度:', `${downloadInfo.usedQuality} (${
  241. {
  242. 120: '4K',
  243. 116: '1080P60帧',
  244. 112: '1080P+高码率',
  245. 80: '1080P',
  246. 64: '720P',
  247. 32: '480P',
  248. 16: '360P'
  249. }[downloadInfo.usedQuality] || '未知清晰度'
  250. })`);
  251. console.log('使用接口: 第三方接口');
  252. console.log('提示: 如需更高清晰度,建议登录后使用官方API下载');
  253. }
  254. console.groupEnd();
  255. console.groupEnd();
  256. const params = new URLSearchParams();
  257. params.append('title', downloadInfo.title || '');
  258. params.append('desc', downloadInfo.desc || '');
  259. params.append('pic', downloadInfo.pic || '');
  260. params.append('downloadUrl', downloadInfo.downloadUrl);
  261. params.append('owner', downloadInfo.owner?.name || '');
  262. params.append('face', downloadInfo.owner?.face || '');
  263. const baseUrl = 'https://saveany.cn/get_video_info';
  264. const finalUrl = `${baseUrl}?${params.toString()}`;
  265. console.log('最终请求URL:', finalUrl);
  266. const downloadWindow = window.open(finalUrl, '_blank');
  267. if (downloadWindow) {
  268. downloadWindow.focus();
  269. }
  270. } catch (error) {
  271. console.error('下载失败:', error);
  272. alert('下载失败: ' + error.message);
  273. }
  274. }
  275. function addDownloadButton() {
  276. const targetArea = document.querySelector("#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-sending-area > div");
  277. if (targetArea && !targetArea.querySelector('.download-btn')) {
  278. const downloadBtn = createDownloadButton();
  279. downloadBtn.classList.add('download-btn');
  280. targetArea.appendChild(downloadBtn);
  281. }
  282. }
  283. function observeDOM() {
  284. const targetNode = document.body;
  285. const config = { childList: true, subtree: true };
  286. const observer = new MutationObserver((mutationsList, observer) => {
  287. for(let mutation of mutationsList) {
  288. if (mutation.type === 'childList') {
  289. addDownloadButton();
  290. }
  291. }
  292. });
  293. observer.observe(targetNode, config);
  294. }
  295. window.addEventListener('load', () => {
  296. addDownloadButton();
  297. observeDOM();
  298. });
  299. setInterval(addDownloadButton, 5000);
  300. })();