Bilibili Music Extractor

从B站上提取带封面的音乐

  1. // ==UserScript==
  2. // @name Bilibili Music Extractor
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4.1
  5. // @description 从B站上提取带封面的音乐
  6. // @author ☆
  7. // @include https://www.bilibili.com/video/*
  8. // @include https://www.bilibili.com/festival/*
  9. // @icon https://www.bilibili.com/favicon.ico
  10. // @require https://unpkg.com/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js
  11. // @license MIT
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Your code here...
  19.  
  20. const sanitizeStringAsFilename = (name) => {
  21. const allowedLength = 64;
  22. const replacement = '_';
  23.  
  24. const reRelativePath = /^\.+(\\|\/)|^\.+$/;
  25. const reTrailingPeriods = /\.+$/;
  26. const reControlChars = /[\u0000-\u001F\u0080-\u009F]/g;
  27. const reRepeatedReservedCharacters = /([<>:"/\\|?*\u0000-\u001F]){2,}/g;
  28. const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g;
  29. const windowsReservedNameRegex= /^(con|prn|aux|nul|com\d|lpt\d)$/i;
  30.  
  31. name = name.replace(reRepeatedReservedCharacters, '$1')
  32. name = name.normalize('NFD');
  33. name = name.replace(reRelativePath, replacement);
  34. name = name.replace(filenameReservedRegex, replacement);
  35. name = name.replace(reControlChars, replacement);
  36. name = name.replace(reTrailingPeriods, '');
  37.  
  38. if (name[0] === '.') {
  39. name = replacement + name;
  40. }
  41.  
  42. if (name[name.length - 1] === '.') {
  43. name += replacement;
  44. }
  45.  
  46. name = windowsReservedNameRegex.test(name) ? name + replacement : name;
  47.  
  48. if (name.length > allowedLength) {
  49. const extensionIndex = name.lastIndexOf('.');
  50. if (extensionIndex === -1) {
  51. name = name.slice(0, allowedLength);
  52. } else {
  53. const filename = name.slice(0, extensionIndex);
  54. const extension = name.slice(extensionIndex);
  55. name = filename.slice(0, Math.max(1, allowedLength - extension.length)) + extension;
  56. }
  57. }
  58.  
  59.  
  60. return name;
  61. }
  62.  
  63. const CHUNK_SIZE = 1024 * 1024 * 1;
  64.  
  65. const download = (url, filename) => {
  66. const stubLink = document.createElement('a');
  67. stubLink.style.display = 'none';
  68. stubLink.href = url;
  69. stubLink.download = filename;
  70. document.body.appendChild(stubLink);
  71. stubLink.click();
  72. document.body.removeChild(stubLink);
  73. }
  74.  
  75. const getAudioPieces = async (baseUrl, start, end) => {
  76. const headers = {
  77. 'Range': 'bytes=' + start + '-' + end,
  78. 'Referer': location.href
  79. };
  80. const result = [];
  81. console.log('start fetching piece...');
  82. try {
  83. const response = await fetch(baseUrl, {
  84. method: 'GET',
  85. cache: 'no-cache',
  86. headers,
  87. referrerPolicy: 'no-referrer-when-downgrade',
  88. });
  89. if (response.status === 416) {
  90. console.log('reached last piece');
  91. throw response;
  92. }
  93. if (!response.ok) {
  94. console.error(response);
  95. throw new Error('Network response was not ok');
  96. }
  97. if (!response.headers.get('Content-Range')) {
  98. console.log('content reached the end');
  99. const endError = new Error('reached the end');
  100. endError.status = 204;
  101. throw endError;
  102. }
  103. const audioBuffer = await response.blob();
  104. result.push(audioBuffer);
  105. const buffers = await getAudioPieces(baseUrl, end + 1, end + CHUNK_SIZE);
  106. return result.concat(buffers);
  107. } catch (err) {
  108. if (err.status === 204) {
  109. return result;
  110. } else if (err.status === 416) {
  111. const lastPiece = await getLastAudioPiece(baseUrl, start)
  112. result.push(lastPiece);
  113. return result;
  114. } else {
  115. throw err;
  116. }
  117. }
  118. }
  119.  
  120. const getLastAudioPiece = async (baseUrl, start) => {
  121. const headers = {
  122. 'Range': '' + start + '-',
  123. 'Referer': location.href
  124. };
  125. console.log('start fetching last piece...');
  126. const response = await fetch(baseUrl, {
  127. method: 'GET',
  128. cache: 'no-cache',
  129. headers,
  130. referrerPolicy: 'no-referrer-when-downgrade',
  131. })
  132. if (!response.ok) {
  133. console.error(response);
  134. throw new Error('Network response was not ok');
  135. }
  136. return await response.blob();
  137. }
  138.  
  139. const getAudio = (baseUrl) => {
  140. const start = 0;
  141. const end = CHUNK_SIZE - 1;
  142. return getAudioPieces(baseUrl, start, end);
  143. }
  144.  
  145. const getInfo = (fieldname) => {
  146. let info = '';
  147. const infoMetadataElement = document.head.querySelector(`meta[itemprop="${fieldname}"]`);
  148. if (infoMetadataElement) {
  149. info = infoMetadataElement.content;
  150. }
  151. if (info.length < 1 && __INITIAL_STATE__) {
  152. // If we fail to get info from head elements,
  153. // then we try to get it from __INITIAL_STATE__ or other element
  154. switch (fieldname) {
  155. case 'image': {
  156. const videoItems = document.querySelectorAll(".video-episode-card.video-episode-card-title-hover");
  157. const activeVideoItem = Array.from(videoItems).find(item => item.textContent.includes(getInfo("name")));
  158. if (activeVideoItem) {
  159. const activeVideoCover = activeVideoItem.querySelector(".activity-image-card.cover-link-image .activity-image-card__image");
  160. if (activeVideoCover) {
  161. info = activeVideoCover.style.backgroundImage;
  162. info = info.replace(/url\("(.+)@.*"\)/, "$1");
  163. }
  164. }
  165. break;
  166. }
  167. case 'name':
  168. if (__INITIAL_STATE__.videoInfo) {
  169. info = __INITIAL_STATE__.videoInfo.title || '';
  170. } else if (__INITIAL_STATE__.videoData) {
  171. info = __INITIAL_STATE__.videoData.title || '';
  172. }
  173. break;
  174. case 'author':
  175. if (__INITIAL_STATE__.videoInfo) {
  176. info = __INITIAL_STATE__.videoInfo.upName || '';
  177. } else if (__INITIAL_STATE__.videoData) {
  178. info = __INITIAL_STATE__.videoData.author || __INITIAL_STATE__.videoData.owner?.name || '';
  179. }
  180. break;
  181. case 'cid': {
  182. const videoData = __INITIAL_STATE__.videoInfo || __INITIAL_STATE__.videoData;
  183. if (videoData && Array.isArray(videoData.pages) && videoData.pages.length > 0) {
  184. let page = parseInt(__INITIAL_STATE__.p);
  185. if (Number.isNaN(page)) {
  186. page = 0;
  187. } else {
  188. page = Math.max(page - 1, 0);
  189. }
  190. info = `${videoData.pages[page].cid}`;
  191. break;
  192. }
  193. // otherwise, fallback to default handler
  194. }
  195. default:
  196. if (__INITIAL_STATE__.videoInfo) {
  197. info = __INITIAL_STATE__.videoInfo[fieldname] || '';
  198. } else if (__INITIAL_STATE__.videoData) {
  199. info = __INITIAL_STATE__.videoData[fieldname] || '';
  200. }
  201. info = `${info}`;
  202. break;
  203. }
  204. }
  205. if (fieldname === 'image') {
  206. // try to get original image url
  207. try {
  208. info = info.replace(/(.+)(@.*)/, "$1");
  209. info = `http:${info}`;
  210. } catch (e) {
  211. }
  212. }
  213. return info.trim();
  214. }
  215.  
  216. const getLyricsTime = (seconds) => {
  217. const minutes = Math.floor(seconds / 60);
  218. const rest = seconds - minutes * 60;
  219. return `${minutes < 10 ? '0' : ''}${minutes}:${rest < 10 ? '0' : ''}${rest.toFixed(2)}`;
  220. };
  221.  
  222. const getLyrics = async () => {
  223. if (
  224. !__INITIAL_STATE__
  225. || !__INITIAL_STATE__.videoData
  226. || !__INITIAL_STATE__.videoData.subtitle
  227. || !Array.isArray(__INITIAL_STATE__.videoData.subtitle.list)
  228. || __INITIAL_STATE__.videoData.subtitle.list.length === 0
  229. ) return Promise.resolve(null);
  230. const defaultLyricsUrl = __INITIAL_STATE__.videoData.subtitle.list[0].subtitle_url;
  231. const response = await fetch(defaultLyricsUrl.replace('http', 'https'));
  232. const lyricsObject = await response.json();
  233. if (!lyricsObject) return null;
  234. const videoElement = document.querySelector('#bilibiliPlayer .bilibili-player-video bwp-video') || document.querySelector('#bilibiliPlayer .bilibili-player-video video');
  235. if (!videoElement) return null;
  236. const totalLength = videoElement.duration;
  237. const lyrics = lyricsObject.body;
  238. const lyricsText = lyricsObject.body.reduce((accu, current) => {
  239. accu += `[${getLyricsTime(current.from)}]${current.content}\r\n`;
  240. return accu;
  241. }, '');
  242. return lyricsText;
  243. }
  244.  
  245. const parse = async () => {
  246. try {
  247. const bvid = getInfo("bvid");
  248. const cid = getInfo("cid");
  249. // api from: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/video/videostream_url.md
  250. const videoMetadataResponse = await fetch(`https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&fnval=80`, {
  251. method: 'GET',
  252. cache: 'no-cache',
  253. referrerPolicy: 'no-referrer-when-downgrade',
  254. });
  255. const videoMetadata = await videoMetadataResponse.json();
  256. const audioUrlList = videoMetadata.data.dash.audio;
  257. if (Array.isArray(audioUrlList) && audioUrlList.length > 0) {
  258. const {baseUrl, mimeType} = audioUrlList[0];
  259. const audioResult = await getAudio(baseUrl);
  260. const wholeBlob = new Blob(audioResult, {type: mimeType});
  261. const buffer = await wholeBlob.arrayBuffer();
  262. console.log("audio buffer fetched");
  263. return { buffer, mimeType };
  264. }
  265. } catch (err) {
  266. console.error('There has been a problem with your fetch operation:', err);
  267. }
  268. throw new Error("failed to get audio data");
  269. }
  270.  
  271. const buildPluginElement = () => {
  272. const styles = {
  273. color: {
  274. primary: '#00a1d6',
  275. secondary: '#fb7299',
  276. lightText: '#f4f4f4'
  277. },
  278. spacing: {
  279. xsmall: '0.25rem',
  280. small: '0.5rem',
  281. medium: '1rem',
  282. large: '2rem',
  283. xlarge: '3rem'
  284. }
  285. };
  286. const strings = {
  287. cover: {
  288. title: '封面'
  289. },
  290. infoItems: {
  291. filename: '文件名',
  292. title: '标题',
  293. author: '作者'
  294. },
  295. download: {
  296. idle: '下载音乐',
  297. processing: '处理中…',
  298. lyrics: '下载歌词',
  299. noLyrics: '无歌词'
  300. }
  301. }
  302.  
  303. const box = document.createElement('div');
  304. box.isOpen = false;
  305. // ------------- Container Box START -------------
  306. const resetBoxStyle = () => {
  307. box.style.position = 'absolute';
  308. box.style.left = `-${styles.spacing.xlarge}`;
  309. box.style.top = 0;
  310. box.style.transition = box.style.webkitTransition = 'all 0.25s ease';
  311. box.style.width = box.style.height = styles.spacing.xlarge;
  312. box.style.borderRadius = styles.spacing.xsmall;
  313. box.style.opacity = 0.5;
  314. box.style.cursor = 'pointer';
  315. box.style.zIndex = 100;
  316. box.style.boxSizing = 'border-box';
  317. box.style.overflow = 'hidden';
  318. box.style.padding = styles.spacing.small;
  319. box.style.display = 'flex';
  320. box.style.flexDirection = 'column';
  321. box.style.boxShadow = "none";
  322. };
  323. const openBox = () => {
  324. box.style.width = '40rem';
  325. box.style.height = '40rem';
  326. box.style.backgroundColor = 'white';
  327. box.style.cursor = 'auto';
  328. box.style.boxShadow = "0 0 6px gainsboro";
  329.  
  330. box.isOpen = true;
  331. coverImage.src = coverImageUrl = getInfo('image');
  332. }
  333. const closeBox = () => {
  334. resetBoxStyle();
  335. box.isOpen = false;
  336. }
  337. resetBoxStyle();
  338. box.addEventListener('mouseenter', () => {
  339. box.style.opacity = 1;
  340. });
  341. box.addEventListener('mouseleave', () => {
  342. if (!box.isOpen) box.style.opacity = 0.5;
  343. });
  344. box.addEventListener('click', () => {
  345. if (!box.isOpen) openBox();
  346. });
  347. // ------------- Container Box END -------------
  348.  
  349. // ------------- Icon START -------------
  350. const icon = new DOMParser().parseFromString('<svg id="channel-icon-music" viewBox="0 0 1024 1024" class="icon"><path d="M881.92 460.8a335.36 335.36 0 0 0-334.336-335.104h-73.216A335.616 335.616 0 0 0 139.776 460.8v313.6a18.688 18.688 0 0 0 18.432 18.688h41.984c13.568 46.336 37.888 80.384 88.576 80.384h98.304a37.376 37.376 0 0 0 37.376-36.864l1.28-284.672a36.864 36.864 0 0 0-37.12-37.12h-99.84a111.616 111.616 0 0 0-51.2 12.8V454.4a242.432 242.432 0 0 1 241.664-241.664h67.328A242.176 242.176 0 0 1 787.968 454.4v74.496a110.592 110.592 0 0 0-54.272-14.08h-99.84a36.864 36.864 0 0 0-37.12 37.12v284.672a37.376 37.376 0 0 0 37.376 36.864h98.304c51.2 0 75.008-34.048 88.576-80.384h41.984a18.688 18.688 0 0 0 18.432-18.688z" fill="#45C7DD"></path><path d="m646.1859999999999 792.7090000000001.274-196.096q.046-32.512 32.558-32.466l1.024.001q32.512.045 32.466 32.557l-.274 196.096q-.045 32.512-32.557 32.467l-1.024-.002q-32.512-.045-32.467-32.557ZM307.26800000000003 792.7349999999999l.274-196.096q.045-32.512 32.557-32.467l1.024.002q32.512.045 32.467 32.557l-.274 196.096q-.045 32.512-32.557 32.466l-1.024-.001q-32.512-.045-32.467-32.557Z" fill="#FF5C7A"></path></svg>', 'text/html').getElementById('channel-icon-music');
  351. icon.style.width = icon.style.height = styles.spacing.large;
  352. icon.style.flexShrink = 0;
  353. // ------------- Icon END -------------
  354.  
  355. // ------------- Close Button START -------------
  356. const closeIcon = new DOMParser().parseFromString('<svg id="download__close-button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 6.939l3.182-3.182a.75.75 0 111.061 1.061L9.061 8l3.182 3.182a.75.75 0 11-1.061 1.061L8 9.061l-3.182 3.182a.75.75 0 11-1.061-1.061L6.939 8 3.757 4.818a.75.75 0 111.061-1.061L8 6.939z"></path></svg>', 'text/html').getElementById('download__close-button');
  357. const closeButton = document.createElement('button');
  358. closeButton.className = 'bilifont';
  359. closeButton.style.width = closeButton.style.height = styles.spacing.large;
  360. closeButton.style.position = 'absolute';
  361. closeButton.style.left = `max(${styles.spacing.xlarge} + ${styles.spacing.small}, 100% - ${styles.spacing.large} - ${styles.spacing.small})`;
  362. closeButton.style.top = styles.spacing.small;
  363. closeButton.style.display = 'flex';
  364. closeButton.style.alignItems = 'center';
  365. closeButton.style.justifyContent = 'center';
  366. closeButton.style.fontSize = '1.5em';
  367. closeButton.style.color = styles.color.primary;
  368. closeButton.addEventListener('click', (e) => {
  369. e.stopPropagation();
  370. closeBox();
  371. });
  372. closeButton.appendChild(closeIcon);
  373. // ------------- Close Button END -------------
  374.  
  375. // ------------- Panel START -------------
  376. const panel = document.createElement('div');
  377. panel.style.flex = '1';
  378. panel.style.margin = '0';
  379. panel.style.alignSelf = 'stretch';
  380. panel.style.overflow = 'auto';
  381. panel.style.marginTop = styles.spacing.small;
  382. panel.style.paddingTop = styles.spacing.small;
  383. panel.style.borderTop = `solid 0.125rem ${styles.color.primary}`;
  384. // ------------- Panel END -------------
  385.  
  386. const setTitleStyles = element => {
  387. element.style.lineHeight = 1.5;
  388. element.style.margin = 0;
  389. element.style.padding = 0;
  390. element.style.color = styles.color.primary;
  391. };
  392.  
  393. let coverImageUrl = getInfo('image');
  394. console.log('coverImageUrl set to: ', coverImageUrl);
  395.  
  396. // ------------- Cover START -------------
  397. const coverContainer = document.createElement('div');
  398. coverContainer.style.width = '100%';
  399. coverContainer.style.marginBottom = styles.spacing.small;
  400. const coverTitle = document.createElement('h5');
  401. coverTitle.textContent = strings.cover.title;
  402. setTitleStyles(coverTitle);
  403. const coverImage = document.createElement('img');
  404. coverImage.style.width = '100%';
  405. coverImage.objectFit = 'contain';
  406. coverImage.src = coverImageUrl;
  407. coverContainer.append(coverTitle, coverImage);
  408. // ------------- Cover END -------------
  409.  
  410. // ------------- Info Item START -------------
  411. const buildInfoItem = (title, text) => {
  412. const infoContainer = document.createElement('div');
  413. infoContainer.style.width = '100%';
  414. infoContainer.style.display = 'flex';
  415. infoContainer.style.alignItems = 'center';
  416. infoContainer.style.flexWrap = 'nowrap';
  417. infoContainer.style.overflow = 'hidden';
  418. infoContainer.style.marginBottom = styles.spacing.small;
  419. const infoTitle = document.createElement('h5');
  420. infoTitle.style.flexBasis = '3em';
  421. infoTitle.textContent = title;
  422. setTitleStyles(infoTitle);
  423. infoTitle.display = 'inline';
  424. const infoText = document.createElement('input');
  425. infoText.type = 'text';
  426. infoText.value = text;
  427. infoText.style.flex = '1';
  428. infoText.style.marginLeft = styles.spacing.xsmall;
  429. infoText.style.background = 'none';
  430. infoText.style.border = '0';
  431. infoText.style.borderBottom = `solid 1px ${styles.color.primary}`;
  432. infoText.style.padding = styles.spacing.xsmall;
  433. infoContainer.append(infoTitle, infoText);
  434. infoContainer.textInput = infoText;
  435. return infoContainer;
  436. }
  437.  
  438. const dummyText = /_哔哩哔哩.+/;
  439. const titleText = getInfo('name').replace(dummyText, '');
  440. const filenameItem = buildInfoItem(strings.infoItems.filename, titleText + '.mp3');
  441. const titleItem = buildInfoItem(strings.infoItems.title, titleText);
  442. const authorItem = buildInfoItem(strings.infoItems.author, getInfo('author'));
  443. // ------------- Info Item END -------------
  444.  
  445. // ------------- Download Button START -------------
  446. const downloadButton = document.createElement('button');
  447. downloadButton.className = "bi-btn";
  448. downloadButton.textContent = strings.download.idle;
  449. downloadButton.style.background = 'none';
  450. downloadButton.style.border = '0';
  451. downloadButton.style.backgroundColor = styles.color.primary;
  452. downloadButton.style.color = styles.color.lightText;
  453. downloadButton.style.width = '45%';
  454. downloadButton.style.cursor = 'pointer';
  455. downloadButton.style.textAlign = 'center';
  456. downloadButton.style.padding = styles.spacing.small;
  457. downloadButton.style.marginBottom = styles.spacing.small;
  458. downloadButton.style.transition = downloadButton.style.webkitTransition = 'all 0.25s ease';
  459. downloadButton.addEventListener('mouseenter', () => {
  460. downloadButton.style.filter = 'brightness(1.1)';
  461. });
  462. downloadButton.addEventListener('mouseleave', () => {
  463. downloadButton.style.filter = 'none';
  464. });
  465. downloadButton.addEventListener('mousedown', () => {
  466. downloadButton.style.filter = 'brightness(0.9)';
  467. });
  468. downloadButton.addEventListener('mouseup', () => {
  469. downloadButton.style.filter = 'brightness(1.1)';
  470. });
  471. downloadButton.addEventListener('click', async (e) => {
  472. if (downloadButton.disabled) return;
  473. e.stopPropagation();
  474. downloadButton.textContent = strings.download.processing;
  475. downloadButton.disabled = true;
  476. downloadButton.style.cursor = 'not-allowed';
  477. try {
  478. let encoding = false;
  479. const title = sanitizeStringAsFilename(titleItem.textInput.value);
  480. const author = sanitizeStringAsFilename(authorItem.textInput.value);
  481. const { createFFmpeg, fetchFile } = FFmpeg;
  482. const ffmpeg = createFFmpeg({
  483. // log: true,
  484. progress: p => {
  485. if (encoding) {
  486. console.log(p.ratio);
  487. downloadButton.textContent = `${strings.download.processing}${(p.ratio / 100).toFixed(0)}%`;
  488. }
  489. },
  490. corePath: "https://unpkg.zhimg.com/@ffmpeg/core-st/dist/ffmpeg-core.js", // https://unpkg.com/@ffmpeg/core-st/dist/ffmpeg-core.js
  491. mainName: "main"
  492. });
  493. const { buffer, mimeType } = await parse(filenameItem.textInput.value);
  494. await ffmpeg.load()
  495. const imageResponse = await fetch(coverImageUrl.replace('http', 'https'));
  496. const coverImageBlob = await imageResponse.blob();
  497. const imageFile = await fetchFile(coverImageBlob);
  498. ffmpeg.FS('writeFile', 'cover.jpg', imageFile);
  499. console.log('cover image fetched');
  500. const file = await fetchFile(buffer);
  501. ffmpeg.FS('writeFile', 'original.mp3', file);
  502. console.log('encoding file...');
  503. encoding = true;
  504. await ffmpeg.run(
  505. '-i', 'original.mp3',
  506. '-i', 'cover.jpg',
  507. '-map', '0',
  508. '-map', '1:v',
  509. '-ar', '44100',
  510. '-b:a', '320k',
  511. '-disposition:v:1', 'attached_pic',
  512. 'out.mp3'
  513. );
  514. encoding = false;
  515. console.log('file encoded...');
  516. const fileBuffer = await ffmpeg.FS('readFile', 'out.mp3');
  517. const fileBlob = new Blob([fileBuffer]);
  518. const fileBlobURL = URL.createObjectURL(fileBlob);
  519. await ffmpeg.exit();
  520. await ffmpeg.load();
  521. await ffmpeg.FS('writeFile', 'out.mp3', fileBuffer);
  522. console.log('adding metadata...');
  523. const videoElement = document.querySelector('#bilibiliPlayer .bilibili-player-video bwp-video')
  524. || document.querySelector('#bilibiliPlayer .bilibili-player-video video')
  525. || document.querySelector('#bilibili-player bwp-video')
  526. || document.querySelector('#bilibili-player video');
  527. await ffmpeg.run(
  528. '-i', 'out.mp3',
  529. '-codec', 'copy',
  530. '-t', `${videoElement.duration}`,
  531. '-metadata', `title=${title}`,
  532. '-metadata', `artist=${author}`,
  533. '-metadata', `publisher=https://${window.location.hostname + window.location.pathname}`,
  534. 'outWithMetadata.mp3'
  535. );
  536. const { buffer: encodedBuffer } = ffmpeg.FS('readFile', 'outWithMetadata.mp3');
  537. const audioBlob = new Blob([encodedBuffer], {type: mimeType});
  538. const audioUrl = URL.createObjectURL(audioBlob);
  539. await download(audioUrl, filenameItem.textInput.value);
  540. } catch (err) {
  541. console.error("Failed: ", err);
  542. } finally {
  543. downloadButton.textContent = strings.download.idle;
  544. downloadButton.disabled = false;
  545. downloadButton.style.cursor = 'pointer';
  546. }
  547. });
  548. const downloadLyricsButton = downloadButton.cloneNode();
  549. downloadLyricsButton.className = "bi-btn";
  550. downloadLyricsButton.disabled = true;
  551. downloadLyricsButton.style.cursor = 'not-allowed';
  552. downloadLyricsButton.style.marginRight = '10%';
  553. downloadLyricsButton.textContent = strings.download.noLyrics;
  554. downloadLyricsButton.addEventListener('mouseenter', () => {
  555. downloadLyricsButton.style.filter = 'brightness(1.1)';
  556. });
  557. downloadLyricsButton.addEventListener('mouseleave', () => {
  558. downloadLyricsButton.style.filter = 'none';
  559. });
  560. downloadLyricsButton.addEventListener('mousedown', () => {
  561. downloadLyricsButton.style.filter = 'brightness(0.9)';
  562. });
  563. downloadLyricsButton.addEventListener('mouseup', () => {
  564. downloadLyricsButton.style.filter = 'brightness(1.1)';
  565. });
  566. let lyricsText = null;
  567. getLyrics().then(lyrics => {
  568. if (!lyrics) return;
  569. lyricsText = lyrics;
  570. downloadLyricsButton.disabled = false;
  571. downloadLyricsButton.style.cursor = 'pointer';
  572. downloadLyricsButton.textContent = strings.download.lyrics;
  573. })
  574. downloadLyricsButton.addEventListener('click', (e) => {
  575. if (downloadLyricsButton.disabled) return;
  576. e.stopPropagation();
  577. downloadLyricsButton.textContent = strings.download.processing;
  578. downloadLyricsButton.disabled = true;
  579. downloadLyricsButton.style.cursor = 'not-allowed';
  580. const title = titleItem.textInput.value;
  581. const author = authorItem.textInput.value;
  582. lyricsText = `[ti:${title}]\n[ar:${author}]\n${lyricsText}`.trim();
  583. const lyrics = new Blob([lyricsText], {type: 'text/plain'});
  584. const lyricsUrl = URL.createObjectURL(lyrics);
  585. download(lyricsUrl, filenameItem.textInput.value.replace(/\.[^\s\.]+$/, '.lrc'));
  586. downloadLyricsButton.textContent = strings.download.lyrics;
  587. downloadButton.disabled = false;
  588. downloadLyricsButton.style.cursor = 'pointer';
  589. });
  590. // ------------- Download Button END -------------
  591. panel.append(
  592. coverContainer,
  593. filenameItem,
  594. titleItem,
  595. authorItem,
  596. downloadLyricsButton,
  597. downloadButton
  598. );
  599.  
  600. box.append(
  601. icon,
  602. closeButton,
  603. panel
  604. );
  605.  
  606. return box;
  607. }
  608.  
  609. const bilibiliPlayer = document.querySelector('#bilibiliPlayer') || document.querySelector('#bilibili-player');
  610. if (bilibiliPlayer) {
  611. const pluginBox = buildPluginElement();
  612. bilibiliPlayer.appendChild(pluginBox);
  613. }
  614. })();
  615.  
  616.  
  617.  
  618.  
  619.  
  620.  
  621.  
  622.  
  623.  
  624.  
  625.  
  626.  
  627.  

QingJ © 2025

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