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; package top.baogutang.music.client;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity; import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.AbstractMusicReq; import top.baogutang.music.domain.req.AbstractMusicReq;
import top.baogutang.music.domain.req.search.MusicSearchReq; 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 top.baogutang.music.domain.res.search.*;
import java.io.IOException; 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(); 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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.baogutang.music.annos.ChannelInfo; import top.baogutang.music.annos.ChannelInfo;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity; import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.search.MusicSearchReq; import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.download.MusicDownloadRes; 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.enums.ChannelEnum;
import top.baogutang.music.processor.AbstractAudioProcessor; import top.baogutang.music.processor.AbstractAudioProcessor;
import top.baogutang.music.properties.NetEaseMusicProperties; import top.baogutang.music.properties.NetEaseMusicProperties;
import top.baogutang.music.service.IMusicDownloadRecordService;
import top.baogutang.music.service.IMusicRecordService; import top.baogutang.music.service.IMusicRecordService;
import top.baogutang.music.utils.OkHttpUtil; import top.baogutang.music.utils.OkHttpUtil;
@ -23,6 +25,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
@ -44,6 +47,9 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
@Resource @Resource
private IMusicRecordService musicRecordService; private IMusicRecordService musicRecordService;
@Resource
private IMusicDownloadRecordService musicDownloadRecordService;
@Override @Override
public MusicSearchRes search(MusicSearchReq req) { public MusicSearchRes search(MusicSearchReq req) {
String searchUrl = String.format(netEaseMusicProperties.getQueryBaseUrl(), req.getKeywords(), req.getLimit(), req.getOffset(), req.getType()); 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(); 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.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.baogutang.music.annos.ChannelInfo; import top.baogutang.music.annos.ChannelInfo;
import top.baogutang.music.dao.entity.MusicDownloadRecordEntity;
import top.baogutang.music.dao.entity.MusicRecordEntity; import top.baogutang.music.dao.entity.MusicRecordEntity;
import top.baogutang.music.domain.req.search.MusicSearchReq; import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.download.MusicDownloadRes; 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.exceptions.BusinessException;
import top.baogutang.music.processor.AbstractAudioProcessor; import top.baogutang.music.processor.AbstractAudioProcessor;
import top.baogutang.music.properties.QQMusicProperties; import top.baogutang.music.properties.QQMusicProperties;
import top.baogutang.music.service.IMusicDownloadRecordService;
import top.baogutang.music.service.IMusicRecordService; import top.baogutang.music.service.IMusicRecordService;
import top.baogutang.music.utils.OkHttpUtil; import top.baogutang.music.utils.OkHttpUtil;
@ -49,6 +51,9 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
@Resource @Resource
private IMusicRecordService musicRecordService; private IMusicRecordService musicRecordService;
@Resource
private IMusicDownloadRecordService musicDownloadRecordService;
@Override @Override
public MusicSearchRes search(MusicSearchReq req) { public MusicSearchRes search(MusicSearchReq req) {
Integer pageNo = (req.getOffset() / req.getLimit()) + 1; Integer pageNo = (req.getOffset() / req.getLimit()) + 1;
@ -347,4 +352,19 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
return qqMusicProperties.getDownloadPath(); 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; package top.baogutang.music.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping; import top.baogutang.music.annos.Vip;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.baogutang.music.annos.Login;
import top.baogutang.music.domain.Results; import top.baogutang.music.domain.Results;
import top.baogutang.music.domain.res.download.MusicDownloadRes; import top.baogutang.music.domain.req.download.MusicDownloadReq;
import top.baogutang.music.domain.res.search.MusicPlaylistRes;
import top.baogutang.music.service.IMusicService; import top.baogutang.music.service.IMusicService;
import top.baogutang.music.utils.UserThreadLocal;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
/** /**
* *
@ -29,11 +27,16 @@ public class MusicDownloadController {
private IMusicService musicService; private IMusicService musicService;
@GetMapping @GetMapping
@Login @Vip
public Results<MusicDownloadRes> download(@RequestParam(name = "channel") Integer channel, public Results<String> download(@RequestBody MusicDownloadReq req) {
@RequestParam(name = "id") String id) { String downloadUrl = musicService.getMusicService(req.getChannel()).downloads(req.getIdList(), UserThreadLocal.get());
MusicDownloadRes res = musicService.getMusicService(channel).download(id); return Results.ok(downloadUrl);
return Results.ok(res); }
@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 lombok.Setter;
import top.baogutang.music.enums.AudioFileTypeEnum; import top.baogutang.music.enums.AudioFileTypeEnum;
import top.baogutang.music.enums.ChannelEnum; 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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.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.AbstractMusicReq;
import top.baogutang.music.domain.req.search.MusicSearchReq; import top.baogutang.music.domain.req.search.MusicSearchReq;
import top.baogutang.music.domain.res.AbstractMusicRes; import top.baogutang.music.domain.res.AbstractMusicRes;
@ -13,10 +17,19 @@ import top.baogutang.music.exceptions.BusinessException;
import top.baogutang.music.factory.ChannelClientFactory; import top.baogutang.music.factory.ChannelClientFactory;
import javax.annotation.Resource; 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.io.IOException;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; 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; 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.domain.res.download.MusicDownloadRes;
import top.baogutang.music.enums.ChannelEnum; import top.baogutang.music.enums.ChannelEnum;
import java.util.List;
/** /**
* *
* @description: * @description:
@ -17,4 +19,6 @@ public interface IMusicRecordService extends IService<MusicRecordEntity> {
void save(MusicDownloadRes res, ChannelEnum channel); void save(MusicDownloadRes res, ChannelEnum channel);
MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId); 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 top.baogutang.music.service.IMusicRecordService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
@ -66,4 +67,13 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
.last(" limit 1 ") .last(" limit 1 ")
.one(); .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"> <html lang="zh-CN">
<head> <head>
<link rel="icon" type="image/png" href="NIKO.png"> <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"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>BAOGUTANG-MUSIC</title> <title>BAOGUTANG-MUSIC</title>
<style> <style>
@ -12,21 +12,24 @@
margin: 0; margin: 0;
padding: 20px; padding: 20px;
} }
.container { .container {
background: #fff; background: #fff;
padding: 20px; padding: 20px;
border-radius: 8px; 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%; width: 95%;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
} }
h1 { h1 {
color: #62D2A1; color: #62D2A1;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 1.8rem; font-size: 1.8rem;
} }
.header { .header {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -34,6 +37,7 @@
margin-bottom: 20px; margin-bottom: 20px;
position: relative; position: relative;
} }
.user-info { .user-info {
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
@ -42,6 +46,7 @@
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
} }
/* Dropdown Menu Styles */ /* Dropdown Menu Styles */
.dropdown { .dropdown {
position: absolute; position: absolute;
@ -50,15 +55,17 @@
background-color: #f9f9f9; /* 修改为浅灰色 */ background-color: #f9f9f9; /* 修改为浅灰色 */
border: 1px solid #ccc; /* 较深的边框 */ border: 1px solid #ccc; /* 较深的边框 */
border-radius: 5px; 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; display: none;
min-width: 150px; min-width: 150px;
z-index: 1000; z-index: 1000;
transition: opacity 0.3s ease; /* 可选:添加过渡效果 */ transition: opacity 0.3s ease; /* 可选:添加过渡效果 */
} }
.dropdown.visible { .dropdown.visible {
display: block; display: block;
} }
.dropdown button { .dropdown button {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
@ -70,19 +77,23 @@
color: #333; /* 深色文本 */ color: #333; /* 深色文本 */
transition: background-color 0.3s ease; /* 可选:添加过渡效果 */ transition: background-color 0.3s ease; /* 可选:添加过渡效果 */
} }
.dropdown button:hover { .dropdown button:hover {
background-color: #e0e0e0; /* 悬停时颜色变化 */ background-color: #e0e0e0; /* 悬停时颜色变化 */
} }
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
label { label {
font-size: 1rem; font-size: 1rem;
color: #555; color: #555;
margin-bottom: 8px; margin-bottom: 8px;
} }
input[type="text"] { input[type="text"] {
padding: 10px; padding: 10px;
font-size: 1rem; font-size: 1rem;
@ -91,12 +102,14 @@
background-color: #f9f9f9; background-color: #f9f9f9;
margin-top: 10px; margin-top: 10px;
} }
.item-list { .item-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 15px; gap: 15px;
margin-top: 10px; margin-top: 10px;
} }
.item { .item {
cursor: pointer; cursor: pointer;
padding: 8px 12px; padding: 8px 12px;
@ -109,18 +122,22 @@
white-space: nowrap; white-space: nowrap;
background-color: #fff; background-color: #fff;
} }
.item:hover { .item:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.item.selected { .item.selected {
background-color: #d0f0d0; background-color: #d0f0d0;
border-color: #4CAF50; border-color: #4CAF50;
} }
.platform-logo { .platform-logo {
width: 30px; width: 30px;
height: 30px; height: 30px;
object-fit: contain; object-fit: contain;
} }
.search-btn { .search-btn {
background-color: #62D2A1; background-color: #62D2A1;
color: white; color: white;
@ -133,9 +150,11 @@
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
} }
.search-btn:hover { .search-btn:hover {
background-color: #45a049; background-color: #45a049;
} }
button { button {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
@ -146,17 +165,21 @@
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
} }
button:hover { button:hover {
background-color: #45a049; background-color: #45a049;
} }
.search-result { .search-result {
margin-top: 20px; margin-top: 20px;
} }
.loading { .loading {
text-align: center; text-align: center;
padding: 10px; padding: 10px;
color: #666; color: #666;
} }
.list-header { .list-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -164,6 +187,7 @@
padding: 10px 0; padding: 10px 0;
gap: 10px; gap: 10px;
} }
.header-info { .header-info {
display: grid; display: grid;
grid-template-columns: 30px 2fr 1fr 1fr; grid-template-columns: 30px 2fr 1fr 1fr;
@ -171,16 +195,20 @@
gap: 10px; gap: 10px;
flex: 1; flex: 1;
} }
.header-info > div:first-child { .header-info > div:first-child {
text-align: center; text-align: center;
} }
.download-btn { .download-btn {
display: none; display: none;
margin-left: auto; margin-left: auto;
} }
.song-list { .song-list {
margin-top: 10px; margin-top: 10px;
} }
.song-row { .song-row {
display: grid; display: grid;
grid-template-columns: 30px 2fr 1fr 1fr; grid-template-columns: 30px 2fr 1fr 1fr;
@ -190,19 +218,23 @@
font-size: 0.95rem; font-size: 0.95rem;
gap: 10px; gap: 10px;
} }
.song-row:hover { .song-row:hover {
background-color: #f7f7f7; background-color: #f7f7f7;
} }
.song-checkbox { .song-checkbox {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.song-name, .song-album, .song-artists { .song-name, .song-album, .song-artists {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.header-info, .song-row { .header-info, .song-row {
grid-template-columns: 30px 1fr 1fr 1fr; grid-template-columns: 30px 1fr 1fr 1fr;
@ -217,6 +249,7 @@
gap: 15px; gap: 15px;
margin-top: 20px; margin-top: 20px;
} }
.album-item { .album-item {
width: 200px; width: 200px;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -231,15 +264,18 @@
text-align: center; text-align: center;
padding: 10px; padding: 10px;
} }
.album-item:hover { .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 { .album-cover {
width: 100%; width: 100%;
height: 200px; height: 200px;
object-fit: cover; object-fit: cover;
border-radius: 5px; border-radius: 5px;
} }
.album-name { .album-name {
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
@ -250,6 +286,7 @@
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
} }
.album-type { .album-type {
font-size: 0.9rem; font-size: 0.9rem;
color: #666; color: #666;
@ -262,6 +299,7 @@
gap: 15px; gap: 15px;
margin-top: 20px; margin-top: 20px;
} }
.artist-item { .artist-item {
width: 150px; width: 150px;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -276,15 +314,18 @@
text-align: center; text-align: center;
padding: 10px; padding: 10px;
} }
.artist-item:hover { .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 { .artist-pic {
width: 100%; width: 100%;
height: 150px; height: 150px;
object-fit: cover; object-fit: cover;
border-radius: 5px; border-radius: 5px;
} }
.artist-name { .artist-name {
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
@ -303,6 +344,7 @@
gap: 15px; gap: 15px;
margin-top: 20px; margin-top: 20px;
} }
.playlist-item { .playlist-item {
width: 200px; width: 200px;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -317,15 +359,18 @@
text-align: center; text-align: center;
padding: 10px; padding: 10px;
} }
.playlist-item:hover { .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 { .playlist-cover {
width: 100%; width: 100%;
height: 200px; height: 200px;
object-fit: cover; object-fit: cover;
border-radius: 5px; border-radius: 5px;
} }
.playlist-name { .playlist-name {
font-size: 1rem; font-size: 1rem;
color: #333; color: #333;
@ -341,19 +386,23 @@
margin-top: 10px; margin-top: 10px;
display: none; display: none;
} }
.progress-container p { .progress-container p {
margin-bottom: 5px; margin-bottom: 5px;
font-size: 0.9rem; font-size: 0.9rem;
color: #333; color: #333;
} }
progress { progress {
width: 100%; width: 100%;
height: 20px; height: 20px;
} }
progress::-webkit-progress-bar { progress::-webkit-progress-bar {
background-color: #eee; background-color: #eee;
border-radius: 5px; border-radius: 5px;
} }
progress::-webkit-progress-value { progress::-webkit-progress-value {
background-color: #4CAF50; background-color: #4CAF50;
border-radius: 5px; border-radius: 5px;
@ -381,7 +430,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" id="keywords" placeholder="输入关键词" /> <input type="text" id="keywords" placeholder="输入关键词"/>
</div> </div>
<button class="search-btn" onclick="startSearch()">搜索</button> <button class="search-btn" onclick="startSearch()">搜索</button>
@ -415,7 +464,7 @@
let selectedSongs = []; let selectedSongs = [];
let allSongs = []; let allSongs = [];
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
fetchPlatforms(); fetchPlatforms();
fetchSearchTypes(); fetchSearchTypes();
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
@ -424,13 +473,13 @@
// 点击用户名时,切换下拉菜单的显示状态 // 点击用户名时,切换下拉菜单的显示状态
const userInfoDiv = document.getElementById('user-info'); const userInfoDiv = document.getElementById('user-info');
const dropdownMenu = document.getElementById('dropdown-menu'); const dropdownMenu = document.getElementById('dropdown-menu');
userInfoDiv.addEventListener('click', function(event) { userInfoDiv.addEventListener('click', function (event) {
event.stopPropagation(); // 防止事件冒泡 event.stopPropagation(); // 防止事件冒泡
dropdownMenu.classList.toggle('visible'); dropdownMenu.classList.toggle('visible');
}); });
// 点击页面其他部分时,隐藏下拉菜单 // 点击页面其他部分时,隐藏下拉菜单
document.addEventListener('click', function() { document.addEventListener('click', function () {
dropdownMenu.classList.remove('visible'); dropdownMenu.classList.remove('visible');
}); });
}); });
@ -930,7 +979,7 @@
alert(msg); alert(msg);
setTimeout(() => { setTimeout(() => {
window.location.href = '/login.html'; // 确保登录页面的路径正确 window.location.href = '/login.html'; // 确保登录页面的路径正确
}, 1000); }, 500);
} }
// 退出登录函数 // 退出登录函数
@ -966,7 +1015,7 @@
// 辅助函数转义HTML以防止XSS攻击 // 辅助函数转义HTML以防止XSS攻击
function escapeHTML(str) { function escapeHTML(str) {
if (!str) return ''; if (!str) return '';
return str.replace(/[&<>"'`=\/]/g, function(s) { return str.replace(/[&<>"'`=\/]/g, function (s) {
return ({ return ({
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',