// ==UserScript== // @name B站音频在线随机播放器 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 从B站收藏夹获取音频播放 // @author 无夏不春风orz // @match https://t.bilibili.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @connect api.bilibili.com // @connect *.bilibili.com // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; // 添加自定义样式 GM_addStyle(` #bilibili-audio-player { position: fixed; top: 100px; left: 20px; width: 520px; background: rgba(255, 255, 255, 0.98); border: 2px solid #00a1d6; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); z-index: 10000; font-family: 'Microsoft YaHei', sans-serif; backdrop-filter: blur(12px); transition: all 0.2s ease-out; user-select: none; } #bilibili-audio-player.dragging { transition: none; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); transform: scale(1.02); z-index: 10001; } .player-header { background: linear-gradient(135deg, #00a1d6, #0092c4); color: white; padding: 12px 16px; border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center; cursor: grab; user-select: none; } .player-header:active { cursor: grabbing; } .player-title { font-size: 16px; font-weight: bold; } .player-controls { display: flex; gap: 6px; } .player-btn { background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 26px; height: 26px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; transition: all 0.3s; user-select: none; } .player-btn:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.1); } .player-content { padding: 16px; } .player-info { margin-bottom: 16px; } .player-track-title { font-size: 14px; font-weight: bold; margin-bottom: 6px; color: #333; line-height: 1.4; max-height: 42px; overflow: hidden; text-overflow: ellipsis; } .player-audio-info { font-size: 12px; color: #666; margin-bottom: 4px; } .player-progress { margin-bottom: 16px; } .progress-bar { width: 100%; height: 5px; background: #e8e8e8; border-radius: 3px; cursor: pointer; position: relative; transition: height 0.2s; } .progress-bar:hover { height: 8px; } .progress-filled { height: 100%; background: linear-gradient(90deg, #00a1d6, #0092c4); border-radius: 3px; width: 0%; transition: width 0.1s ease; } .time-display { display: flex; justify-content: space-between; font-size: 11px; color: #999; margin-top: 6px; } .player-buttons { display: flex; justify-content: center; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; } .control-btn { background: #00a1d6; border: none; color: white; padding: 8px 16px; border-radius: 20px; cursor: pointer; font-size: 12px; transition: all 0.3s; white-space: nowrap; min-width: 60px; } .control-btn:hover { background: #0092c4; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 161, 214, 0.3); } .control-btn:disabled { background: #ccc; cursor: not-allowed; transform: none; box-shadow: none; } .control-btn.small { padding: 6px 12px; font-size: 11px; min-width: 50px; } .control-btn.active { background: #ff6b6b; box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3); } .play-mode-btn { position: relative; } .volume-control { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; } .volume-slider { flex: 1; height: 5px; background: #e8e8e8; border-radius: 3px; outline: none; cursor: pointer; transition: height 0.2s; } .volume-slider:hover { height: 8px; } .volume-slider::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #00a1d6; cursor: pointer; transition: all 0.2s; } .volume-slider::-webkit-slider-thumb:hover { background: #0092c4; transform: scale(1.2); } .favorite-management { margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; } .favorite-header { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } .favorite-input { flex: 1; min-width: 120px; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 12px; transition: border-color 0.3s; } .favorite-input:focus { outline: none; border-color: #00a1d6; box-shadow: 0 0 0 2px rgba(0, 161, 214, 0.1); } .favorite-select { flex: 1; min-width: 150px; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 12px; background: white; transition: border-color 0.3s; } .favorite-select:focus { outline: none; border-color: #00a1d6; } .favorite-list { max-height: 120px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 6px; background: white; } .favorite-item { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; font-size: 12px; display: flex; justify-content: space-between; align-items: center; transition: all 0.2s; } .favorite-item:last-child { border-bottom: none; } .favorite-item:hover { background: #f8f9fa; } .favorite-item.active { background: #00a1d6; color: white; } .favorite-actions { display: flex; gap: 4px; } .favorite-action { background: none; border: none; cursor: pointer; font-size: 11px; opacity: 0.6; transition: opacity 0.2s; padding: 4px; border-radius: 3px; } .favorite-action:hover { opacity: 1; background: rgba(255, 255, 255, 0.2); } .favorite-item.active .favorite-action:hover { background: rgba(255, 255, 255, 0.3); } .loading-progress { margin: 8px 0; font-size: 12px; color: #666; } .progress-text { display: flex; justify-content: space-between; margin-bottom: 6px; } .progress-bar-container { width: 100%; height: 8px; background: #e8e8e8; border-radius: 4px; overflow: hidden; } .progress-bar-fill { height: 100%; background: linear-gradient(90deg, #00a1d6, #0092c4); border-radius: 4px; width: 0%; transition: width 0.3s ease; } .playlist-section { max-height: 220px; overflow-y: auto; border-top: 1px solid #eee; padding-top: 12px; } .playlist-title { font-size: 13px; font-weight: bold; margin-bottom: 10px; color: #333; display: flex; justify-content: space-between; align-items: center; } .playlist-item { padding: 8px 12px; background: #f8f9fa; border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 1.4; transition: all 0.2s; border-left: 3px solid transparent; display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .playlist-item:hover { background: #e9ecef; transform: translateX(2px); } .playlist-item.active { background: #00a1d6; color: white; border-left-color: #ff6b6b; } .playlist-item-content { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .playlist-item-actions { display: flex; gap: 4px; margin-left: 8px; opacity: 0; transition: opacity 0.2s; } .playlist-item:hover .playlist-item-actions { opacity: 1; } .playlist-item-action { background: none; border: none; color: inherit; cursor: pointer; font-size: 11px; padding: 2px 6px; border-radius: 3px; transition: all 0.2s; } .playlist-item-action:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } .status-message { text-align: center; padding: 10px; font-size: 12px; color: #666; border-radius: 6px; margin-top: 12px; transition: all 0.3s; } .status-message.info { background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb; } .status-message.success { background: #e8f5e8; color: #2e7d32; border: 1px solid #c8e6c9; } .status-message.error { background: #ffebee; color: #c62828; border: 1px solid #ffcdd2; } .status-message.loading { background: #fff3e0; color: #ef6c00; border: 1px solid #ffe0b2; } .loading { display: inline-block; width: 16px; height: 16px; border: 2px solid #f3f3f3; border-top: 2px solid #00a1d6; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px; vertical-align: middle; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .hidden { display: none; } /* 滚动条样式 */ .favorite-list::-webkit-scrollbar, .playlist-section::-webkit-scrollbar { width: 6px; } .favorite-list::-webkit-scrollbar-track, .playlist-section::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; } .favorite-list::-webkit-scrollbar-thumb, .playlist-section::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; } .favorite-list::-webkit-scrollbar-thumb:hover, .playlist-section::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } /* 防止拖拽时文本选择 */ .player-header * { user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } /* 拖拽时的视觉反馈 */ #bilibili-audio-player.dragging .player-header { background: linear-gradient(135deg, #0092c4, #0082b3); } `); class BilibiliAudioPlayer { constructor() { this.audio = null; this.currentTrack = null; this.playlist = []; this.favorites = []; this.currentFavoriteId = null; this.isPlaying = false; this.currentVolume = 0.7; this.isDragging = false; this.isLoading = false; this.playMode = 'random'; this.dragData = null; this.init(); } init() { this.loadSettings(); this.createUI(); this.bindEvents(); this.loadVolume(); this.setInitialPosition(); this.restoreWindowState(); this.updateFavoriteDisplay(); // 尝试加载上次选择的收藏夹 this.loadLastFavorite(); } setInitialPosition() { const player = $('#bilibili-audio-player'); const savedPosition = GM_getValue('playerPosition', null); if (savedPosition) { player.css({ left: savedPosition.left + 'px', top: savedPosition.top + 'px' }); } else { const windowWidth = $(window).width(); const playerWidth = player.outerWidth(); player.css({ left: (windowWidth - playerWidth - 20) + 'px', top: '100px' }); } } createUI() { const playerHTML = `
🎵 B站合集音频播放器
请选择收藏夹开始播放
-
00:00 00:00
🔊 70%
播放列表 (0)
准备就绪,请选择收藏夹
`; $('body').append(playerHTML); this.audio = new Audio(); this.setupAudio(); } setupAudio() { this.audio.volume = this.currentVolume; this.audio.preload = 'metadata'; this.audio.addEventListener('timeupdate', () => { this.updateProgress(); }); this.audio.addEventListener('loadedmetadata', () => { this.updateDuration(); }); this.audio.addEventListener('canplaythrough', () => { this.showStatus('音频加载完成', 'success'); }); this.audio.addEventListener('ended', () => { this.handleTrackEnd(); }); this.audio.addEventListener('error', (e) => { this.showStatus('音频加载失败,尝试下一首...', 'error'); setTimeout(() => this.playNext(), 2000); }); } bindEvents() { $('#play-btn').click(() => this.togglePlay()); $('#prev-btn').click(() => this.playPrev()); $('#next-btn').click(() => this.playNext()); $('#mode-btn').click(() => this.togglePlayMode()); $('#progress-bar').on('click', (e) => this.seek(e)); $('#volume-slider').on('input', (e) => { this.setVolume(e.target.value / 100); $('#volume-value').text(e.target.value + '%'); GM_setValue('volume', this.currentVolume); }); $('#add-favorite-btn').click(() => this.addFavorite()); $('#update-favorite-btn').click(() => this.updateCurrentFavorite()); $('#favorite-select').change(() => this.selectFavorite()); $('#shuffle-btn').click(() => this.shufflePlaylist()); $('#clear-cache-btn').click(() => this.clearPlaylistCache()); $('#minimize-btn').click(() => this.toggleMinimize()); this.makeDraggable(); } makeDraggable() { const player = $('#bilibili-audio-player')[0]; const header = $('.player-header')[0]; if (!player || !header) return; header.addEventListener('mousedown', (e) => { if (e.target.closest('.player-btn')) return; e.preventDefault(); e.stopPropagation(); const rect = player.getBoundingClientRect(); const startX = e.clientX; const startY = e.clientY; const startLeft = rect.left; const startTop = rect.top; player.classList.add('dragging'); this.dragData = { startX: startX, startY: startY, startLeft: startLeft, startTop: startTop, isDragging: true }; const onMouseMove = (e) => { if (!this.dragData || !this.dragData.isDragging) return; const dx = e.clientX - this.dragData.startX; const dy = e.clientY - this.dragData.startY; const newLeft = Math.max(10, Math.min( window.innerWidth - player.offsetWidth - 10, this.dragData.startLeft + dx )); const newTop = Math.max(10, Math.min( window.innerHeight - player.offsetHeight - 10, this.dragData.startTop + dy )); player.style.left = newLeft + 'px'; player.style.top = newTop + 'px'; }; const onMouseUp = (e) => { if (this.dragData) { this.dragData.isDragging = false; const finalLeft = parseInt(player.style.left); const finalTop = parseInt(player.style.top); GM_setValue('playerPosition', { left: finalLeft, top: finalTop }); player.classList.remove('dragging'); } document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('mouseleave', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); document.addEventListener('mouseleave', onMouseUp); }); header.addEventListener('selectstart', (e) => { if (!e.target.closest('.player-btn')) { e.preventDefault(); } return false; }); this.checkBoundaries(); $(window).on('resize', () => this.checkBoundaries()); } checkBoundaries() { const player = $('#bilibili-audio-player')[0]; if (!player) return; const rect = player.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; let needsUpdate = false; let newLeft = parseInt(player.style.left) || 0; let newTop = parseInt(player.style.top) || 0; if (rect.right > windowWidth) { newLeft = windowWidth - player.offsetWidth - 10; needsUpdate = true; } if (rect.bottom > windowHeight) { newTop = windowHeight - player.offsetHeight - 10; needsUpdate = true; } if (rect.left < 0) { newLeft = 10; needsUpdate = true; } if (rect.top < 0) { newTop = 10; needsUpdate = true; } if (needsUpdate) { player.style.left = newLeft + 'px'; player.style.top = newTop + 'px'; GM_setValue('playerPosition', { left: newLeft, top: newTop }); } } loadSettings() { this.loadFavorites(); this.playMode = GM_getValue('playMode', 'random'); } saveSettings() { GM_setValue('playMode', this.playMode); } loadFavorites() { const savedFavorites = GM_getValue('favorites', []); this.favorites = savedFavorites; } saveFavorites() { GM_setValue('favorites', this.favorites); } loadLastFavorite() { const lastFavoriteId = GM_getValue('lastFavoriteId', null); if (lastFavoriteId && this.favorites.find(fav => fav.id === lastFavoriteId)) { setTimeout(() => { this.selectFavorite(lastFavoriteId); }, 500); } } async addFavorite() { const sid = $('#sid-input').val().trim(); if (!sid) { this.showStatus('请输入收藏夹ID', 'error'); return; } if (this.favorites.find(fav => fav.id === sid)) { this.showStatus('收藏夹已存在', 'error'); return; } this.showStatus('
获取收藏夹信息中...', 'info'); try { const favoriteInfo = await this.getFavoriteInfo(sid); const favoriteName = `${favoriteInfo.title} - ${favoriteInfo.upper.name}`; const newFavorite = { id: sid, name: favoriteName, lastUpdated: Date.now(), trackCount: 0 }; this.favorites.push(newFavorite); this.saveFavorites(); this.updateFavoriteDisplay(); this.selectFavorite(sid); this.showStatus('收藏夹添加成功', 'success'); $('#sid-input').val(''); } catch (error) { this.showStatus('获取收藏夹信息失败: ' + error.message, 'error'); } } async getFavoriteInfo(sid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/space/fav/season/list?season_id=${sid}&pn=1&ps=1000`, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data.info || '未知'); } else { reject(new Error(data.message || 'API错误')); } } catch (e) { reject(e); } }, onerror: reject }); }); } selectFavorite(favoriteId = null) { const selectedId = favoriteId || $('#favorite-select').val(); if (!selectedId) { this.disableControls(); this.clearPlaylist(); return; } if (this.currentFavoriteId === selectedId) { this.showStatus('已选择当前收藏夹', 'info'); this.enableControls(); this.enablePlaybackControls(); return; } this.currentFavoriteId = selectedId; GM_setValue('lastFavoriteId', selectedId); this.enableControls(); this.loadFavorite(selectedId); } clearPlaylist() { this.playlist = []; this.currentTrack = null; this.audio.src = ''; this.updatePlaylistDisplay(); $('#track-title').text('请选择收藏夹开始播放'); $('#audio-info').text('-'); $('#current-time').text('00:00'); $('#duration').text('00:00'); $('#progress-filled').css('width', '0%'); } async loadFavorite(favoriteId) { const favorite = this.favorites.find(fav => fav.id === favoriteId); if (!favorite) return; $('#favorite-select').val(favoriteId); this.updateFavoriteListDisplay(); this.clearPlaylist(); const cachedPlaylist = this.getCachedPlaylist(favoriteId); if (cachedPlaylist && cachedPlaylist.length > 0) { this.showStatus('使用缓存播放列表', 'success'); this.playlist = cachedPlaylist; this.updatePlaylistDisplay(); this.enablePlaybackControls(); this.checkForUpdates(favoriteId); } else { await this.loadPlaylist(favoriteId); } } getCachedPlaylist(favoriteId) { try { const cachedData = GM_getValue(`playlist_${favoriteId}`, null); if (cachedData) { const data = JSON.parse(cachedData); if (Date.now() - data.timestamp < 7 * 24 * 60 * 60 * 1000) { return data.playlist; } } } catch (e) { console.error('读取缓存失败:', e); } return null; } savePlaylistToCache(favoriteId, playlist) { try { const cacheData = { playlist: playlist, timestamp: Date.now(), count: playlist.length }; GM_setValue(`playlist_${favoriteId}`, JSON.stringify(cacheData)); } catch (e) { console.error('保存缓存失败:', e); } } async checkForUpdates(favoriteId) { try { const favorite = this.favorites.find(fav => fav.id === favoriteId); if (!favorite) return; const medias = await this.getBvidData(favoriteId); if (!medias || medias.length === 0) return; const cachedPlaylist = this.getCachedPlaylist(favoriteId); if (cachedPlaylist && medias.length !== cachedPlaylist.length) { this.showStatus(`发现新内容 (${medias.length - cachedPlaylist.length}首新音频),点击更新按钮加载`, 'info'); } } catch (error) { console.error('检查更新失败:', error); } } async updateCurrentFavorite() { if (!this.currentFavoriteId) { this.showStatus('请先选择收藏夹', 'error'); return; } this.clearPlaylistCache(this.currentFavoriteId); await this.loadPlaylist(this.currentFavoriteId, true); } removeFavorite(favoriteId) { if (this.favorites.length <= 1) { this.showStatus('至少需要保留一个收藏夹', 'error'); return; } this.favorites = this.favorites.filter(fav => fav.id !== favoriteId); this.clearPlaylistCache(favoriteId); if (this.currentFavoriteId === favoriteId) { this.currentFavoriteId = null; GM_setValue('lastFavoriteId', null); this.clearPlaylist(); this.disableControls(); } this.saveFavorites(); this.updateFavoriteDisplay(); } clearPlaylistCache(favoriteId = null) { if (favoriteId) { GM_deleteValue(`playlist_${favoriteId}`); this.showStatus(`已清除收藏夹缓存`, 'success'); } else if (this.currentFavoriteId) { GM_deleteValue(`playlist_${this.currentFavoriteId}`); this.showStatus(`已清除当前收藏夹缓存`, 'success'); } else { this.favorites.forEach(fav => { GM_deleteValue(`playlist_${fav.id}`); }); this.showStatus(`已清除所有缓存`, 'success'); } } updateFavoriteDisplay() { const select = $('#favorite-select'); select.empty(); select.append(''); this.favorites.forEach(favorite => { select.append(new Option( `${favorite.name} (${favorite.trackCount}首)`, favorite.id )); }); if (this.currentFavoriteId) { select.val(this.currentFavoriteId); this.enableControls(); } else { this.disableControls(); } this.updateFavoriteListDisplay(); } updateFavoriteListDisplay() { const list = $('#favorite-list'); list.empty(); this.favorites.forEach(favorite => { const isActive = favorite.id === this.currentFavoriteId; const item = $(`
${favorite.name} (${favorite.trackCount}首)
`); item.click(() => this.selectFavorite(favorite.id)); item.find('.favorite-action').eq(0).click((e) => { e.stopPropagation(); this.removeFavorite(favorite.id); }); list.append(item); }); } disableControls() { $('#update-favorite-btn').prop('disabled', true); $('#play-btn').prop('disabled', true); $('#prev-btn').prop('disabled', true); $('#next-btn').prop('disabled', true); $('#shuffle-btn').prop('disabled', true); $('#clear-cache-btn').prop('disabled', true); } enableControls() { $('#update-favorite-btn').prop('disabled', false); $('#clear-cache-btn').prop('disabled', false); } enablePlaybackControls() { $('#play-btn').prop('disabled', false); $('#prev-btn').prop('disabled', false); $('#next-btn').prop('disabled', false); $('#shuffle-btn').prop('disabled', false); } async loadPlaylist(sid, isUpdate = false) { if (this.isLoading) { this.showStatus('正在加载中,请稍候...', 'info'); return; } this.isLoading = true; this.showStatus('
加载收藏夹中...', 'info'); this.disableControls(); const progressEl = $('#loading-progress'); progressEl.removeClass('hidden'); try { const medias = await this.getBvidData(sid); if (!medias || medias.length === 0) { this.showStatus('收藏夹为空或不存在', 'error'); return; } if (isUpdate && this.playlist.length > 0) { await this.updatePlaylistWithNewData(medias, sid); } else { await this.createNewPlaylist(medias, sid); } } catch (error) { this.showStatus('加载失败: ' + error.message, 'error'); console.error('Load playlist error:', error); } finally { this.isLoading = false; progressEl.addClass('hidden'); this.enableControls(); this.enablePlaybackControls(); } } async createNewPlaylist(medias, sid) { this.playlist = []; this.updatePlaylistDisplay(); let successCount = 0; let errorCount = 0; const updateProgress = (current, total) => { const percent = (current / total) * 100; $('#progress-status').text(`加载音频信息中...`); $('#progress-count').text(`${current}/${total}`); $('#progress-bar-fill').css('width', percent + '%'); }; for (let i = 0; i < medias.length; i++) { const media = medias[i]; updateProgress(i + 1, medias.length); try { // 只获取视频基本信息,不获取音频URL const track = await this.processMediaItem(media); if (track) { this.playlist.push(track); successCount++; } } catch (e) { console.error(`Error processing ${media.bvid}:`, e); errorCount++; } await new Promise(resolve => setTimeout(resolve, 100)); } this.finalizePlaylistLoading(sid, successCount, errorCount); } async updatePlaylistWithNewData(medias, sid) { let newCount = 0; let updatedCount = 0; let errorCount = 0; const updateProgress = (current, total) => { const percent = (current / total) * 100; $('#progress-status').text(`更新音频信息中...`); $('#progress-count').text(`${current}/${total}`); $('#progress-bar-fill').css('width', percent + '%'); }; const existingTracksMap = new Map(); this.playlist.forEach(track => { existingTracksMap.set(track.bvid, track); }); for (let i = 0; i < medias.length; i++) { const media = medias[i]; updateProgress(i + 1, medias.length); if (existingTracksMap.has(media.bvid)) { updatedCount++; continue; } try { const track = await this.processMediaItem(media); if (track) { this.playlist.push(track); newCount++; } } catch (e) { console.error(`Error processing ${media.bvid}:`, e); errorCount++; } await new Promise(resolve => setTimeout(resolve, 100)); } let statusMsg = `更新完成: 新增 ${newCount} 首`; if (updatedCount > 0) { statusMsg += `, 已存在 ${updatedCount} 首`; } if (errorCount > 0) { statusMsg += `, 失败 ${errorCount} 首`; } this.finalizePlaylistLoading(sid, this.playlist.length, errorCount, statusMsg); } async processMediaItem(media) { // 只获取视频基本信息,不获取音频URL const videoInfo = await this.getVideoInfo(media.bvid); return { bvid: media.bvid, title: videoInfo.title, duration: videoInfo.duration, cid: videoInfo.cid, // 保存cid用于播放时获取音频URL lastUpdated: Date.now() }; } // 播放时实时获取音频URL async getAudioUrl(bvid, cid) { try { const audioInfo = await this.getAudioPlayInfo(bvid, cid); if (audioInfo.dash && audioInfo.dash.audio) { const bestAudio = audioInfo.dash.audio.reduce((best, current) => (current.bandwidth || 0) > (best.bandwidth || 0) ? current : best ); return bestAudio.baseUrl || bestAudio.backupUrl; } if (audioInfo.durl && audioInfo.durl.length > 0) { return audioInfo.durl[0].url; } throw new Error('无法获取音频URL'); } catch (error) { console.error('获取音频URL失败:', error); throw error; } } finalizePlaylistLoading(sid, successCount, errorCount, customMessage = null) { const favoriteIndex = this.favorites.findIndex(fav => fav.id === sid); if (favoriteIndex !== -1) { this.favorites[favoriteIndex].trackCount = successCount; this.favorites[favoriteIndex].lastUpdated = Date.now(); this.saveFavorites(); this.updateFavoriteDisplay(); } this.savePlaylistToCache(sid, this.playlist); this.updatePlaylistDisplay(); let statusMsg = customMessage || `成功加载 ${successCount} 首音频信息`; if (errorCount > 0 && !customMessage) { statusMsg += ` (${errorCount} 首加载失败)`; } this.showStatus(statusMsg, 'success'); } getBvidData(sid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/space/fav/season/list?season_id=${sid}&pn=1&ps=1000`, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data.medias || []); } else { reject(new Error(data.message || 'API错误')); } } catch (e) { reject(e); } }, onerror: reject }); }); } getVideoInfo(bvid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data); } else { reject(new Error(data.message || 'API错误')); } } catch (e) { reject(e); } }, onerror: reject }); }); } getAudioPlayInfo(bvid, cid) { return new Promise((resolve, reject) => { // 尝试多种参数组合 const urls = [ `https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&qn=0&fnval=16&fourk=1`, `https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&qn=16&fnval=16`, `https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}&qn=0&fnval=4048` ]; const tryNextUrl = (index) => { if (index >= urls.length) { reject(new Error('所有音频格式尝试失败')); return; } GM_xmlhttpRequest({ method: 'GET', url: urls[index], headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data); } else if (index < urls.length - 1) { tryNextUrl(index + 1); } else { reject(new Error(data.message || 'API错误')); } } catch (e) { if (index < urls.length - 1) { tryNextUrl(index + 1); } else { reject(e); } } }, onerror: function(error) { if (index < urls.length - 1) { tryNextUrl(index + 1); } else { reject(error); } } }); }; tryNextUrl(0); }); } togglePlayMode() { const modes = ['sequential', 'loop', 'random']; const currentIndex = modes.indexOf(this.playMode); this.playMode = modes[(currentIndex + 1) % modes.length]; this.updateModeButton(); this.saveSettings(); const modeNames = {'sequential': '顺序播放', 'loop': '列表循环', 'random': '随机播放'}; this.showStatus(`播放模式: ${modeNames[this.playMode]}`, 'success'); } updateModeButton() { const btn = $('#mode-btn'); btn.attr('data-mode', this.playMode); btn.toggleClass('active', this.playMode === 'random'); btn.text({'sequential': '顺序', 'loop': '循环', 'random': '随机'}[this.playMode]); } handleTrackEnd() { this.playNext(); } playNext() { if (this.playlist.length === 0 || this.isLoading) return; if (this.playMode === 'sequential') { this.playNextSequential(); } else if (this.playMode === 'loop') { this.playNextLoop(); } else { this.playNextRandom(); } } playNextSequential() { const currentIndex = this.playlist.findIndex(track => track.bvid === this.currentTrack?.bvid); const nextIndex = (currentIndex + 1) % this.playlist.length; this.playTrack(nextIndex); } playNextLoop() { const currentIndex = this.playlist.findIndex(track => track.bvid === this.currentTrack?.bvid); const nextIndex = (currentIndex + 1) % this.playlist.length; this.playTrack(nextIndex); } playNextRandom() { if (this.playlist.length === 0) return; const randomIndex = Math.floor(Math.random() * this.playlist.length); this.playTrack(randomIndex); } playPrev() { if (this.playlist.length === 0 || this.isLoading) return; const currentIndex = this.playlist.findIndex(track => track.bvid === this.currentTrack?.bvid); if (currentIndex === -1) return; let prevIndex; if (this.playMode === 'random') { prevIndex = Math.floor(Math.random() * this.playlist.length); } else { prevIndex = currentIndex <= 0 ? this.playlist.length - 1 : currentIndex - 1; } this.playTrack(prevIndex); } playRandom() { if (this.playlist.length === 0 || this.isLoading) return; this.playNextRandom(); } async playTrack(index) { if (index < 0 || index >= this.playlist.length) return; const track = this.playlist[index]; this.currentTrack = track; $('#track-title').text(track.title); $('#audio-info').text(`时长: ${this.formatTime(track.duration)} 加载中...`); // 设置音频源之前先停止当前播放 this.audio.pause(); this.audio.currentTime = 0; try { this.showStatus('
获取音频地址中...', 'info'); // 播放时实时获取音频URL const audioUrl = await this.getAudioUrl(track.bvid, track.cid); if (!audioUrl) { throw new Error('无法获取音频地址'); } this.audio.src = audioUrl; this.audio.load(); $('#audio-info').text(`时长: ${this.formatTime(track.duration)}`); this.updatePlaylistDisplay(); this.play(); } catch (error) { this.showStatus('获取音频地址失败: ' + error.message, 'error'); setTimeout(() => this.playNext(), 2000); } } play() { this.audio.play().then(() => { this.isPlaying = true; $('#play-btn').text('暂停'); this.showStatus('开始播放', 'success'); }).catch(error => { this.showStatus('播放失败: ' + error.message, 'error'); // 如果是源文件问题,尝试下一首 if (error.name === 'NotSupportedError' || error.name === 'MediaError') { setTimeout(() => this.playNext(), 2000); } }); } pause() { this.audio.pause(); this.isPlaying = false; $('#play-btn').text('播放'); } togglePlay() { if (!this.currentTrack) { if (this.playlist.length > 0) this.playRandom(); return; } if (this.isLoading) return; if (this.isPlaying) { this.pause(); } else { this.play(); } } removeTrack(bvid) { if (!bvid || this.playlist.length <= 1) { this.showStatus('播放列表至少需要保留一首歌曲', 'error'); return; } const trackIndex = this.playlist.findIndex(track => track.bvid === bvid); if (trackIndex === -1) return; const trackTitle = this.playlist[trackIndex].title; this.playlist.splice(trackIndex, 1); if (this.currentTrack && this.currentTrack.bvid === bvid) { this.currentTrack = null; this.audio.src = ''; this.isPlaying = false; $('#play-btn').text('播放'); $('#track-title').text('请选择歌曲开始播放'); $('#audio-info').text('-'); } if (this.currentFavoriteId) { this.savePlaylistToCache(this.currentFavoriteId, this.playlist); } this.updatePlaylistDisplay(); this.showStatus(`已删除歌曲: ${this.truncateTitle(trackTitle)}`, 'success'); } updatePlaylistDisplay() { const container = $('#playlist-container'); container.empty(); $('#playlist-count').text(this.playlist.length); this.playlist.forEach((track, index) => { const isActive = this.currentTrack && track.bvid === this.currentTrack.bvid; const item = $(`
${index + 1}. ${this.truncateTitle(track.title)}
`); item.click((e) => { if (!$(e.target).closest('.playlist-item-actions').length) { this.playTrack(index); } }); item.find('.playlist-item-action').click((e) => { e.stopPropagation(); this.removeTrack(track.bvid); }); container.append(item); }); const hasPlaylist = this.playlist.length > 0; $('#shuffle-btn').prop('disabled', !hasPlaylist); } shufflePlaylist() { if (this.isLoading || this.playlist.length === 0) return; for (let i = this.playlist.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.playlist[i], this.playlist[j]] = [this.playlist[j], this.playlist[i]]; } this.updatePlaylistDisplay(); this.showStatus('播放列表已打乱', 'success'); } seek(e) { if (!this.audio.duration || this.isLoading) return; const progressBar = $('#progress-bar')[0]; const rect = progressBar.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const newTime = percent * this.audio.duration; this.audio.currentTime = newTime; this.updateProgress(); } updateProgress() { if (!this.audio.duration) return; const percent = (this.audio.currentTime / this.audio.duration) * 100; $('#progress-filled').css('width', percent + '%'); $('#current-time').text(this.formatTime(this.audio.currentTime)); } updateDuration() { $('#duration').text(this.formatTime(this.audio.duration)); } formatTime(seconds) { if (!seconds || isNaN(seconds)) return '00:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } setVolume(volume) { this.currentVolume = volume; this.audio.volume = volume; } loadVolume() { const savedVolume = GM_getValue('volume', 0.7); this.setVolume(savedVolume); $('#volume-slider').val(savedVolume * 100); $('#volume-value').text(Math.round(savedVolume * 100) + '%'); } showStatus(message, type = 'info') { const statusEl = $('#status-message'); statusEl.html(message).removeClass('info error success loading').addClass(type); if (type !== 'loading') { setTimeout(() => { if (statusEl.text() === message) { statusEl.removeClass('info error success').addClass('info').text('准备就绪'); } }, 5000); } } truncateTitle(title, maxLength = 40) { return title.length > maxLength ? title.substring(0, maxLength) + '...' : title; } toggleMinimize() { const content = $('.player-content'); const isVisible = content.is(':visible'); content.slideToggle(300); $('#minimize-btn').text(isVisible ? '+' : '−'); GM_setValue('playerMinimized', !isVisible); } restoreWindowState() { const isMinimized = GM_getValue('playerMinimized', false); if (isMinimized) { $('.player-content').hide(); $('#minimize-btn').text('+'); } } } // 初始化播放器 $(document).ready(() => { setTimeout(() => { try { new BilibiliAudioPlayer(); console.log('B站音频播放器初始化成功'); } catch (error) { console.error('B站音频播放器初始化失败:', error); } }, 2000); }); // 全局键盘快捷键 $(document).on('keydown', (e) => { if (e.ctrlKey && e.altKey && e.key === 'p') { e.preventDefault(); const player = $('#bilibili-audio-player'); if (player.is(':visible')) { player.fadeOut(300); } else { player.fadeIn(300); } } if (e.key === ' ' && !e.target.matches('input, textarea, select')) { e.preventDefault(); const playBtn = $('#play-btn'); if (playBtn.length) { playBtn.click(); } } }); })();