mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 03:15:07 +08:00
693 lines
19 KiB
HTML
693 lines
19 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>歌单格式转换器</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.header p {
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.content {
|
||
padding: 40px;
|
||
}
|
||
|
||
.upload-section {
|
||
background: #f8f9ff;
|
||
border: 3px dashed #4facfe;
|
||
border-radius: 15px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.upload-section:hover {
|
||
border-color: #00f2fe;
|
||
background: #f0f8ff;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.upload-section.dragover {
|
||
border-color: #00f2fe;
|
||
background: #e8f4ff;
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 4em;
|
||
color: #4facfe;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 1.3em;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-subtext {
|
||
color: #666;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.file-input {
|
||
display: none;
|
||
}
|
||
|
||
.preview-section {
|
||
display: none;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.preview-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.preview-title {
|
||
font-size: 1.5em;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.song-count {
|
||
background: #4facfe;
|
||
color: white;
|
||
padding: 5px 15px;
|
||
border-radius: 20px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.preview-content {
|
||
background: #f8f9ff;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.song-item {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||
display: flex;
|
||
align-items: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.song-item:hover {
|
||
transform: translateX(5px);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.song-number {
|
||
background: #4facfe;
|
||
color: white;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
margin-right: 15px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.song-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.song-name {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.song-artist {
|
||
color: #666;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-bottom: 30px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 25px;
|
||
font-size: 1em;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f1f3f4;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.result-section {
|
||
display: none;
|
||
background: #f8f9ff;
|
||
border-radius: 15px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.result-icon {
|
||
font-size: 4em;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.result-success {
|
||
color: #28a745;
|
||
}
|
||
|
||
.result-error {
|
||
color: #dc3545;
|
||
}
|
||
|
||
.result-message {
|
||
font-size: 1.2em;
|
||
margin-bottom: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.download-link {
|
||
display: inline-block;
|
||
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||
color: white;
|
||
padding: 12px 30px;
|
||
border-radius: 25px;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.download-link:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.loading {
|
||
display: none;
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.spinner {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #4facfe;
|
||
border-radius: 50%;
|
||
width: 50px;
|
||
height: 50px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.error-details {
|
||
background: #fff5f5;
|
||
border: 1px solid #fed7d7;
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
margin-top: 15px;
|
||
text-align: left;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
color: #c53030;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
margin: 10px;
|
||
border-radius: 15px;
|
||
}
|
||
|
||
.content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎵 澜音歌单格式转换器</h1>
|
||
<p>将洛雪音乐平台的歌单转换为澜音音乐的加密格式</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="upload-section" id="uploadSection">
|
||
<div class="upload-icon">📁</div>
|
||
<div class="upload-text">点击或拖拽洛雪歌单文件到此处</div>
|
||
<div class="upload-subtext">支持 *.json 和 *.lxmc 格式的歌单文件</div>
|
||
<input type="file" id="fileInput" class="file-input" accept=".json,.lxmc" />
|
||
</div>
|
||
|
||
<div class="preview-section" id="previewSection">
|
||
<div class="preview-header">
|
||
<div class="preview-title">歌单预览</div>
|
||
<div class="song-count" id="songCount">0 首歌曲</div>
|
||
</div>
|
||
<div class="preview-content" id="previewContent">
|
||
<!-- 歌曲列表将在这里显示 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls" id="controls" style="display: none">
|
||
<button class="btn btn-primary" id="convertBtn">
|
||
<span>🔐</span>
|
||
转换并加密
|
||
</button>
|
||
<button class="btn btn-secondary" id="resetBtn">
|
||
<span>🔄</span>
|
||
重新选择
|
||
</button>
|
||
</div>
|
||
|
||
<div class="loading" id="loadingSection">
|
||
<div class="spinner"></div>
|
||
<div>正在转换中...</div>
|
||
</div>
|
||
|
||
<div class="result-section" id="resultSection">
|
||
<div class="result-icon" id="resultIcon"></div>
|
||
<div class="result-message" id="resultMessage"></div>
|
||
<a href="#" class="download-link" id="downloadLink" style="display: none">
|
||
<span>📥</span>
|
||
下载澜音歌单
|
||
</a>
|
||
<div class="error-details" id="errorDetails" style="display: none"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let currentPlaylist = null
|
||
let convertedPlaylist = null
|
||
const SECRET_KEY = 'CeruMusic-PlaylistSecretKey'
|
||
|
||
// Gzip解压函数
|
||
async function decompressGzip(arrayBuffer) {
|
||
try {
|
||
// 使用浏览器的 DecompressionStream API
|
||
const stream = new Response(arrayBuffer).body.pipeThrough(new DecompressionStream('gzip'))
|
||
const decompressed = await new Response(stream).arrayBuffer()
|
||
return new TextDecoder().decode(decompressed)
|
||
} catch (error) {
|
||
throw new Error('Gzip解压失败: ' + error.message)
|
||
}
|
||
}
|
||
|
||
// DOM元素
|
||
const uploadSection = document.getElementById('uploadSection')
|
||
const fileInput = document.getElementById('fileInput')
|
||
const previewSection = document.getElementById('previewSection')
|
||
const previewContent = document.getElementById('previewContent')
|
||
const songCount = document.getElementById('songCount')
|
||
const controls = document.getElementById('controls')
|
||
const convertBtn = document.getElementById('convertBtn')
|
||
const resetBtn = document.getElementById('resetBtn')
|
||
const loadingSection = document.getElementById('loadingSection')
|
||
const resultSection = document.getElementById('resultSection')
|
||
const resultIcon = document.getElementById('resultIcon')
|
||
const resultMessage = document.getElementById('resultMessage')
|
||
const downloadLink = document.getElementById('downloadLink')
|
||
const errorDetails = document.getElementById('errorDetails')
|
||
|
||
// 文件上传处理
|
||
uploadSection.addEventListener('click', () => fileInput.click())
|
||
|
||
uploadSection.addEventListener('dragover', (e) => {
|
||
e.preventDefault()
|
||
uploadSection.classList.add('dragover')
|
||
})
|
||
|
||
uploadSection.addEventListener('dragleave', () => {
|
||
uploadSection.classList.remove('dragover')
|
||
})
|
||
|
||
uploadSection.addEventListener('drop', (e) => {
|
||
e.preventDefault()
|
||
uploadSection.classList.remove('dragover')
|
||
const files = e.dataTransfer.files
|
||
if (files.length > 0) {
|
||
handleFile(files[0])
|
||
}
|
||
})
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
const file = e.target.files[0]
|
||
if (file) {
|
||
handleFile(file)
|
||
}
|
||
})
|
||
|
||
// 按钮事件
|
||
convertBtn.addEventListener('click', convertPlaylist)
|
||
resetBtn.addEventListener('click', resetConverter)
|
||
|
||
// 处理文件
|
||
function handleFile(file) {
|
||
const fileName = file.name.toLowerCase()
|
||
|
||
if (fileName.endsWith('.json')) {
|
||
// 处理JSON文件
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
try {
|
||
const content = JSON.parse(e.target.result)
|
||
currentPlaylist = content
|
||
displayPreview(content)
|
||
} catch (error) {
|
||
showError('JSON文件解析失败: ' + error.message)
|
||
}
|
||
}
|
||
reader.readAsText(file)
|
||
} else if (fileName.endsWith('.lxmc')) {
|
||
// 处理LXMC文件(gzip压缩)
|
||
const reader = new FileReader()
|
||
reader.onload = async (e) => {
|
||
try {
|
||
const arrayBuffer = e.target.result
|
||
const decompressed = await decompressGzip(arrayBuffer)
|
||
const content = JSON.parse(decompressed)
|
||
currentPlaylist = content
|
||
displayPreview(content)
|
||
} catch (error) {
|
||
showError('LXMC文件处理失败: ' + error.message)
|
||
}
|
||
}
|
||
reader.readAsArrayBuffer(file)
|
||
} else {
|
||
showError('请选择JSON或LXMC格式的文件')
|
||
return
|
||
}
|
||
}
|
||
|
||
// 显示预览
|
||
function displayPreview(playlist) {
|
||
previewContent.innerHTML = ''
|
||
|
||
const playlistData =
|
||
playlist.type === 'playListPart_v2' && playlist.data ? playlist.data : playlist
|
||
const songList = playlistData.list
|
||
|
||
if (!songList || !Array.isArray(songList)) {
|
||
showError('无效的歌单格式:缺少歌曲列表')
|
||
return
|
||
}
|
||
|
||
// 显示歌曲列表
|
||
songList.forEach((song, index) => {
|
||
const songItem = document.createElement('div')
|
||
songItem.className = 'song-item'
|
||
songItem.innerHTML = `
|
||
<div class="song-number">${index + 1}</div>
|
||
<div class="song-info">
|
||
<div class="song-name">${song.name || '未知歌曲'}</div>
|
||
<div class="song-artist">${song.singer || '未知艺术家'} • ${song.interval || '0:00'}</div>
|
||
</div>
|
||
`
|
||
previewContent.appendChild(songItem)
|
||
})
|
||
|
||
songCount.textContent = `${songList.length} 首歌曲`
|
||
previewSection.style.display = 'block'
|
||
controls.style.display = 'flex'
|
||
uploadSection.style.display = 'none'
|
||
}
|
||
|
||
// 转换歌单
|
||
function convertPlaylist() {
|
||
if (!currentPlaylist) return
|
||
|
||
showLoading()
|
||
|
||
try {
|
||
// 转换格式
|
||
const convertedSongs = convertToTargetFormat(currentPlaylist)
|
||
|
||
// 加密
|
||
const encryptedData = encryptPlaylist(convertedSongs)
|
||
|
||
convertedPlaylist = {
|
||
original: currentPlaylist,
|
||
converted: convertedSongs,
|
||
encrypted: encryptedData
|
||
}
|
||
|
||
showSuccess(convertedPlaylist)
|
||
} catch (error) {
|
||
showError('转换失败: ' + error.message, error.stack)
|
||
}
|
||
}
|
||
|
||
// 转换为目标格式
|
||
function convertToTargetFormat(originalPlaylist) {
|
||
if (originalPlaylist.type === 'playListPart_v2' && originalPlaylist.data) {
|
||
originalPlaylist = originalPlaylist.data
|
||
}
|
||
if (!originalPlaylist.list || !Array.isArray(originalPlaylist.list)) {
|
||
throw new Error('原始歌单格式无效')
|
||
}
|
||
|
||
return originalPlaylist.list.map((song) => {
|
||
// 从meta中提取额外信息
|
||
const meta = song.meta || {}
|
||
|
||
return {
|
||
songmid: meta.hash || meta.songId,
|
||
singer: song.singer || '未知艺术家',
|
||
name: song.name || '未知歌曲',
|
||
albumName: meta.albumName || '未知专辑',
|
||
albumId: meta.albumId,
|
||
source: song.source || 'unknown',
|
||
interval: song.interval || '0:00',
|
||
img: meta.picUrl || '',
|
||
lrc: null,
|
||
types: meta.qualitys,
|
||
_types: meta._qualitys,
|
||
typeUrl: {},
|
||
url: ''
|
||
}
|
||
})
|
||
}
|
||
|
||
// 转换音质类型
|
||
function convertQualityTypes(qualities) {
|
||
const types = {}
|
||
if (!qualities || !Array.isArray(qualities)) {
|
||
types['128k'] = { size: '未知' }
|
||
return types
|
||
}
|
||
|
||
qualities.forEach((quality) => {
|
||
if (typeof quality.type !== 'string') return
|
||
const q = quality.type.toLowerCase()
|
||
if (q.includes('flac')) {
|
||
if (q.includes('24')) {
|
||
types['flac24bit'] = { size: '未知' }
|
||
} else {
|
||
types['flac'] = { size: '未知' }
|
||
}
|
||
} else if (q.includes('320')) {
|
||
types['320k'] = { size: '未知' }
|
||
} else if (q.includes('128')) {
|
||
types['128k'] = { size: '未知' }
|
||
} else {
|
||
types[q] = { size: '未知' }
|
||
}
|
||
})
|
||
|
||
return Object.keys(types).length > 0 ? types : { '128k': { size: '未知' } }
|
||
}
|
||
|
||
// 生成随机ID
|
||
function generateRandomId() {
|
||
return Math.random().toString(36).substr(2, 9)
|
||
}
|
||
|
||
// 加密歌单
|
||
function encryptPlaylist(songs) {
|
||
const dataToEncrypt = JSON.stringify(songs)
|
||
return CryptoJS.AES.encrypt(dataToEncrypt, SECRET_KEY).toString()
|
||
}
|
||
|
||
// 显示加载状态
|
||
function showLoading() {
|
||
loadingSection.style.display = 'block'
|
||
controls.style.display = 'none'
|
||
resultSection.style.display = 'none'
|
||
}
|
||
|
||
// 显示成功结果
|
||
function showSuccess(result) {
|
||
loadingSection.style.display = 'none'
|
||
resultSection.style.display = 'block'
|
||
|
||
resultIcon.className = 'result-icon result-success'
|
||
resultIcon.textContent = '✅'
|
||
resultMessage.textContent = '歌单转换成功!'
|
||
|
||
// 创建下载链接
|
||
const encryptedData = result.encrypted
|
||
const blob = new Blob([encryptedData], {
|
||
type: 'application/octet-stream'
|
||
})
|
||
const url = URL.createObjectURL(blob)
|
||
|
||
downloadLink.href = url
|
||
downloadLink.download = `cerumusic-playlist-${new Date().toISOString().slice(0, 10)}.cpl`
|
||
downloadLink.style.display = 'inline-block'
|
||
|
||
// 清理之前的URL
|
||
if (downloadLink.dataset.url) {
|
||
URL.revokeObjectURL(downloadLink.dataset.url)
|
||
}
|
||
downloadLink.dataset.url = url
|
||
}
|
||
|
||
// 显示错误信息
|
||
function showError(message, details = null) {
|
||
loadingSection.style.display = 'none'
|
||
resultSection.style.display = 'block'
|
||
|
||
resultIcon.className = 'result-icon result-error'
|
||
resultIcon.textContent = '❌'
|
||
resultMessage.textContent = message
|
||
|
||
if (details) {
|
||
errorDetails.textContent = details
|
||
errorDetails.style.display = 'block'
|
||
}
|
||
|
||
downloadLink.style.display = 'none'
|
||
}
|
||
|
||
// 重置转换器
|
||
function resetConverter() {
|
||
currentPlaylist = null
|
||
convertedPlaylist = null
|
||
|
||
uploadSection.style.display = 'block'
|
||
previewSection.style.display = 'none'
|
||
controls.style.display = 'none'
|
||
loadingSection.style.display = 'none'
|
||
resultSection.style.display = 'none'
|
||
|
||
fileInput.value = ''
|
||
previewContent.innerHTML = ''
|
||
errorDetails.style.display = 'none'
|
||
|
||
// 清理下载链接
|
||
if (downloadLink.dataset.url) {
|
||
URL.revokeObjectURL(downloadLink.dataset.url)
|
||
delete downloadLink.dataset.url
|
||
}
|
||
}
|
||
|
||
// 页面卸载时清理资源
|
||
window.addEventListener('beforeunload', () => {
|
||
if (downloadLink.dataset.url) {
|
||
URL.revokeObjectURL(downloadLink.dataset.url)
|
||
}
|
||
})
|
||
</script>
|
||
</body>
|
||
</html>
|