您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
从B站上提取带封面的音乐
// ==UserScript== // @name Bilibili Music Extractor // @namespace http://tampermonkey.net/ // @version 0.4.2 // @description 从B站上提取带封面的音乐 // @author ☆ // @include https://www.bilibili.com/video/* // @include https://www.bilibili.com/festival/* // @icon https://www.bilibili.com/favicon.ico // @require https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; // Your code here... const sanitizeStringAsFilename = (name) => { const allowedLength = 64; const replacement = '_'; const reRelativePath = /^\.+(\\|\/)|^\.+$/; const reTrailingPeriods = /\.+$/; const reControlChars = /[\u0000-\u001F\u0080-\u009F]/g; const reRepeatedReservedCharacters = /([<>:"/\\|?*\u0000-\u001F]){2,}/g; const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g; const windowsReservedNameRegex= /^(con|prn|aux|nul|com\d|lpt\d)$/i; name = name.replace(reRepeatedReservedCharacters, '$1') name = name.normalize('NFD'); name = name.replace(reRelativePath, replacement); name = name.replace(filenameReservedRegex, replacement); name = name.replace(reControlChars, replacement); name = name.replace(reTrailingPeriods, ''); if (name[0] === '.') { name = replacement + name; } if (name[name.length - 1] === '.') { name += replacement; } name = windowsReservedNameRegex.test(name) ? name + replacement : name; if (name.length > allowedLength) { const extensionIndex = name.lastIndexOf('.'); if (extensionIndex === -1) { name = name.slice(0, allowedLength); } else { const filename = name.slice(0, extensionIndex); const extension = name.slice(extensionIndex); name = filename.slice(0, Math.max(1, allowedLength - extension.length)) + extension; } } return name; } const CHUNK_SIZE = 1024 * 1024 * 1; const download = (url, filename) => { const stubLink = document.createElement('a'); stubLink.style.display = 'none'; stubLink.href = url; stubLink.download = filename; document.body.appendChild(stubLink); stubLink.click(); document.body.removeChild(stubLink); } const getAudioPieces = async (baseUrl, start, end) => { const headers = { 'Range': 'bytes=' + start + '-' + end, 'Referer': location.href }; const result = []; console.log('start fetching piece...'); try { const response = await fetch(baseUrl, { method: 'GET', cache: 'no-cache', headers, referrerPolicy: 'no-referrer-when-downgrade', }); if (response.status === 416) { console.log('reached last piece'); throw response; } if (!response.ok) { console.error(response); throw new Error('Network response was not ok'); } if (!response.headers.get('Content-Range')) { console.log('content reached the end'); const endError = new Error('reached the end'); endError.status = 204; throw endError; } const audioBuffer = await response.blob(); result.push(audioBuffer); const buffers = await getAudioPieces(baseUrl, end + 1, end + CHUNK_SIZE); return result.concat(buffers); } catch (err) { if (err.status === 204) { return result; } else if (err.status === 416) { const lastPiece = await getLastAudioPiece(baseUrl, start) result.push(lastPiece); return result; } else { throw err; } } } const getLastAudioPiece = async (baseUrl, start) => { const headers = { 'Range': '' + start + '-', 'Referer': location.href }; console.log('start fetching last piece...'); const response = await fetch(baseUrl, { method: 'GET', cache: 'no-cache', headers, referrerPolicy: 'no-referrer-when-downgrade', }) if (!response.ok) { console.error(response); throw new Error('Network response was not ok'); } return await response.blob(); } const getAudio = (baseUrl) => { const start = 0; const end = CHUNK_SIZE - 1; return getAudioPieces(baseUrl, start, end); } const getInfo = (fieldname) => { let info = ''; const infoMetadataElement = document.head.querySelector(`meta[itemprop="${fieldname}"]`); if (infoMetadataElement) { info = infoMetadataElement.content; } if (info.length < 1 && __INITIAL_STATE__) { // If we fail to get info from head elements, // then we try to get it from __INITIAL_STATE__ or other element switch (fieldname) { case 'image': { const videoItems = document.querySelectorAll(".video-episode-card.video-episode-card-title-hover"); const activeVideoItem = Array.from(videoItems).find(item => item.textContent.includes(getInfo("name"))); if (activeVideoItem) { const activeVideoCover = activeVideoItem.querySelector(".activity-image-card.cover-link-image .activity-image-card__image"); if (activeVideoCover) { info = activeVideoCover.style.backgroundImage; info = info.replace(/url\("(.+)@.*"\)/, "$1"); } } break; } case 'name': if (__INITIAL_STATE__.videoInfo) { info = __INITIAL_STATE__.videoInfo.title || ''; } else if (__INITIAL_STATE__.videoData) { info = __INITIAL_STATE__.videoData.title || ''; } break; case 'author': if (__INITIAL_STATE__.videoInfo) { info = __INITIAL_STATE__.videoInfo.upName || ''; } else if (__INITIAL_STATE__.videoData) { info = __INITIAL_STATE__.videoData.author || __INITIAL_STATE__.videoData.owner?.name || ''; } break; case 'cid': { const videoData = __INITIAL_STATE__.videoInfo || __INITIAL_STATE__.videoData; if (videoData && Array.isArray(videoData.pages) && videoData.pages.length > 0) { let page = parseInt(__INITIAL_STATE__.p); if (Number.isNaN(page)) { page = 0; } else { page = Math.max(page - 1, 0); } info = `${videoData.pages[page].cid}`; break; } // otherwise, fallback to default handler } default: if (__INITIAL_STATE__.videoInfo) { info = __INITIAL_STATE__.videoInfo[fieldname] || ''; } else if (__INITIAL_STATE__.videoData) { info = __INITIAL_STATE__.videoData[fieldname] || ''; } info = `${info}`; break; } } if (fieldname === 'image') { // try to get original image url try { info = info.replace(/(.+)(@.*)/, "$1"); info = `http:${info}`; } catch (e) { } } return info.trim(); } const getLyricsTime = (seconds) => { const minutes = Math.floor(seconds / 60); const rest = seconds - minutes * 60; return `${minutes < 10 ? '0' : ''}${minutes}:${rest < 10 ? '0' : ''}${rest.toFixed(2)}`; }; const getLyrics = async () => { if ( !__INITIAL_STATE__ || !__INITIAL_STATE__.videoData || !__INITIAL_STATE__.videoData.subtitle || !Array.isArray(__INITIAL_STATE__.videoData.subtitle.list) || __INITIAL_STATE__.videoData.subtitle.list.length === 0 ) return Promise.resolve(null); const defaultLyricsUrl = __INITIAL_STATE__.videoData.subtitle.list[0].subtitle_url; const response = await fetch(defaultLyricsUrl.replace('http', 'https')); const lyricsObject = await response.json(); if (!lyricsObject) return null; const videoElement = document.querySelector('#bilibiliPlayer .bilibili-player-video bwp-video') || document.querySelector('#bilibiliPlayer .bilibili-player-video video'); if (!videoElement) return null; const totalLength = videoElement.duration; const lyrics = lyricsObject.body; const lyricsText = lyricsObject.body.reduce((accu, current) => { accu += `[${getLyricsTime(current.from)}]${current.content}\r\n`; return accu; }, ''); return lyricsText; } const parse = async () => { try { const bvid = getInfo("bvid"); const cid = getInfo("cid"); // api from: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/video/videostream_url.md const videoMetadataResponse = await fetch(`https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&fnval=80`, { method: 'GET', cache: 'no-cache', referrerPolicy: 'no-referrer-when-downgrade', }); const videoMetadata = await videoMetadataResponse.json(); const audioUrlList = videoMetadata.data.dash.audio; if (Array.isArray(audioUrlList) && audioUrlList.length > 0) { const {baseUrl, mimeType} = audioUrlList[0]; const audioResult = await getAudio(baseUrl); const wholeBlob = new Blob(audioResult, {type: mimeType}); const buffer = await wholeBlob.arrayBuffer(); console.log("audio buffer fetched"); return { buffer, mimeType }; } } catch (err) { console.error('There has been a problem with your fetch operation:', err); } throw new Error("failed to get audio data"); } const buildPluginElement = () => { const styles = { color: { primary: '#00a1d6', secondary: '#fb7299', lightText: '#f4f4f4' }, spacing: { xsmall: '0.25rem', small: '0.5rem', medium: '1rem', large: '2rem', xlarge: '3rem' } }; const strings = { cover: { title: '封面' }, infoItems: { filename: '文件名', title: '标题', author: '作者' }, download: { idle: '下载音乐', processing: '处理中…', lyrics: '下载歌词', noLyrics: '无歌词' } } const box = document.createElement('div'); box.isOpen = false; // ------------- Container Box START ------------- const resetBoxStyle = () => { box.style.position = 'absolute'; box.style.left = `-${styles.spacing.xlarge}`; box.style.top = 0; box.style.transition = box.style.webkitTransition = 'all 0.25s ease'; box.style.width = box.style.height = styles.spacing.xlarge; box.style.borderRadius = styles.spacing.xsmall; box.style.opacity = 0.5; box.style.cursor = 'pointer'; box.style.zIndex = 100; box.style.boxSizing = 'border-box'; box.style.overflow = 'hidden'; box.style.padding = styles.spacing.small; box.style.display = 'flex'; box.style.flexDirection = 'column'; box.style.boxShadow = "none"; }; const openBox = () => { box.style.width = '40rem'; box.style.height = '40rem'; box.style.backgroundColor = 'white'; box.style.cursor = 'auto'; box.style.boxShadow = "0 0 6px gainsboro"; box.isOpen = true; coverImage.src = coverImageUrl = getInfo('image'); } const closeBox = () => { resetBoxStyle(); box.isOpen = false; } resetBoxStyle(); box.addEventListener('mouseenter', () => { box.style.opacity = 1; }); box.addEventListener('mouseleave', () => { if (!box.isOpen) box.style.opacity = 0.5; }); box.addEventListener('click', () => { if (!box.isOpen) openBox(); }); // ------------- Container Box END ------------- // ------------- Icon START ------------- 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'); icon.style.width = icon.style.height = styles.spacing.large; icon.style.flexShrink = 0; // ------------- Icon END ------------- // ------------- Close Button START ------------- 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'); const closeButton = document.createElement('button'); closeButton.className = 'bilifont'; closeButton.style.width = closeButton.style.height = styles.spacing.large; closeButton.style.position = 'absolute'; closeButton.style.left = `max(${styles.spacing.xlarge} + ${styles.spacing.small}, 100% - ${styles.spacing.large} - ${styles.spacing.small})`; closeButton.style.top = styles.spacing.small; closeButton.style.display = 'flex'; closeButton.style.alignItems = 'center'; closeButton.style.justifyContent = 'center'; closeButton.style.fontSize = '1.5em'; closeButton.style.color = styles.color.primary; closeButton.addEventListener('click', (e) => { e.stopPropagation(); closeBox(); }); closeButton.appendChild(closeIcon); // ------------- Close Button END ------------- // ------------- Panel START ------------- const panel = document.createElement('div'); panel.style.flex = '1'; panel.style.margin = '0'; panel.style.alignSelf = 'stretch'; panel.style.overflow = 'auto'; panel.style.marginTop = styles.spacing.small; panel.style.paddingTop = styles.spacing.small; panel.style.borderTop = `solid 0.125rem ${styles.color.primary}`; // ------------- Panel END ------------- const setTitleStyles = element => { element.style.lineHeight = 1.5; element.style.margin = 0; element.style.padding = 0; element.style.color = styles.color.primary; }; let coverImageUrl = getInfo('image'); console.log('coverImageUrl set to: ', coverImageUrl); // ------------- Cover START ------------- const coverContainer = document.createElement('div'); coverContainer.style.width = '100%'; coverContainer.style.marginBottom = styles.spacing.small; const coverTitle = document.createElement('h5'); coverTitle.textContent = strings.cover.title; setTitleStyles(coverTitle); const coverImage = document.createElement('img'); coverImage.style.width = '100%'; coverImage.objectFit = 'contain'; coverImage.src = coverImageUrl; coverContainer.append(coverTitle, coverImage); // ------------- Cover END ------------- // ------------- Info Item START ------------- const buildInfoItem = (title, text) => { const infoContainer = document.createElement('div'); infoContainer.style.width = '100%'; infoContainer.style.display = 'flex'; infoContainer.style.alignItems = 'center'; infoContainer.style.flexWrap = 'nowrap'; infoContainer.style.overflow = 'hidden'; infoContainer.style.marginBottom = styles.spacing.small; const infoTitle = document.createElement('h5'); infoTitle.style.flexBasis = '3em'; infoTitle.textContent = title; setTitleStyles(infoTitle); infoTitle.display = 'inline'; const infoText = document.createElement('input'); infoText.type = 'text'; infoText.value = text; infoText.style.flex = '1'; infoText.style.marginLeft = styles.spacing.xsmall; infoText.style.background = 'none'; infoText.style.border = '0'; infoText.style.borderBottom = `solid 1px ${styles.color.primary}`; infoText.style.padding = styles.spacing.xsmall; infoContainer.append(infoTitle, infoText); infoContainer.textInput = infoText; return infoContainer; } const dummyText = /_哔哩哔哩.+/; const titleText = getInfo('name').replace(dummyText, ''); const filenameItem = buildInfoItem(strings.infoItems.filename, titleText + '.mp3'); const titleItem = buildInfoItem(strings.infoItems.title, titleText); const authorItem = buildInfoItem(strings.infoItems.author, getInfo('author')); // ------------- Info Item END ------------- // ------------- Download Button START ------------- const downloadButton = document.createElement('button'); downloadButton.className = "bi-btn"; downloadButton.textContent = strings.download.idle; downloadButton.style.background = 'none'; downloadButton.style.border = '0'; downloadButton.style.backgroundColor = styles.color.primary; downloadButton.style.color = styles.color.lightText; downloadButton.style.width = '45%'; downloadButton.style.cursor = 'pointer'; downloadButton.style.textAlign = 'center'; downloadButton.style.padding = styles.spacing.small; downloadButton.style.marginBottom = styles.spacing.small; downloadButton.style.transition = downloadButton.style.webkitTransition = 'all 0.25s ease'; downloadButton.addEventListener('mouseenter', () => { downloadButton.style.filter = 'brightness(1.1)'; }); downloadButton.addEventListener('mouseleave', () => { downloadButton.style.filter = 'none'; }); downloadButton.addEventListener('mousedown', () => { downloadButton.style.filter = 'brightness(0.9)'; }); downloadButton.addEventListener('mouseup', () => { downloadButton.style.filter = 'brightness(1.1)'; }); downloadButton.addEventListener('click', async (e) => { if (downloadButton.disabled) return; e.stopPropagation(); downloadButton.textContent = strings.download.processing; downloadButton.disabled = true; downloadButton.style.cursor = 'not-allowed'; try { let encoding = false; const title = sanitizeStringAsFilename(titleItem.textInput.value); const author = sanitizeStringAsFilename(authorItem.textInput.value); const { createFFmpeg, fetchFile } = FFmpeg; const ffmpeg = createFFmpeg({ // log: true, progress: p => { if (encoding) { console.log(p.ratio); downloadButton.textContent = `${strings.download.processing}${(p.ratio / 100).toFixed(0)}%`; } }, corePath: "https://unpkg.com/@ffmpeg/core-st/dist/ffmpeg-core.js", mainName: "main" }); const { buffer, mimeType } = await parse(filenameItem.textInput.value); await ffmpeg.load() const imageResponse = await fetch(coverImageUrl.replace('http', 'https')); const coverImageBlob = await imageResponse.blob(); const imageFile = await fetchFile(coverImageBlob); ffmpeg.FS('writeFile', 'cover.jpg', imageFile); console.log('cover image fetched'); const file = await fetchFile(buffer); ffmpeg.FS('writeFile', 'original.mp3', file); console.log('encoding file...'); encoding = true; await ffmpeg.run( '-i', 'original.mp3', '-i', 'cover.jpg', '-map', '0', '-map', '1:v', '-ar', '44100', '-b:a', '320k', '-disposition:v:1', 'attached_pic', 'out.mp3' ); encoding = false; console.log('file encoded...'); const fileBuffer = await ffmpeg.FS('readFile', 'out.mp3'); const fileBlob = new Blob([fileBuffer]); const fileBlobURL = URL.createObjectURL(fileBlob); await ffmpeg.exit(); await ffmpeg.load(); await ffmpeg.FS('writeFile', 'out.mp3', fileBuffer); console.log('adding metadata...'); const videoElement = document.querySelector('#bilibiliPlayer .bilibili-player-video bwp-video') || document.querySelector('#bilibiliPlayer .bilibili-player-video video') || document.querySelector('#bilibili-player bwp-video') || document.querySelector('#bilibili-player video'); await ffmpeg.run( '-i', 'out.mp3', '-codec', 'copy', '-t', `${videoElement.duration}`, '-metadata', `title=${title}`, '-metadata', `artist=${author}`, '-metadata', `publisher=https://${window.location.hostname + window.location.pathname}`, 'outWithMetadata.mp3' ); const { buffer: encodedBuffer } = ffmpeg.FS('readFile', 'outWithMetadata.mp3'); const audioBlob = new Blob([encodedBuffer], {type: mimeType}); const audioUrl = URL.createObjectURL(audioBlob); await download(audioUrl, filenameItem.textInput.value); } catch (err) { console.error("Failed: ", err); } finally { downloadButton.textContent = strings.download.idle; downloadButton.disabled = false; downloadButton.style.cursor = 'pointer'; } }); const downloadLyricsButton = downloadButton.cloneNode(); downloadLyricsButton.className = "bi-btn"; downloadLyricsButton.disabled = true; downloadLyricsButton.style.cursor = 'not-allowed'; downloadLyricsButton.style.marginRight = '10%'; downloadLyricsButton.textContent = strings.download.noLyrics; downloadLyricsButton.addEventListener('mouseenter', () => { downloadLyricsButton.style.filter = 'brightness(1.1)'; }); downloadLyricsButton.addEventListener('mouseleave', () => { downloadLyricsButton.style.filter = 'none'; }); downloadLyricsButton.addEventListener('mousedown', () => { downloadLyricsButton.style.filter = 'brightness(0.9)'; }); downloadLyricsButton.addEventListener('mouseup', () => { downloadLyricsButton.style.filter = 'brightness(1.1)'; }); let lyricsText = null; getLyrics().then(lyrics => { if (!lyrics) return; lyricsText = lyrics; downloadLyricsButton.disabled = false; downloadLyricsButton.style.cursor = 'pointer'; downloadLyricsButton.textContent = strings.download.lyrics; }) downloadLyricsButton.addEventListener('click', (e) => { if (downloadLyricsButton.disabled) return; e.stopPropagation(); downloadLyricsButton.textContent = strings.download.processing; downloadLyricsButton.disabled = true; downloadLyricsButton.style.cursor = 'not-allowed'; const title = titleItem.textInput.value; const author = authorItem.textInput.value; lyricsText = `[ti:${title}]\n[ar:${author}]\n${lyricsText}`.trim(); const lyrics = new Blob([lyricsText], {type: 'text/plain'}); const lyricsUrl = URL.createObjectURL(lyrics); download(lyricsUrl, filenameItem.textInput.value.replace(/\.[^\s\.]+$/, '.lrc')); downloadLyricsButton.textContent = strings.download.lyrics; downloadButton.disabled = false; downloadLyricsButton.style.cursor = 'pointer'; }); // ------------- Download Button END ------------- panel.append( coverContainer, filenameItem, titleItem, authorItem, downloadLyricsButton, downloadButton ); box.append( icon, closeButton, panel ); return box; } const bilibiliPlayer = document.querySelector('#bilibiliPlayer') || document.querySelector('#bilibili-player'); if (bilibiliPlayer) { const pluginBox = buildPluginElement(); bilibiliPlayer.appendChild(pluginBox); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址