download
This commit is contained in:
parent
f4b5c4dc06
commit
b6f3ff6dce
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@ -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> {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user