This commit is contained in:
N1KO 2024-12-16 11:20:46 +08:00
parent f4b5c4dc06
commit b6f3ff6dce
14 changed files with 380 additions and 26 deletions

View File

@ -1,5 +1,6 @@
package top.baogutang.music.client;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.AbstractMusicReq;
import top.baogutang.music.domain.req.search.MusicSearchReq;
@ -8,7 +9,7 @@ import top.baogutang.music.domain.res.download.MusicDownloadRes;
import top.baogutang.music.domain.res.search.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
*
@ -39,4 +40,9 @@ public interface ChannelClient<Q extends AbstractMusicReq, S extends AbstractMus
String getDownloadBasePath();
void saveDownloadRecord(Long userId, String batchNo, String musicId);
List<MusicDownloadRecordEntity> queryDownloadRecord(String batchNo);
List<MusicRecordEntity> queryMusicByPlatformIdList(List<String> platformIdList);
}

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.baogutang.music.annos.ChannelInfo;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.download.MusicDownloadRes;
@ -12,6 +13,7 @@ import top.baogutang.music.enums.AudioFileTypeEnum;
import top.baogutang.music.enums.ChannelEnum;
import top.baogutang.music.processor.AbstractAudioProcessor;
import top.baogutang.music.properties.NetEaseMusicProperties;
import top.baogutang.music.service.IMusicDownloadRecordService;
import top.baogutang.music.service.IMusicRecordService;
import top.baogutang.music.utils.OkHttpUtil;
@ -23,6 +25,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
/**
@ -44,6 +47,9 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
@Resource
private IMusicRecordService musicRecordService;
@Resource
private IMusicDownloadRecordService musicDownloadRecordService;
@Override
public MusicSearchRes search(MusicSearchReq req) {
String searchUrl = String.format(netEaseMusicProperties.getQueryBaseUrl(), req.getKeywords(), req.getLimit(), req.getOffset(), req.getType());
@ -126,4 +132,20 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
return netEaseMusicProperties.getDownloadPath();
}
@Override
public void saveDownloadRecord(Long userId, String batchNo, String musicId) {
musicDownloadRecordService.save(userId, batchNo, ChannelEnum.NET_EASE_MUSIC, musicId);
}
@Override
public List<MusicDownloadRecordEntity> queryDownloadRecord(String batchNo) {
return musicDownloadRecordService.queryByBatchNo(batchNo);
}
@Override
public List<MusicRecordEntity> queryMusicByPlatformIdList(List<String> platformIdList) {
return musicRecordService.queryByPlatformIdList(platformIdList);
}
}

View File

@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import top.baogutang.music.annos.ChannelInfo;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.download.MusicDownloadRes;
@ -16,6 +17,7 @@ import top.baogutang.music.enums.SearchTypeEnum;
import top.baogutang.music.exceptions.BusinessException;
import top.baogutang.music.processor.AbstractAudioProcessor;
import top.baogutang.music.properties.QQMusicProperties;
import top.baogutang.music.service.IMusicDownloadRecordService;
import top.baogutang.music.service.IMusicRecordService;
import top.baogutang.music.utils.OkHttpUtil;
@ -49,6 +51,9 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
@Resource
private IMusicRecordService musicRecordService;
@Resource
private IMusicDownloadRecordService musicDownloadRecordService;
@Override
public MusicSearchRes search(MusicSearchReq req) {
Integer pageNo = (req.getOffset() / req.getLimit()) + 1;
@ -347,4 +352,19 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
return qqMusicProperties.getDownloadPath();
}
@Override
public void saveDownloadRecord(Long userId, String batchNo, String musicId) {
musicDownloadRecordService.save(userId, batchNo, ChannelEnum.QQ_MUSIC, musicId);
}
@Override
public List<MusicDownloadRecordEntity> queryDownloadRecord(String batchNo) {
return musicDownloadRecordService.queryByBatchNo(batchNo);
}
@Override
public List<MusicRecordEntity> queryMusicByPlatformIdList(List<String> platformIdList) {
return musicRecordService.queryByPlatformIdList(platformIdList);
}
}

View File

@ -1,17 +1,15 @@
package top.baogutang.music.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.baogutang.music.annos.Login;
import org.springframework.web.bind.annotation.*;
import top.baogutang.music.annos.Vip;
import top.baogutang.music.domain.Results;
import top.baogutang.music.domain.res.download.MusicDownloadRes;
import top.baogutang.music.domain.res.search.MusicPlaylistRes;
import top.baogutang.music.domain.req.download.MusicDownloadReq;
import top.baogutang.music.service.IMusicService;
import top.baogutang.music.utils.UserThreadLocal;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
/**
*
@ -29,11 +27,16 @@ public class MusicDownloadController {
private IMusicService musicService;
@GetMapping
@Login
public Results<MusicDownloadRes> download(@RequestParam(name = "channel") Integer channel,
@RequestParam(name = "id") String id) {
MusicDownloadRes res = musicService.getMusicService(channel).download(id);
return Results.ok(res);
@Vip
public Results<String> download(@RequestBody MusicDownloadReq req) {
String downloadUrl = musicService.getMusicService(req.getChannel()).downloads(req.getIdList(), UserThreadLocal.get());
return Results.ok(downloadUrl);
}
@GetMapping("/file")
@Vip
public void downloadFile(@RequestParam(name = "batchNo") String batchNo, @RequestParam(name = "channel") Integer channel, HttpServletResponse response) {
musicService.getMusicService(channel).downloadFile(batchNo, response);
}

View File

@ -0,0 +1,31 @@
package top.baogutang.music.dao.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import top.baogutang.music.enums.ChannelEnum;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/16 : 10:01
*/
@Getter
@Setter
@TableName("t_music_download_record")
public class MusicDownloadRecordEntity extends BaseEntity {
private static final long serialVersionUID = -1789966661010962297L;
private String platformId;
private ChannelEnum channel;
private String batchNo;
private Long userId;
}

View File

@ -5,7 +5,6 @@ import lombok.Getter;
import lombok.Setter;
import top.baogutang.music.enums.AudioFileTypeEnum;
import top.baogutang.music.enums.ChannelEnum;
import top.baogutang.music.enums.MusicQualityEnum;
/**
*

View File

@ -0,0 +1,16 @@
package top.baogutang.music.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/16 : 10:08
*/
@Mapper
public interface MusicDownloadRecordMapper extends BaseMapper<MusicDownloadRecordEntity> {
}

View File

@ -0,0 +1,23 @@
package top.baogutang.music.domain.req.download;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/16 : 09:48
*/
@Data
public class MusicDownloadReq implements Serializable {
private static final long serialVersionUID = -1159631672581622400L;
private Integer channel;
private List<String> idList;
}

View File

@ -2,7 +2,11 @@ package top.baogutang.music.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.util.CollectionUtils;
import top.baogutang.music.client.ChannelClient;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.AbstractMusicReq;
import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.AbstractMusicRes;
@ -13,10 +17,19 @@ import top.baogutang.music.exceptions.BusinessException;
import top.baogutang.music.factory.ChannelClientFactory;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
*
@ -98,4 +111,97 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
// }
return res;
}
public String downloads(List<String> idList, Long userId) {
if (CollectionUtils.isEmpty(idList)) {
throw new BusinessException("请选择下载内容");
}
if (idList.size() > 100) {
throw new BusinessException("单次下载限制最多100首");
}
ChannelEnum channelEnum = getChannelEnum();
ChannelClient<Q, S> channelClient = channelClientFactory.getClient(channelEnum);
String batchNo = UUID.randomUUID().toString();
idList.stream()
.map(id ->
CompletableFuture.runAsync(() -> {
MusicDownloadRes res = channelClient.download(id);
if (Objects.isNull(res)) {
log.error(">>>>>>>>>>query detail error! channel:{},song id:{}<<<<<<<<<<", channelEnum.getDesc(), id);
return;
}
if (StringUtils.isNotBlank(res.getArtistName())) {
String[] split = res.getArtistName().split("/");
String artistName = String.join(",", split);
res.setArtistName(artistName);
}
channelClient.saveMusic(res);
channelClient.saveDownloadRecord(userId, batchNo, id);
try {
channelClient.processFile(res);
log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName());
} catch (IOException e) {
log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
throw new BusinessException("下载异常");
}
}))
.forEach(CompletableFuture::join);
return batchNo;
}
public void downloadFile(String batchNo, HttpServletResponse response) {
ChannelEnum channelEnum = getChannelEnum();
ChannelClient<Q, S> channelClient = channelClientFactory.getClient(channelEnum);
List<MusicDownloadRecordEntity> musicDownloadRecordList = channelClient.queryDownloadRecord(batchNo);
if (CollectionUtils.isEmpty(musicDownloadRecordList)) {
return;
}
List<String> platformIdList = musicDownloadRecordList.stream()
.map(MusicDownloadRecordEntity::getPlatformId)
.collect(Collectors.toList());
List<MusicRecordEntity> musicRecordList = channelClient.queryMusicByPlatformIdList(platformIdList);
if (CollectionUtils.isEmpty(musicRecordList)) {
return;
}
String downloadBasePath = channelClient.getDownloadBasePath();
// 设置响应头
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=\"music_files" + batchNo + ".zip\"");
try (ServletOutputStream out = response.getOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(out)) {
for (MusicRecordEntity musicRecord : musicRecordList) {
// 构建文件路径
String filePath = String.format("%s/%s/%s/%s.%s",
downloadBasePath,
musicRecord.getArtistName(),
musicRecord.getAlbumName(),
musicRecord.getName(),
musicRecord.getFileType().name().toLowerCase());
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
continue;
}
// 将文件写入 ZIP
try (FileInputStream fis = new FileInputStream(file)) {
String zipEntryName = String.format("%s/%s/%s.%s",
musicRecord.getArtistName(),
musicRecord.getAlbumName(),
musicRecord.getName(),
musicRecord.getFileType().name().toLowerCase());
zipOut.putNextEntry(new ZipEntry(zipEntryName));
IOUtils.copy(fis, zipOut);
zipOut.closeEntry();
} catch (IOException e) {
log.error(">>>>>>>>>>write file error:{}<<<<<<<<<<", e.getMessage(), e);
}
}
zipOut.finish();
} catch (IOException e) {
log.error(">>>>>>>>>>Error while creating zip responser:{}<<<<<<<<<<", e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,21 @@
package top.baogutang.music.service;
import com.baomidou.mybatisplus.extension.service.IService;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.enums.ChannelEnum;
import java.util.List;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/16 : 10:08
*/
public interface IMusicDownloadRecordService extends IService<MusicDownloadRecordEntity> {
void save(Long userId, String batchNo, ChannelEnum channelEnum, String musicId);
List<MusicDownloadRecordEntity> queryByBatchNo(String batchNo);
}

View File

@ -5,6 +5,8 @@ import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.res.download.MusicDownloadRes;
import top.baogutang.music.enums.ChannelEnum;
import java.util.List;
/**
*
* @description:
@ -17,4 +19,6 @@ public interface IMusicRecordService extends IService<MusicRecordEntity> {
void save(MusicDownloadRes res, ChannelEnum channel);
MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId);
List<MusicRecordEntity> queryByPlatformIdList(List<String> platformIdList);
}

View File

@ -0,0 +1,44 @@
package top.baogutang.music.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.mapper.MusicDownloadRecordMapper;
import top.baogutang.music.enums.ChannelEnum;
import top.baogutang.music.service.IMusicDownloadRecordService;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/16 : 10:09
*/
@Slf4j
@Service
public class MusicDownloadRecordServiceImpl extends ServiceImpl<MusicDownloadRecordMapper, MusicDownloadRecordEntity> implements IMusicDownloadRecordService {
@Override
public void save(Long userId, String batchNo, ChannelEnum channelEnum, String musicId) {
MusicDownloadRecordEntity musicDownloadRecordEntity = new MusicDownloadRecordEntity();
musicDownloadRecordEntity.setPlatformId(musicId);
musicDownloadRecordEntity.setChannel(channelEnum);
musicDownloadRecordEntity.setBatchNo(batchNo);
musicDownloadRecordEntity.setUserId(userId);
musicDownloadRecordEntity.setCreateTime(LocalDateTime.now());
baseMapper.insert(musicDownloadRecordEntity);
}
@Override
public List<MusicDownloadRecordEntity> queryByBatchNo(String batchNo) {
return new LambdaQueryChainWrapper<>(baseMapper)
.eq(MusicDownloadRecordEntity::getBatchNo, batchNo)
.eq(MusicDownloadRecordEntity::getDeleted, Boolean.FALSE)
.list();
}
}

View File

@ -12,6 +12,7 @@ import top.baogutang.music.enums.MusicQualityEnum;
import top.baogutang.music.service.IMusicRecordService;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
@ -66,4 +67,13 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
.last(" limit 1 ")
.one();
}
@Override
public List<MusicRecordEntity> queryByPlatformIdList(List<String> platformIdList) {
return new LambdaQueryChainWrapper<>(baseMapper)
.in(MusicRecordEntity::getPlatformId, platformIdList)
.eq(MusicRecordEntity::getDeleted, Boolean.FALSE)
.list();
}
}

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<link rel="icon" type="image/png" href="NIKO.png">
<meta charset="UTF-8" />
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>BAOGUTANG-MUSIC</title>
<style>
@ -12,21 +12,24 @@
margin: 0;
padding: 20px;
}
.container {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 95%;
max-width: 1200px;
margin: 0 auto;
position: relative;
}
h1 {
color: #62D2A1;
margin-bottom: 20px;
font-size: 1.8rem;
}
.header {
display: flex;
justify-content: flex-end;
@ -34,6 +37,7 @@
margin-bottom: 20px;
position: relative;
}
.user-info {
font-size: 1rem;
color: #333;
@ -42,6 +46,7 @@
padding: 5px 10px;
border-radius: 5px;
}
/* Dropdown Menu Styles */
.dropdown {
position: absolute;
@ -50,15 +55,17 @@
background-color: #f9f9f9; /* 修改为浅灰色 */
border: 1px solid #ccc; /* 较深的边框 */
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: none;
min-width: 150px;
z-index: 1000;
transition: opacity 0.3s ease; /* 可选:添加过渡效果 */
}
.dropdown.visible {
display: block;
}
.dropdown button {
width: 100%;
padding: 10px;
@ -70,19 +77,23 @@
color: #333; /* 深色文本 */
transition: background-color 0.3s ease; /* 可选:添加过渡效果 */
}
.dropdown button:hover {
background-color: #e0e0e0; /* 悬停时颜色变化 */
}
.form-group {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
label {
font-size: 1rem;
color: #555;
margin-bottom: 8px;
}
input[type="text"] {
padding: 10px;
font-size: 1rem;
@ -91,12 +102,14 @@
background-color: #f9f9f9;
margin-top: 10px;
}
.item-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.item {
cursor: pointer;
padding: 8px 12px;
@ -109,18 +122,22 @@
white-space: nowrap;
background-color: #fff;
}
.item:hover {
background-color: #f0f0f0;
}
.item.selected {
background-color: #d0f0d0;
border-color: #4CAF50;
}
.platform-logo {
width: 30px;
height: 30px;
object-fit: contain;
}
.search-btn {
background-color: #62D2A1;
color: white;
@ -133,9 +150,11 @@
width: 100%;
margin-top: 10px;
}
.search-btn:hover {
background-color: #45a049;
}
button {
background-color: #4CAF50;
color: white;
@ -146,17 +165,21 @@
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.search-result {
margin-top: 20px;
}
.loading {
text-align: center;
padding: 10px;
color: #666;
}
.list-header {
display: flex;
align-items: center;
@ -164,6 +187,7 @@
padding: 10px 0;
gap: 10px;
}
.header-info {
display: grid;
grid-template-columns: 30px 2fr 1fr 1fr;
@ -171,16 +195,20 @@
gap: 10px;
flex: 1;
}
.header-info > div:first-child {
text-align: center;
}
.download-btn {
display: none;
margin-left: auto;
}
.song-list {
margin-top: 10px;
}
.song-row {
display: grid;
grid-template-columns: 30px 2fr 1fr 1fr;
@ -190,19 +218,23 @@
font-size: 0.95rem;
gap: 10px;
}
.song-row:hover {
background-color: #f7f7f7;
}
.song-checkbox {
display: flex;
justify-content: center;
align-items: center;
}
.song-name, .song-album, .song-artists {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 768px) {
.header-info, .song-row {
grid-template-columns: 30px 1fr 1fr 1fr;
@ -217,6 +249,7 @@
gap: 15px;
margin-top: 20px;
}
.album-item {
width: 200px;
border: 1px solid #ddd;
@ -231,15 +264,18 @@
text-align: center;
padding: 10px;
}
.album-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.album-cover {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 5px;
}
.album-name {
font-size: 1rem;
color: #333;
@ -250,6 +286,7 @@
overflow: hidden;
width: 100%;
}
.album-type {
font-size: 0.9rem;
color: #666;
@ -262,6 +299,7 @@
gap: 15px;
margin-top: 20px;
}
.artist-item {
width: 150px;
border: 1px solid #ddd;
@ -276,15 +314,18 @@
text-align: center;
padding: 10px;
}
.artist-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.artist-pic {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 5px;
}
.artist-name {
font-size: 1rem;
color: #333;
@ -303,6 +344,7 @@
gap: 15px;
margin-top: 20px;
}
.playlist-item {
width: 200px;
border: 1px solid #ddd;
@ -317,15 +359,18 @@
text-align: center;
padding: 10px;
}
.playlist-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.playlist-cover {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 5px;
}
.playlist-name {
font-size: 1rem;
color: #333;
@ -341,19 +386,23 @@
margin-top: 10px;
display: none;
}
.progress-container p {
margin-bottom: 5px;
font-size: 0.9rem;
color: #333;
}
progress {
width: 100%;
height: 20px;
}
progress::-webkit-progress-bar {
background-color: #eee;
border-radius: 5px;
}
progress::-webkit-progress-value {
background-color: #4CAF50;
border-radius: 5px;
@ -381,7 +430,7 @@
</div>
<div class="form-group">
<input type="text" id="keywords" placeholder="输入关键词" />
<input type="text" id="keywords" placeholder="输入关键词"/>
</div>
<button class="search-btn" onclick="startSearch()">搜索</button>
@ -415,7 +464,7 @@
let selectedSongs = [];
let allSongs = [];
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
fetchPlatforms();
fetchSearchTypes();
window.addEventListener('scroll', handleScroll);
@ -424,13 +473,13 @@
// 点击用户名时,切换下拉菜单的显示状态
const userInfoDiv = document.getElementById('user-info');
const dropdownMenu = document.getElementById('dropdown-menu');
userInfoDiv.addEventListener('click', function(event) {
userInfoDiv.addEventListener('click', function (event) {
event.stopPropagation(); // 防止事件冒泡
dropdownMenu.classList.toggle('visible');
});
// 点击页面其他部分时,隐藏下拉菜单
document.addEventListener('click', function() {
document.addEventListener('click', function () {
dropdownMenu.classList.remove('visible');
});
});
@ -930,7 +979,7 @@
alert(msg);
setTimeout(() => {
window.location.href = '/login.html'; // 确保登录页面的路径正确
}, 1000);
}, 500);
}
// 退出登录函数
@ -966,7 +1015,7 @@
// 辅助函数转义HTML以防止XSS攻击
function escapeHTML(str) {
if (!str) return '';
return str.replace(/[&<>"'`=\/]/g, function(s) {
return str.replace(/[&<>"'`=\/]/g, function (s) {
return ({
'&': '&amp;',
'<': '&lt;',