DockerCompose备份
编辑
memos
version: '3.8'
services:
memos:
image: neosmemo/memos:stable
container_name: memos
ports:
- "5230:5230"
volumes:
- ~/.memos:/var/opt/memos
restart: unless-stoppedeasyvoice
services:
easyvoice:
image: cosincox/easyvoice:latest
restart: unless-stopped
container_name: easyvoice
ports:
- "9549:3000"
environment:
- DEBUG=true
- OPENAI_BASE_URL=https://openrouter.ai/api/v1/
volumes:
- ./audio:/app/audiomoments
services:
moments:
image: kingwrcy/moments:latest
container_name: moments
restart: always
environment:
PORT: 3000
JWT_KEY: $JWT_KEY
ports:
- 3000:3000
volumes:
- /var/moments:/app/data # 持久化数据到主机的 /var/moments 目录,可以按需修改ouonnkitv
services:
ouonnkitv:
image: ghcr.io/ouonnki/ouonnkitv:latest
ports:
- "23001:80"
restart: unless-stopped镜像源
镜像源
[
{"id":"source1","name":"电影天堂资源","url":"http://caiji.dyttzyapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source2","name":"黑木耳","url":"https://json.heimuer.xyz/api.php/provide/vod","isEnabled":true},
{"id":"source3","name":"如意资源","url":"http://cj.rycjapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source4","name":"暴风资源","url":"https://bfzyapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source5","name":"天涯资源","url":"https://tyyszy.com/api.php/provide/vod","isEnabled":true},
{"id":"source6","name":"非凡影视","url":"http://ffzy5.tv/api.php/provide/vod","isEnabled":true},
{"id":"source7","name":"360资源","url":"https://360zy.com/api.php/provide/vod","isEnabled":true},
{"id":"source8","name":"茅台资源","url":"https://caiji.maotaizy.cc/api.php/provide/vod","isEnabled":true},
{"id":"source9","name":"卧龙资源","url":"https://wolongzyw.com/api.php/provide/vod","isEnabled":true},
{"id":"source10","name":"极速资源","url":"https://jszyapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source11","name":"豆瓣资源","url":"https://dbzy.tv/api.php/provide/vod","isEnabled":true},
{"id":"source12","name":"魔爪资源","url":"https://mozhuazy.com/api.php/provide/vod","isEnabled":true},
{"id":"source13","name":"魔都资源","url":"https://www.mdzyapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source14","name":"最大资源","url":"https://api.zuidapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source15","name":"樱花资源","url":"https://m3u8.apiyhzy.com/api.php/provide/vod","isEnabled":true},
{"id":"source16","name":"无尽资源","url":"https://api.wujinapi.me/api.php/provide/vod","isEnabled":true},
{"id":"source17","name":"旺旺短剧","url":"https://wwzy.tv/api.php/provide/vod","isEnabled":true},
{"id":"source18","name":"iKun资源","url":"https://ikunzyapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source19","name":"量子资源站","url":"https://cj.lziapi.com/api.php/provide/vod","isEnabled":true},
{"id":"source20","name":"小猫咪资源","url":"https://zy.xmm.hk/api.php/provide/vod","isEnabled":true}
]
FlatNas
services:
flatnas:
image: qdnas/flatnas:latest
container_name: flatnas
restart: unless-stopped
ports:
- '23000:3000'
volumes:
- ./data:/app/server/data
- ./music:/app/server/music #映射音乐文件路径给随机音乐播放使用PanSou
services:
pansou:
image: ghcr.io/fish2018/pansou-web
container_name: pansou
ports:
- "28880:80"
restart: unless-stopped单文件播放器
播放器代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐播放器 (桌面/移动端优化)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<style>
:root {
--font-main: 'Noto Sans SC', 'Segoe UI', Arial, sans-serif;
--bg-gradient: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
--container-bg: rgba(255, 255, 255, 0.6);
--container-shadow: rgba(0, 0, 0, 0.1);
--backdrop-blur: 12px;
--text-color: #2c3e50;
--text-secondary-color: #7f8c8d;
--primary-color: #3498db;
--primary-color-dark: #2980b9;
--warning-color: #e74c3c;
--item-hover-bg: rgba(52, 152, 219, 0.1);
--item-current-bg: rgba(52, 152, 219, 0.2);
--item-current-text: var(--primary-color);
--border-color: rgba(0, 0, 0, 0.1);
--lyrics-highlight-bg: rgba(46, 204, 113, 0.15);
--lyrics-highlight-text: #27ae60;
--lyrics-highlight-shadow: rgba(46, 204, 113, 0.2);
--component-bg: rgba(255, 255, 255, 0.5);
--scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
--scrollbar-track-bg: transparent;
}
.dark-mode {
--bg-gradient: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
--container-bg: rgba(30, 30, 30, 0.6);
--container-shadow: rgba(0, 0, 0, 0.3);
--text-color: #ecf0f1;
--text-secondary-color: #95a5a6;
--primary-color: #5dade2;
--primary-color-dark: #3498db;
--item-hover-bg: rgba(93, 173, 226, 0.1);
--item-current-bg: rgba(93, 173, 226, 0.2);
--border-color: rgba(255, 255, 255, 0.15);
--lyrics-highlight-bg: rgba(26, 188, 156, 0.15);
--lyrics-highlight-text: #1abc9c;
--component-bg: rgba(44, 44, 44, 0.5);
--scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track-bg); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-bg); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary-color); }
body {
font-family: var(--font-main);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
background-image: var(--bg-gradient);
background-attachment: fixed;
color: var(--text-color);
transition: background 0.5s, color 0.5s;
}
.container {
background: var(--container-bg);
backdrop-filter: blur(var(--backdrop-blur));
-webkit-backdrop-filter: blur(var(--backdrop-blur));
padding: 35px;
border-radius: 24px;
box-shadow: 0 15px 30px var(--container-shadow);
border: 1px solid var(--border-color);
width: 100%;
max-width: 850px;
display: grid;
grid-template-rows: auto auto auto 1fr auto;
gap: 20px;
transition: background 0.5s, box-shadow 0.5s;
height: 90vh;
max-height: 700px;
}
.header { text-align: center; position: relative; }
.header h1 { margin: 0; font-size: 2.2em; font-weight: 700; color: var(--primary-color); letter-spacing: 1px; }
.header .warning { color: var(--text-secondary-color); font-size: 0.9em; margin-top: 10px; font-style: italic; }
.file-input-area {
background: var(--component-bg);
border: 2px dashed var(--border-color);
border-radius: 16px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.file-input-area:hover, .file-input-area.dragover { border-color: var(--primary-color); background: var(--item-hover-bg); }
.file-input-area .file-input-label { display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-secondary-color); }
.file-input-label i { font-size: 2em; margin-bottom: 10px; color: var(--primary-color); }
.search-area {
display: flex;
gap: 10px;
align-items: center;
}
.search-area input {
flex-grow: 1;
border: 1px solid var(--border-color);
background-color: var(--component-bg);
border-radius: 20px;
padding: 10px 18px;
font-size: 1em;
color: var(--text-color);
outline: none;
transition: border-color 0.3s;
font-family: inherit;
}
.search-area input:focus {
border-color: var(--primary-color);
}
.search-area button {
background: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.1em;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.search-area button:hover {
background: var(--primary-color-dark);
transform: scale(1.05);
}
.search-area button:disabled {
background: var(--text-secondary-color);
cursor: not-allowed;
transform: none;
}
.view-toggle { display: none; }
.main-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; overflow: hidden; }
.playlist, .lyrics { background: var(--component-bg); border-radius: 16px; padding: 20px; border: 1px solid var(--border-color); height: 100%; overflow-y: auto; }
.playlist div { padding: 12px 15px; border-radius: 10px; transition: all 0.2s ease-in-out; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.playlist div:hover { background-color: var(--item-hover-bg); color: var(--item-current-text); transform: translateX(5px); }
.playlist .current { color: var(--item-current-text); font-weight: 500; background-color: var(--item-current-bg); }
.lyrics { text-align: center; font-size: 1.1em; line-height: 2.2; }
.lyrics div { white-space: pre-wrap; }
.lyrics .current { color: var(--lyrics-highlight-text); font-weight: 700; background-color: var(--lyrics-highlight-bg); transform: scale(1.05); box-shadow: 0 4px 15px var(--lyrics-highlight-shadow); }
.controls { display: flex; justify-content: center; align-items: center; gap: 20px; flex-wrap: wrap; }
.controls button { background: var(--primary-color); color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 1.2em; width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; transition: all 0.2s ease; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.controls button:hover { background: var(--primary-color-dark); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
.controls button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
.controls button#loadOnlineBtn { width: auto; border-radius: 25px; padding: 0 25px; gap: 10px; }
.controls button:disabled { background: var(--text-secondary-color); cursor: not-allowed; transform: none; box-shadow: none; }
.controls audio { flex-grow: 1; min-width: 300px; }
.loader { width: 20px; height: 20px; border: 3px solid var(--primary-color); border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; }
@keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.theme-switch-wrapper { position: absolute; top: 0px; right: 0px; }
.theme-switch { display: inline-block; height: 24px; position: relative; width: 48px; }
.theme-switch input { display:none; }
.slider { background-color: #ccc; bottom: 0; cursor: pointer; left: 0; position: absolute; right: 0; top: 0; transition: .4s; border-radius: 24px; }
.slider:before { background-color: #fff; bottom: 3px; content: ""; height: 18px; left: 3px; position: absolute; transition: .4s; width: 18px; border-radius: 50%; }
.slider .fa-sun, .slider .fa-moon { position: absolute; top: 50%; transform: translateY(-50%); color: #fff; font-size: 12px; opacity: 0; transition: opacity 0.4s; }
.slider .fa-sun { left: 6px; opacity: 1; }
.slider .fa-moon { right: 6px; }
input:checked + .slider { background-color: var(--primary-color); }
input:checked + .slider:before { transform: translateX(24px); }
input:checked + .slider .fa-sun { opacity: 0; }
input:checked + .slider .fa-moon { opacity: 1; }
@media (max-width: 768px) {
body { padding: 0; }
.container {
padding: 15px;
gap: 15px;
grid-template-rows: auto auto auto auto 1fr auto;
border-radius: 0;
border: none;
height: 100vh;
max-height: none;
}
.header h1 { font-size: 1.8em; }
.file-input-area { padding: 15px; }
.file-input-label i { font-size: 1.8em; }
.view-toggle { display: flex; gap: 10px; }
.view-toggle button { flex-grow: 1; background: var(--component-bg); color: var(--text-secondary-color); border: 1px solid var(--border-color); padding: 10px; border-radius: 10px; font-size: 0.9em; font-family: inherit; cursor: pointer; transition: all 0.3s ease; }
.view-toggle button.active { background: var(--primary-color); color: white; font-weight: 500; border-color: var(--primary-color); }
.main-content { display: flex; grid-template-columns: none; }
.playlist, .lyrics { width: 100%; flex-shrink: 0; }
.mobile-hidden { display: none !important; }
.lyrics { line-height: 1.8; font-size: 1em; }
.controls { flex-wrap: wrap; gap: 10px; align-items: center; }
.controls audio { order: -1; width: 100%; min-width: unset; }
.controls button { font-size: 1.1em; width: 48px; height: 48px; }
.controls button#loadOnlineBtn { flex-grow: 1; width: auto; max-width: 180px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="theme-switch-wrapper">
<label class="theme-switch" for="checkbox"><input type="checkbox" id="checkbox" /><div class="slider"><i class="fas fa-sun"></i><i class="fas fa-moon"></i></div></label>
</div>
<h1>音乐播放器</h1>
<div class="warning">支持本地音乐与在线雷达探索</div>
</div>
<div class="file-input-area" id="fileDropArea">
<input type="file" id="fileInput" multiple accept="audio/*,.lrc,.txt" style="display: none;">
<label for="fileInput" class="file-input-label"><i class="fas fa-folder-plus"></i><span>点击选择 或 拖放本地音乐文件至此</span></label>
</div>
<div class="search-area">
<input type="search" id="searchInput" placeholder="搜索在线歌曲、歌手...">
<button id="searchBtn" title="搜索"><i class="fas fa-search"></i></button>
</div>
<div class="view-toggle">
<button id="showPlaylistBtn" class="active">播放列表</button>
<button id="showLyricsBtn">歌词</button>
</div>
<div class="main-content">
<div class="playlist" id="playlist">请选择本地音乐或探索在线雷达</div>
<div class="lyrics mobile-hidden" id="lyrics">歌词将在此处同步显示</div>
</div>
<div class="controls">
<button onclick="playPrevious()" title="上一曲"><i class="fas fa-backward-step"></i></button>
<audio id="audioPlayer" controls></audio>
<button onclick="playNext()" title="下一曲"><i class="fas fa-forward-step"></i></button>
<button id="loadOnlineBtn" title="聚合所有雷达,探索新音乐">
<span class="btn-text"><i class="fas fa-satellite-dish"></i> 探索雷达</span>
<span class="loader" style="display: none;"></span>
</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.7/jsmediatags.min.js"></script>
<script>
const dom = {
playlist: document.getElementById('playlist'),
lyrics: document.getElementById('lyrics'),
audioPlayer: document.getElementById('audioPlayer'),
themeToggle: document.getElementById('checkbox'),
loadOnlineBtn: document.getElementById('loadOnlineBtn'),
fileInput: document.getElementById('fileInput'),
fileDropArea: document.getElementById('fileDropArea'),
showPlaylistBtn: document.getElementById('showPlaylistBtn'),
showLyricsBtn: document.getElementById('showLyricsBtn'),
searchInput: document.getElementById('searchInput'),
searchBtn: document.getElementById('searchBtn'),
};
const API = {
name: 'GD Studio API',
baseUrl: 'https://music-api.gdstudio.xyz/api.php',
getList: async (keyword) => {
const url = `${API.baseUrl}?types=search&source=netease,kuwo&name=${encodeURIComponent(keyword)}&count=100`;
const response = await fetch(url);
if (!response.ok) throw new Error('API request failed');
const data = await response.json();
if (!Array.isArray(data)) throw new Error('Invalid API response');
return data.map(song => ({
id: song.id,
name: song.name,
artists: [{ name: song.artist.join(' / ') }],
source: song.source,
lyric_id: song.lyric_id,
}));
},
getSongUrl: (song) => `${API.baseUrl}?types=url&id=${song.id}&source=${song.source || 'netease'}&br=320000`,
getLyric: (song) => `${API.baseUrl}?types=lyric&id=${song.lyric_id || song.id}&source=${song.source || 'netease'}`,
};
const state = {
isOnlineMode: false,
audioFiles: [],
lyricsFiles: {},
onlineSongs: [],
currentTrackIndex: -1,
currentAudioUrl: null,
lyricsData: [],
currentLyricLine: -1,
};
window.addEventListener('load', setupInteractions);
dom.audioPlayer.addEventListener('ended', autoPlayNext);
dom.audioPlayer.addEventListener('timeupdate', syncLyrics);
function setupInteractions() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.body.classList.toggle('dark-mode', savedTheme === 'dark');
dom.themeToggle.checked = savedTheme === 'dark';
}
dom.themeToggle.addEventListener('change', (e) => {
const isDark = e.target.checked;
document.body.classList.toggle('dark-mode', isDark);
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
dom.loadOnlineBtn.addEventListener('click', () => fetchOnlineMusic('热门', true));
dom.searchBtn.addEventListener('click', handleSearch);
dom.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleSearch();
}
});
dom.fileInput.addEventListener('change', (e) => processFiles(e.target.files));
dom.fileDropArea.addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.classList.add('dragover'); });
dom.fileDropArea.addEventListener('dragleave', (e) => e.currentTarget.classList.remove('dragover'));
dom.fileDropArea.addEventListener('drop', (e) => {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
processFiles(e.dataTransfer.files);
});
dom.showPlaylistBtn.addEventListener('click', () => switchMobileView('playlist'));
dom.showLyricsBtn.addEventListener('click', () => switchMobileView('lyrics'));
}
function handleSearch() {
const keyword = dom.searchInput.value.trim();
if (keyword) {
fetchOnlineMusic(keyword, false); // false 表示不随机打乱搜索结果
} else {
// 可以给用户一个提示,例如输入框闪烁
dom.searchInput.focus();
}
}
async function fetchOnlineMusic(keyword, shuffle = false) {
state.isOnlineMode = true;
if (window.innerWidth <= 768) switchMobileView('playlist');
const btnText = dom.loadOnlineBtn.querySelector('.btn-text');
const loader = dom.loadOnlineBtn.querySelector('.loader');
// 同时禁用“探索”和“搜索”按钮,防止重复点击
dom.loadOnlineBtn.disabled = true;
dom.searchBtn.disabled = true;
btnText.style.display = 'none';
loader.style.display = 'inline-block';
dom.playlist.innerHTML = `<div>正在加载 "${keyword}"...</div>`;
try {
const songs = await API.getList(keyword);
if (shuffle) { // 如果是探索模式,则打乱数组
shuffleArray(songs);
}
state.onlineSongs = songs;
renderOnlinePlaylist(); // 渲染播放列表
if (state.onlineSongs.length > 0) {
// 搜索后不自动播放第一首,让用户自己选择。探索模式则保持自动播放。
if (shuffle) {
findAndPlayOnlineSong(0);
}
} else {
dom.playlist.innerHTML = `<div>未能找到关于 "${keyword}" 的在线歌曲。</div>`;
}
} catch (error) {
console.error("加载在线歌曲时发生错误:", error);
dom.playlist.innerHTML = "<div>加载失败,请检查网络或API源。</div>";
} finally {
dom.loadOnlineBtn.disabled = false;
dom.searchBtn.disabled = false;
btnText.style.display = 'inline-block';
loader.style.display = 'none';
}
}
async function playOnlineSong(index) {
state.isOnlineMode = true;
state.currentTrackIndex = index;
updatePlaylistHighlight();
resetLyrics();
const song = state.onlineSongs[index];
dom.lyrics.innerHTML = `<div>正在加载: ${song.name}</div>`;
const urlEndpoint = API.getSongUrl(song);
const urlData = await fetch(urlEndpoint).then(res => res.json());
const audioUrl = urlData.url?.replace(/^http:/, 'https');
if (!audioUrl) throw new Error('无效的播放地址');
dom.audioPlayer.src = audioUrl;
dom.audioPlayer.load();
dom.audioPlayer.play();
try {
const lyricEndpoint = API.getLyric(song);
const lyricData = await fetch(lyricEndpoint).then(res => res.json());
if (lyricData.lyric) {
displayLyrics(lyricData.lyric);
} else {
dom.lyrics.innerHTML = '<div>未找到在线歌词。</div>';
}
} catch (lyricError) {
dom.lyrics.innerHTML = '<div>歌词加载失败。</div>';
}
}
function renderOnlinePlaylist() {
dom.playlist.innerHTML = state.onlineSongs.map((s, i) =>
`<div id="track-item-${i}" onclick="findAndPlayOnlineSong(${i})">
${s.name} - ${s.artists.map(a => a.name).join('/')}
</div>`
).join('') || "<div>列表为空</div>";
}
function switchMobileView(view) {
if (window.innerWidth > 768) return;
if (view === 'playlist') {
dom.showPlaylistBtn.classList.add('active');
dom.showLyricsBtn.classList.remove('active');
dom.playlist.classList.remove('mobile-hidden');
dom.lyrics.classList.add('mobile-hidden');
} else {
dom.showPlaylistBtn.classList.remove('active');
dom.showLyricsBtn.classList.add('active');
dom.playlist.classList.add('mobile-hidden');
dom.lyrics.classList.remove('mobile-hidden');
}
}
function playPrevious() {
if (state.currentTrackIndex <= 0) return;
const newIndex = state.currentTrackIndex - 1;
state.isOnlineMode ? findAndPlayOnlineSong(newIndex) : playAudio(newIndex);
}
function playNext() {
const limit = state.isOnlineMode ? state.onlineSongs.length : state.audioFiles.length;
if (state.currentTrackIndex >= limit - 1) {
dom.lyrics.innerHTML = '<div>已是列表最后一首。</div>';
return;
}
const newIndex = state.currentTrackIndex + 1;
state.isOnlineMode ? findAndPlayOnlineSong(newIndex) : playAudio(newIndex);
}
function autoPlayNext() { playNext(); }
function processFiles(files) {
if (files.length === 0) return;
state.isOnlineMode = false;
if (window.innerWidth <= 768) switchMobileView('playlist');
let newFilesAdded = false;
for (const file of files) {
const ext = file.name.split('.').pop().toLowerCase();
const baseName = file.name.split('.').slice(0, -1).join('.');
if (['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'wma'].includes(ext)) {
if (!state.audioFiles.some(f => f.name === file.name)) {
state.audioFiles.push(file);
newFilesAdded = true;
}
} else if (['lrc', 'txt'].includes(ext)) {
state.lyricsFiles[baseName] = file;
}
}
if (newFilesAdded) renderLocalPlaylist();
if (state.audioFiles.length > 0 && dom.audioPlayer.paused) playAudio(0);
}
function playAudio(index) {
if (index < 0 || index >= state.audioFiles.length) return;
if (state.currentAudioUrl) URL.revokeObjectURL(state.currentAudioUrl);
state.isOnlineMode = false;
state.currentTrackIndex = index;
updatePlaylistHighlight();
const audioFile = state.audioFiles[index];
state.currentAudioUrl = URL.createObjectURL(audioFile);
dom.audioPlayer.src = state.currentAudioUrl;
dom.audioPlayer.load();
dom.audioPlayer.play();
resetLyrics();
loadLyricsForFile(audioFile);
}
async function findAndPlayOnlineSong(startIndex) {
for (let i = startIndex; i < state.onlineSongs.length; i++) {
try {
await playOnlineSong(i);
return;
} catch (error) {
console.warn(`第 ${i + 1} 首 (${state.onlineSongs[i].name}) 尝试失败,跳过...`, error.message);
}
}
dom.lyrics.innerHTML = '<div>抱歉,当前列表中的歌曲均无法播放。</div>';
}
function renderLocalPlaylist() {
dom.playlist.innerHTML = state.audioFiles.map((f, i) => `<div id="track-item-${i}" onclick="playAudio(${i})">${f.name}</div>`).join('') || "<div>请选择本地音乐</div>";
}
function updatePlaylistHighlight() {
const items = dom.playlist.querySelectorAll('div');
let currentItem = null;
items.forEach((item, index) => {
const isCurrent = index === state.currentTrackIndex;
item.classList.toggle('current', isCurrent);
if (isCurrent) currentItem = item;
});
if (currentItem) {
currentItem.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
}
}
function resetLyrics() {
state.lyricsData = [];
state.currentLyricLine = -1;
dom.lyrics.innerHTML = '<div>...</div>';
}
async function loadLyricsForFile(audioFile) {
const baseName = audioFile.name.split('.').slice(0, -1).join('.');
const lyricsFile = state.lyricsFiles[baseName];
if (lyricsFile) {
const reader = new FileReader();
reader.onload = (e) => displayLyrics(e.target.result);
reader.readAsText(lyricsFile);
} else {
readEmbeddedLyrics(audioFile);
}
}
function readEmbeddedLyrics(file) {
jsmediatags.read(file, {
onSuccess: (tag) => {
const lyricsText = tag.tags.USLT?.data ?? tag.tags.SYLT?.data ?? tag.tags.lyrics;
if (lyricsText) {
displayLyrics(lyricsText);
} else {
dom.lyrics.innerHTML = '<div>未找到任何歌词。</div>';
}
},
onError: () => {
dom.lyrics.innerHTML = '<div>读取内嵌歌词失败。</div>';
}
});
}
function parseLrc(text) {
if (!text) return [];
const lines = text.split('\n');
const result = [];
const timeRegex = /\[(\d{2}):(\d{2})[.:](\d{2,3})\]/g;
for (const line of lines) {
const textContent = line.replace(timeRegex, '').trim();
if (textContent) {
let match;
timeRegex.lastIndex = 0;
while ((match = timeRegex.exec(line)) !== null) {
const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);
const milliseconds = parseInt(match[3].padEnd(3, '0'), 10);
result.push({
time: minutes * 60 + seconds + milliseconds / 1000,
text: textContent
});
}
}
}
return result.sort((a, b) => a.time - b.time);
}
function displayLyrics(lrcText) {
state.lyricsData = parseLrc(lrcText);
if (state.lyricsData.length > 0) {
dom.lyrics.innerHTML = state.lyricsData.map((item, index) => `<div id="lyric-line-${index}">${item.text}</div>`).join('');
} else {
dom.lyrics.innerHTML = lrcText.split('\n').map(line => `<div>${line || ' '}</div>`).join('');
}
state.currentLyricLine = -1;
syncLyrics();
}
function syncLyrics() {
if (state.lyricsData.length === 0 || dom.audioPlayer.paused) return;
const currentTime = dom.audioPlayer.currentTime;
let newIndex = state.lyricsData.findIndex((line, i) => {
const nextLine = state.lyricsData[i + 1];
return currentTime >= line.time && (nextLine ? currentTime < nextLine.time : true);
});
if (newIndex !== -1 && newIndex !== state.currentLyricLine) {
const prevLine = document.getElementById(`lyric-line-${state.currentLyricLine}`);
if (prevLine) prevLine.classList.remove('current');
const currentLine = document.getElementById(`lyric-line-${newIndex}`);
if (currentLine) {
currentLine.classList.add('current');
if (dom.lyrics.offsetParent !== null) {
currentLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
state.currentLyricLine = newIndex;
}
}
function shuffleArray(array) {
let currentIndex = array.length, randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
}
</script>
</body>
</html>- 4
- 0
-
分享